001/* 002 * Copyright (c) 2006-2011 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.Field; 015import java.lang.reflect.Method; 016import java.net.URL; 017import java.util.ArrayList; 018import java.util.Calendar; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.Map; 026 027import org.nuxeo.ecm.automation.AutomationService; 028import org.nuxeo.ecm.automation.OperationContext; 029import org.nuxeo.ecm.automation.OperationDocumentation; 030import org.nuxeo.ecm.automation.OperationException; 031import org.nuxeo.ecm.automation.OperationType; 032import org.nuxeo.ecm.automation.OutputCollector; 033import org.nuxeo.ecm.automation.core.Constants; 034import org.nuxeo.ecm.automation.core.annotations.Context; 035import org.nuxeo.ecm.automation.core.annotations.Operation; 036import org.nuxeo.ecm.automation.core.annotations.OperationMethod; 037import org.nuxeo.ecm.automation.core.annotations.Param; 038import org.nuxeo.ecm.automation.core.scripting.Expression; 039import org.nuxeo.ecm.automation.core.util.BlobList; 040import org.nuxeo.ecm.core.api.Blob; 041import org.nuxeo.ecm.core.api.DocumentModel; 042import org.nuxeo.ecm.core.api.DocumentModelList; 043import org.nuxeo.ecm.core.api.DocumentRef; 044import org.nuxeo.ecm.core.api.DocumentRefList; 045import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition; 046 047/** 048 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 049 * @author <a href="mailto:grenard@nuxeo.com">Guillaume Renard</a> 050 */ 051public class OperationTypeImpl implements OperationType { 052 053 /** 054 * The service that registered the operation 055 */ 056 protected AutomationService service; 057 058 /** 059 * The operation ID - used for lookups. 060 */ 061 protected String id; 062 063 /** 064 * The operation ID Aliases array. 065 * 066 * @since 7.1 067 */ 068 protected String[] aliases; 069 070 /** 071 * The operation type 072 */ 073 protected Class<?> type; 074 075 /** 076 * Injectable parameters. a map between the parameter name and the Field object 077 */ 078 protected Map<String, Field> params; 079 080 /** 081 * Invocable methods 082 */ 083 protected List<InvokableMethod> methods; 084 085 /** 086 * Fields that should be injected from context 087 */ 088 protected List<Field> injectableFields; 089 090 /** 091 * The input type of a chain/operation. If set, the following input types {"document", "documents", "blob", "blobs"} 092 * for all 'run method(s)' will handled. Other values will be adapted as java.lang.Object. If not set, Automation 093 * will set the input type(s) as the 'run methods(s)' parameter types (by introspection). 094 * 095 * @since 7.4 096 */ 097 protected String inputType; 098 099 protected String contributingComponent; 100 101 protected List<WidgetDefinition> widgetDefinitionList; 102 103 public OperationTypeImpl(AutomationService service, Class<?> type) { 104 this(service, type, null); 105 } 106 107 public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent) { 108 this(service, type, contributingComponent, null); 109 } 110 111 /** 112 * @since 5.9.5 113 */ 114 public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent, 115 List<WidgetDefinition> widgetDefinitionList) { 116 Operation anno = type.getAnnotation(Operation.class); 117 if (anno == null) { 118 throw new IllegalArgumentException("Invalid operation class: " + type 119 + ". No @Operation annotation found on class."); 120 } 121 this.service = service; 122 this.type = type; 123 this.widgetDefinitionList = widgetDefinitionList; 124 this.contributingComponent = contributingComponent; 125 id = anno.id(); 126 if (id.length() == 0) { 127 id = type.getName(); 128 } 129 aliases = anno.aliases(); 130 params = new HashMap<String, Field>(); 131 methods = new ArrayList<InvokableMethod>(); 132 injectableFields = new ArrayList<Field>(); 133 initMethods(); 134 initFields(); 135 } 136 137 public OperationTypeImpl() { 138 } 139 140 static class Match implements Comparable<Match> { 141 protected InvokableMethod method; 142 143 int priority; 144 145 Match(InvokableMethod method, int priority) { 146 this.method = method; 147 this.priority = priority; 148 } 149 150 @Override 151 public int compareTo(Match o) { 152 return o.priority - priority; 153 } 154 155 @Override 156 public String toString() { 157 return "Match(" + method + ", " + priority + ")"; 158 } 159 } 160 161 @Override 162 public AutomationService getService() { 163 return service; 164 } 165 166 @Override 167 public String getId() { 168 return id; 169 } 170 171 @Override 172 public String[] getAliases() { 173 return aliases; 174 } 175 176 @Override 177 public Class<?> getType() { 178 return type; 179 } 180 181 @Override 182 public String getInputType() { 183 return inputType; 184 } 185 186 protected void initMethods() { 187 for (Method method : type.getMethods()) { 188 OperationMethod anno = method.getAnnotation(OperationMethod.class); 189 if (anno == null) { // skip method 190 continue; 191 } 192 // register regular method 193 InvokableMethod im = new InvokableMethod(this, method, anno); 194 methods.add(im); 195 // check for iterable input support 196 if (anno.collector() != OutputCollector.class) { 197 // an iterable method - register it 198 im = new InvokableIteratorMethod(this, method, anno); 199 methods.add(im); 200 } 201 } 202 // method order depends on the JDK, make it deterministic 203 Collections.sort(methods); 204 } 205 206 protected void initFields() { 207 for (Field field : type.getDeclaredFields()) { 208 Param param = field.getAnnotation(Param.class); 209 if (param != null) { 210 field.setAccessible(true); 211 params.put(param.name(), field); 212 } else if (field.isAnnotationPresent(Context.class)) { 213 field.setAccessible(true); 214 injectableFields.add(field); 215 } 216 } 217 } 218 219 @Override 220 public Object newInstance(OperationContext ctx, Map<String, Object> args) throws OperationException { 221 Object obj; 222 try { 223 obj = type.newInstance(); 224 } catch (ReflectiveOperationException e) { 225 throw new OperationException(e); 226 } 227 inject(ctx, args, obj); 228 return obj; 229 } 230 231 /** 232 * @since 5.9.2 233 */ 234 protected Object resolveObject(final OperationContext ctx, final String key, Map<String, Object> args) { 235 Object obj = args.get(key); 236 if (obj instanceof Expression) { 237 obj = ((Expression) obj).eval(ctx); 238 } 239 // Trying to fallback on Chain Parameters sub context if cannot 240 // find it 241 if (obj == null) { 242 if (ctx.containsKey(Constants.VAR_RUNTIME_CHAIN)) { 243 obj = ((Map) ctx.get(Constants.VAR_RUNTIME_CHAIN)).get(key); 244 } 245 } 246 return obj; 247 } 248 249 public void inject(OperationContext ctx, Map<String, Object> args, Object target) throws OperationException { 250 for (Map.Entry<String, Field> entry : params.entrySet()) { 251 Object obj = resolveObject(ctx, entry.getKey(), args); 252 if (obj == null) { 253 // We did not resolve object according to its param name, let's 254 // check with potential alias 255 String[] aliases = entry.getValue().getAnnotation(Param.class).alias(); 256 if (aliases != null) { 257 for (String alias : entry.getValue().getAnnotation(Param.class).alias()) { 258 obj = resolveObject(ctx, alias, args); 259 if (obj != null) { 260 break; 261 } 262 } 263 } 264 } 265 if (obj == null) { 266 if (entry.getValue().getAnnotation(Param.class).required()) { 267 throw new OperationException("Failed to inject parameter '" + entry.getKey() 268 + "'. Seems it is missing from the context. Operation: " + getId()); 269 } // else do nothing 270 } else { 271 Field field = entry.getValue(); 272 Class<?> cl = obj.getClass(); 273 if (!field.getType().isAssignableFrom(cl)) { 274 // try to adapt 275 obj = service.getAdaptedValue(ctx, obj, field.getType()); 276 } 277 try { 278 field.set(target, obj); 279 } catch (ReflectiveOperationException e) { 280 throw new OperationException(e); 281 } 282 } 283 } 284 for (Field field : injectableFields) { 285 Object obj = ctx.getAdapter(field.getType()); 286 try { 287 field.set(target, obj); 288 } catch (ReflectiveOperationException e) { 289 throw new OperationException(e); 290 } 291 } 292 } 293 294 @Override 295 public InvokableMethod[] getMethodsMatchingInput(Class<?> in) { 296 List<Match> result = new ArrayList<Match>(); 297 for (InvokableMethod m : methods) { 298 int priority = m.inputMatch(in); 299 if (priority > 0) { 300 result.add(new Match(m, priority)); 301 } 302 } 303 int size = result.size(); 304 if (size == 0) { 305 return null; 306 } 307 if (size == 1) { 308 return new InvokableMethod[] { result.get(0).method }; 309 } 310 Collections.sort(result); 311 InvokableMethod[] ar = new InvokableMethod[result.size()]; 312 for (int i = 0; i < ar.length; i++) { 313 ar[i] = result.get(i).method; 314 } 315 return ar; 316 } 317 318 @Override 319 public OperationDocumentation getDocumentation() { 320 Operation op = type.getAnnotation(Operation.class); 321 OperationDocumentation doc = new OperationDocumentation(op.id()); 322 doc.label = op.label(); 323 doc.requires = op.requires(); 324 doc.category = op.category(); 325 doc.since = op.since(); 326 doc.deprecatedSince = op.deprecatedSince(); 327 doc.addToStudio = op.addToStudio(); 328 doc.setAliases(op.aliases()); 329 doc.implementationClass = type.getName(); 330 if (doc.requires.length() == 0) { 331 doc.requires = null; 332 } 333 if (doc.label.length() == 0) { 334 doc.label = doc.id; 335 } 336 doc.description = op.description(); 337 // load parameters information 338 List<OperationDocumentation.Param> paramsAccumulator = new LinkedList<OperationDocumentation.Param>(); 339 for (Field field : params.values()) { 340 Param p = field.getAnnotation(Param.class); 341 OperationDocumentation.Param param = new OperationDocumentation.Param(); 342 param.name = p.name(); 343 param.description = p.description(); 344 param.type = getParamDocumentationType(field.getType()); 345 param.widget = p.widget(); 346 if (param.widget.length() == 0) { 347 param.widget = null; 348 } 349 param.order = p.order(); 350 param.values = p.values(); 351 param.isRequired = p.required(); 352 paramsAccumulator.add(param); 353 } 354 Collections.sort(paramsAccumulator); 355 doc.params = paramsAccumulator.toArray(new OperationDocumentation.Param[paramsAccumulator.size()]); 356 // load signature 357 ArrayList<String> result = new ArrayList<String>(methods.size() * 2); 358 Collection<String> collectedSigs = new HashSet<String>(); 359 for (InvokableMethod m : methods) { 360 String in = getParamDocumentationType(m.getInputType(), m.isIterable()); 361 String out = getParamDocumentationType(m.getOutputType()); 362 String sigKey = in + ":" + out; 363 if (!collectedSigs.contains(sigKey)) { 364 result.add(in); 365 result.add(out); 366 collectedSigs.add(sigKey); 367 } 368 } 369 doc.signature = result.toArray(new String[result.size()]); 370 // widgets descriptor 371 if (widgetDefinitionList != null) { 372 doc.widgetDefinitions = widgetDefinitionList.toArray(new WidgetDefinition[widgetDefinitionList.size()]); 373 } 374 return doc; 375 } 376 377 @Override 378 public String getContributingComponent() { 379 return contributingComponent; 380 } 381 382 protected String getParamDocumentationType(Class<?> type) { 383 return getParamDocumentationType(type, false); 384 } 385 386 protected String getParamDocumentationType(Class<?> type, boolean isIterable) { 387 String t; 388 if (DocumentModel.class.isAssignableFrom(type) || DocumentRef.class.isAssignableFrom(type)) { 389 t = isIterable ? Constants.T_DOCUMENTS : Constants.T_DOCUMENT; 390 } else if (DocumentModelList.class.isAssignableFrom(type) || DocumentRefList.class.isAssignableFrom(type)) { 391 t = Constants.T_DOCUMENTS; 392 } else if (BlobList.class.isAssignableFrom(type)) { 393 t = Constants.T_BLOBS; 394 } else if (Blob.class.isAssignableFrom(type)) { 395 t = isIterable ? Constants.T_BLOBS : Constants.T_BLOB; 396 } else if (URL.class.isAssignableFrom(type)) { 397 t = Constants.T_RESOURCE; 398 } else if (Calendar.class.isAssignableFrom(type)) { 399 t = Constants.T_DATE; 400 } else { 401 t = type.getSimpleName().toLowerCase(); 402 } 403 return t; 404 } 405 406 @Override 407 public String toString() { 408 return "OperationTypeImpl [id=" + id + ", type=" + type + ", params=" + params + "]"; 409 } 410 411 /** 412 * @since 5.7.2 413 */ 414 @Override 415 public List<InvokableMethod> getMethods() { 416 return methods; 417 } 418}