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                CacheKey cacheKey = new CacheKey(operationType.getId(), inputType.getName());
195                chain = compiledChains.get(cacheKey);
196                if (chain == null) {
197                    chain = (CompiledChainImpl) operationType.newInstance(ctx, params);
198                    // Registered Chains are the only ones that can be cached
199                    // Runtime ones can update their operations, model...
200                    if (hasOperation(operationType.getId())) {
201                        compiledChains.put(cacheKey, chain);
202                    }
203                }
204            } else {
205                chain = CompiledChainImpl.buildChain(inputType, toParams(operationType.getId()));
206            }
207            Object ret = chain.invoke(ctx);
208            tracer.onOutput(ret);
209            if (ctx.getCoreSession() != null && ctx.isCommit()) {
210                // auto save session if any.
211                ctx.getCoreSession().save();
212            }
213            // Log at the end of the main chain execution.
214            if (mainChain && tracer.getTrace() != null && tracerFactory.getRecordingState()) {
215                log.info(tracer.getFormattedText());
216            }
217            return ret;
218        } catch (OperationException oe) {
219            // Record trace
220            tracer.onError(oe);
221            // Handle exception chain and rollback
222            String operationTypeId = operationType.getId();
223            if (hasChainException(operationTypeId)) {
224                // Rollback is handled by chain exception
225                return run(ctx, getChainExceptionToRun(ctx, operationTypeId, oe));
226            } else if (oe.isRollback()) {
227                ctx.setRollback();
228            }
229            // Handle exception
230            if (mainChain) {
231                throw new TraceException(tracer, oe);
232            } else {
233                throw new TraceException(oe);
234            }
235        } finally {
236            ctx.dispose();
237        }
238    }
239
240    /**
241     * @since 5.7.3 Fetch the right chain id to run when catching exception for given chain failure.
242     */
243    protected String getChainExceptionToRun(OperationContext ctx, String operationTypeId, OperationException oe)
244            throws OperationException {
245        // Inject exception name into the context
246        //since 6.0-HF05 should use exceptionName and exceptionObject on the context instead of Exception
247        ctx.put("Exception", oe.getClass().getSimpleName());
248        ctx.put("exceptionName", oe.getClass().getSimpleName());
249        ctx.put("exceptionObject", oe);
250        
251        ChainException chainException = getChainException(operationTypeId);
252        CatchChainException catchChainException = new CatchChainException();
253        for (CatchChainException catchChainExceptionItem : chainException.getCatchChainExceptions()) {
254            // Check first a possible filter value
255            if (catchChainExceptionItem.hasFilter()) {
256                AutomationFilter filter = getAutomationFilter(catchChainExceptionItem.getFilterId());
257                try {
258                    String filterValue = (String) filter.getValue().eval(ctx);
259                    // Check if priority for this chain exception is higher
260                    if (Boolean.parseBoolean(filterValue)) {
261                        catchChainException = getCatchChainExceptionByPriority(catchChainException,
262                                catchChainExceptionItem);
263                    }
264                } catch (RuntimeException e) { // TODO more specific exceptions?
265                    throw new OperationException("Cannot evaluate Automation Filter " + filter.getId()
266                            + " mvel expression.", e);
267                }
268            } else {
269                // Check if priority for this chain exception is higher
270                catchChainException = getCatchChainExceptionByPriority(catchChainException, catchChainExceptionItem);
271            }
272        }
273        String chainId = catchChainException.getChainId();
274        if (chainId.isEmpty()) {
275            throw new OperationException(
276                    "No chain exception has been selected to be run. You should verify Automation filters applied.");
277        }
278        if (catchChainException.getRollBack()) {
279            ctx.setRollback();
280        }
281        return catchChainException.getChainId();
282    }
283
284    /**
285     * @since 5.7.3
286     */
287    protected CatchChainException getCatchChainExceptionByPriority(CatchChainException catchChainException,
288            CatchChainException catchChainExceptionItem) {
289        return catchChainException.getPriority() <= catchChainExceptionItem.getPriority() ? catchChainExceptionItem
290                : catchChainException;
291    }
292
293    public static OperationParameters[] toParams(String... ids) {
294        OperationParameters[] operationParameters = new OperationParameters[ids.length];
295        for (int i = 0; i < ids.length; ++i) {
296            operationParameters[i] = new OperationParameters(ids[i]);
297        }
298        return operationParameters;
299    }
300
301    @Override
302    public synchronized void putOperationChain(OperationChain chain) throws OperationException {
303        putOperationChain(chain, false);
304    }
305
306    @Override
307    public synchronized void putOperationChain(OperationChain chain, boolean replace) throws OperationException {
308        OperationType docChainType = new ChainTypeImpl(this, chain);
309        this.putOperation(docChainType, replace);
310    }
311
312    @Override
313    public synchronized void removeOperationChain(String id) {
314        OperationChain chain = new OperationChain(id);
315        OperationType docChainType = new ChainTypeImpl(this, chain);
316        operations.removeContribution(docChainType);
317    }
318
319    @Override
320    public OperationChain getOperationChain(String id) throws OperationNotFoundException {
321        ChainTypeImpl chain = (ChainTypeImpl) getOperation(id);
322        return chain.getChain();
323    }
324
325    @Override
326    public List<OperationChain> getOperationChains() {
327        List<ChainTypeImpl> chainsType = new ArrayList<ChainTypeImpl>();
328        List<OperationChain> chains = new ArrayList<OperationChain>();
329        for (OperationType operationType : operations.lookup().values()) {
330            if (operationType instanceof ChainTypeImpl) {
331                chainsType.add((ChainTypeImpl) operationType);
332            }
333        }
334        for (ChainTypeImpl chainType : chainsType) {
335            chains.add(chainType.getChain());
336        }
337        return chains;
338    }
339
340    @Override
341    public synchronized void flushCompiledChains() {
342        compiledChains.clear();
343    }
344
345    @Override
346    public void putOperation(Class<?> type) throws OperationException {
347        OperationTypeImpl op = new OperationTypeImpl(this, type);
348        putOperation(op, false);
349    }
350
351    @Override
352    public void putOperation(Class<?> type, boolean replace) throws OperationException {
353        putOperation(type, replace, null);
354    }
355
356    @Override
357    public void putOperation(Class<?> type, boolean replace, String contributingComponent) throws OperationException {
358        OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent);
359        putOperation(op, replace);
360    }
361
362    @Override
363    public void putOperation(Class<?> type, boolean replace, String contributingComponent,
364            List<WidgetDefinition> widgetDefinitionList) throws OperationException {
365        OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent, widgetDefinitionList);
366        putOperation(op, replace);
367    }
368
369    @Override
370    public synchronized void putOperation(OperationType op, boolean replace) throws OperationException {
371        operations.addContribution(op, replace);
372    }
373
374    @Override
375    public synchronized void removeOperation(Class<?> key) {
376        OperationType type = operations.getOperationType(key);
377        if (type == null) {
378            log.warn("Cannot remove operation, no such operation " + key);
379            return;
380        }
381        removeOperation(type);
382    }
383
384    @Override
385    public synchronized void removeOperation(OperationType type) {
386        operations.removeContribution(type);
387    }
388
389    @Override
390    public OperationType[] getOperations() {
391        HashSet<OperationType> values = new HashSet<>(operations.lookup().values());
392        return values.toArray(new OperationType[values.size()]);
393    }
394
395    @Override
396    public OperationType getOperation(String id) throws OperationNotFoundException {
397        OperationType op = operations.lookup().get(id);
398        if (op == null) {
399            throw new OperationNotFoundException("No operation was bound on ID: " + id);
400        }
401        return op;
402    }
403
404    /**
405     * @since 5.7.2
406     * @param id operation ID.
407     * @return true if operation registry contains the given operation.
408     */
409    @Override
410    public boolean hasOperation(String id) {
411        OperationType op = operations.lookup().get(id);
412        if (op == null) {
413            return false;
414        }
415        return true;
416    }
417
418    @Override
419    public CompiledChain compileChain(Class<?> inputType, OperationChain chain) throws OperationException {
420        List<OperationParameters> ops = chain.getOperations();
421        return compileChain(inputType, ops.toArray(new OperationParameters[ops.size()]));
422    }
423
424    @Override
425    public CompiledChain compileChain(Class<?> inputType, OperationParameters... operations) throws OperationException {
426        return CompiledChainImpl.buildChain(this, inputType == null ? Void.TYPE : inputType, operations);
427    }
428
429    @Override
430    public void putTypeAdapter(Class<?> accept, Class<?> produce, TypeAdapter adapter) {
431        adapters.put(new TypeAdapterKey(accept, produce), adapter);
432    }
433
434    @Override
435    public void removeTypeAdapter(Class<?> accept, Class<?> produce) {
436        adapters.remove(new TypeAdapterKey(accept, produce));
437    }
438
439    @Override
440    public TypeAdapter getTypeAdapter(Class<?> accept, Class<?> produce) {
441        return adapters.get(new TypeAdapterKey(accept, produce));
442    }
443
444    @Override
445    public boolean isTypeAdaptable(Class<?> typeToAdapt, Class<?> targetType) {
446        return getTypeAdapter(typeToAdapt, targetType) != null;
447    }
448
449    @Override
450    @SuppressWarnings("unchecked")
451    public <T> T getAdaptedValue(OperationContext ctx, Object toAdapt, Class<?> targetType) throws OperationException {
452        if (toAdapt == null) {
453            return null;
454        }
455        // handle primitive types
456        Class<?> toAdaptClass = toAdapt.getClass();
457        if (targetType.isPrimitive()) {
458            targetType = getTypeForPrimitive(targetType);
459            if (targetType.isAssignableFrom(toAdaptClass)) {
460                return (T) toAdapt;
461            }
462        }
463        if (targetType.isArray() && toAdapt instanceof List) {
464            return (T) Iterables.toArray((Iterable) toAdapt, targetType.getComponentType());
465        }
466        TypeAdapter adapter = getTypeAdapter(toAdaptClass, targetType);
467        if (adapter == null) {
468            if (toAdapt instanceof JsonNode) {
469                // fall-back to generic jackson adapter
470                ObjectMapper mapper = new ObjectMapper();
471                return (T) mapper.convertValue(toAdapt, targetType);
472            }
473            throw new OperationException("No type adapter found for input: " + toAdapt.getClass() + " and output "
474                    + targetType);
475        }
476        return (T) adapter.getAdaptedValue(ctx, toAdapt);
477    }
478
479    @Override
480    public List<OperationDocumentation> getDocumentation() throws OperationException {
481        List<OperationDocumentation> result = new ArrayList<OperationDocumentation>();
482        HashSet<OperationType> ops = new HashSet<>(operations.lookup().values());
483        OperationCompoundExceptionBuilder errorBuilder = new OperationCompoundExceptionBuilder();
484        for (OperationType ot : ops.toArray(new OperationType[ops.size()])) {
485            try {
486                result.add(ot.getDocumentation());
487            } catch (OperationNotFoundException e) {
488                errorBuilder.add(e);
489            }
490        }
491        errorBuilder.throwOnError();
492        Collections.sort(result);
493        return result;
494    }
495
496    public static Class<?> getTypeForPrimitive(Class<?> primitiveType) {
497        if (primitiveType == Boolean.TYPE) {
498            return Boolean.class;
499        } else if (primitiveType == Integer.TYPE) {
500            return Integer.class;
501        } else if (primitiveType == Long.TYPE) {
502            return Long.class;
503        } else if (primitiveType == Float.TYPE) {
504            return Float.class;
505        } else if (primitiveType == Double.TYPE) {
506            return Double.class;
507        } else if (primitiveType == Character.TYPE) {
508            return Character.class;
509        } else if (primitiveType == Byte.TYPE) {
510            return Byte.class;
511        } else if (primitiveType == Short.TYPE) {
512            return Short.class;
513        }
514        return primitiveType;
515    }
516
517    /**
518     * @since 5.7.3
519     */
520    @Override
521    public void putChainException(ChainException exceptionChain) {
522        chainExceptionRegistry.addContribution(exceptionChain);
523    }
524
525    /**
526     * @since 5.7.3
527     */
528    @Override
529    public void removeExceptionChain(ChainException exceptionChain) {
530        chainExceptionRegistry.removeContribution(exceptionChain);
531    }
532
533    /**
534     * @since 5.7.3
535     */
536    @Override
537    public ChainException[] getChainExceptions() {
538        Collection<ChainException> chainExceptions = chainExceptionRegistry.lookup().values();
539        return chainExceptions.toArray(new ChainException[chainExceptions.size()]);
540    }
541
542    /**
543     * @since 5.7.3
544     */
545    @Override
546    public ChainException getChainException(String onChainId) {
547        return chainExceptionRegistry.getChainException(onChainId);
548    }
549
550    /**
551     * @since 5.7.3
552     */
553    @Override
554    public boolean hasChainException(String onChainId) {
555        return chainExceptionRegistry.getChainException(onChainId) != null;
556    }
557
558    /**
559     * @since 5.7.3
560     */
561    @Override
562    public void putAutomationFilter(AutomationFilter automationFilter) {
563        automationFilterRegistry.addContribution(automationFilter);
564    }
565
566    /**
567     * @since 5.7.3
568     */
569    @Override
570    public void removeAutomationFilter(AutomationFilter automationFilter) {
571        automationFilterRegistry.removeContribution(automationFilter);
572    }
573
574    /**
575     * @since 5.7.3
576     */
577    @Override
578    public AutomationFilter getAutomationFilter(String id) {
579        return automationFilterRegistry.getAutomationFilter(id);
580    }
581
582    /**
583     * @since 5.7.3
584     */
585    @Override
586    public AutomationFilter[] getAutomationFilters() {
587        Collection<AutomationFilter> automationFilters = automationFilterRegistry.lookup().values();
588        return automationFilters.toArray(new AutomationFilter[automationFilters.size()]);
589    }
590
591    /**
592     * @since 5.8 - Composite key to handle several operations with same id and different input types.
593     */
594    protected static class CacheKey {
595
596        String operationId;
597
598        String inputType;
599
600        public CacheKey(String operationId, String inputType) {
601            this.operationId = operationId;
602            this.inputType = inputType;
603        }
604
605        @Override
606        public boolean equals(Object o) {
607            if (this == o) {
608                return true;
609            }
610            if (o == null || getClass() != o.getClass()) {
611                return false;
612            }
613
614            CacheKey cacheKey = (CacheKey) o;
615
616            if (inputType != null ? !inputType.equals(cacheKey.inputType) : cacheKey.inputType != null) {
617                return false;
618            }
619            if (operationId != null ? !operationId.equals(cacheKey.operationId) : cacheKey.operationId != null) {
620                return false;
621            }
622
623            return true;
624        }
625
626        @Override
627        public int hashCode() {
628            int result = operationId != null ? operationId.hashCode() : 0;
629            result = 31 * result + (inputType != null ? inputType.hashCode() : 0);
630            return result;
631        }
632    }
633}