View Javadoc

1   package cz.cuni.amis.pogamut.sposh.executor;
2   
3   import cz.cuni.amis.pogamut.sposh.elements.EnumValue;
4   import cz.cuni.amis.pogamut.sposh.engine.VariableContext;
5   import cz.cuni.amis.pogamut.sposh.exceptions.FubarException;
6   import cz.cuni.amis.pogamut.sposh.exceptions.MethodException;
7   import java.lang.annotation.Annotation;
8   import java.lang.reflect.Field;
9   import java.lang.reflect.InvocationTargetException;
10  import java.lang.reflect.Method;
11  import java.lang.reflect.Modifier;
12  import java.util.LinkedList;
13  import java.util.List;
14  import java.util.logging.Level;
15  import java.util.logging.Logger;
16  
17  /**
18   *
19   * @author Honza
20   */
21  class ParamsMethod<RETURN> {
22  
23      private final Class<?>[] allowedParamClasses = new Class<?>[]{
24          String.class,
25          Integer.class,
26          int.class,
27          Double.class,
28          double.class,};
29      private final String methodName;
30      private final Class<RETURN> returnCls;
31      private final Class<?> methodClass;
32      private final Method method;
33  
34      ParamsMethod(Class methodClass, String methodName, Class<RETURN> returnCls) {
35          this.methodClass = methodClass;
36          this.methodName = methodName;
37          this.returnCls = returnCls;
38  
39          this.method = findMethod();
40      }
41  
42      /**
43       * Is every parameter of the @method among @acceptedTypes?
44       *
45       * @param method Method whose parameters are checked
46       * @param isEnumAcceptable Is an enum acceptable parameter?
47       * @param acceptedTypes Acceptable types of parameters
48       * @return
49       */
50      private boolean areParamsAcceptable(Method method, boolean isEnumAcceptable, Class<?>... acceptedTypes) {
51          for (Class<?> paramType : method.getParameterTypes()) {
52              boolean paramAcceptable = false;
53              for (Class<?> acceptedType : acceptedTypes) {
54                  if (paramType.equals(acceptedType)) {
55                      paramAcceptable = true;
56                  }
57              }
58              if (isEnumAcceptable && paramType.isEnum() && Modifier.isPublic(paramType.getModifiers())) {
59                  paramAcceptable = true;
60              }
61              if (!paramAcceptable) {
62                  return false;
63              }
64          }
65          return true;
66      }
67  
68      /**
69       * Find @seekedAnnotation among array of passed annotations and return it.
70       *
71       * @param <T> Type of annotation.
72       * @param annotations Array of annotations that are being searched in.
73       * @param seekedAnnotation Type of annotation this method is looking for
74       * @return Return found annotation or null if not present.
75       */
76      private <T extends Annotation> T getAnnotation(Annotation[] annotations, Class<T> seekedAnnotation) {
77          for (Annotation annotation : annotations) {
78              if (annotation.annotationType().equals(seekedAnnotation)) {
79                  return (T) annotation;
80              }
81          }
82          return null;
83      }
84  
85      /**
86       * Are all parameters of @method annotated with {@link Param}?
87       *
88       * @param method Method whose parameters are checked.
89       * @return
90       */
91      private boolean areParamsAnnotated(Method method) {
92          for (Annotation[] paramAnnotations : method.getParameterAnnotations()) {
93              if (getAnnotation(paramAnnotations, Param.class) == null) {
94                  return false;
95              }
96          }
97          return true;
98      }
99  
100     /**
101      * Filter all methods to ensure following conditions:
102      *
103      * <ul><li>Method name is @name</li>
104      *
105      * <li>Method is <tt>public<tt></li>
106      *
107      * <li>Method is not <tt>abstract</tt></li>.
108      *
109      * <li>Method does not have variable number of arguments</li>
110      *
111      * <li>Return type of method is @returnType</li>
112      *
113      * <li>All parameter fields are of type {@link String}, {@link Integer} or {@link Double}</li>
114      *
115      * <li>All parameters are annotated with {@link Param}</li> </ul>
116      *
117      * @param methods Array of methods to be filtered.
118      * @param seekedName Name of the methods we are looking for.
119      * @param returnType Expected return type of filtered methods.
120      * @return Array of methods with @name.
121      */
122     private Method[] filterMethods(Method[] methods, String seekedName, Class<?> returnType) {
123         List<Method> filteredMethods = new LinkedList<Method>();
124 
125         for (Method testedMethod : methods) {
126             String testedMethodName = testedMethod.getName();
127             boolean methodIsPublic = (testedMethod.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC;
128             boolean methodIsAbstract = (testedMethod.getModifiers() & Modifier.ABSTRACT) == Modifier.ABSTRACT;
129             boolean correctReturnType = returnType.isAssignableFrom(testedMethod.getReturnType());
130             boolean acceptedParams = areParamsAcceptable(testedMethod, true, allowedParamClasses);
131             boolean annotatedParams = areParamsAnnotated(testedMethod);
132 
133             if (testedMethodName.equals(seekedName)
134                     && methodIsPublic
135                     && !methodIsAbstract
136                     && !testedMethod.isVarArgs()
137                     && correctReturnType
138                     && acceptedParams
139                     && annotatedParams) {
140                 filteredMethods.add(testedMethod);
141             }
142         }
143         return filteredMethods.toArray(new Method[filteredMethods.size()]);
144     }
145 
146     /**
147      * Go through all methods of the {@link #methodClass} and find the one which
148      * has suits our tastes (see {@link #filterMethods(java.lang.reflect.Method[], java.lang.String, java.lang.Class)
149      * }) for details.
150      *
151      * @throws NoSuchMethodError If no such method exists
152      * @throws UnsupportedOperationException If there is more than one such
153      * method.
154      *
155      * @return Found method.
156      */
157     final Method findMethod() {
158         Method[] methods = filterMethods(methodClass.getMethods(), methodName, returnCls);
159         if (methods.length == 0) {
160             throw new NoSuchMethodError("Unable to find method " + methodName);
161         }
162         if (methods.length > 1) {
163             throw new UnsupportedOperationException("Multiple (" + methods.length + ") possible " + methodName + " methods, overloading is not supported.");
164         }
165         return methods[0];
166     }
167 
168     /**
169      * Take the passed @thisObject and call parametrized method on it.
170      *
171      * @param params Variable context that will fill the parameters of the
172      * method with its variables ({@link Param#value() ) is used as name of variable in the context.
173      * @throws InvocationTargetException When invoked method throws an
174      * exception.
175      */
176     public final RETURN invoke(Object thisObject, VariableContext params) throws InvocationTargetException {
177         Class<?>[] paramTypes = method.getParameterTypes();
178         Annotation[][] paramsAnnotations = method.getParameterAnnotations();
179 
180         assert paramsAnnotations.length == paramTypes.length;
181 
182         int paramCount = paramTypes.length;
183 
184         List methodArguments = new LinkedList();
185         for (int paramIndex = 0; paramIndex < paramCount; ++paramIndex) {
186             Annotation[] paramAnnotations = paramsAnnotations[paramIndex];
187             Param param = getAnnotation(paramAnnotations, Param.class);
188 
189             String variableName = param.value();
190             Object argumentValue = getArgumentValue(thisObject, params, paramTypes[paramIndex], variableName);
191             methodArguments.add(argumentValue);
192         }
193 
194         try {
195             Object ret = method.invoke(thisObject, methodArguments.toArray());
196             return (RETURN) ret;
197         } catch (IllegalAccessException ex) {
198             throw new FubarException("findMethod filters for public methods", ex);
199         } catch (IllegalArgumentException ex) {
200             throw new FubarException("Error with parameter maching code", ex);
201         } catch (InvocationTargetException ex) {
202             throw ex;
203         }
204     }
205 
206     private Object getArgumentValue(Object thisObject, VariableContext ctx, Class<?> paramType, String variableName) {
207         try {
208             Object argumentValue = ctx.getValue(variableName);
209             // If the parameter is an enum, the argument passed should be string containing FQN
210             // of the enum value.
211             if (paramType.isEnum()) {
212                 if (!argumentValue.getClass().equals(EnumValue.class)) {
213                     throw new MethodException("Variable " + variableName + " should be an " + EnumValue.class.getSimpleName() + " string containing the FQN of an enum value.");
214                 }
215                 argumentValue = convertToEnumConstant((Class<Enum>)paramType, (EnumValue) argumentValue);
216             }
217 
218             return argumentValue;
219         } catch (IllegalArgumentException ex) {
220             String thisObjectName = thisObject.getClass().getName();
221             throw new MethodException("No variable " + variableName + " for " + thisObjectName + '.' + methodName + '.', ex);
222         }
223     }
224 
225     /**
226      *
227      * @param enumClass Class of an enum.
228      * @param enumValueFQN Fully qualified name of an value of the enum.
229      * @return
230      * @throws IllegalArgumentException When enumValueFQN doesn't match the
231      * enumClass and its values.
232      */
233     private Object convertToEnumConstant(Class<Enum> enumClass, EnumValue enumValue) {
234         String enumValueFQNPrefix = enumClass.getName() + '.';
235         String enumValueFQN = enumValue.getName();
236         if (!enumValueFQN.startsWith(enumValueFQNPrefix)) {
237             throw new IllegalArgumentException("Unable to convert \"" + enumValueFQN + "\" to the value of enum " + enumClass.getName() + ".");
238         }
239         String enumName = enumValueFQN.substring(enumValueFQNPrefix.length());
240         return Enum.valueOf(enumClass, enumName);
241     }
242 }