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