001/*
002 * Copyright (c) 2006-2011 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 *     bstefanescu
011 */
012package org.nuxeo.ecm.automation.core.impl;
013
014import java.lang.reflect.Field;
015import java.lang.reflect.Method;
016import java.net.URL;
017import java.util.ArrayList;
018import java.util.Calendar;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026
027import org.nuxeo.ecm.automation.AutomationService;
028import org.nuxeo.ecm.automation.OperationContext;
029import org.nuxeo.ecm.automation.OperationDocumentation;
030import org.nuxeo.ecm.automation.OperationException;
031import org.nuxeo.ecm.automation.OperationType;
032import org.nuxeo.ecm.automation.OutputCollector;
033import org.nuxeo.ecm.automation.core.Constants;
034import org.nuxeo.ecm.automation.core.annotations.Context;
035import org.nuxeo.ecm.automation.core.annotations.Operation;
036import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
037import org.nuxeo.ecm.automation.core.annotations.Param;
038import org.nuxeo.ecm.automation.core.scripting.Expression;
039import org.nuxeo.ecm.automation.core.util.BlobList;
040import org.nuxeo.ecm.core.api.Blob;
041import org.nuxeo.ecm.core.api.DocumentModel;
042import org.nuxeo.ecm.core.api.DocumentModelList;
043import org.nuxeo.ecm.core.api.DocumentRef;
044import org.nuxeo.ecm.core.api.DocumentRefList;
045import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition;
046
047/**
048 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
049 * @author <a href="mailto:grenard@nuxeo.com">Guillaume Renard</a>
050 */
051public class OperationTypeImpl implements OperationType {
052
053    /**
054     * The service that registered the operation
055     */
056    protected AutomationService service;
057
058    /**
059     * The operation ID - used for lookups.
060     */
061    protected String id;
062
063    /**
064     * The operation ID Aliases array.
065     *
066     * @since 7.1
067     */
068    protected String[] aliases;
069
070    /**
071     * The operation type
072     */
073    protected Class<?> type;
074
075    /**
076     * Injectable parameters. a map between the parameter name and the Field object
077     */
078    protected Map<String, Field> params;
079
080    /**
081     * Invocable methods
082     */
083    protected List<InvokableMethod> methods;
084
085    /**
086     * Fields that should be injected from context
087     */
088    protected List<Field> injectableFields;
089
090    /**
091     * The input type of a chain/operation. If set, the following input types {"document", "documents", "blob", "blobs"}
092     * for all 'run method(s)' will handled. Other values will be adapted as java.lang.Object. If not set, Automation
093     * will set the input type(s) as the 'run methods(s)' parameter types (by introspection).
094     *
095     * @since 7.4
096     */
097    protected String inputType;
098
099    protected String contributingComponent;
100
101    protected List<WidgetDefinition> widgetDefinitionList;
102
103    public OperationTypeImpl(AutomationService service, Class<?> type) {
104        this(service, type, null);
105    }
106
107    public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent) {
108        this(service, type, contributingComponent, null);
109    }
110
111    /**
112     * @since 5.9.5
113     */
114    public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent,
115            List<WidgetDefinition> widgetDefinitionList) {
116        Operation anno = type.getAnnotation(Operation.class);
117        if (anno == null) {
118            throw new IllegalArgumentException("Invalid operation class: " + type
119                    + ". No @Operation annotation found on class.");
120        }
121        this.service = service;
122        this.type = type;
123        this.widgetDefinitionList = widgetDefinitionList;
124        this.contributingComponent = contributingComponent;
125        id = anno.id();
126        if (id.length() == 0) {
127            id = type.getName();
128        }
129        aliases = anno.aliases();
130        params = new HashMap<String, Field>();
131        methods = new ArrayList<InvokableMethod>();
132        injectableFields = new ArrayList<Field>();
133        initMethods();
134        initFields();
135    }
136
137    public OperationTypeImpl() {
138    }
139
140    static class Match implements Comparable<Match> {
141        protected InvokableMethod method;
142
143        int priority;
144
145        Match(InvokableMethod method, int priority) {
146            this.method = method;
147            this.priority = priority;
148        }
149
150        @Override
151        public int compareTo(Match o) {
152            return o.priority - priority;
153        }
154
155        @Override
156        public String toString() {
157            return "Match(" + method + ", " + priority + ")";
158        }
159    }
160
161    @Override
162    public AutomationService getService() {
163        return service;
164    }
165
166    @Override
167    public String getId() {
168        return id;
169    }
170
171    @Override
172    public String[] getAliases() {
173        return aliases;
174    }
175
176    @Override
177    public Class<?> getType() {
178        return type;
179    }
180
181    @Override
182    public String getInputType() {
183        return inputType;
184    }
185
186    protected void initMethods() {
187        for (Method method : type.getMethods()) {
188            OperationMethod anno = method.getAnnotation(OperationMethod.class);
189            if (anno == null) { // skip method
190                continue;
191            }
192            // register regular method
193            InvokableMethod im = new InvokableMethod(this, method, anno);
194            methods.add(im);
195            // check for iterable input support
196            if (anno.collector() != OutputCollector.class) {
197                // an iterable method - register it
198                im = new InvokableIteratorMethod(this, method, anno);
199                methods.add(im);
200            }
201        }
202        // method order depends on the JDK, make it deterministic
203        Collections.sort(methods);
204    }
205
206    protected void initFields() {
207        for (Field field : type.getDeclaredFields()) {
208            Param param = field.getAnnotation(Param.class);
209            if (param != null) {
210                field.setAccessible(true);
211                params.put(param.name(), field);
212            } else if (field.isAnnotationPresent(Context.class)) {
213                field.setAccessible(true);
214                injectableFields.add(field);
215            }
216        }
217    }
218
219    @Override
220    public Object newInstance(OperationContext ctx, Map<String, Object> args) throws OperationException {
221        Object obj;
222        try {
223            obj = type.newInstance();
224        } catch (ReflectiveOperationException e) {
225            throw new OperationException(e);
226        }
227        inject(ctx, args, obj);
228        return obj;
229    }
230
231    /**
232     * @since 5.9.2
233     */
234    protected Object resolveObject(final OperationContext ctx, final String key, Map<String, Object> args) {
235        Object obj = args.get(key);
236        if (obj instanceof Expression) {
237            obj = ((Expression) obj).eval(ctx);
238        }
239        // Trying to fallback on Chain Parameters sub context if cannot
240        // find it
241        if (obj == null) {
242            if (ctx.containsKey(Constants.VAR_RUNTIME_CHAIN)) {
243                obj = ((Map) ctx.get(Constants.VAR_RUNTIME_CHAIN)).get(key);
244            }
245        }
246        return obj;
247    }
248
249    public void inject(OperationContext ctx, Map<String, Object> args, Object target) throws OperationException {
250        for (Map.Entry<String, Field> entry : params.entrySet()) {
251            Object obj = resolveObject(ctx, entry.getKey(), args);
252            if (obj == null) {
253                // We did not resolve object according to its param name, let's
254                // check with potential alias
255                String[] aliases = entry.getValue().getAnnotation(Param.class).alias();
256                if (aliases != null) {
257                    for (String alias : entry.getValue().getAnnotation(Param.class).alias()) {
258                        obj = resolveObject(ctx, alias, args);
259                        if (obj != null) {
260                            break;
261                        }
262                    }
263                }
264            }
265            if (obj == null) {
266                if (entry.getValue().getAnnotation(Param.class).required()) {
267                    throw new OperationException("Failed to inject parameter '" + entry.getKey()
268                            + "'. Seems it is missing from the context. Operation: " + getId());
269                } // else do nothing
270            } else {
271                Field field = entry.getValue();
272                Class<?> cl = obj.getClass();
273                if (!field.getType().isAssignableFrom(cl)) {
274                    // try to adapt
275                    obj = service.getAdaptedValue(ctx, obj, field.getType());
276                }
277                try {
278                    field.set(target, obj);
279                } catch (ReflectiveOperationException e) {
280                    throw new OperationException(e);
281                }
282            }
283        }
284        for (Field field : injectableFields) {
285            Object obj = ctx.getAdapter(field.getType());
286            try {
287                field.set(target, obj);
288            } catch (ReflectiveOperationException e) {
289                throw new OperationException(e);
290            }
291        }
292    }
293
294    @Override
295    public InvokableMethod[] getMethodsMatchingInput(Class<?> in) {
296        List<Match> result = new ArrayList<Match>();
297        for (InvokableMethod m : methods) {
298            int priority = m.inputMatch(in);
299            if (priority > 0) {
300                result.add(new Match(m, priority));
301            }
302        }
303        int size = result.size();
304        if (size == 0) {
305            return null;
306        }
307        if (size == 1) {
308            return new InvokableMethod[] { result.get(0).method };
309        }
310        Collections.sort(result);
311        InvokableMethod[] ar = new InvokableMethod[result.size()];
312        for (int i = 0; i < ar.length; i++) {
313            ar[i] = result.get(i).method;
314        }
315        return ar;
316    }
317
318    @Override
319    public OperationDocumentation getDocumentation() {
320        Operation op = type.getAnnotation(Operation.class);
321        OperationDocumentation doc = new OperationDocumentation(op.id());
322        doc.label = op.label();
323        doc.requires = op.requires();
324        doc.category = op.category();
325        doc.since = op.since();
326        doc.deprecatedSince = op.deprecatedSince();
327        doc.addToStudio = op.addToStudio();
328        doc.setAliases(op.aliases());
329        doc.implementationClass = type.getName();
330        if (doc.requires.length() == 0) {
331            doc.requires = null;
332        }
333        if (doc.label.length() == 0) {
334            doc.label = doc.id;
335        }
336        doc.description = op.description();
337        // load parameters information
338        List<OperationDocumentation.Param> paramsAccumulator = new LinkedList<OperationDocumentation.Param>();
339        for (Field field : params.values()) {
340            Param p = field.getAnnotation(Param.class);
341            OperationDocumentation.Param param = new OperationDocumentation.Param();
342            param.name = p.name();
343            param.description = p.description();
344            param.type = getParamDocumentationType(field.getType());
345            param.widget = p.widget();
346            if (param.widget.length() == 0) {
347                param.widget = null;
348            }
349            param.order = p.order();
350            param.values = p.values();
351            param.isRequired = p.required();
352            paramsAccumulator.add(param);
353        }
354        Collections.sort(paramsAccumulator);
355        doc.params = paramsAccumulator.toArray(new OperationDocumentation.Param[paramsAccumulator.size()]);
356        // load signature
357        ArrayList<String> result = new ArrayList<String>(methods.size() * 2);
358        Collection<String> collectedSigs = new HashSet<String>();
359        for (InvokableMethod m : methods) {
360            String in = getParamDocumentationType(m.getInputType(), m.isIterable());
361            String out = getParamDocumentationType(m.getOutputType());
362            String sigKey = in + ":" + out;
363            if (!collectedSigs.contains(sigKey)) {
364                result.add(in);
365                result.add(out);
366                collectedSigs.add(sigKey);
367            }
368        }
369        doc.signature = result.toArray(new String[result.size()]);
370        // widgets descriptor
371        if (widgetDefinitionList != null) {
372            doc.widgetDefinitions = widgetDefinitionList.toArray(new WidgetDefinition[widgetDefinitionList.size()]);
373        }
374        return doc;
375    }
376
377    @Override
378    public String getContributingComponent() {
379        return contributingComponent;
380    }
381
382    protected String getParamDocumentationType(Class<?> type) {
383        return getParamDocumentationType(type, false);
384    }
385
386    protected String getParamDocumentationType(Class<?> type, boolean isIterable) {
387        String t;
388        if (DocumentModel.class.isAssignableFrom(type) || DocumentRef.class.isAssignableFrom(type)) {
389            t = isIterable ? Constants.T_DOCUMENTS : Constants.T_DOCUMENT;
390        } else if (DocumentModelList.class.isAssignableFrom(type) || DocumentRefList.class.isAssignableFrom(type)) {
391            t = Constants.T_DOCUMENTS;
392        } else if (BlobList.class.isAssignableFrom(type)) {
393            t = Constants.T_BLOBS;
394        } else if (Blob.class.isAssignableFrom(type)) {
395            t = isIterable ? Constants.T_BLOBS : Constants.T_BLOB;
396        } else if (URL.class.isAssignableFrom(type)) {
397            t = Constants.T_RESOURCE;
398        } else if (Calendar.class.isAssignableFrom(type)) {
399            t = Constants.T_DATE;
400        } else {
401            t = type.getSimpleName().toLowerCase();
402        }
403        return t;
404    }
405
406    @Override
407    public String toString() {
408        return "OperationTypeImpl [id=" + id + ", type=" + type + ", params=" + params + "]";
409    }
410
411    /**
412     * @since 5.7.2
413     */
414    @Override
415    public List<InvokableMethod> getMethods() {
416        return methods;
417    }
418}