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