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    @SuppressWarnings("rawtypes")
068    protected Class<? extends AsyncService> asyncService;
069
070    public InvokableMethod(OperationType op, Method method, OperationMethod anno) {
071        produce = method.getReturnType();
072        Class<?>[] p = method.getParameterTypes();
073        if (p.length > 1) {
074            throw new IllegalArgumentException("Operation method must accept at most one argument: " + method);
075        }
076        // if produce is Void => a control operation
077        // if (produce == Void.TYPE) {
078        // throw new IllegalArgumentException("Operation method must return a
079        // value: "+method);
080        // }
081        this.op = op;
082        this.method = method;
083        priority = anno.priority();
084        if (priority > 0) {
085            priority += USER_PRIORITY;
086        }
087        consume = p.length == 0 ? Void.TYPE : p[0];
088        asyncService = anno.asyncService();
089    }
090
091    public InvokableMethod(OperationType op, Method method) {
092        produce = method.getReturnType();
093        Class<?>[] p = method.getParameterTypes();
094        if (p.length > 1) {
095            throw new IllegalArgumentException("Operation method must accept at most one argument: " + method);
096        }
097        this.op = op;
098        this.method = method;
099        String inputType = this.op.getInputType();
100        if (inputType != null) {
101            switch (inputType) {
102            case "document":
103                consume = DocumentModel.class;
104                break;
105            case "documents":
106                consume = DocumentModelList.class;
107                break;
108            case "blob":
109                consume = Blob.class;
110                break;
111            case "blobs":
112                consume = BlobList.class;
113                break;
114            default:
115                consume = Object.class;
116                break;
117            }
118        } else {
119            consume = p.length == 0 ? Void.TYPE : p[0];
120        }
121    }
122
123    public boolean isIterable() {
124        return false;
125    }
126
127    public int getPriority() {
128        return priority;
129    }
130
131    public OperationType getOperation() {
132        return op;
133    }
134
135    public final Class<?> getOutputType() {
136        return produce;
137    }
138
139    public final Class<?> getInputType() {
140        return consume;
141    }
142
143    /**
144     * Return 0 for no match.
145     */
146    public int inputMatch(Class<?> in) {
147        if (consume == in) {
148            return priority > 0 ? priority : EXACT_MATCH_PRIORITY;
149        }
150        if (consume.isAssignableFrom(in)) {
151            return priority > 0 ? priority : ISTANCE_OF_PRIORITY;
152        }
153        if (op.getService().isTypeAdaptable(in, consume)) {
154            return priority > 0 ? priority : ADAPTABLE_PRIORITY;
155        }
156        if (consume == Void.TYPE) {
157            return priority > 0 ? priority : VOID_PRIORITY;
158        }
159        return 0;
160    }
161
162    protected Object doInvoke(OperationContext ctx, Map<String, Object> args)
163            throws OperationException, ReflectiveOperationException {
164        Object target = op.newInstance(ctx, args);
165        Object input = ctx.getInput();
166        if (consume == Void.TYPE) {
167            // preserve last output for void methods
168            Object out = method.invoke(target);
169            return produce == Void.TYPE ? input : out;
170        }
171        if (input == null || !consume.isAssignableFrom(input.getClass())) {
172            // try to adapt
173            input = op.getService().getAdaptedValue(ctx, input, consume);
174        }
175        return method.invoke(target, input);
176    }
177
178    public Object invoke(OperationContext ctx, Map<String, Object> args) throws OperationException {
179        try {
180            return doInvoke(ctx, args);
181        } catch (InvocationTargetException e) {
182            Throwable t = e.getTargetException();
183            if (t instanceof OperationException) {
184                throw (OperationException) t;
185            } else if (t instanceof NuxeoException) {
186                NuxeoException nuxeoException = (NuxeoException) t;
187                nuxeoException.addInfo(getExceptionMessage());
188                throw nuxeoException;
189            } else {
190                throw new OperationException(getExceptionMessage(), t);
191            }
192        } catch (ReflectiveOperationException e) {
193            throw new OperationException(getExceptionMessage(), e);
194        }
195    }
196
197    protected String getExceptionMessage() {
198        String exceptionMessage = "Failed to invoke operation " + op.getId();
199        if (op.getAliases() != null && op.getAliases().length > 0) {
200            exceptionMessage += " with aliases " + Arrays.toString(op.getAliases());
201        }
202        return exceptionMessage;
203    }
204
205    @Override
206    public String toString() {
207        return getClass().getSimpleName() + "(" + method + ", " + priority + ")";
208    }
209
210    @Override
211    // used for methods of the same class, so ignore the class
212    public int compareTo(InvokableMethod o) {
213        // compare on name
214        int cmp = method.getName().compareTo(o.method.getName());
215        if (cmp != 0) {
216            return cmp;
217        }
218        // same name, compare on parameter types
219        Class<?>[] pt = method.getParameterTypes();
220        Class<?>[] opt = o.method.getParameterTypes();
221        // smaller length first
222        cmp = pt.length - opt.length;
223        if (cmp != 0) {
224            return cmp;
225        }
226        // compare parameter classes lexicographically
227        for (int i = 0; i < pt.length; i++) {
228            cmp = pt[i].getName().compareTo(opt[i].getName());
229            if (cmp != 0) {
230                return cmp;
231            }
232        }
233        return 0;
234    }
235
236    public Method getMethod() {
237        return method;
238    }
239
240    public Class<?> getProduce() {
241        return produce;
242    }
243
244    public Class<?> getConsume() {
245        return consume;
246    }
247
248    @SuppressWarnings("rawtypes")
249    public Class<? extends AsyncService> getAsyncService() {
250        return asyncService;
251    }
252}