package nl.ipo.cds.attributemapping.operations.discover.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import nl.ipo.cds.attributemapping.MapperContext; import nl.ipo.cds.attributemapping.MappingDestination; import nl.ipo.cds.attributemapping.MappingSource; import nl.ipo.cds.attributemapping.executer.OperationExecuter; import nl.ipo.cds.attributemapping.executer.OperationExecutionException; import nl.ipo.cds.attributemapping.operations.OperationInputType; import nl.ipo.cds.attributemapping.operations.OperationType; import nl.ipo.cds.attributemapping.operations.annotation.After; import nl.ipo.cds.attributemapping.operations.annotation.Before; import nl.ipo.cds.attributemapping.operations.annotation.Execute; import nl.ipo.cds.attributemapping.operations.annotation.Input; import nl.ipo.cds.attributemapping.operations.annotation.MappingOperation; import nl.ipo.cds.attributemapping.operations.discover.OperationDiscovererException; import org.springframework.context.MessageSource; import org.springframework.context.NoSuchMessageException; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import com.googlecode.gentyref.GenericTypeReflector; public abstract class AnnotationOperationType implements OperationType { private final static ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer (); private final String name; private final MessageSource messageSource; private final Object bean; private final Class<?> cls; private final Method operationMethod; private final List<AnnotationOperationInputType> inputs; private final Class<?> propertiesClass; private final boolean internal; private final Method beforeMethod; private final Method afterMethod; AnnotationOperationType (final Object bean, final String name, final MessageSource messageSource) { this.name = name; this.messageSource = messageSource; this.bean = bean; this.cls = bean.getClass (); this.operationMethod = getOperationMethod (bean); this.internal = isInternal (bean); propertiesClass = getPropertiesClass (bean); this.beforeMethod = getBeforeMethod (bean, propertiesClass); this.afterMethod = getAfterMethod (bean, propertiesClass); // Collect inputs: this.inputs = createInputs (); } public boolean isInternal () { return internal; } @Override public String getName () { return name; } @Override public String getDescription (final Locale locale) { try { return messageSource.getMessage (String.format ("%s.description", getMessageKey ()), null, locale); } catch (NoSuchMessageException e) { return name; } } @Override public String getLabel (final Locale locale) { try { return messageSource.getMessage (String.format ("%s.label", getMessageKey ()), null, locale); } catch (NoSuchMessageException e) { return getDescription (locale); } } @Override public String getFormatLabel (final Locale locale) { try { return messageSource.getMessage (String.format ("%s.formatLabel", getMessageKey ()), null, locale); } catch (NoSuchMessageException e) { return getLabel (locale); } } @Override public Type getReturnType () { return operationMethod.getGenericReturnType (); } @Override public Class<?> getPropertyBeanClass () { final MappingOperation mappingOperation = bean.getClass ().getAnnotation (MappingOperation.class); if (mappingOperation == null || Object.class.equals (mappingOperation.propertiesClass ())) { return null; } return mappingOperation.propertiesClass (); } @Override public List<OperationInputType> getInputs () { return Collections.<OperationInputType>unmodifiableList (inputs); } @Override public OperationExecuter createExecuter (final Object operationProperties, final MapperContext context) { // Substitute: // - Operation properties (static) // - Mapper context (static) // - Source (dynamic) // - Destination (dynamic) // - Inputs (dynamic) final int argumentCount = operationMethod.getParameterTypes().length; final Class<?>[] parameterTypes = operationMethod.getParameterTypes (); // Calculate a mapping from inputs to arguments: final int[] inputMapping = new int[inputs.size ()]; final int inputCount = inputs.size (); for (int i = 0; i < inputs.size (); ++ i) { inputMapping[i] = inputs.get (i).getParameterIndex (); } // Determine a mapping for static properties: final int contextOffset = getParameterIndexOfType (operationMethod, MapperContext.class); final int propertiesOffset = propertiesClass == null ? -1 : getParameterIndexOfType (operationMethod, propertiesClass); // Determine a mapping for dynamic properties: final int sourceOffset = getParameterIndexOfType (operationMethod, MappingSource.class); final int destOffset = getParameterIndexOfType (operationMethod, MappingDestination.class); // Varargs: final boolean isVarArgs = operationMethod.isVarArgs (); final Class<?> varArgsClass; final int copyInputCount; final int varArgsOffset; if (isVarArgs) { varArgsClass = parameterTypes[parameterTypes.length - 1].getComponentType (); copyInputCount = inputCount - 1; varArgsOffset = operationMethod.getParameterTypes().length - 1; } else { varArgsClass = null; copyInputCount = inputCount; varArgsOffset = 0; } return new OperationExecuter() { @Override public Object execute (final MappingSource source, final MappingDestination destination, final List<Object> inputs) throws OperationExecutionException { final Object[] args = new Object[argumentCount]; // Copy the inputs: int i; for (i = 0; i < copyInputCount; ++ i) { args[inputMapping[i]] = inputs.get (i); } if (isVarArgs) { final Object[] a = (Object[])Array.newInstance (varArgsClass, inputs.size () - copyInputCount); for (int len = inputs.size (), j = 0; i < len; ++ i, ++ j) { a[j] = inputs.get (i); } args[varArgsOffset] = a; } // Copy static properties: if (contextOffset >= 0) { args[contextOffset] = context; } if (propertiesOffset >= 0) { args[propertiesOffset] = operationProperties; } // Copy dynamic properties: if (sourceOffset >= 0) { args[sourceOffset] = source; } if (destOffset >= 0) { args[destOffset] = destination; } try { return operationMethod.invoke (bean, args); } catch (Throwable e) { throw new OperationExecutionException (String.format ("An error has occured while executing %s.%s: %s", operationMethod.getDeclaringClass ().getCanonicalName (), operationMethod.getName (), e.getMessage ()), e); } } @Override public void before () throws OperationExecutionException { executeBeforeAfterMethod (beforeMethod); } @Override public void after () throws OperationExecutionException { executeBeforeAfterMethod (afterMethod); } private void executeBeforeAfterMethod (final Method method) throws OperationExecutionException { if (method == null) { return; } try { if (method.getParameterTypes ().length == 1) { method.invoke (bean, operationProperties); } else { method.invoke (bean); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new OperationExecutionException (String.format ("An error has occured while executing %s.%s: %s", operationMethod.getDeclaringClass ().getCanonicalName (), afterMethod.getName (), e.getMessage ()), e); } } }; } public MessageSource getMessageSource () { return messageSource; } public String getMessageKey () { return cls.getCanonicalName (); } public Object getBean () { return bean; } private List<AnnotationOperationInputType> createInputs () { final List<AnnotationOperationInputType> inputTypes = new ArrayList<AnnotationOperationInputType> (); final Set<String> names = new HashSet<String> (); for (final ParameterAnnotation<Input> param: getInputParameters (operationMethod)) { final String name = param.annotation.value ().length() > 0 ? param.annotation.value () : param.name; // Check for duplicate names: if (names.contains (name)) { throw new OperationDiscovererException (String.format ( "Execute method `%s.%s` has a duplicate input parameter named `%s`", operationMethod.getDeclaringClass ().getCanonicalName (), operationMethod.getName (), name )); } names.add (name); inputTypes.add (new AnnotationOperationInputType ( this, name, param.parameterIndex, param.parameterType, param.varArgs )); } return inputTypes; } static boolean isInternal (final Object bean) { final MappingOperation annotation = bean.getClass ().getAnnotation (MappingOperation.class); if (annotation == null) { return false; } return annotation.internal (); } static boolean isInput (final Method method) { return !method.getReturnType ().equals (Void.TYPE) && getInputParameters (method).size () == 0; } static boolean isOutput (final Method method) { return method.getReturnType ().equals (Void.TYPE) && getInputParameters (method).size () == 1; } static boolean isTransform (final Method method) { return !method.getReturnType ().equals (Void.TYPE) && getInputParameters (method).size () > 0; } private static int getParameterIndexOfType (final Method method, final Class<?> cls) { final Class<?>[] parameterTypes = method.getParameterTypes (); final Annotation[][] parameterAnnotations = method.getParameterAnnotations (); for (int i = 0; i < parameterTypes.length; ++ i) { if (parameterTypes[i].equals (cls) && !hasAnnotation (parameterAnnotations[i], Input.class)) { return i; } } return -1; } private static boolean hasAnnotation (final Annotation[] annotations, final Class<? extends Annotation> cls) { for (final Annotation annotation: annotations) { if (cls.isAssignableFrom (annotation.getClass ())) { return true; } } return false; } private static List<ParameterAnnotation<Input>> getInputParameters (final Method method) { final Type[] parameterTypes = method.getGenericParameterTypes (); final String[] parameterNames = parameterNameDiscoverer.getParameterNames (method); final Annotation[][] parameterAnnotations = method.getParameterAnnotations (); final List<ParameterAnnotation<Input>> inputParameters = new ArrayList<ParameterAnnotation<Input>> (); for (int i = 0; i < parameterTypes.length; ++ i) { // Locate the Input annotation: Input annotation = null; for (final Annotation a: parameterAnnotations[i]) { if (a instanceof Input) { annotation = (Input)a; break; } } if (annotation == null) { continue; } final boolean isVarargs = (i == parameterTypes.length - 1 && method.isVarArgs () && GenericTypeReflector.erase (parameterTypes[i]).isArray ()); final Type parameterType; if (isVarargs) { parameterType = GenericTypeReflector.getArrayComponentType (parameterTypes[i]); } else { parameterType = parameterTypes[i]; } inputParameters.add (new ParameterAnnotation<Input> ( parameterType, parameterNames == null ? ("input" + inputParameters.size ()) : parameterNames[i], i, isVarargs, annotation )); } return inputParameters; } /** * Returns the method on the given bean that implements the operation. * * @param bean * @return The method that implements the operation. */ public static Method getOperationMethod (final Object bean) { final Class<?> cls = bean.getClass (); // Look for a method that is annotated with the Execute annotation: final Method annotatedMethod = findMethodWithAnnotation (cls, Execute.class); if (annotatedMethod != null) { validateOperationMethod (annotatedMethod); return annotatedMethod; } // If the bean has a single public method, use that as the operation method: final Method[] methods = cls.getDeclaredMethods (); if (methods.length == 1) { validateOperationMethod (methods[0]); return methods[0]; } throw new OperationDiscovererException (String.format ("Bean of class `%s` has no execute method.", bean.getClass ().getCanonicalName ())); } public static Method getBeforeMethod (final Object bean, final Class<?> propertiesClass) { final Method beforeMethod = findMethodWithAnnotation (bean.getClass (), Before.class); if (beforeMethod != null) { validateBeforeAfterMethod (beforeMethod, propertiesClass); } return beforeMethod; } public static Method getAfterMethod (final Object bean, final Class<?> propertiesClass) { final Method afterMethod = findMethodWithAnnotation (bean.getClass (), After.class); if (afterMethod != null) { validateBeforeAfterMethod (afterMethod, propertiesClass); } return afterMethod; } static void validateOperationMethod (final Method method) { if (method.getReturnType ().equals (Void.TYPE) && getInputParameters (method).size () == 0) { throw new OperationDiscovererException (String.format ("Mapping operation method `%s.%s` must not have a void return value and an empty argument list.", method.getDeclaringClass ().getCanonicalName (), method.getName ())); } if (!isInput (method) && !isOutput (method) && !isTransform (method)) { throw new OperationDiscovererException (String.format ("Mapping operation `%s.%s` has an unknown type", method.getDeclaringClass ().getCanonicalName (), method.getName ())); } } static void validateBeforeAfterMethod (final Method method, final Class<?> propertiesClass) { if (!method.getReturnType ().equals (Void.TYPE)) { throw new OperationDiscovererException (String.format ("Mapping operation method `%s.%s` must have a void return type", method.getDeclaringClass ().getCanonicalName (), method.getName ())); } final Class<?>[] parameterTypes = method.getParameterTypes (); if (parameterTypes.length > 1) { throw new OperationDiscovererException (String.format ("Mapping operation method `%s.%s` can have at most one parameter", method.getDeclaringClass ().getCanonicalName (), method.getName ())); } if (parameterTypes.length == 1 && propertiesClass == null) { throw new OperationDiscovererException (String.format ("Mapping operation method `%s.%s` cannot have an argument: there is no settings class", method.getDeclaringClass ().getCanonicalName (), method.getName ())); } if (parameterTypes.length == 1 && !propertiesClass.isAssignableFrom (parameterTypes[0])) { throw new OperationDiscovererException (String.format ("Mapping operation method `%s.%s` has a settings parameter of wrong type `%s`, should be `%s`", method.getDeclaringClass ().getCanonicalName (), method.getName (), parameterTypes[0], propertiesClass)); } } static Method findMethodWithAnnotation (final Class<?> cls, final Class<? extends Annotation> annotation) { Method result = null; for (final Method method: cls.getMethods ()) { if (method.isAnnotationPresent (annotation)) { if (result != null) { throw new OperationDiscovererException (String.format ("Multiple methods annotated with %s in %s", annotation.getName (), cls.getCanonicalName ())); } result = method; } } return result; } static Class<?> getPropertiesClass (final Object bean) { final Class<?> cls = bean.getClass (); final MappingOperation operation = cls.getAnnotation (MappingOperation.class); if (operation == null || Object.class.equals (operation.propertiesClass ())) { return null; } return operation.propertiesClass (); } private static class ParameterAnnotation<T extends Annotation> { public final Type parameterType; public final String name; public final int parameterIndex; public final T annotation; public final boolean varArgs; ParameterAnnotation (final Type parameterType, final String name, final int parameterIndex, final boolean varArgs, final T annotation) { this.parameterType = parameterType; this.name = name; this.parameterIndex = parameterIndex; this.annotation = annotation; this.varArgs = varArgs; } } }