001/*
002 * Copyright (c) 2006-2013 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     vpasquier
011 *     slacoin
012 */
013package org.nuxeo.ecm.automation.core.impl;
014
015import java.net.URL;
016import java.util.ArrayList;
017import java.util.Arrays;
018import java.util.Calendar;
019import java.util.Collection;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023
024import org.nuxeo.ecm.automation.AutomationService;
025import org.nuxeo.ecm.automation.InvalidChainException;
026import org.nuxeo.ecm.automation.OperationChain;
027import org.nuxeo.ecm.automation.OperationContext;
028import org.nuxeo.ecm.automation.OperationDocumentation;
029import org.nuxeo.ecm.automation.OperationException;
030import org.nuxeo.ecm.automation.OperationNotFoundException;
031import org.nuxeo.ecm.automation.OperationParameters;
032import org.nuxeo.ecm.automation.OperationType;
033import org.nuxeo.ecm.automation.core.Constants;
034import org.nuxeo.ecm.automation.core.OperationChainContribution;
035import org.nuxeo.ecm.automation.core.util.BlobList;
036import org.nuxeo.ecm.core.api.Blob;
037import org.nuxeo.ecm.core.api.DocumentModel;
038import org.nuxeo.ecm.core.api.DocumentModelList;
039import org.nuxeo.ecm.core.api.DocumentRef;
040import org.nuxeo.ecm.core.api.DocumentRefList;
041
042/**
043 * @since 5.7.2 Operation Type Implementation for a chain
044 */
045public class ChainTypeImpl implements OperationType {
046
047    protected final OperationChain chain;
048
049    /**
050     * Chain/Operation Parameters.
051     */
052    protected Map<String, Object> chainParameters;
053
054    /**
055     * The service that registered the operation.
056     */
057    protected AutomationService service;
058
059    /**
060     * The operation ID - used for lookups.
061     */
062    protected String id;
063
064    /**
065     * The operation ID Aliases array.
066     *
067     * @since 7.1
068     */
069    protected final String[] aliases;
070
071    /**
072     * Chain/Operation Parameters.
073     */
074    protected OperationDocumentation.Param[] params;
075
076    /**
077     * Invocable methods.
078     */
079    protected InvokableMethod[] methods = new InvokableMethod[] { runMethod() };
080
081    /**
082     * The contribution fragment name.
083     */
084    protected String contributingComponent;
085
086    /**
087     * The operations listing.
088     */
089    protected OperationParameters[] operations;
090
091    /**
092     * The operation chain XMAP contribution
093     */
094    protected OperationChainContribution contribution;
095
096    /**
097     * An output of operation type
098     */
099    protected Class<?> outputChain;
100
101    /**
102     * A method of operation type
103     */
104    protected InvokableMethod method;
105
106    /**
107     * The input type of a chain/operation. If set, the following input types {"document", "documents", "blob", "blobs"}
108     * for all 'run method(s)' will handled. Other values will be adapted as java.lang.Object. If not set, Automation
109     * will set the input type(s) as the 'run methods(s)' parameter types (by introspection).
110     *
111     * @since 7.4
112     */
113    protected String inputType;
114
115    public ChainTypeImpl(AutomationService service, OperationChain chain) {
116        this.service = service;
117        operations = chain.getOperations().toArray(new OperationParameters[chain.getOperations().size()]);
118        id = chain.getId();
119        aliases = chain.getAliases();
120        chainParameters = chain.getChainParameters();
121        this.chain = chain;
122    }
123
124    public ChainTypeImpl(AutomationService service, OperationChain chain, OperationChainContribution contribution) {
125        this.service = service;
126        operations = chain.getOperations().toArray(new OperationParameters[chain.getOperations().size()]);
127        id = chain.getId();
128        aliases = chain.getAliases();
129        chainParameters = chain.getChainParameters();
130        this.contribution = contribution;
131        this.chain = chain;
132    }
133
134    public OperationChain getChain() {
135        return chain;
136    }
137
138    public Map<String, Object> getChainParameters() {
139        return chainParameters;
140    }
141
142    @Override
143    public Object newInstance(OperationContext ctx, Map<String, Object> args) throws OperationNotFoundException,
144            InvalidChainException {
145        Object input = ctx.getInput();
146        Class<?> inputType = input == null ? Void.TYPE : input.getClass();
147        CompiledChainImpl op = CompiledChainImpl.buildChain(service, inputType, operations);
148        op.context = ctx;
149        return op;
150    }
151
152    @Override
153    public AutomationService getService() {
154        return service;
155    }
156
157    @Override
158    public String getId() {
159        return id;
160    }
161
162    @Override
163    public String[] getAliases() {
164        return aliases;
165    }
166
167    @Override
168    public Class<?> getType() {
169        return CompiledChainImpl.class;
170    }
171
172    @Override
173    public String getInputType() {
174        return inputType;
175    }
176
177    @Override
178    public OperationDocumentation getDocumentation() throws OperationException {
179        OperationDocumentation doc = new OperationDocumentation(id);
180        doc.label = id;
181        doc.requires = contribution.getRequires();
182        doc.category = contribution.getCategory();
183        doc.setAliases(contribution.getAliases());
184        OperationChainContribution.Operation[] operations = contribution.getOps();
185        doc.operations = operations;
186        doc.since = contribution.getSince();
187        if (doc.requires.length() == 0) {
188            doc.requires = null;
189        }
190        if (doc.label.length() == 0) {
191            doc.label = doc.id;
192        }
193        doc.description = contribution.getDescription();
194        doc.params = contribution.getParams();
195        // load signature
196        if (operations.length != 0) {
197            // Fill signature with first inputs of the first operation and
198            // related outputs of last operation
199            // following the proper automation path
200            ArrayList<String> result = getSignature(operations);
201            doc.signature = result.toArray(new String[result.size()]);
202        } else {
203            doc.signature = new String[] { "void", "void" };
204        }
205        return doc;
206    }
207
208    /**
209     * @since 5.7.2
210     * @param operations operations listing that chain contains.
211     * @return the chain signature.
212     */
213    protected ArrayList<String> getSignature(OperationChainContribution.Operation[] operations)
214            throws OperationException {
215        ArrayList<String> result = new ArrayList<String>();
216        Collection<String> collectedSigs = new HashSet<String>();
217        OperationType operationType = service.getOperation(operations[0].getId());
218        for (InvokableMethod method : operationType.getMethods()) {
219            String inputChain = getParamDocumentationType(method.getInputType(), method.isIterable());
220            outputChain = method.getInputType();
221            String outputChain = getParamDocumentationType(getChainOutput(operations));
222            String sigKey = inputChain + ":" + outputChain;
223            if (!collectedSigs.contains(sigKey)) {
224                result.add(inputChain);
225                result.add(outputChain);
226                collectedSigs.add(sigKey);
227            }
228        }
229        return result;
230    }
231
232    /**
233     * @since 5.7.2
234     */
235    protected Class<?> getChainOutput(OperationChainContribution.Operation[] operations) throws OperationException {
236        for (OperationChainContribution.Operation operation : operations) {
237            OperationType operationType = service.getOperation(operation.getId());
238            if (operationType instanceof OperationTypeImpl) {
239                outputChain = getOperationOutput(outputChain, operationType);
240            } else {
241                outputChain = getChainOutput(operationType.getDocumentation().getOperations());
242            }
243        }
244        return outputChain;
245    }
246
247    /**
248     * @since 5.7.2
249     */
250    public Class<?> getOperationOutput(Class<?> input, OperationType operationType) {
251        InvokableMethod[] methods = operationType.getMethodsMatchingInput(input);
252        if (methods == null || methods.length == 0) {
253            return input;
254        }
255        // Choose the top priority method
256        InvokableMethod topMethod = getTopMethod(methods);
257        Class<?> nextInput = topMethod.getOutputType();
258        // If output is void, skip this method
259        if (nextInput == Void.TYPE) {
260            return input;
261        }
262        return nextInput;
263    }
264
265    /**
266     * @since 5.7.2 Define the top priority method to take into account for chain operations signature.
267     */
268    protected InvokableMethod getTopMethod(InvokableMethod[] methods) {
269        InvokableMethod topMethod = methods[0];
270        for (InvokableMethod method : methods) {
271            if (method.getPriority() > topMethod.getPriority()) {
272                topMethod = method;
273            }
274        }
275        return topMethod;
276    }
277
278    @Override
279    public String getContributingComponent() {
280        return contributingComponent;
281    }
282
283    @Override
284    public InvokableMethod[] getMethodsMatchingInput(Class<?> in) {
285        return methods;
286    }
287
288    protected InvokableMethod runMethod() {
289        try {
290            return new InvokableMethod(this, CompiledChainImpl.class.getMethod("run"));
291        } catch (NoSuchMethodException | SecurityException e) {
292            throw new UnsupportedOperationException("Cannot use reflection for run method", e);
293        }
294    }
295
296    /**
297     * @since 5.7.2
298     */
299    protected String getParamDocumentationType(Class<?> type) {
300        return getParamDocumentationType(type, false);
301    }
302
303    /**
304     * @since 5.7.2
305     */
306    protected String getParamDocumentationType(Class<?> type, boolean isIterable) {
307        String t;
308        if (DocumentModel.class.isAssignableFrom(type) || DocumentRef.class.isAssignableFrom(type)) {
309            t = isIterable ? Constants.T_DOCUMENTS : Constants.T_DOCUMENT;
310        } else if (DocumentModelList.class.isAssignableFrom(type) || DocumentRefList.class.isAssignableFrom(type)) {
311            t = Constants.T_DOCUMENTS;
312        } else if (BlobList.class.isAssignableFrom(type)) {
313            t = Constants.T_BLOBS;
314        } else if (Blob.class.isAssignableFrom(type)) {
315            t = isIterable ? Constants.T_BLOBS : Constants.T_BLOB;
316        } else if (URL.class.isAssignableFrom(type)) {
317            t = Constants.T_RESOURCE;
318        } else if (Calendar.class.isAssignableFrom(type)) {
319            t = Constants.T_DATE;
320        } else {
321            t = type.getSimpleName().toLowerCase();
322        }
323        return t;
324    }
325
326    @Override
327    public String toString() {
328        return "ChainTypeImpl [id=" + id + "]";
329    }
330
331    public OperationChainContribution getContribution() {
332        return contribution;
333    }
334
335    /**
336     * @since 5.7.2
337     */
338    @Override
339    public List<InvokableMethod> getMethods() {
340        return Arrays.asList(methods);
341    }
342}