001/*
002 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     bstefanescu
016 *     vpasquier <vpasquier@nuxeo.com>
017 *     slacoin <slacoin@nuxeo.com>
018 */
019package org.nuxeo.ecm.automation.core.impl;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028
029import com.google.common.collect.Iterables;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.codehaus.jackson.JsonNode;
033import org.codehaus.jackson.map.ObjectMapper;
034import org.nuxeo.ecm.automation.AutomationAdmin;
035import org.nuxeo.ecm.automation.AutomationFilter;
036import org.nuxeo.ecm.automation.AutomationService;
037import org.nuxeo.ecm.automation.ChainException;
038import org.nuxeo.ecm.automation.CompiledChain;
039import org.nuxeo.ecm.automation.OperationCallback;
040import org.nuxeo.ecm.automation.OperationChain;
041import org.nuxeo.ecm.automation.OperationCompoundExceptionBuilder;
042import org.nuxeo.ecm.automation.OperationContext;
043import org.nuxeo.ecm.automation.OperationDocumentation;
044import org.nuxeo.ecm.automation.OperationException;
045import org.nuxeo.ecm.automation.OperationNotFoundException;
046import org.nuxeo.ecm.automation.OperationParameters;
047import org.nuxeo.ecm.automation.OperationType;
048import org.nuxeo.ecm.automation.TraceException;
049import org.nuxeo.ecm.automation.TypeAdapter;
050import org.nuxeo.ecm.automation.core.Constants;
051import org.nuxeo.ecm.automation.core.exception.CatchChainException;
052import org.nuxeo.ecm.automation.core.exception.ChainExceptionRegistry;
053import org.nuxeo.ecm.automation.core.trace.TracerFactory;
054import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition;
055import org.nuxeo.runtime.api.Framework;
056import org.nuxeo.runtime.transaction.TransactionHelper;
057
058/**
059 * The operation registry is thread safe and optimized for modifications at startup and lookups at runtime.
060 *
061 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
062 */
063public class OperationServiceImpl implements AutomationService, AutomationAdmin {
064
065    private static final Log log = LogFactory.getLog(OperationServiceImpl.class);
066
067    protected final OperationTypeRegistry operations;
068
069    protected final ChainExceptionRegistry chainExceptionRegistry;
070
071    protected final AutomationFilterRegistry automationFilterRegistry;
072
073    protected Map<CacheKey, CompiledChainImpl> compiledChains = new HashMap<CacheKey, CompiledChainImpl>();
074
075    /**
076     * Adapter registry.
077     */
078    protected AdapterKeyedRegistry adapters;
079
080    public OperationServiceImpl() {
081        operations = new OperationTypeRegistry();
082        adapters = new AdapterKeyedRegistry();
083        chainExceptionRegistry = new ChainExceptionRegistry();
084        automationFilterRegistry = new AutomationFilterRegistry();
085    }
086
087    @Override
088    public Object run(OperationContext ctx, String operationId) throws OperationException {
089        OperationType operationType = getOperation(operationId);
090        if (operationType instanceof ChainTypeImpl) {
091            return run(ctx, operationType, ((ChainTypeImpl) operationType).getChainParameters());
092        } else {
093            return run(ctx, operationType, null);
094        }
095    }
096
097    @Override
098    public Object run(OperationContext ctx, OperationChain chain) throws OperationException {
099        Map<String, Object> chainParameters = Collections.<String, Object> emptyMap();
100        if (!chain.getChainParameters().isEmpty()) {
101            chainParameters = chain.getChainParameters();
102        }
103        ChainTypeImpl chainType = new ChainTypeImpl(this, chain);
104        return run(ctx, chainType, chainParameters);
105    }
106
107    /**
108     * TODO avoid creating a temporary chain and then compile it. try to find a way to execute the single operation
109     * without compiling it. (for optimization)
110     */
111    @Override
112    public Object run(OperationContext ctx, String operationId, Map<String, Object> runtimeParameters)
113            throws OperationException {
114        OperationType type = getOperation(operationId);
115        return run(ctx, type, runtimeParameters);
116    }
117
118    /**
119     * {@inheritDoc}
120     */
121    @Override
122    @SuppressWarnings("unchecked")
123    public Object runInNewTx(OperationContext ctx, String chainId, Map chainParameters, Integer timeout,
124            boolean rollbackGlobalOnError) throws OperationException {
125        Object result = null;
126        // if the current transaction was already marked for rollback,
127        // do nothing
128        if (TransactionHelper.isTransactionMarkedRollback()) {
129            return null;
130        }
131        // commit the current transaction
132        TransactionHelper.commitOrRollbackTransaction();
133
134        int to = timeout == null ? 0 : timeout;
135
136        TransactionHelper.startTransaction(to);
137        boolean ok = false;
138
139        try {
140            result = run(ctx, chainId, chainParameters);
141            ok = true;
142        } catch (OperationException e) {
143            if (rollbackGlobalOnError) {
144                throw e;
145            } else {
146                // just log, no rethrow
147                log.error("Error while executing operation " + chainId, e);
148            }
149        } finally {
150            if (!ok) {
151                // will be logged by Automation framework
152                TransactionHelper.setTransactionRollbackOnly();
153            }
154            TransactionHelper.commitOrRollbackTransaction();
155            // caller expects a transaction to be started
156            TransactionHelper.startTransaction();
157        }
158        return result;
159    }
160
161    /**
162     * @since 5.7.2
163     * @param ctx the operation context.
164     * @param operationType a chain or an operation.
165     * @param params The chain parameters.
166     */
167    public Object run(OperationContext ctx, OperationType operationType, Map<String, Object> params)
168            throws OperationException {
169        Boolean mainChain = true;
170        CompiledChainImpl chain;
171        if (params == null) {
172            params = new HashMap<>();
173        }
174        ctx.put(Constants.VAR_RUNTIME_CHAIN, params);
175        // Put Chain parameters into the context - even for cached chains
176        if (!params.isEmpty()) {
177            ctx.put(Constants.VAR_RUNTIME_CHAIN, params);
178        }
179        OperationCallback tracer;
180        TracerFactory tracerFactory = Framework.getLocalService(TracerFactory.class);
181        if (ctx.getChainCallback() == null) {
182            tracer = tracerFactory.newTracer(operationType.getId());
183            ctx.addChainCallback(tracer);
184        } else {
185            // Not logging at output if success for a child chain
186            mainChain = false;
187            tracer = ctx.getChainCallback();
188        }
189        try {
190            Object input = ctx.getInput();
191            Class<?> inputType = input == null ? Void.TYPE : input.getClass();
192            tracer.onChain(operationType);
193            if (ChainTypeImpl.class.isAssignableFrom(operationType.getClass())) {
194                ctx.put(Constants.VAR_IS_CHAIN, true);
195                CacheKey cacheKey = new CacheKey(operationType.getId(), inputType.getName());
196                chain = compiledChains.get(cacheKey);
197                if (chain == null) {
198                    chain = (CompiledChainImpl) operationType.newInstance(ctx, params);
199                    // Registered Chains are the only ones that can be cached
200                    // Runtime ones can update their operations, model...
201                    if (hasOperation(operationType.getId())) {
202                        compiledChains.put(cacheKey, chain);
203                    }
204                }
205            } else {
206                chain = CompiledChainImpl.buildChain(inputType, toParams(operationType.getId()));
207            }
208            Object ret = chain.invoke(ctx);
209            tracer.onOutput(ret);
210            if (ctx.getCoreSession() != null && ctx.isCommit()) {
211                // auto save session if any.
212                ctx.getCoreSession().save();
213            }
214            // Log at the end of the main chain execution.
215            if (mainChain && tracer.getTrace() != null && tracerFactory.getRecordingState()) {
216                log.info(tracer.getFormattedText());
217            }
218            return ret;
219        } catch (OperationException oe) {
220            // Record trace
221            tracer.onError(oe);
222            // Handle exception chain and rollback
223            String operationTypeId = operationType.getId();
224            if (hasChainException(operationTypeId)) {
225                // Rollback is handled by chain exception
226                return run(ctx, getChainExceptionToRun(ctx, operationTypeId, oe));
227            } else if (oe.isRollback()) {
228                ctx.setRollback();
229            }
230            // Handle exception
231            if (mainChain) {
232                throw new TraceException(tracer, oe);
233            } else {
234                throw new TraceException(oe);
235            }
236        } finally {
237            ctx.dispose();
238        }
239    }
240
241    /**
242     * @since 5.7.3 Fetch the right chain id to run when catching exception for given chain failure.
243     */
244    protected String getChainExceptionToRun(OperationContext ctx, String operationTypeId, OperationException oe)
245            throws OperationException {
246        // Inject exception name into the context
247        //since 6.0-HF05 should use exceptionName and exceptionObject on the context instead of Exception
248        ctx.put("Exception", oe.getClass().getSimpleName());
249        ctx.put("exceptionName", oe.getClass().getSimpleName());
250        ctx.put("exceptionObject", oe);
251        
252        ChainException chainException = getChainException(operationTypeId);
253        CatchChainException catchChainException = new CatchChainException();
254        for (CatchChainException catchChainExceptionItem : chainException.getCatchChainExceptions()) {
255            // Check first a possible filter value
256            if (catchChainExceptionItem.hasFilter()) {
257                AutomationFilter filter = getAutomationFilter(catchChainExceptionItem.getFilterId());
258                try {
259                    String filterValue = (String) filter.getValue().eval(ctx);
260                    // Check if priority for this chain exception is higher
261                    if (Boolean.parseBoolean(filterValue)) {
262                        catchChainException = getCatchChainExceptionByPriority(catchChainException,
263                                catchChainExceptionItem);
264                    }
265                } catch (RuntimeException e) { // TODO more specific exceptions?
266                    throw new OperationException("Cannot evaluate Automation Filter " + filter.getId()
267                            + " mvel expression.", e);
268                }
269            } else {
270                // Check if priority for this chain exception is higher
271                catchChainException = getCatchChainExceptionByPriority(catchChainException, catchChainExceptionItem);
272            }
273        }
274        String chainId = catchChainException.getChainId();
275        if (chainId.isEmpty()) {
276            throw new OperationException(
277                    "No chain exception has been selected to be run. You should verify Automation filters applied.");
278        }
279        if (catchChainException.getRollBack()) {
280            ctx.setRollback();
281        }
282        return catchChainException.getChainId();
283    }
284
285    /**
286     * @since 5.7.3
287     */
288    protected CatchChainException getCatchChainExceptionByPriority(CatchChainException catchChainException,
289            CatchChainException catchChainExceptionItem) {
290        return catchChainException.getPriority() <= catchChainExceptionItem.getPriority() ? catchChainExceptionItem
291                : catchChainException;
292    }
293
294    public static OperationParameters[] toParams(String... ids) {
295        OperationParameters[] operationParameters = new OperationParameters[ids.length];
296        for (int i = 0; i < ids.length; ++i) {
297            operationParameters[i] = new OperationParameters(ids[i]);
298        }
299        return operationParameters;
300    }
301
302    @Override
303    public synchronized void putOperationChain(OperationChain chain) throws OperationException {
304        putOperationChain(chain, false);
305    }
306
307    @Override
308    public synchronized void putOperationChain(OperationChain chain, boolean replace) throws OperationException {
309        OperationType docChainType = new ChainTypeImpl(this, chain);
310        this.putOperation(docChainType, replace);
311    }
312
313    @Override
314    public synchronized void removeOperationChain(String id) {
315        OperationChain chain = new OperationChain(id);
316        OperationType docChainType = new ChainTypeImpl(this, chain);
317        operations.removeContribution(docChainType);
318    }
319
320    @Override
321    public OperationChain getOperationChain(String id) throws OperationNotFoundException {
322        ChainTypeImpl chain = (ChainTypeImpl) getOperation(id);
323        return chain.getChain();
324    }
325
326    @Override
327    public List<OperationChain> getOperationChains() {
328        List<ChainTypeImpl> chainsType = new ArrayList<ChainTypeImpl>();
329        List<OperationChain> chains = new ArrayList<OperationChain>();
330        for (OperationType operationType : operations.lookup().values()) {
331            if (operationType instanceof ChainTypeImpl) {
332                chainsType.add((ChainTypeImpl) operationType);
333            }
334        }
335        for (ChainTypeImpl chainType : chainsType) {
336            chains.add(chainType.getChain());
337        }
338        return chains;
339    }
340
341    @Override
342    public synchronized void flushCompiledChains() {
343        compiledChains.clear();
344    }
345
346    @Override
347    public void putOperation(Class<?> type) throws OperationException {
348        OperationTypeImpl op = new OperationTypeImpl(this, type);
349        putOperation(op, false);
350    }
351
352    @Override
353    public void putOperation(Class<?> type, boolean replace) throws OperationException {
354        putOperation(type, replace, null);
355    }
356
357    @Override
358    public void putOperation(Class<?> type, boolean replace, String contributingComponent) throws OperationException {
359        OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent);
360        putOperation(op, replace);
361    }
362
363    @Override
364    public void putOperation(Class<?> type, boolean replace, String contributingComponent,
365            List<WidgetDefinition> widgetDefinitionList) throws OperationException {
366        OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent, widgetDefinitionList);
367        putOperation(op, replace);
368    }
369
370    @Override
371    public synchronized void putOperation(OperationType op, boolean replace) throws OperationException {
372        operations.addContribution(op, replace);
373    }
374
375    @Override
376    public synchronized void removeOperation(Class<?> key) {
377        OperationType type = operations.getOperationType(key);
378        if (type == null) {
379            log.warn("Cannot remove operation, no such operation " + key);
380            return;
381        }
382        removeOperation(type);
383    }
384
385    @Override
386    public synchronized void removeOperation(OperationType type) {
387        operations.removeContribution(type);
388    }
389
390    @Override
391    public OperationType[] getOperations() {
392        HashSet<OperationType> values = new HashSet<>(operations.lookup().values());
393        return values.toArray(new OperationType[values.size()]);
394    }
395
396    @Override
397    public OperationType getOperation(String id) throws OperationNotFoundException {
398        OperationType op = operations.lookup().get(id);
399        if (op == null) {
400            throw new OperationNotFoundException("No operation was bound on ID: " + id);
401        }
402        return op;
403    }
404
405    /**
406     * @since 5.7.2
407     * @param id operation ID.
408     * @return true if operation registry contains the given operation.
409     */
410    @Override
411    public boolean hasOperation(String id) {
412        OperationType op = operations.lookup().get(id);
413        if (op == null) {
414            return false;
415        }
416        return true;
417    }
418
419    @Override
420    public CompiledChain compileChain(Class<?> inputType, OperationChain chain) throws OperationException {
421        List<OperationParameters> ops = chain.getOperations();
422        return compileChain(inputType, ops.toArray(new OperationParameters[ops.size()]));
423    }
424
425    @Override
426    public CompiledChain compileChain(Class<?> inputType, OperationParameters... operations) throws OperationException {
427        return CompiledChainImpl.buildChain(this, inputType == null ? Void.TYPE : inputType, operations);
428    }
429
430    @Override
431    public void putTypeAdapter(Class<?> accept, Class<?> produce, TypeAdapter adapter) {
432        adapters.put(new TypeAdapterKey(accept, produce), adapter);
433    }
434
435    @Override
436    public void removeTypeAdapter(Class<?> accept, Class<?> produce) {
437        adapters.remove(new TypeAdapterKey(accept, produce));
438    }
439
440    @Override
441    public TypeAdapter getTypeAdapter(Class<?> accept, Class<?> produce) {
442        return adapters.get(new TypeAdapterKey(accept, produce));
443    }
444
445    @Override
446    public boolean isTypeAdaptable(Class<?> typeToAdapt, Class<?> targetType) {
447        return getTypeAdapter(typeToAdapt, targetType) != null;
448    }
449
450    @Override
451    @SuppressWarnings("unchecked")
452    public <T> T getAdaptedValue(OperationContext ctx, Object toAdapt, Class<?> targetType) throws OperationException {
453        if (toAdapt == null) {
454            return null;
455        }
456        // handle primitive types
457        Class<?> toAdaptClass = toAdapt.getClass();
458        if (targetType.isPrimitive()) {
459            targetType = getTypeForPrimitive(targetType);
460            if (targetType.isAssignableFrom(toAdaptClass)) {
461                return (T) toAdapt;
462            }
463        }
464        if (targetType.isArray() && toAdapt instanceof List) {
465            return (T) Iterables.toArray((Iterable) toAdapt, targetType.getComponentType());
466        }
467        TypeAdapter adapter = getTypeAdapter(toAdaptClass, targetType);
468        if (adapter == null) {
469            if (toAdapt instanceof JsonNode) {
470                // fall-back to generic jackson adapter
471                ObjectMapper mapper = new ObjectMapper();
472                return (T) mapper.convertValue(toAdapt, targetType);
473            }
474            throw new OperationException("No type adapter found for input: " + toAdapt.getClass() + " and output "
475                    + targetType);
476        }
477        return (T) adapter.getAdaptedValue(ctx, toAdapt);
478    }
479
480    @Override
481    public List<OperationDocumentation> getDocumentation() throws OperationException {
482        List<OperationDocumentation> result = new ArrayList<OperationDocumentation>();
483        HashSet<OperationType> ops = new HashSet<>(operations.lookup().values());
484        OperationCompoundExceptionBuilder errorBuilder = new OperationCompoundExceptionBuilder();
485        for (OperationType ot : ops.toArray(new OperationType[ops.size()])) {
486            try {
487                result.add(ot.getDocumentation());
488            } catch (OperationNotFoundException e) {
489                errorBuilder.add(e);
490            }
491        }
492        errorBuilder.throwOnError();
493        Collections.sort(result);
494        return result;
495    }
496
497    public static Class<?> getTypeForPrimitive(Class<?> primitiveType) {
498        if (primitiveType == Boolean.TYPE) {
499            return Boolean.class;
500        } else if (primitiveType == Integer.TYPE) {
501            return Integer.class;
502        } else if (primitiveType == Long.TYPE) {
503            return Long.class;
504        } else if (primitiveType == Float.TYPE) {
505            return Float.class;
506        } else if (primitiveType == Double.TYPE) {
507            return Double.class;
508        } else if (primitiveType == Character.TYPE) {
509            return Character.class;
510        } else if (primitiveType == Byte.TYPE) {
511            return Byte.class;
512        } else if (primitiveType == Short.TYPE) {
513            return Short.class;
514        }
515        return primitiveType;
516    }
517
518    /**
519     * @since 5.7.3
520     */
521    @Override
522    public void putChainException(ChainException exceptionChain) {
523        chainExceptionRegistry.addContribution(exceptionChain);
524    }
525
526    /**
527     * @since 5.7.3
528     */
529    @Override
530    public void removeExceptionChain(ChainException exceptionChain) {
531        chainExceptionRegistry.removeContribution(exceptionChain);
532    }
533
534    /**
535     * @since 5.7.3
536     */
537    @Override
538    public ChainException[] getChainExceptions() {
539        Collection<ChainException> chainExceptions = chainExceptionRegistry.lookup().values();
540        return chainExceptions.toArray(new ChainException[chainExceptions.size()]);
541    }
542
543    /**
544     * @since 5.7.3
545     */
546    @Override
547    public ChainException getChainException(String onChainId) {
548        return chainExceptionRegistry.getChainException(onChainId);
549    }
550
551    /**
552     * @since 5.7.3
553     */
554    @Override
555    public boolean hasChainException(String onChainId) {
556        return chainExceptionRegistry.getChainException(onChainId) != null;
557    }
558
559    /**
560     * @since 5.7.3
561     */
562    @Override
563    public void putAutomationFilter(AutomationFilter automationFilter) {
564        automationFilterRegistry.addContribution(automationFilter);
565    }
566
567    /**
568     * @since 5.7.3
569     */
570    @Override
571    public void removeAutomationFilter(AutomationFilter automationFilter) {
572        automationFilterRegistry.removeContribution(automationFilter);
573    }
574
575    /**
576     * @since 5.7.3
577     */
578    @Override
579    public AutomationFilter getAutomationFilter(String id) {
580        return automationFilterRegistry.getAutomationFilter(id);
581    }
582
583    /**
584     * @since 5.7.3
585     */
586    @Override
587    public AutomationFilter[] getAutomationFilters() {
588        Collection<AutomationFilter> automationFilters = automationFilterRegistry.lookup().values();
589        return automationFilters.toArray(new AutomationFilter[automationFilters.size()]);
590    }
591
592    /**
593     * @since 5.8 - Composite key to handle several operations with same id and different input types.
594     */
595    protected static class CacheKey {
596
597        String operationId;
598
599        String inputType;
600
601        public CacheKey(String operationId, String inputType) {
602            this.operationId = operationId;
603            this.inputType = inputType;
604        }
605
606        @Override
607        public boolean equals(Object o) {
608            if (this == o) {
609                return true;
610            }
611            if (o == null || getClass() != o.getClass()) {
612                return false;
613            }
614
615            CacheKey cacheKey = (CacheKey) o;
616
617            if (inputType != null ? !inputType.equals(cacheKey.inputType) : cacheKey.inputType != null) {
618                return false;
619            }
620            if (operationId != null ? !operationId.equals(cacheKey.operationId) : cacheKey.operationId != null) {
621                return false;
622            }
623
624            return true;
625        }
626
627        @Override
628        public int hashCode() {
629            int result = operationId != null ? operationId.hashCode() : 0;
630            result = 31 * result + (inputType != null ? inputType.hashCode() : 0);
631            return result;
632        }
633    }
634}