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