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