001/* 002 * (C) Copyright 2006-2011 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.Field; 022import java.lang.reflect.Method; 023import java.net.URL; 024import java.util.ArrayList; 025import java.util.Calendar; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.LinkedList; 031import java.util.List; 032import java.util.Map; 033 034import org.nuxeo.ecm.automation.AutomationService; 035import org.nuxeo.ecm.automation.OperationContext; 036import org.nuxeo.ecm.automation.OperationDocumentation; 037import org.nuxeo.ecm.automation.OperationException; 038import org.nuxeo.ecm.automation.OperationType; 039import org.nuxeo.ecm.automation.OutputCollector; 040import org.nuxeo.ecm.automation.core.Constants; 041import org.nuxeo.ecm.automation.core.annotations.Context; 042import org.nuxeo.ecm.automation.core.annotations.Operation; 043import org.nuxeo.ecm.automation.core.annotations.OperationMethod; 044import org.nuxeo.ecm.automation.core.annotations.Param; 045import org.nuxeo.ecm.automation.core.util.BlobList; 046import org.nuxeo.ecm.core.api.Blob; 047import org.nuxeo.ecm.core.api.DocumentModel; 048import org.nuxeo.ecm.core.api.DocumentModelList; 049import org.nuxeo.ecm.core.api.DocumentRef; 050import org.nuxeo.ecm.core.api.DocumentRefList; 051import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition; 052 053/** 054 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 055 * @author <a href="mailto:grenard@nuxeo.com">Guillaume Renard</a> 056 */ 057public class OperationTypeImpl implements OperationType { 058 059 /** 060 * The service that registered the operation 061 */ 062 protected final AutomationService service; 063 064 /** 065 * The operation ID - used for lookups. 066 */ 067 protected final String id; 068 069 /** 070 * The operation ID Aliases array. 071 * 072 * @since 7.1 073 */ 074 protected final String[] aliases; 075 076 /** 077 * The operation type 078 */ 079 protected final Class<?> type; 080 081 /** 082 * Injectable parameters. a map between the parameter name and the Field object 083 */ 084 protected final Map<String, Field> params; 085 086 /** 087 * Invocable methods 088 */ 089 protected List<InvokableMethod> methods; 090 091 /** 092 * Fields that should be injected from context 093 */ 094 protected List<Field> injectableFields; 095 096 /** 097 * The input type of a chain/operation. If set, the following input types {"document", "documents", "blob", "blobs"} 098 * for all 'run method(s)' will handled. Other values will be adapted as java.lang.Object. If not set, Automation 099 * will set the input type(s) as the 'run methods(s)' parameter types (by introspection). 100 * 101 * @since 7.4 102 */ 103 protected String inputType; 104 105 protected String contributingComponent; 106 107 protected List<WidgetDefinition> widgetDefinitionList; 108 109 public OperationTypeImpl(AutomationService service, Class<?> type) { 110 this(service, type, null); 111 } 112 113 public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent) { 114 this(service, type, contributingComponent, null); 115 } 116 117 /** 118 * @since 5.9.5 119 */ 120 public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent, 121 List<WidgetDefinition> widgetDefinitionList) { 122 Operation anno = type.getAnnotation(Operation.class); 123 if (anno == null) { 124 throw new IllegalArgumentException( 125 "Invalid operation class: " + type + ". No @Operation annotation found on class."); 126 } 127 this.service = service; 128 this.type = type; 129 this.widgetDefinitionList = widgetDefinitionList; 130 this.contributingComponent = contributingComponent; 131 id = anno.id().length() == 0 ? type.getName() : anno.id(); 132 aliases = anno.aliases(); 133 params = new HashMap<String, Field>(); 134 methods = new ArrayList<InvokableMethod>(); 135 injectableFields = new ArrayList<Field>(); 136 initMethods(); 137 initFields(); 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, ?> args) { 235 Object obj = args.get(key); 236 if (obj != null) { 237 return ctx.resolve(obj); 238 } 239 return ctx.get(key); 240 } 241 242 public void inject(OperationContext ctx, Map<String, ?> args, Object target) throws OperationException { 243 for (Map.Entry<String, Field> entry : params.entrySet()) { 244 Object obj = resolveObject(ctx, entry.getKey(), args); 245 if (obj == null) { 246 // We did not resolve object according to its param name, let's 247 // check with potential alias 248 String[] aliases = entry.getValue().getAnnotation(Param.class).alias(); 249 if (aliases != null) { 250 for (String alias : entry.getValue().getAnnotation(Param.class).alias()) { 251 obj = resolveObject(ctx, alias, args); 252 if (obj != null) { 253 break; 254 } 255 } 256 } 257 } 258 if (obj == null) { 259 if (entry.getValue().getAnnotation(Param.class).required()) { 260 throw new OperationException("Failed to inject parameter '" + entry.getKey() 261 + "'. Seems it is missing from the context. Operation: " + getId()); 262 } // else do nothing 263 } else { 264 Field field = entry.getValue(); 265 Class<?> cl = obj.getClass(); 266 if (!field.getType().isAssignableFrom(cl)) { 267 // try to adapt 268 obj = service.getAdaptedValue(ctx, obj, field.getType()); 269 } 270 try { 271 field.set(target, obj); 272 } catch (ReflectiveOperationException e) { 273 throw new OperationException(e); 274 } 275 } 276 } 277 for (Field field : injectableFields) { 278 Object obj = ctx.getAdapter(field.getType()); 279 try { 280 field.set(target, obj); 281 } catch (ReflectiveOperationException e) { 282 throw new OperationException(e); 283 } 284 } 285 } 286 287 @Override 288 public InvokableMethod[] getMethodsMatchingInput(Class<?> in) { 289 List<Match> result = new ArrayList<Match>(); 290 for (InvokableMethod m : methods) { 291 int priority = m.inputMatch(in); 292 if (priority > 0) { 293 result.add(new Match(m, priority)); 294 } 295 } 296 int size = result.size(); 297 if (size == 0) { 298 return null; 299 } 300 if (size == 1) { 301 return new InvokableMethod[] { result.get(0).method }; 302 } 303 Collections.sort(result); 304 InvokableMethod[] ar = new InvokableMethod[result.size()]; 305 for (int i = 0; i < ar.length; i++) { 306 ar[i] = result.get(i).method; 307 } 308 return ar; 309 } 310 311 @Override 312 public OperationDocumentation getDocumentation() { 313 Operation op = type.getAnnotation(Operation.class); 314 OperationDocumentation doc = new OperationDocumentation(op.id()); 315 doc.label = op.label(); 316 doc.requires = op.requires(); 317 doc.category = op.category(); 318 doc.since = op.since(); 319 doc.deprecatedSince = op.deprecatedSince(); 320 doc.addToStudio = op.addToStudio(); 321 doc.setAliases(op.aliases()); 322 doc.implementationClass = type.getName(); 323 if (doc.requires.length() == 0) { 324 doc.requires = null; 325 } 326 if (doc.label.length() == 0) { 327 doc.label = doc.id; 328 } 329 doc.description = op.description(); 330 // load parameters information 331 List<OperationDocumentation.Param> paramsAccumulator = new LinkedList<OperationDocumentation.Param>(); 332 for (Field field : params.values()) { 333 Param p = field.getAnnotation(Param.class); 334 OperationDocumentation.Param param = new OperationDocumentation.Param(); 335 param.name = p.name(); 336 param.description = p.description(); 337 param.type = getParamDocumentationType(field.getType()); 338 param.widget = p.widget(); 339 if (param.widget.length() == 0) { 340 param.widget = null; 341 } 342 param.order = p.order(); 343 param.values = p.values(); 344 param.required = p.required(); 345 paramsAccumulator.add(param); 346 } 347 Collections.sort(paramsAccumulator); 348 doc.params = paramsAccumulator.toArray(new OperationDocumentation.Param[paramsAccumulator.size()]); 349 // load signature 350 ArrayList<String> result = new ArrayList<String>(methods.size() * 2); 351 Collection<String> collectedSigs = new HashSet<String>(); 352 for (InvokableMethod m : methods) { 353 String in = getParamDocumentationType(m.getInputType(), m.isIterable()); 354 String out = getParamDocumentationType(m.getOutputType()); 355 String sigKey = in + ":" + out; 356 if (!collectedSigs.contains(sigKey)) { 357 result.add(in); 358 result.add(out); 359 collectedSigs.add(sigKey); 360 } 361 } 362 doc.signature = result.toArray(new String[result.size()]); 363 // widgets descriptor 364 if (widgetDefinitionList != null) { 365 doc.widgetDefinitions = widgetDefinitionList.toArray(new WidgetDefinition[widgetDefinitionList.size()]); 366 } 367 return doc; 368 } 369 370 @Override 371 public String getContributingComponent() { 372 return contributingComponent; 373 } 374 375 protected String getParamDocumentationType(Class<?> type) { 376 return getParamDocumentationType(type, false); 377 } 378 379 protected String getParamDocumentationType(Class<?> type, boolean isIterable) { 380 String t; 381 if (DocumentModel.class.isAssignableFrom(type) || DocumentRef.class.isAssignableFrom(type)) { 382 t = isIterable ? Constants.T_DOCUMENTS : Constants.T_DOCUMENT; 383 } else if (DocumentModelList.class.isAssignableFrom(type) || DocumentRefList.class.isAssignableFrom(type)) { 384 t = Constants.T_DOCUMENTS; 385 } else if (BlobList.class.isAssignableFrom(type)) { 386 t = Constants.T_BLOBS; 387 } else if (Blob.class.isAssignableFrom(type)) { 388 t = isIterable ? Constants.T_BLOBS : Constants.T_BLOB; 389 } else if (URL.class.isAssignableFrom(type)) { 390 t = Constants.T_RESOURCE; 391 } else if (Calendar.class.isAssignableFrom(type)) { 392 t = Constants.T_DATE; 393 } else { 394 t = type.getSimpleName().toLowerCase(); 395 } 396 return t; 397 } 398 399 @Override 400 public String toString() { 401 return "OperationTypeImpl [id=" + id + ", type=" + type + ", params=" + params + "]"; 402 } 403 404 /** 405 * @since 5.7.2 406 */ 407 @Override 408 public List<InvokableMethod> getMethods() { 409 return methods; 410 } 411}