/* * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * bstefanescu */ package org.nuxeo.ecm.automation.core.impl; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.nuxeo.ecm.automation.AutomationService; import org.nuxeo.ecm.automation.OperationContext; import org.nuxeo.ecm.automation.OperationDocumentation; import org.nuxeo.ecm.automation.OperationException; import org.nuxeo.ecm.automation.OperationType; import org.nuxeo.ecm.automation.OutputCollector; import org.nuxeo.ecm.automation.core.Constants; import org.nuxeo.ecm.automation.core.annotations.Context; import org.nuxeo.ecm.automation.core.annotations.Operation; import org.nuxeo.ecm.automation.core.annotations.OperationMethod; import org.nuxeo.ecm.automation.core.annotations.Param; import org.nuxeo.ecm.automation.core.util.BlobList; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.DocumentRefList; import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition; /** * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> * @author <a href="mailto:grenard@nuxeo.com">Guillaume Renard</a> */ public class OperationTypeImpl implements OperationType { /** * The service that registered the operation */ protected final AutomationService service; /** * The operation ID - used for lookups. */ protected final String id; /** * The operation ID Aliases array. * * @since 7.1 */ protected final String[] aliases; /** * The operation type */ protected final Class<?> type; /** * Injectable parameters. a map between the parameter name and the Field object */ protected final Map<String, Field> params; /** * Invocable methods */ protected List<InvokableMethod> methods; /** * Fields that should be injected from context */ protected List<Field> injectableFields; /** * The input type of a chain/operation. If set, the following input types {"document", "documents", "blob", "blobs"} * for all 'run method(s)' will handled. Other values will be adapted as java.lang.Object. If not set, Automation * will set the input type(s) as the 'run methods(s)' parameter types (by introspection). * * @since 7.4 */ protected String inputType; protected String contributingComponent; protected List<WidgetDefinition> widgetDefinitionList; public OperationTypeImpl(AutomationService service, Class<?> type) { this(service, type, null); } public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent) { this(service, type, contributingComponent, null); } /** * @since 5.9.5 */ public OperationTypeImpl(AutomationService service, Class<?> type, String contributingComponent, List<WidgetDefinition> widgetDefinitionList) { Operation anno = type.getAnnotation(Operation.class); if (anno == null) { throw new IllegalArgumentException( "Invalid operation class: " + type + ". No @Operation annotation found on class."); } this.service = service; this.type = type; this.widgetDefinitionList = widgetDefinitionList; this.contributingComponent = contributingComponent; id = anno.id().length() == 0 ? type.getName() : anno.id(); aliases = anno.aliases(); params = new HashMap<String, Field>(); methods = new ArrayList<InvokableMethod>(); injectableFields = new ArrayList<Field>(); initMethods(); initFields(); } static class Match implements Comparable<Match> { protected InvokableMethod method; int priority; Match(InvokableMethod method, int priority) { this.method = method; this.priority = priority; } @Override public int compareTo(Match o) { return o.priority - priority; } @Override public String toString() { return "Match(" + method + ", " + priority + ")"; } } @Override public AutomationService getService() { return service; } @Override public String getId() { return id; } @Override public String[] getAliases() { return aliases; } @Override public Class<?> getType() { return type; } @Override public String getInputType() { return inputType; } protected void initMethods() { for (Method method : type.getMethods()) { OperationMethod anno = method.getAnnotation(OperationMethod.class); if (anno == null) { // skip method continue; } // register regular method InvokableMethod im = new InvokableMethod(this, method, anno); methods.add(im); // check for iterable input support if (anno.collector() != OutputCollector.class) { // an iterable method - register it im = new InvokableIteratorMethod(this, method, anno); methods.add(im); } } // method order depends on the JDK, make it deterministic Collections.sort(methods); } protected void initFields() { for (Field field : type.getDeclaredFields()) { Param param = field.getAnnotation(Param.class); if (param != null) { field.setAccessible(true); params.put(param.name(), field); } else if (field.isAnnotationPresent(Context.class)) { field.setAccessible(true); injectableFields.add(field); } } } @Override public Object newInstance(OperationContext ctx, Map<String, Object> args) throws OperationException { Object obj; try { obj = type.newInstance(); } catch (ReflectiveOperationException e) { throw new OperationException(e); } inject(ctx, args, obj); return obj; } /** * @since 5.9.2 */ protected Object resolveObject(final OperationContext ctx, final String key, Map<String, ?> args) { Object obj = args.get(key); if (obj != null) { return ctx.resolve(obj); } return ctx.get(key); } public void inject(OperationContext ctx, Map<String, ?> args, Object target) throws OperationException { for (Map.Entry<String, Field> entry : params.entrySet()) { Object obj = resolveObject(ctx, entry.getKey(), args); if (obj == null) { // We did not resolve object according to its param name, let's // check with potential alias String[] aliases = entry.getValue().getAnnotation(Param.class).alias(); if (aliases != null) { for (String alias : entry.getValue().getAnnotation(Param.class).alias()) { obj = resolveObject(ctx, alias, args); if (obj != null) { break; } } } } if (obj == null) { if (entry.getValue().getAnnotation(Param.class).required()) { throw new OperationException("Failed to inject parameter '" + entry.getKey() + "'. Seems it is missing from the context. Operation: " + getId()); } // else do nothing } else { Field field = entry.getValue(); Class<?> cl = obj.getClass(); if (!field.getType().isAssignableFrom(cl)) { // try to adapt obj = service.getAdaptedValue(ctx, obj, field.getType()); } try { field.set(target, obj); } catch (ReflectiveOperationException e) { throw new OperationException(e); } } } for (Field field : injectableFields) { Object obj = ctx.getAdapter(field.getType()); try { field.set(target, obj); } catch (ReflectiveOperationException e) { throw new OperationException(e); } } } @Override public InvokableMethod[] getMethodsMatchingInput(Class<?> in) { List<Match> result = new ArrayList<Match>(); for (InvokableMethod m : methods) { int priority = m.inputMatch(in); if (priority > 0) { result.add(new Match(m, priority)); } } int size = result.size(); if (size == 0) { return null; } if (size == 1) { return new InvokableMethod[] { result.get(0).method }; } Collections.sort(result); InvokableMethod[] ar = new InvokableMethod[result.size()]; for (int i = 0; i < ar.length; i++) { ar[i] = result.get(i).method; } return ar; } @Override public OperationDocumentation getDocumentation() { Operation op = type.getAnnotation(Operation.class); OperationDocumentation doc = new OperationDocumentation(op.id()); doc.label = op.label(); doc.requires = op.requires(); doc.category = op.category(); doc.since = op.since(); doc.deprecatedSince = op.deprecatedSince(); doc.addToStudio = op.addToStudio(); doc.setAliases(op.aliases()); doc.implementationClass = type.getName(); if (doc.requires.length() == 0) { doc.requires = null; } if (doc.label.length() == 0) { doc.label = doc.id; } doc.description = op.description(); // load parameters information List<OperationDocumentation.Param> paramsAccumulator = new LinkedList<OperationDocumentation.Param>(); for (Field field : params.values()) { Param p = field.getAnnotation(Param.class); OperationDocumentation.Param param = new OperationDocumentation.Param(); param.name = p.name(); param.description = p.description(); param.type = getParamDocumentationType(field.getType()); param.widget = p.widget(); if (param.widget.length() == 0) { param.widget = null; } param.order = p.order(); param.values = p.values(); param.required = p.required(); paramsAccumulator.add(param); } Collections.sort(paramsAccumulator); doc.params = paramsAccumulator.toArray(new OperationDocumentation.Param[paramsAccumulator.size()]); // load signature ArrayList<String> result = new ArrayList<String>(methods.size() * 2); Collection<String> collectedSigs = new HashSet<String>(); for (InvokableMethod m : methods) { String in = getParamDocumentationType(m.getInputType(), m.isIterable()); String out = getParamDocumentationType(m.getOutputType()); String sigKey = in + ":" + out; if (!collectedSigs.contains(sigKey)) { result.add(in); result.add(out); collectedSigs.add(sigKey); } } doc.signature = result.toArray(new String[result.size()]); // widgets descriptor if (widgetDefinitionList != null) { doc.widgetDefinitions = widgetDefinitionList.toArray(new WidgetDefinition[widgetDefinitionList.size()]); } return doc; } @Override public String getContributingComponent() { return contributingComponent; } protected String getParamDocumentationType(Class<?> type) { return getParamDocumentationType(type, false); } protected String getParamDocumentationType(Class<?> type, boolean isIterable) { String t; if (DocumentModel.class.isAssignableFrom(type) || DocumentRef.class.isAssignableFrom(type)) { t = isIterable ? Constants.T_DOCUMENTS : Constants.T_DOCUMENT; } else if (DocumentModelList.class.isAssignableFrom(type) || DocumentRefList.class.isAssignableFrom(type)) { t = Constants.T_DOCUMENTS; } else if (BlobList.class.isAssignableFrom(type)) { t = Constants.T_BLOBS; } else if (Blob.class.isAssignableFrom(type)) { t = isIterable ? Constants.T_BLOBS : Constants.T_BLOB; } else if (URL.class.isAssignableFrom(type)) { t = Constants.T_RESOURCE; } else if (Calendar.class.isAssignableFrom(type)) { t = Constants.T_DATE; } else { t = type.getSimpleName().toLowerCase(); } return t; } @Override public String toString() { return "OperationTypeImpl [id=" + id + ", type=" + type + ", params=" + params + "]"; } /** * @since 5.7.2 */ @Override public List<InvokableMethod> getMethods() { return methods; } }