org.nuxeo.ecm.automation.core.impl.OperationServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.ecm.automation.core.impl.OperationServiceImpl.java

Source

/*
 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 *     bstefanescu
 *     vpasquier <vpasquier@nuxeo.com>
 *     slacoin <slacoin@nuxeo.com>
 */
package org.nuxeo.ecm.automation.core.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import com.google.common.collect.Iterables;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.nuxeo.ecm.automation.AutomationAdmin;
import org.nuxeo.ecm.automation.AutomationFilter;
import org.nuxeo.ecm.automation.AutomationService;
import org.nuxeo.ecm.automation.ChainException;
import org.nuxeo.ecm.automation.CompiledChain;
import org.nuxeo.ecm.automation.OperationCallback;
import org.nuxeo.ecm.automation.OperationChain;
import org.nuxeo.ecm.automation.OperationCompoundExceptionBuilder;
import org.nuxeo.ecm.automation.OperationContext;
import org.nuxeo.ecm.automation.OperationDocumentation;
import org.nuxeo.ecm.automation.OperationException;
import org.nuxeo.ecm.automation.OperationNotFoundException;
import org.nuxeo.ecm.automation.OperationParameters;
import org.nuxeo.ecm.automation.OperationType;
import org.nuxeo.ecm.automation.TraceException;
import org.nuxeo.ecm.automation.TypeAdapter;
import org.nuxeo.ecm.automation.core.Constants;
import org.nuxeo.ecm.automation.core.exception.CatchChainException;
import org.nuxeo.ecm.automation.core.exception.ChainExceptionRegistry;
import org.nuxeo.ecm.automation.core.trace.TracerFactory;
import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.transaction.TransactionHelper;

/**
 * The operation registry is thread safe and optimized for modifications at startup and lookups at runtime.
 *
 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
 */
public class OperationServiceImpl implements AutomationService, AutomationAdmin {

    private static final Log log = LogFactory.getLog(OperationServiceImpl.class);

    protected final OperationTypeRegistry operations;

    protected final ChainExceptionRegistry chainExceptionRegistry;

    protected final AutomationFilterRegistry automationFilterRegistry;

    protected Map<CacheKey, CompiledChainImpl> compiledChains = new HashMap<CacheKey, CompiledChainImpl>();

    /**
     * Adapter registry.
     */
    protected AdapterKeyedRegistry adapters;

    public OperationServiceImpl() {
        operations = new OperationTypeRegistry();
        adapters = new AdapterKeyedRegistry();
        chainExceptionRegistry = new ChainExceptionRegistry();
        automationFilterRegistry = new AutomationFilterRegistry();
    }

    @Override
    public Object run(OperationContext ctx, String operationId) throws OperationException {
        OperationType operationType = getOperation(operationId);
        if (operationType instanceof ChainTypeImpl) {
            return run(ctx, operationType, ((ChainTypeImpl) operationType).getChainParameters());
        } else {
            return run(ctx, operationType, null);
        }
    }

    @Override
    public Object run(OperationContext ctx, OperationChain chain) throws OperationException {
        Map<String, Object> chainParameters = Collections.<String, Object>emptyMap();
        if (!chain.getChainParameters().isEmpty()) {
            chainParameters = chain.getChainParameters();
        }
        ChainTypeImpl chainType = new ChainTypeImpl(this, chain);
        return run(ctx, chainType, chainParameters);
    }

    /**
     * TODO avoid creating a temporary chain and then compile it. try to find a way to execute the single operation
     * without compiling it. (for optimization)
     */
    @Override
    public Object run(OperationContext ctx, String operationId, Map<String, Object> runtimeParameters)
            throws OperationException {
        OperationType type = getOperation(operationId);
        return run(ctx, type, runtimeParameters);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public Object runInNewTx(OperationContext ctx, String chainId, Map chainParameters, Integer timeout,
            boolean rollbackGlobalOnError) throws OperationException {
        Object result = null;
        // if the current transaction was already marked for rollback,
        // do nothing
        if (TransactionHelper.isTransactionMarkedRollback()) {
            return null;
        }
        // commit the current transaction
        TransactionHelper.commitOrRollbackTransaction();

        int to = timeout == null ? 0 : timeout;

        TransactionHelper.startTransaction(to);
        boolean ok = false;

        try {
            result = run(ctx, chainId, chainParameters);
            ok = true;
        } catch (OperationException e) {
            if (rollbackGlobalOnError) {
                throw e;
            } else {
                // just log, no rethrow
                log.error("Error while executing operation " + chainId, e);
            }
        } finally {
            if (!ok) {
                // will be logged by Automation framework
                TransactionHelper.setTransactionRollbackOnly();
            }
            TransactionHelper.commitOrRollbackTransaction();
            // caller expects a transaction to be started
            TransactionHelper.startTransaction();
        }
        return result;
    }

    /**
     * @since 5.7.2
     * @param ctx the operation context.
     * @param operationType a chain or an operation.
     * @param params The chain parameters.
     */
    public Object run(OperationContext ctx, OperationType operationType, Map<String, Object> params)
            throws OperationException {
        Boolean mainChain = true;
        CompiledChainImpl chain;
        if (params == null) {
            params = new HashMap<>();
        }
        ctx.put(Constants.VAR_RUNTIME_CHAIN, params);
        // Put Chain parameters into the context - even for cached chains
        if (!params.isEmpty()) {
            ctx.put(Constants.VAR_RUNTIME_CHAIN, params);
        }
        OperationCallback tracer;
        TracerFactory tracerFactory = Framework.getLocalService(TracerFactory.class);
        if (ctx.getChainCallback() == null) {
            tracer = tracerFactory.newTracer(operationType.getId());
            ctx.addChainCallback(tracer);
        } else {
            // Not logging at output if success for a child chain
            mainChain = false;
            tracer = ctx.getChainCallback();
        }
        try {
            Object input = ctx.getInput();
            Class<?> inputType = input == null ? Void.TYPE : input.getClass();
            tracer.onChain(operationType);
            if (ChainTypeImpl.class.isAssignableFrom(operationType.getClass())) {
                ctx.put(Constants.VAR_IS_CHAIN, true);
                CacheKey cacheKey = new CacheKey(operationType.getId(), inputType.getName());
                chain = compiledChains.get(cacheKey);
                if (chain == null) {
                    chain = (CompiledChainImpl) operationType.newInstance(ctx, params);
                    // Registered Chains are the only ones that can be cached
                    // Runtime ones can update their operations, model...
                    if (hasOperation(operationType.getId())) {
                        compiledChains.put(cacheKey, chain);
                    }
                }
            } else {
                chain = CompiledChainImpl.buildChain(inputType, toParams(operationType.getId()));
            }
            Object ret = chain.invoke(ctx);
            tracer.onOutput(ret);
            if (ctx.getCoreSession() != null && ctx.isCommit()) {
                // auto save session if any.
                ctx.getCoreSession().save();
            }
            // Log at the end of the main chain execution.
            if (mainChain && tracer.getTrace() != null && tracerFactory.getRecordingState()) {
                log.info(tracer.getFormattedText());
            }
            return ret;
        } catch (OperationException oe) {
            // Record trace
            tracer.onError(oe);
            // Handle exception chain and rollback
            String operationTypeId = operationType.getId();
            if (hasChainException(operationTypeId)) {
                // Rollback is handled by chain exception
                return run(ctx, getChainExceptionToRun(ctx, operationTypeId, oe));
            } else if (oe.isRollback()) {
                ctx.setRollback();
            }
            // Handle exception
            if (mainChain) {
                throw new TraceException(tracer, oe);
            } else {
                throw new TraceException(oe);
            }
        } finally {
            ctx.dispose();
        }
    }

    /**
     * @since 5.7.3 Fetch the right chain id to run when catching exception for given chain failure.
     */
    protected String getChainExceptionToRun(OperationContext ctx, String operationTypeId, OperationException oe)
            throws OperationException {
        // Inject exception name into the context
        //since 6.0-HF05 should use exceptionName and exceptionObject on the context instead of Exception
        ctx.put("Exception", oe.getClass().getSimpleName());
        ctx.put("exceptionName", oe.getClass().getSimpleName());
        ctx.put("exceptionObject", oe);

        ChainException chainException = getChainException(operationTypeId);
        CatchChainException catchChainException = new CatchChainException();
        for (CatchChainException catchChainExceptionItem : chainException.getCatchChainExceptions()) {
            // Check first a possible filter value
            if (catchChainExceptionItem.hasFilter()) {
                AutomationFilter filter = getAutomationFilter(catchChainExceptionItem.getFilterId());
                try {
                    String filterValue = (String) filter.getValue().eval(ctx);
                    // Check if priority for this chain exception is higher
                    if (Boolean.parseBoolean(filterValue)) {
                        catchChainException = getCatchChainExceptionByPriority(catchChainException,
                                catchChainExceptionItem);
                    }
                } catch (RuntimeException e) { // TODO more specific exceptions?
                    throw new OperationException(
                            "Cannot evaluate Automation Filter " + filter.getId() + " mvel expression.", e);
                }
            } else {
                // Check if priority for this chain exception is higher
                catchChainException = getCatchChainExceptionByPriority(catchChainException,
                        catchChainExceptionItem);
            }
        }
        String chainId = catchChainException.getChainId();
        if (chainId.isEmpty()) {
            throw new OperationException(
                    "No chain exception has been selected to be run. You should verify Automation filters applied.");
        }
        if (catchChainException.getRollBack()) {
            ctx.setRollback();
        }
        return catchChainException.getChainId();
    }

    /**
     * @since 5.7.3
     */
    protected CatchChainException getCatchChainExceptionByPriority(CatchChainException catchChainException,
            CatchChainException catchChainExceptionItem) {
        return catchChainException.getPriority() <= catchChainExceptionItem.getPriority() ? catchChainExceptionItem
                : catchChainException;
    }

    public static OperationParameters[] toParams(String... ids) {
        OperationParameters[] operationParameters = new OperationParameters[ids.length];
        for (int i = 0; i < ids.length; ++i) {
            operationParameters[i] = new OperationParameters(ids[i]);
        }
        return operationParameters;
    }

    @Override
    public synchronized void putOperationChain(OperationChain chain) throws OperationException {
        putOperationChain(chain, false);
    }

    @Override
    public synchronized void putOperationChain(OperationChain chain, boolean replace) throws OperationException {
        OperationType docChainType = new ChainTypeImpl(this, chain);
        this.putOperation(docChainType, replace);
    }

    @Override
    public synchronized void removeOperationChain(String id) {
        OperationChain chain = new OperationChain(id);
        OperationType docChainType = new ChainTypeImpl(this, chain);
        operations.removeContribution(docChainType);
    }

    @Override
    public OperationChain getOperationChain(String id) throws OperationNotFoundException {
        ChainTypeImpl chain = (ChainTypeImpl) getOperation(id);
        return chain.getChain();
    }

    @Override
    public List<OperationChain> getOperationChains() {
        List<ChainTypeImpl> chainsType = new ArrayList<ChainTypeImpl>();
        List<OperationChain> chains = new ArrayList<OperationChain>();
        for (OperationType operationType : operations.lookup().values()) {
            if (operationType instanceof ChainTypeImpl) {
                chainsType.add((ChainTypeImpl) operationType);
            }
        }
        for (ChainTypeImpl chainType : chainsType) {
            chains.add(chainType.getChain());
        }
        return chains;
    }

    @Override
    public synchronized void flushCompiledChains() {
        compiledChains.clear();
    }

    @Override
    public void putOperation(Class<?> type) throws OperationException {
        OperationTypeImpl op = new OperationTypeImpl(this, type);
        putOperation(op, false);
    }

    @Override
    public void putOperation(Class<?> type, boolean replace) throws OperationException {
        putOperation(type, replace, null);
    }

    @Override
    public void putOperation(Class<?> type, boolean replace, String contributingComponent)
            throws OperationException {
        OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent);
        putOperation(op, replace);
    }

    @Override
    public void putOperation(Class<?> type, boolean replace, String contributingComponent,
            List<WidgetDefinition> widgetDefinitionList) throws OperationException {
        OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent, widgetDefinitionList);
        putOperation(op, replace);
    }

    @Override
    public synchronized void putOperation(OperationType op, boolean replace) throws OperationException {
        operations.addContribution(op, replace);
    }

    @Override
    public synchronized void removeOperation(Class<?> key) {
        OperationType type = operations.getOperationType(key);
        if (type == null) {
            log.warn("Cannot remove operation, no such operation " + key);
            return;
        }
        removeOperation(type);
    }

    @Override
    public synchronized void removeOperation(OperationType type) {
        operations.removeContribution(type);
    }

    @Override
    public OperationType[] getOperations() {
        HashSet<OperationType> values = new HashSet<>(operations.lookup().values());
        return values.toArray(new OperationType[values.size()]);
    }

    @Override
    public OperationType getOperation(String id) throws OperationNotFoundException {
        OperationType op = operations.lookup().get(id);
        if (op == null) {
            throw new OperationNotFoundException("No operation was bound on ID: " + id);
        }
        return op;
    }

    /**
     * @since 5.7.2
     * @param id operation ID.
     * @return true if operation registry contains the given operation.
     */
    @Override
    public boolean hasOperation(String id) {
        OperationType op = operations.lookup().get(id);
        if (op == null) {
            return false;
        }
        return true;
    }

    @Override
    public CompiledChain compileChain(Class<?> inputType, OperationChain chain) throws OperationException {
        List<OperationParameters> ops = chain.getOperations();
        return compileChain(inputType, ops.toArray(new OperationParameters[ops.size()]));
    }

    @Override
    public CompiledChain compileChain(Class<?> inputType, OperationParameters... operations)
            throws OperationException {
        return CompiledChainImpl.buildChain(this, inputType == null ? Void.TYPE : inputType, operations);
    }

    @Override
    public void putTypeAdapter(Class<?> accept, Class<?> produce, TypeAdapter adapter) {
        adapters.put(new TypeAdapterKey(accept, produce), adapter);
    }

    @Override
    public void removeTypeAdapter(Class<?> accept, Class<?> produce) {
        adapters.remove(new TypeAdapterKey(accept, produce));
    }

    @Override
    public TypeAdapter getTypeAdapter(Class<?> accept, Class<?> produce) {
        return adapters.get(new TypeAdapterKey(accept, produce));
    }

    @Override
    public boolean isTypeAdaptable(Class<?> typeToAdapt, Class<?> targetType) {
        return getTypeAdapter(typeToAdapt, targetType) != null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T getAdaptedValue(OperationContext ctx, Object toAdapt, Class<?> targetType)
            throws OperationException {
        if (toAdapt == null) {
            return null;
        }
        // handle primitive types
        Class<?> toAdaptClass = toAdapt.getClass();
        if (targetType.isPrimitive()) {
            targetType = getTypeForPrimitive(targetType);
            if (targetType.isAssignableFrom(toAdaptClass)) {
                return (T) toAdapt;
            }
        }
        if (targetType.isArray() && toAdapt instanceof List) {
            return (T) Iterables.toArray((Iterable) toAdapt, targetType.getComponentType());
        }
        TypeAdapter adapter = getTypeAdapter(toAdaptClass, targetType);
        if (adapter == null) {
            if (toAdapt instanceof JsonNode) {
                // fall-back to generic jackson adapter
                ObjectMapper mapper = new ObjectMapper();
                return (T) mapper.convertValue(toAdapt, targetType);
            }
            throw new OperationException(
                    "No type adapter found for input: " + toAdapt.getClass() + " and output " + targetType);
        }
        return (T) adapter.getAdaptedValue(ctx, toAdapt);
    }

    @Override
    public List<OperationDocumentation> getDocumentation() throws OperationException {
        List<OperationDocumentation> result = new ArrayList<OperationDocumentation>();
        HashSet<OperationType> ops = new HashSet<>(operations.lookup().values());
        OperationCompoundExceptionBuilder errorBuilder = new OperationCompoundExceptionBuilder();
        for (OperationType ot : ops.toArray(new OperationType[ops.size()])) {
            try {
                result.add(ot.getDocumentation());
            } catch (OperationNotFoundException e) {
                errorBuilder.add(e);
            }
        }
        errorBuilder.throwOnError();
        Collections.sort(result);
        return result;
    }

    public static Class<?> getTypeForPrimitive(Class<?> primitiveType) {
        if (primitiveType == Boolean.TYPE) {
            return Boolean.class;
        } else if (primitiveType == Integer.TYPE) {
            return Integer.class;
        } else if (primitiveType == Long.TYPE) {
            return Long.class;
        } else if (primitiveType == Float.TYPE) {
            return Float.class;
        } else if (primitiveType == Double.TYPE) {
            return Double.class;
        } else if (primitiveType == Character.TYPE) {
            return Character.class;
        } else if (primitiveType == Byte.TYPE) {
            return Byte.class;
        } else if (primitiveType == Short.TYPE) {
            return Short.class;
        }
        return primitiveType;
    }

    /**
     * @since 5.7.3
     */
    @Override
    public void putChainException(ChainException exceptionChain) {
        chainExceptionRegistry.addContribution(exceptionChain);
    }

    /**
     * @since 5.7.3
     */
    @Override
    public void removeExceptionChain(ChainException exceptionChain) {
        chainExceptionRegistry.removeContribution(exceptionChain);
    }

    /**
     * @since 5.7.3
     */
    @Override
    public ChainException[] getChainExceptions() {
        Collection<ChainException> chainExceptions = chainExceptionRegistry.lookup().values();
        return chainExceptions.toArray(new ChainException[chainExceptions.size()]);
    }

    /**
     * @since 5.7.3
     */
    @Override
    public ChainException getChainException(String onChainId) {
        return chainExceptionRegistry.getChainException(onChainId);
    }

    /**
     * @since 5.7.3
     */
    @Override
    public boolean hasChainException(String onChainId) {
        return chainExceptionRegistry.getChainException(onChainId) != null;
    }

    /**
     * @since 5.7.3
     */
    @Override
    public void putAutomationFilter(AutomationFilter automationFilter) {
        automationFilterRegistry.addContribution(automationFilter);
    }

    /**
     * @since 5.7.3
     */
    @Override
    public void removeAutomationFilter(AutomationFilter automationFilter) {
        automationFilterRegistry.removeContribution(automationFilter);
    }

    /**
     * @since 5.7.3
     */
    @Override
    public AutomationFilter getAutomationFilter(String id) {
        return automationFilterRegistry.getAutomationFilter(id);
    }

    /**
     * @since 5.7.3
     */
    @Override
    public AutomationFilter[] getAutomationFilters() {
        Collection<AutomationFilter> automationFilters = automationFilterRegistry.lookup().values();
        return automationFilters.toArray(new AutomationFilter[automationFilters.size()]);
    }

    /**
     * @since 5.8 - Composite key to handle several operations with same id and different input types.
     */
    protected static class CacheKey {

        String operationId;

        String inputType;

        public CacheKey(String operationId, String inputType) {
            this.operationId = operationId;
            this.inputType = inputType;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            CacheKey cacheKey = (CacheKey) o;

            if (inputType != null ? !inputType.equals(cacheKey.inputType) : cacheKey.inputType != null) {
                return false;
            }
            if (operationId != null ? !operationId.equals(cacheKey.operationId) : cacheKey.operationId != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = operationId != null ? operationId.hashCode() : 0;
            result = 31 * result + (inputType != null ? inputType.hashCode() : 0);
            return result;
        }
    }
}