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