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