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