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