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