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