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