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.util.BlobList;
046import org.nuxeo.ecm.core.api.Blob;
047import org.nuxeo.ecm.core.api.DocumentModel;
048import org.nuxeo.ecm.core.api.DocumentModelList;
049import org.nuxeo.ecm.core.api.DocumentRef;
050import org.nuxeo.ecm.core.api.DocumentRefList;
051import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition;
052
053/**
054 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
055 * @author <a href="mailto:grenard@nuxeo.com">Guillaume Renard</a>
056 */
057public class OperationTypeImpl implements OperationType {
058
059    /**
060     * The service that registered the operation
061     */
062    protected final AutomationService service;
063
064    /**
065     * The operation ID - used for lookups.
066     */
067    protected final String id;
068
069    /**
070     * The operation ID Aliases array.
071     *
072     * @since 7.1
073     */
074    protected final String[] aliases;
075
076    /**
077     * The operation type
078     */
079    protected final Class<?> type;
080
081    /**
082     * Injectable parameters. a map between the parameter name and the Field object
083     */
084    protected final Map<String, Field> params;
085
086    /**
087     * Invocable methods
088     */
089    protected List<InvokableMethod> methods;
090
091    /**
092     * Fields that should be injected from context
093     */
094    protected List<Field> injectableFields;
095
096    /**
097     * The input type of a chain/operation. If set, the following input types {"document", "documents", "blob", "blobs"}
098     * for all 'run method(s)' will handled. Other values will be adapted as java.lang.Object. If not set, Automation
099     * will set the input type(s) as the 'run methods(s)' parameter types (by introspection).
100     *
101     * @since 7.4
102     */
103    protected String inputType;
104
105    protected String contributingComponent;
106
107    protected List<WidgetDefinition> widgetDefinitionList;
108
109    public OperationTypeImpl(AutomationService service, Class<?> type) {
110        this(service, type, null);
111    }
112
113    public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent) {
114        this(service, type, contributingComponent, null);
115    }
116
117    /**
118     * @since 5.9.5
119     */
120    public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent,
121            List<WidgetDefinition> widgetDefinitionList) {
122        Operation anno = type.getAnnotation(Operation.class);
123        if (anno == null) {
124            throw new IllegalArgumentException(
125                    "Invalid operation class: " + type + ". No @Operation annotation found on class.");
126        }
127        this.service = service;
128        this.type = type;
129        this.widgetDefinitionList = widgetDefinitionList;
130        this.contributingComponent = contributingComponent;
131        id = anno.id().length() == 0 ? type.getName() : anno.id();
132        aliases = anno.aliases();
133        params = new HashMap<>();
134        methods = new ArrayList<>();
135        injectableFields = new ArrayList<>();
136        initMethods();
137        initFields();
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, ?> args) {
235        Object obj = args.get(key);
236        if (obj != null) {
237            return ctx.resolve(obj);
238        }
239        return ctx.getChainParameter(key);
240    }
241
242    public void inject(OperationContext ctx, Map<String, ?> args, Object target) throws OperationException {
243        for (Map.Entry<String, Field> entry : params.entrySet()) {
244            Object obj = resolveObject(ctx, entry.getKey(), args);
245            if (obj == null) {
246                // We did not resolve object according to its param name, let's
247                // check with potential alias
248                String[] entryAliases = entry.getValue().getAnnotation(Param.class).alias();
249                if (entryAliases != null) {
250                    for (String alias : entry.getValue().getAnnotation(Param.class).alias()) {
251                        obj = resolveObject(ctx, alias, args);
252                        if (obj != null) {
253                            break;
254                        }
255                    }
256                }
257            }
258            if (obj == null) {
259                if (entry.getValue().getAnnotation(Param.class).required()) {
260                    throw new OperationException("Failed to inject parameter '" + entry.getKey()
261                            + "'. Seems it is missing from the context. Operation: " + getId());
262                } // else do nothing
263            } else {
264                Field field = entry.getValue();
265                Class<?> cl = obj.getClass();
266                if (!field.getType().isAssignableFrom(cl)) {
267                    // try to adapt
268                    obj = service.getAdaptedValue(ctx, obj, field.getType());
269                }
270                try {
271                    field.set(target, obj);
272                } catch (ReflectiveOperationException e) {
273                    throw new OperationException(e);
274                }
275            }
276        }
277        for (Field field : injectableFields) {
278            Object obj = ctx.getAdapter(field.getType());
279            try {
280                field.set(target, obj);
281            } catch (ReflectiveOperationException e) {
282                throw new OperationException(e);
283            }
284        }
285    }
286
287    @Override
288    public InvokableMethod[] getMethodsMatchingInput(Class<?> in) {
289        List<Match> result = new ArrayList<>();
290        for (InvokableMethod m : methods) {
291            int priority = m.inputMatch(in);
292            if (priority > 0) {
293                result.add(new Match(m, priority));
294            }
295        }
296        int size = result.size();
297        if (size == 0) {
298            return new InvokableMethod[] {};
299        }
300        if (size == 1) {
301            return new InvokableMethod[] { result.get(0).method };
302        }
303        Collections.sort(result);
304        InvokableMethod[] ar = new InvokableMethod[result.size()];
305        for (int i = 0; i < ar.length; i++) {
306            ar[i] = result.get(i).method;
307        }
308        return ar;
309    }
310
311    @Override
312    public OperationDocumentation getDocumentation() {
313        Operation op = type.getAnnotation(Operation.class);
314        OperationDocumentation doc = new OperationDocumentation(op.id());
315        doc.label = op.label();
316        doc.requires = op.requires();
317        doc.category = op.category();
318        doc.since = op.since();
319        doc.deprecatedSince = op.deprecatedSince();
320        doc.addToStudio = op.addToStudio();
321        doc.setAliases(op.aliases());
322        doc.implementationClass = type.getName();
323        if (doc.requires.length() == 0) {
324            doc.requires = null;
325        }
326        if (doc.label.length() == 0) {
327            doc.label = doc.id;
328        }
329        doc.description = op.description();
330        // load parameters information
331        List<OperationDocumentation.Param> paramsAccumulator = new LinkedList<>();
332        for (Field field : params.values()) {
333            Param p = field.getAnnotation(Param.class);
334            OperationDocumentation.Param param = new OperationDocumentation.Param();
335            param.name = p.name();
336            param.description = p.description();
337            param.type = getParamDocumentationType(field.getType());
338            param.widget = p.widget();
339            if (param.widget.length() == 0) {
340                param.widget = null;
341            }
342            param.order = p.order();
343            param.values = p.values();
344            param.required = p.required();
345            paramsAccumulator.add(param);
346        }
347        Collections.sort(paramsAccumulator);
348        doc.params = paramsAccumulator.toArray(new OperationDocumentation.Param[paramsAccumulator.size()]);
349        // load signature
350        ArrayList<String> result = new ArrayList<>(methods.size() * 2);
351        Collection<String> collectedSigs = new HashSet<>();
352        for (InvokableMethod m : methods) {
353            String in = getParamDocumentationType(m.getInputType(), m.isIterable());
354            String out = getParamDocumentationType(m.getOutputType());
355            String sigKey = in + ":" + out;
356            if (!collectedSigs.contains(sigKey)) {
357                result.add(in);
358                result.add(out);
359                collectedSigs.add(sigKey);
360            }
361        }
362        doc.signature = result.toArray(new String[result.size()]);
363        // widgets descriptor
364        if (widgetDefinitionList != null) {
365            doc.widgetDefinitions = widgetDefinitionList.toArray(new WidgetDefinition[widgetDefinitionList.size()]);
366        }
367        return doc;
368    }
369
370    @Override
371    public String getContributingComponent() {
372        return contributingComponent;
373    }
374
375    protected String getParamDocumentationType(Class<?> type) {
376        return getParamDocumentationType(type, false);
377    }
378
379    protected String getParamDocumentationType(Class<?> type, boolean isIterable) {
380        String t;
381        if (DocumentModel.class.isAssignableFrom(type) || DocumentRef.class.isAssignableFrom(type)) {
382            t = isIterable ? Constants.T_DOCUMENTS : Constants.T_DOCUMENT;
383        } else if (DocumentModelList.class.isAssignableFrom(type) || DocumentRefList.class.isAssignableFrom(type)) {
384            t = Constants.T_DOCUMENTS;
385        } else if (BlobList.class.isAssignableFrom(type)) {
386            t = Constants.T_BLOBS;
387        } else if (Blob.class.isAssignableFrom(type)) {
388            t = isIterable ? Constants.T_BLOBS : Constants.T_BLOB;
389        } else if (URL.class.isAssignableFrom(type)) {
390            t = Constants.T_RESOURCE;
391        } else if (Calendar.class.isAssignableFrom(type)) {
392            t = Constants.T_DATE;
393        } else {
394            t = type.getSimpleName().toLowerCase();
395        }
396        return t;
397    }
398
399    @Override
400    public String toString() {
401        return "OperationTypeImpl [id=" + id + ", type=" + type + ", params=" + params + "]";
402    }
403
404    /**
405     * @since 5.7.2
406     */
407    @Override
408    public List<InvokableMethod> getMethods() {
409        return methods;
410    }
411}