/* * (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.Arrays; 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 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.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.TypeAdapter; import org.nuxeo.ecm.automation.core.exception.CatchChainException; import org.nuxeo.ecm.automation.core.exception.ChainExceptionRegistry; import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.services.config.ConfigurationService; import org.nuxeo.runtime.transaction.TransactionHelper; import com.google.common.collect.Iterables; /** * 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); public static final String EXPORT_ALIASES_CONFIGURATION_PARAM = "nuxeo.automation.export.aliases"; protected final OperationTypeRegistry operations; protected final ChainExceptionRegistry chainExceptionRegistry; protected final AutomationFilterRegistry automationFilterRegistry; protected final OperationChainCompiler compiler = new OperationChainCompiler(this); /** * 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 { return run(ctx, getOperationChain(operationId)); } @Override public Object run(OperationContext ctx, String operationId, Map<String, ?> args) throws OperationException { OperationType op = operations.lookup().get(operationId); if (op == null) { throw new IllegalArgumentException("No such operation " + operationId); } if (args == null) { log.warn("null operation parameters given for " + operationId, new Throwable("stack trace")); args = Collections.emptyMap(); } ctx.push(args); try { return run(ctx, getOperationChain(operationId)); } finally { ctx.pop(args); } } @Override public Object run(OperationContext ctx, OperationChain chain) throws OperationException { Object input = ctx.getInput(); Class<?> inputType = input == null ? Void.TYPE : input.getClass(); CompiledChain compiled = compileChain(inputType, chain); try { return compiled.invoke(ctx); } catch (OperationException cause) { if (hasChainException(chain.getId())) { return run(ctx, getChainExceptionToRun(ctx, chain.getId(), cause)); } else if (cause.isRollback()) { ctx.setRollback(); } throw cause; } } @Override public Object runInNewTx(OperationContext ctx, String chainId, Map<String, ?> 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.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 void putOperationChain(OperationChain chain) throws OperationException { putOperationChain(chain, false); } final Map<String, OperationType> typeofChains = new HashMap<>(); @Override public void putOperationChain(OperationChain chain, boolean replace) throws OperationException { final OperationType typeof = OperationType.typeof(chain, replace); this.putOperation(typeof, replace); typeofChains.put(chain.getId(), typeof); } @Override public void removeOperationChain(String id) { OperationType typeof = operations.lookup().get(id); if (typeof == null) { throw new IllegalArgumentException("no such chain " + id); } this.removeOperation(typeof); } @Override public OperationChain getOperationChain(String id) throws OperationNotFoundException { OperationType type = getOperation(id); if (type instanceof ChainTypeImpl) { return ((ChainTypeImpl) type).chain; } OperationChain chain = new OperationChain(id); chain.add(id); return chain; } @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() { compiler.cache.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 void putOperation(OperationType op, boolean replace) throws OperationException { operations.addContribution(op, replace); } @Override public 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 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, OperationParameters... ops) throws OperationException { return compileChain(inputType, new OperationChain("", Arrays.asList(ops))); } @Override public CompiledChain compileChain(Class<?> inputType, OperationChain chain) throws OperationException { return compiler.compile(ChainTypeImpl.typeof(chain, false), inputType); } @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 (targetType.isAssignableFrom(Void.class)) { return null; } if (OperationContext.class.isAssignableFrom(targetType)) { return (T) ctx; } // handle primitive types Class<?> toAdaptClass = toAdapt == null ? Void.class : toAdapt.getClass(); if (targetType.isPrimitive()) { targetType = getTypeForPrimitive(targetType); if (targetType.isAssignableFrom(toAdaptClass)) { return (T) toAdapt; } } if (targetType.isArray() && toAdapt instanceof List) { @SuppressWarnings("rawtypes") final Iterable iterable = (Iterable) toAdapt; return (T) Iterables.toArray(iterable, targetType.getComponentType()); } TypeAdapter adapter = getTypeAdapter(toAdaptClass, targetType); if (adapter == null) { if (toAdapt == null) { return null; } if (toAdapt instanceof JsonNode) { // fall-back to generic jackson adapter ObjectMapper mapper = new ObjectMapper(); return (T) mapper.convertValue(toAdapt, targetType); } if (targetType.isAssignableFrom(OperationContext.class)) { return (T) ctx; } throw new OperationException( "No type adapter found for input: " + toAdaptClass + " 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(); ConfigurationService configurationService = Framework.getService(ConfigurationService.class); boolean exportAliases = configurationService.isBooleanPropertyTrue(EXPORT_ALIASES_CONFIGURATION_PARAM); for (OperationType ot : ops.toArray(new OperationType[ops.size()])) { try { OperationDocumentation documentation = ot.getDocumentation(); result.add(documentation); // we may want to add an operation documentation for each alias to be backward compatible with old // automation clients String[] aliases = ot.getAliases(); if (exportAliases && aliases != null && aliases.length > 0) { for (String alias : aliases) { result.add(OperationDocumentation.copyForAlias(documentation, alias)); } } } 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()]); } }