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.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031
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.OperationChain;
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.TypeAdapter;
049import org.nuxeo.ecm.automation.core.exception.CatchChainException;
050import org.nuxeo.ecm.automation.core.exception.ChainExceptionRegistry;
051import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition;
052import org.nuxeo.runtime.api.Framework;
053import org.nuxeo.runtime.services.config.ConfigurationService;
054import org.nuxeo.runtime.transaction.TransactionHelper;
055
056import com.google.common.collect.Iterables;
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    public static final String EXPORT_ALIASES_CONFIGURATION_PARAM = "nuxeo.automation.export.aliases";
068
069    protected final OperationTypeRegistry operations;
070
071    protected final ChainExceptionRegistry chainExceptionRegistry;
072
073    protected final AutomationFilterRegistry automationFilterRegistry;
074
075    protected final OperationChainCompiler compiler = new OperationChainCompiler(this);
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        return run(ctx, getOperationChain(operationId));
092    }
093
094    @Override
095    public Object run(OperationContext ctx, String operationId, Map<String, ?> args) throws OperationException {
096        OperationType op = operations.lookup().get(operationId);
097        if (op == null) {
098            throw new IllegalArgumentException("No such operation " + operationId);
099        }
100        if (args == null) {
101            log.warn("null operation parameters given for " + operationId, new Throwable("stack trace"));
102            args = Collections.emptyMap();
103        }
104        ctx.push(args);
105        try {
106            return run(ctx, getOperationChain(operationId));
107        } finally {
108            ctx.pop(args);
109        }
110    }
111
112    @Override
113    public Object run(OperationContext ctx, OperationChain chain) throws OperationException {
114        Object input = ctx.getInput();
115        Class<?> inputType = input == null ? Void.TYPE : input.getClass();
116        CompiledChain compiled = compileChain(inputType, chain);
117        boolean completedAbruptly = true;
118        try {
119            Object result  = compiled.invoke(ctx);
120            completedAbruptly = false;
121            return result ;
122        } catch (OperationException cause) {
123            completedAbruptly = false;
124            if (hasChainException(chain.getId())) {
125                return run(ctx, getChainExceptionToRun(ctx, chain.getId(), cause));
126            } else if (cause.isRollback()) {
127                ctx.setRollback();
128            }
129            throw cause;
130        } finally {
131            if (completedAbruptly) {
132                ctx.setRollback();
133            }
134        }
135    }
136
137    @Override
138    public Object runInNewTx(OperationContext ctx, String chainId, Map<String, ?> chainParameters, Integer timeout,
139            boolean rollbackGlobalOnError) throws OperationException {
140        Object result = null;
141        // if the current transaction was already marked for rollback,
142        // do nothing
143        if (TransactionHelper.isTransactionMarkedRollback()) {
144            return null;
145        }
146        // commit the current transaction
147        TransactionHelper.commitOrRollbackTransaction();
148
149        int to = timeout == null ? 0 : timeout;
150
151        TransactionHelper.startTransaction(to);
152        boolean ok = false;
153
154        try {
155            result = run(ctx, chainId, chainParameters);
156            ok = true;
157        } catch (OperationException e) {
158            if (rollbackGlobalOnError) {
159                throw e;
160            } else {
161                // just log, no rethrow
162                log.error("Error while executing operation " + chainId, e);
163            }
164        } finally {
165            if (!ok) {
166                // will be logged by Automation framework
167                TransactionHelper.setTransactionRollbackOnly();
168            }
169            TransactionHelper.commitOrRollbackTransaction();
170            // caller expects a transaction to be started
171            TransactionHelper.startTransaction();
172        }
173        return result;
174    }
175
176    /**
177     * @since 5.7.3 Fetch the right chain id to run when catching exception for given chain failure.
178     */
179    protected String getChainExceptionToRun(OperationContext ctx, String operationTypeId, OperationException oe)
180            throws OperationException {
181        // Inject exception name into the context
182        // since 6.0-HF05 should use exceptionName and exceptionObject on the context instead of Exception
183        ctx.put("Exception", oe.getClass().getSimpleName());
184        ctx.put("exceptionName", oe.getClass().getSimpleName());
185        ctx.put("exceptionObject", oe);
186
187        ChainException chainException = getChainException(operationTypeId);
188        CatchChainException catchChainException = new CatchChainException();
189        for (CatchChainException catchChainExceptionItem : chainException.getCatchChainExceptions()) {
190            // Check first a possible filter value
191            if (catchChainExceptionItem.hasFilter()) {
192                AutomationFilter filter = getAutomationFilter(catchChainExceptionItem.getFilterId());
193                try {
194                    String filterValue = (String) filter.getValue().eval(ctx);
195                    // Check if priority for this chain exception is higher
196                    if (Boolean.parseBoolean(filterValue)) {
197                        catchChainException = getCatchChainExceptionByPriority(catchChainException,
198                                catchChainExceptionItem);
199                    }
200                } catch (RuntimeException e) { // TODO more specific exceptions?
201                    throw new OperationException(
202                            "Cannot evaluate Automation Filter " + filter.getId() + " mvel expression.", e);
203                }
204            } else {
205                // Check if priority for this chain exception is higher
206                catchChainException = getCatchChainExceptionByPriority(catchChainException, catchChainExceptionItem);
207            }
208        }
209        String chainId = catchChainException.getChainId();
210        if (chainId.isEmpty()) {
211            throw new OperationException(
212                    "No chain exception has been selected to be run. You should verify Automation filters applied.");
213        }
214        if (catchChainException.getRollBack()) {
215            ctx.setRollback();
216        }
217        return catchChainException.getChainId();
218    }
219
220    /**
221     * @since 5.7.3
222     */
223    protected CatchChainException getCatchChainExceptionByPriority(CatchChainException catchChainException,
224            CatchChainException catchChainExceptionItem) {
225        return catchChainException.getPriority() <= catchChainExceptionItem.getPriority() ? catchChainExceptionItem
226                : catchChainException;
227    }
228
229    public static OperationParameters[] toParams(String... ids) {
230        OperationParameters[] operationParameters = new OperationParameters[ids.length];
231        for (int i = 0; i < ids.length; ++i) {
232            operationParameters[i] = new OperationParameters(ids[i]);
233        }
234        return operationParameters;
235    }
236
237    @Override
238    public void putOperationChain(OperationChain chain) throws OperationException {
239        putOperationChain(chain, false);
240    }
241
242    final Map<String, OperationType> typeofChains = new HashMap<>();
243
244    @Override
245    public void putOperationChain(OperationChain chain, boolean replace) throws OperationException {
246        final OperationType typeof = OperationType.typeof(chain, replace);
247        this.putOperation(typeof, replace);
248        typeofChains.put(chain.getId(), typeof);
249    }
250
251    @Override
252    public void removeOperationChain(String id) {
253        OperationType typeof = operations.lookup().get(id);
254        if (typeof == null) {
255            throw new IllegalArgumentException("no such chain " + id);
256        }
257        this.removeOperation(typeof);
258    }
259
260    @Override
261    public OperationChain getOperationChain(String id) throws OperationNotFoundException {
262        OperationType type = getOperation(id);
263        if (type instanceof ChainTypeImpl) {
264            return ((ChainTypeImpl) type).chain;
265        }
266        OperationChain chain = new OperationChain(id);
267        chain.add(id);
268        return chain;
269    }
270
271    @Override
272    public List<OperationChain> getOperationChains() {
273        List<ChainTypeImpl> chainsType = new ArrayList<ChainTypeImpl>();
274        List<OperationChain> chains = new ArrayList<OperationChain>();
275        for (OperationType operationType : operations.lookup().values()) {
276            if (operationType instanceof ChainTypeImpl) {
277                chainsType.add((ChainTypeImpl) operationType);
278            }
279        }
280        for (ChainTypeImpl chainType : chainsType) {
281            chains.add(chainType.getChain());
282        }
283        return chains;
284    }
285
286    @Override
287    public synchronized void flushCompiledChains() {
288        compiler.cache.clear();
289    }
290
291    @Override
292    public void putOperation(Class<?> type) throws OperationException {
293        OperationTypeImpl op = new OperationTypeImpl(this, type);
294        putOperation(op, false);
295    }
296
297    @Override
298    public void putOperation(Class<?> type, boolean replace) throws OperationException {
299        putOperation(type, replace, null);
300    }
301
302    @Override
303    public void putOperation(Class<?> type, boolean replace, String contributingComponent) throws OperationException {
304        OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent);
305        putOperation(op, replace);
306    }
307
308    @Override
309    public void putOperation(Class<?> type, boolean replace, String contributingComponent,
310            List<WidgetDefinition> widgetDefinitionList) throws OperationException {
311        OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent, widgetDefinitionList);
312        putOperation(op, replace);
313    }
314
315    @Override
316    public void putOperation(OperationType op, boolean replace) throws OperationException {
317        operations.addContribution(op, replace);
318    }
319
320    @Override
321    public void removeOperation(Class<?> key) {
322        OperationType type = operations.getOperationType(key);
323        if (type == null) {
324            log.warn("Cannot remove operation, no such operation " + key);
325            return;
326        }
327        removeOperation(type);
328    }
329
330    @Override
331    public void removeOperation(OperationType type) {
332        operations.removeContribution(type);
333    }
334
335    @Override
336    public OperationType[] getOperations() {
337        HashSet<OperationType> values = new HashSet<>(operations.lookup().values());
338        return values.toArray(new OperationType[values.size()]);
339    }
340
341    @Override
342    public OperationType getOperation(String id) throws OperationNotFoundException {
343        OperationType op = operations.lookup().get(id);
344        if (op == null) {
345            throw new OperationNotFoundException("No operation was bound on ID: " + id);
346        }
347        return op;
348    }
349
350    /**
351     * @since 5.7.2
352     * @param id
353     *            operation ID.
354     * @return true if operation registry contains the given operation.
355     */
356    @Override
357    public boolean hasOperation(String id) {
358        OperationType op = operations.lookup().get(id);
359        if (op == null) {
360            return false;
361        }
362        return true;
363    }
364
365    @Override
366    public CompiledChain compileChain(Class<?> inputType, OperationParameters... ops) throws OperationException {
367        return compileChain(inputType, new OperationChain("", Arrays.asList(ops)));
368    }
369
370    @Override
371    public CompiledChain compileChain(Class<?> inputType, OperationChain chain) throws OperationException {
372        return compiler.compile(ChainTypeImpl.typeof(chain, false), inputType);
373    }
374
375    @Override
376    public void putTypeAdapter(Class<?> accept, Class<?> produce, TypeAdapter adapter) {
377        adapters.put(new TypeAdapterKey(accept, produce), adapter);
378    }
379
380    @Override
381    public void removeTypeAdapter(Class<?> accept, Class<?> produce) {
382        adapters.remove(new TypeAdapterKey(accept, produce));
383    }
384
385    @Override
386    public TypeAdapter getTypeAdapter(Class<?> accept, Class<?> produce) {
387        return adapters.get(new TypeAdapterKey(accept, produce));
388    }
389
390    @Override
391    public boolean isTypeAdaptable(Class<?> typeToAdapt, Class<?> targetType) {
392        return getTypeAdapter(typeToAdapt, targetType) != null;
393    }
394
395    @Override
396    @SuppressWarnings("unchecked")
397    public <T> T getAdaptedValue(OperationContext ctx, Object toAdapt, Class<?> targetType) throws OperationException {
398        if (targetType.isAssignableFrom(Void.class)) {
399            return null;
400        }
401        if (OperationContext.class.isAssignableFrom(targetType)) {
402            return (T) ctx;
403        }
404        // handle primitive types
405        Class<?> toAdaptClass = toAdapt == null ? Void.class : toAdapt.getClass();
406        if (targetType.isPrimitive()) {
407            targetType = getTypeForPrimitive(targetType);
408            if (targetType.isAssignableFrom(toAdaptClass)) {
409                return (T) toAdapt;
410            }
411        }
412        if (targetType.isArray() && toAdapt instanceof List) {
413            @SuppressWarnings("rawtypes")
414            final Iterable iterable = (Iterable) toAdapt;
415            return (T) Iterables.toArray(iterable, targetType.getComponentType());
416        }
417        TypeAdapter adapter = getTypeAdapter(toAdaptClass, targetType);
418        if (adapter == null) {
419            if (toAdapt == null) {
420                return null;
421            }
422            if (toAdapt instanceof JsonNode) {
423                // fall-back to generic jackson adapter
424                ObjectMapper mapper = new ObjectMapper();
425                return (T) mapper.convertValue(toAdapt, targetType);
426            }
427            if (targetType.isAssignableFrom(OperationContext.class)) {
428                return (T) ctx;
429            }
430            throw new OperationException(
431                    "No type adapter found for input: " + toAdaptClass + " and output " + targetType);
432        }
433        return (T) adapter.getAdaptedValue(ctx, toAdapt);
434    }
435
436    @Override
437    public List<OperationDocumentation> getDocumentation() throws OperationException {
438        List<OperationDocumentation> result = new ArrayList<OperationDocumentation>();
439        HashSet<OperationType> ops = new HashSet<>(operations.lookup().values());
440        ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
441        boolean exportAliases = configurationService.isBooleanPropertyTrue(EXPORT_ALIASES_CONFIGURATION_PARAM);
442        for (OperationType ot : ops.toArray(new OperationType[ops.size()])) {
443            try {
444                OperationDocumentation documentation = ot.getDocumentation();
445                result.add(documentation);
446
447                // we may want to add an operation documentation for each alias to be backward compatible with old
448                // automation clients
449                String[] aliases = ot.getAliases();
450                if (exportAliases && aliases != null && aliases.length > 0) {
451                    for (String alias : aliases) {
452                        result.add(OperationDocumentation.copyForAlias(documentation, alias));
453                    }
454                }
455            } catch (OperationNotFoundException e) {
456                // do nothing
457            }
458        }
459        Collections.sort(result);
460        return result;
461    }
462
463    public static Class<?> getTypeForPrimitive(Class<?> primitiveType) {
464        if (primitiveType == Boolean.TYPE) {
465            return Boolean.class;
466        } else if (primitiveType == Integer.TYPE) {
467            return Integer.class;
468        } else if (primitiveType == Long.TYPE) {
469            return Long.class;
470        } else if (primitiveType == Float.TYPE) {
471            return Float.class;
472        } else if (primitiveType == Double.TYPE) {
473            return Double.class;
474        } else if (primitiveType == Character.TYPE) {
475            return Character.class;
476        } else if (primitiveType == Byte.TYPE) {
477            return Byte.class;
478        } else if (primitiveType == Short.TYPE) {
479            return Short.class;
480        }
481        return primitiveType;
482    }
483
484    /**
485     * @since 5.7.3
486     */
487    @Override
488    public void putChainException(ChainException exceptionChain) {
489        chainExceptionRegistry.addContribution(exceptionChain);
490    }
491
492    /**
493     * @since 5.7.3
494     */
495    @Override
496    public void removeExceptionChain(ChainException exceptionChain) {
497        chainExceptionRegistry.removeContribution(exceptionChain);
498    }
499
500    /**
501     * @since 5.7.3
502     */
503    @Override
504    public ChainException[] getChainExceptions() {
505        Collection<ChainException> chainExceptions = chainExceptionRegistry.lookup().values();
506        return chainExceptions.toArray(new ChainException[chainExceptions.size()]);
507    }
508
509    /**
510     * @since 5.7.3
511     */
512    @Override
513    public ChainException getChainException(String onChainId) {
514        return chainExceptionRegistry.getChainException(onChainId);
515    }
516
517    /**
518     * @since 5.7.3
519     */
520    @Override
521    public boolean hasChainException(String onChainId) {
522        return chainExceptionRegistry.getChainException(onChainId) != null;
523    }
524
525    /**
526     * @since 5.7.3
527     */
528    @Override
529    public void putAutomationFilter(AutomationFilter automationFilter) {
530        automationFilterRegistry.addContribution(automationFilter);
531    }
532
533    /**
534     * @since 5.7.3
535     */
536    @Override
537    public void removeAutomationFilter(AutomationFilter automationFilter) {
538        automationFilterRegistry.removeContribution(automationFilter);
539    }
540
541    /**
542     * @since 5.7.3
543     */
544    @Override
545    public AutomationFilter getAutomationFilter(String id) {
546        return automationFilterRegistry.getAutomationFilter(id);
547    }
548
549    /**
550     * @since 5.7.3
551     */
552    @Override
553    public AutomationFilter[] getAutomationFilters() {
554        Collection<AutomationFilter> automationFilters = automationFilterRegistry.lookup().values();
555        return automationFilters.toArray(new AutomationFilter[automationFilters.size()]);
556    }
557
558}