/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * bstefanescu */ package org.eclipse.ecr.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.List; import java.util.Map; import org.eclipse.ecr.automation.AutomationService; import org.eclipse.ecr.automation.OperationContext; import org.eclipse.ecr.automation.OperationDocumentation; import org.eclipse.ecr.automation.OperationException; import org.eclipse.ecr.automation.OperationType; import org.eclipse.ecr.automation.OutputCollector; import org.eclipse.ecr.automation.core.Constants; import org.eclipse.ecr.automation.core.annotations.Context; import org.eclipse.ecr.automation.core.annotations.Operation; import org.eclipse.ecr.automation.core.annotations.OperationMethod; import org.eclipse.ecr.automation.core.annotations.Param; import org.eclipse.ecr.automation.core.scripting.Expression; import org.eclipse.ecr.automation.core.util.BlobList; import org.eclipse.ecr.core.api.Blob; import org.eclipse.ecr.core.api.DocumentModel; import org.eclipse.ecr.core.api.DocumentModelList; import org.eclipse.ecr.core.api.DocumentRef; import org.eclipse.ecr.core.api.DocumentRefList; /** * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ public class OperationTypeImpl implements OperationType { /** * The service that registered the operation */ protected AutomationService service; /** * The operation ID - used for lookups. */ protected String id; /** * The operation type */ protected Class<?> type; /** * Injectable parameters. a map between the parameter name and the Field * object */ protected Map<String, Field> params; /** * Invocable methods */ protected List<InvokableMethod> methods; /** * Fields that should be injected from context */ protected List<Field> injectableFields; public OperationTypeImpl(AutomationService service, Class<?> type) { 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; id = anno.id(); if (id.length() == 0) { id = type.getName(); } params = new HashMap<String, Field>(); methods = new ArrayList<InvokableMethod>(); injectableFields = new ArrayList<Field>(); initMethods(); initFields(); } public AutomationService getService() { return service; } public String getId() { return id; } public Class<?> getType() { return type; } 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); } } } 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); } } } public Object newInstance(OperationContext ctx, Map<String, Object> args) throws Exception { Object obj = type.newInstance(); inject(ctx, args, obj); return obj; } public void inject(OperationContext ctx, Map<String, Object> args, Object target) throws Exception { for (Map.Entry<String, Field> entry : params.entrySet()) { Object obj = args.get(entry.getKey()); if (obj instanceof Expression) { obj = ((Expression) obj).eval(ctx); } 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()); } field.set(target, obj); } } for (Field field : injectableFields) { Object obj = ctx.getAdapter(field.getType()); field.set(target, obj); } } public List<InvokableMethod> getMethods() { return methods; } 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; } static class Match implements Comparable<Match> { protected InvokableMethod method; int priority; Match(InvokableMethod method, int priority) { this.method = method; this.priority = priority; } public int compareTo(Match o) { return o.priority - priority; } } 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(); if (doc.requires.length() == 0) { doc.requires = null; } if (doc.label.length() == 0) { doc.label = doc.id; } doc.description = op.description(); // load parameters information doc.params = new ArrayList<OperationDocumentation.Param>(); for (Field field : params.values()) { Param p = field.getAnnotation(Param.class); OperationDocumentation.Param param = new OperationDocumentation.Param(); param.name = p.name(); 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.isRequired = p.required(); doc.params.add(param); } Collections.sort(doc.params); // 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()]); return doc; } 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; } }