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}