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