001/*
002 * (C) Copyright 2006-2018 Nuxeo (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.InvocationTargetException;
022import java.lang.reflect.Method;
023import java.util.Arrays;
024import java.util.Map;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.nuxeo.ecm.automation.OperationContext;
029import org.nuxeo.ecm.automation.OperationException;
030import org.nuxeo.ecm.automation.OperationType;
031import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
032import org.nuxeo.ecm.automation.core.util.BlobList;
033import org.nuxeo.ecm.core.api.AsyncService;
034import org.nuxeo.ecm.core.api.Blob;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentModelList;
037import org.nuxeo.ecm.core.api.NuxeoException;
038
039/**
040 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
041 */
042public class InvokableMethod implements Comparable<InvokableMethod> {
043
044    protected static final Log log = LogFactory.getLog(InvokableMethod.class);
045
046    public static final int VOID_PRIORITY = 1;
047
048    public static final int ADAPTABLE_PRIORITY = 2;
049
050    public static final int ISTANCE_OF_PRIORITY = 3;
051
052    public static final int EXACT_MATCH_PRIORITY = 4;
053
054    // priorities from 1 to 16 are reserved for internal use.
055    public static final int USER_PRIORITY = 16;
056
057    protected OperationType op;
058
059    protected Method method;
060
061    protected Class<?> produce;
062
063    protected Class<?> consume;
064
065    protected int priority;
066
067    protected Class<? extends AsyncService> asyncService;
068
069    public InvokableMethod(OperationType op, Method method, OperationMethod anno) {
070        produce = method.getReturnType();
071        Class<?>[] p = method.getParameterTypes();
072        if (p.length > 1) {
073            throw new IllegalArgumentException("Operation method must accept at most one argument: " + method);
074        }
075        // if produce is Void => a control operation
076        // if (produce == Void.TYPE) {
077        // throw new IllegalArgumentException("Operation method must return a
078        // value: "+method);
079        // }
080        this.op = op;
081        this.method = method;
082        priority = anno.priority();
083        if (priority > 0) {
084            priority += USER_PRIORITY;
085        }
086        consume = p.length == 0 ? Void.TYPE : p[0];
087        asyncService = anno.asyncService();
088    }
089
090    public InvokableMethod(OperationType op, Method method) {
091        produce = method.getReturnType();
092        Class<?>[] p = method.getParameterTypes();
093        if (p.length > 1) {
094            throw new IllegalArgumentException("Operation method must accept at most one argument: " + method);
095        }
096        this.op = op;
097        this.method = method;
098        String inputType = this.op.getInputType();
099        if (inputType != null) {
100            switch (inputType) {
101            case "document":
102                consume = DocumentModel.class;
103                break;
104            case "documents":
105                consume = DocumentModelList.class;
106                break;
107            case "blob":
108                consume = Blob.class;
109                break;
110            case "blobs":
111                consume = BlobList.class;
112                break;
113            default:
114                consume = Object.class;
115                break;
116            }
117        } else {
118            consume = p.length == 0 ? Void.TYPE : p[0];
119        }
120    }
121
122    public boolean isIterable() {
123        return false;
124    }
125
126    public int getPriority() {
127        return priority;
128    }
129
130    public OperationType getOperation() {
131        return op;
132    }
133
134    public final Class<?> getOutputType() {
135        return produce;
136    }
137
138    public final Class<?> getInputType() {
139        return consume;
140    }
141
142    /**
143     * Return 0 for no match.
144     */
145    public int inputMatch(Class<?> in) {
146        if (consume == in) {
147            return priority > 0 ? priority : EXACT_MATCH_PRIORITY;
148        }
149        if (consume.isAssignableFrom(in)) {
150            return priority > 0 ? priority : ISTANCE_OF_PRIORITY;
151        }
152        if (op.getService().isTypeAdaptable(in, consume)) {
153            return priority > 0 ? priority : ADAPTABLE_PRIORITY;
154        }
155        if (consume == Void.TYPE) {
156            return priority > 0 ? priority : VOID_PRIORITY;
157        }
158        return 0;
159    }
160
161    protected Object doInvoke(OperationContext ctx, Map<String, Object> args)
162            throws OperationException, ReflectiveOperationException {
163        Object target = op.newInstance(ctx, args);
164        Object input = ctx.getInput();
165        if (consume == Void.TYPE) {
166            // preserve last output for void methods
167            Object out = method.invoke(target);
168            return produce == Void.TYPE ? input : out;
169        }
170        if (input == null || !consume.isAssignableFrom(input.getClass())) {
171            // try to adapt
172            input = op.getService().getAdaptedValue(ctx, input, consume);
173        }
174        return method.invoke(target, input);
175    }
176
177    public Object invoke(OperationContext ctx, Map<String, Object> args) throws OperationException {
178        try {
179            return doInvoke(ctx, args);
180        } catch (InvocationTargetException e) {
181            Throwable t = e.getTargetException();
182            if (t instanceof OperationException) {
183                throw (OperationException) t;
184            } else if (t instanceof NuxeoException) {
185                NuxeoException nuxeoException = (NuxeoException) t;
186                nuxeoException.addInfo(getExceptionMessage());
187                throw nuxeoException;
188            } else {
189                throw new OperationException(getExceptionMessage(), t);
190            }
191        } catch (ReflectiveOperationException e) {
192            throw new OperationException(getExceptionMessage(), e);
193        }
194    }
195
196    protected String getExceptionMessage() {
197        String exceptionMessage = "Failed to invoke operation " + op.getId();
198        if (op.getAliases() != null && op.getAliases().length > 0) {
199            exceptionMessage += " with aliases " + Arrays.toString(op.getAliases());
200        }
201        return exceptionMessage;
202    }
203
204    @Override
205    public String toString() {
206        return getClass().getSimpleName() + "(" + method + ", " + priority + ")";
207    }
208
209    @Override
210    // used for methods of the same class, so ignore the class
211    public int compareTo(InvokableMethod o) {
212        // compare on name
213        int cmp = method.getName().compareTo(o.method.getName());
214        if (cmp != 0) {
215            return cmp;
216        }
217        // same name, compare on parameter types
218        Class<?>[] pt = method.getParameterTypes();
219        Class<?>[] opt = o.method.getParameterTypes();
220        // smaller length first
221        cmp = pt.length - opt.length;
222        if (cmp != 0) {
223            return cmp;
224        }
225        // compare parameter classes lexicographically
226        for (int i = 0; i < pt.length; i++) {
227            cmp = pt[i].getName().compareTo(opt[i].getName());
228            if (cmp != 0) {
229                return cmp;
230            }
231        }
232        return 0;
233    }
234
235    public Method getMethod() {
236        return method;
237    }
238
239    public Class<?> getProduce() {
240        return produce;
241    }
242
243    public Class<?> getConsume() {
244        return consume;
245    }
246
247    public Class<? extends AsyncService> getAsyncService() {
248        return asyncService;
249    }
250}