View Javadoc

1   /*
2    * Copyright 2006-2016 The JGUIraffe Team.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.jguiraffe.di;
17  
18  import java.lang.reflect.AccessibleObject;
19  import java.lang.reflect.Constructor;
20  import java.lang.reflect.Method;
21  import java.util.Arrays;
22  import java.util.Iterator;
23  import java.util.List;
24  
25  import org.apache.commons.beanutils.PropertyUtils;
26  import org.apache.commons.lang.ClassUtils;
27  
28  /**
29   * <p>
30   * A helper class providing some more complex functionality related to
31   * reflection.
32   * </p>
33   * <p>
34   * This class builds on top of {@link ReflectionUtils} which implements
35   * low-level utility methods for various operations related to reflection.
36   * {@code InvocationHelper} in contrast focuses on some more advanced
37   * operations. It deals with stuff like finding suitable methods and
38   * constructors to be invoked, getting and setting properties, and data type
39   * conversions (this is delegated to an instance of {@link ConversionHelper}
40   * which is maintained by this class).
41   * </p>
42   * <p>
43   * Implementation note: This class is thread-safe.
44   * </p>
45   *
46   * @author Oliver Heger
47   * @version $Id: InvocationHelper.java 205 2012-01-29 18:29:57Z oheger $
48   */
49  public class InvocationHelper
50  {
51      /** Constant for a format string for generating a method signature. */
52      private static final String METHOD_SIGNATURE = "%s.%s(%s), arguments: %s";
53  
54      /** Constant for the name of a method representing a constructor. */
55      private static final String CONSTR_METHOD_NAME = "<init>";
56  
57      /** Stores the associated {@code ConversionHelper} instance. */
58      private final ConversionHelper conversionHelper;
59  
60      /**
61       * Creates a new instance of {@code InvocationHelper}. A default
62       * {@code ConversionHelper} instance is set.
63       */
64      public InvocationHelper()
65      {
66          this(null);
67      }
68  
69      /**
70       * Creates a new instance of {@code InvocationHelper} and initializes it
71       * with the given {@code ConversionHelper} instance. If no helper object is
72       * provided, a new default instance is created.
73       *
74       * @param convHlp the {@code ConversionHelper}
75       */
76      public InvocationHelper(ConversionHelper convHlp)
77      {
78          conversionHelper = (convHlp != null) ? convHlp : new ConversionHelper();
79      }
80  
81      /**
82       * Returns the {@code ConversionHelper} instance associated with this
83       * object.
84       *
85       * @return the {@code ConversionHelper}
86       */
87      public ConversionHelper getConversionHelper()
88      {
89          return conversionHelper;
90      }
91  
92      /**
93       * Determines the method to be called given the name, the parameter types,
94       * and the arguments. This method delegates to
95       * {@link ReflectionUtils#findMethods(Class, String, Class[], boolean)} to
96       * find a single method that matches the specified method signature. It also
97       * takes the specified method arguments into account in order to find a
98       * unique match. If this is not possible, an exception is thrown.
99       *
100      * @param targetClass the class on which to invoke the method
101      * @param methodName the name of the method to be invoked
102      * @param parameterTypes an array with the known parameter types (may be
103      *        <b>null</b> or contain <b>null</b> entries representing wild
104      *        cards)
105      * @param args an array with the method arguments
106      * @return the method to be invoked
107      * @throws InjectionException if the method cannot be determined
108      * @throws IllegalArgumentException if the target class is <b>null</b>
109      */
110     public Method findUniqueMethod(Class<?> targetClass, String methodName,
111             Class<?>[] parameterTypes, Object[] args)
112     {
113         List<Method> methods =
114                 findMethods(targetClass, methodName, parameterTypes, args, true);
115         if (methods.isEmpty())
116         {
117             // try again with relaxed type checking
118             methods =
119                     findMethods(targetClass, methodName, parameterTypes, args,
120                             false);
121             if (methods.isEmpty())
122             {
123                 throw nonUniqueMethodException(methods, targetClass,
124                         methodName, parameterTypes, args);
125             }
126         }
127 
128         if (methods.size() > 1)
129         {
130             // apply the types of the current call arguments
131             Class<?>[] callTypes =
132                     parameterTypesFromArguments(parameterTypes, args);
133             methods =
134                     findMethods(targetClass, methodName, callTypes, args, false);
135         }
136 
137         if (methods.size() != 1)
138         {
139             throw nonUniqueMethodException(methods, targetClass, methodName,
140                     parameterTypes, args);
141         }
142 
143         return methods.get(0);
144     }
145 
146     /**
147      * Tries to match a single constructor of the specified target class given
148      * the parameter types and the call arguments. This method works analogously
149      * to {@link #findUniqueMethod(Class, String, Class[], Object[])}, but it
150      * searches for a matching constructor.
151      *
152      * @param <T> the type of the target class
153      * @param targetClass the class on which to invoke the method
154      * @param parameterTypes an array with the known parameter types (may be
155      *        <b>null</b> or contain <b>null</b> entries representing wild
156      *        cards)
157      * @param args an array with the call arguments
158      * @return the unique constructor matching the specified criteria
159      * @throws InjectionException if no unique constructor can be determined
160      * @throws IllegalArgumentException if the target class is <b>null</b>
161      */
162     public <T> Constructor<T> findUniqueConstructor(Class<T> targetClass,
163             Class<?>[] parameterTypes, Object[] args)
164     {
165         // This code is pretty similar to the one of findUniqueMethod().
166         // Unfortunately, it cannot easily be generalized. This is because
167         // Method and Constructor do not share a common super class that allows
168         // access to the parameter types.
169         List<Constructor<T>> constrs =
170                 findConstructors(targetClass, parameterTypes, args, true);
171         if (constrs.isEmpty())
172         {
173             constrs =
174                     findConstructors(targetClass, parameterTypes, args, false);
175             if (constrs.isEmpty())
176             {
177                 throw nonUniqueMethodException(constrs, targetClass,
178                         CONSTR_METHOD_NAME, parameterTypes, args);
179             }
180         }
181 
182         if (constrs.size() > 1)
183         {
184             Class<?>[] callTypes =
185                     parameterTypesFromArguments(parameterTypes, args);
186             constrs = findConstructors(targetClass, callTypes, args, false);
187         }
188 
189         if (constrs.size() != 1)
190         {
191             throw nonUniqueMethodException(constrs, targetClass,
192                     CONSTR_METHOD_NAME, parameterTypes, args);
193         }
194 
195         return constrs.get(0);
196     }
197 
198     /**
199      * Invokes the specified static method and returns its result.
200      *
201      * @param targetClass the target class on which to invoke the method (must
202      *        not be <b>null</b>)
203      * @param methodName the name of the method
204      * @param parameterTypes an array with the known parameter types (may be
205      *        <b>null</b> or contain <b>null</b> entries representing wild
206      *        cards)
207      * @param args an array with the method arguments
208      * @return the result returned by the method
209      * @throws InjectionException if an exception occurs
210      * @throws IllegalArgumentException if the target class is undefined
211      */
212     public Object invokeStaticMethod(Class<?> targetClass, String methodName,
213             Class<?>[] parameterTypes, Object[] args)
214     {
215         return invokeMethod(targetClass, null, methodName, parameterTypes, args);
216     }
217 
218     /**
219      * Invokes a method on the specified object. The object must not be
220      * <b>null</b> because the target class is obtained from it. Then this
221      * implementation delegates to
222      * {@link #invokeMethod(Class, Object, String, Class[], Object[])}.
223      *
224      * @param instance the instance on which to invoke the method
225      * @param methodName the name of the method to be invoked
226      * @param parameterTypes an array with the known parameter types (may be
227      *        <b>null</b> or contain <b>null</b> entries representing wild
228      *        cards)
229      * @param args an array with the method arguments
230      * @return the result returned by the method
231      * @throws InjectionException if an exception occurs
232      * @throws IllegalArgumentException if the instance is undefined
233      */
234     public Object invokeInstanceMethod(Object instance, String methodName,
235             Class<?>[] parameterTypes, Object[] args)
236     {
237         return invokeMethod(null, instance, methodName, parameterTypes, args);
238     }
239 
240     /**
241      * Invokes a method. This is the most generic form of invoking a method. It
242      * works with both static and instance methods. This method delegates to
243      * {@link #findUniqueMethod(Class, String, Class[], Object[])} to find the
244      * method to be invoked. Then it performs necessary type conversions of the
245      * parameters. Eventually, it invokes the method. Using this method it is
246      * possible to invoke a specific method in a given class, even if the
247      * parameter types are not or only partly known. All possible exceptions
248      * (e.g. no unique method is found, the parameters cannot be converted,
249      * reflection-related exceptions) are thrown as {@link InjectionException}
250      * exceptions.
251      *
252      * @param targetClass the target class on which to invoke the method (may be
253      *        <b>null</b> if an object instance is provided)
254      * @param instance the instance on which to invoke the method (may be
255      *        <b>null</b> for static methods)
256      * @param methodName the name of the method to be invoked
257      * @param parameterTypes an array with the known parameter types (may be
258      *        <b>null</b> or contain <b>null</b> entries representing wild
259      *        cards)
260      * @param args an array with the method arguments
261      * @return the result returned by the method
262      * @throws InjectionException if an exception occurs
263      * @throws IllegalArgumentException if a required parameter is missing or
264      *         the specification of the method to be called is invalid
265      */
266     public Object invokeMethod(Class<?> targetClass, Object instance,
267             String methodName, Class<?>[] parameterTypes, Object[] args)
268     {
269         Class<?> clsToInvoke = getClassToInvoke(targetClass, instance);
270         Method method =
271                 findUniqueMethod(clsToInvoke, methodName, parameterTypes, args);
272         Object[] convertedArgs =
273                 convertArguments(method.getParameterTypes(), args);
274         return ReflectionUtils.invokeMethod(method, instance, convertedArgs);
275     }
276 
277     /**
278      * Invokes a constructor. This method delegates to
279      * {@link #findUniqueConstructor(Class, Class[], Object[])} to obtain the
280      * constructor to be invoked. Then it performs necessary type conversions of
281      * the parameters. Eventually, it invokes the constructor and returns the
282      * newly created instance. Using this method it is possible to invoke a
283      * specific constructor of a given class, even if the parameter types are
284      * not or only partly known. All possible exceptions (e.g. no unique method
285      * is found, the parameters cannot be converted, reflection-related
286      * exceptions) are thrown as {@link InjectionException} exceptions.
287      *
288      * @param <T> the type of the target class
289      * @param targetClass the target class on which the constructor is to be
290      *        invoked
291      * @param parameterTypes an array with the known parameter types (may be
292      *        <b>null</b> or contain <b>null</b> entries representing wild
293      *        cards)
294      * @param args an array with the method arguments
295      * @return the newly created instance
296      * @throws InjectionException if an exception occurs
297      * @throws IllegalArgumentException if the specification of the constructor
298      *         is invalid
299      */
300     public <T> T invokeConstructor(Class<T> targetClass,
301             Class<?>[] parameterTypes, Object[] args)
302     {
303         Constructor<T> ctor =
304                 findUniqueConstructor(targetClass, parameterTypes, args);
305         Object[] convertedArgs =
306                 convertArguments(ctor.getParameterTypes(), args);
307         return ReflectionUtils.invokeConstructor(ctor, convertedArgs);
308     }
309 
310     /**
311      * Sets a property of the given bean. This method obtains the set method for
312      * the property in question. If necessary, type conversion is performed.
313      * Then the set method is invoked so that the new value of the property is
314      * written. Occurring exceptions are redirected either as
315      * {@link InjectionException} (if they are related to reflection operations)
316      * or as {@code IllegalArgumentException} if they are related to the
317      * parameters passed to this method.
318      *
319      * @param bean the bean on which to set the property
320      * @param property the name of the property to be set
321      * @param value the value of the property
322      * @throws InjectionException if an error occurs related to reflection
323      * @throws IllegalArgumentException if invalid arguments are passed in
324      */
325     public void setProperty(Object bean, String property, Object value)
326     {
327         Class<?> propertyType = getPropertyType(bean, property);
328         Object convertedValue =
329                 getConversionHelper().convert(propertyType, value);
330         ReflectionUtils.setProperty(bean, property, convertedValue);
331     }
332 
333     /**
334      * Performs necessary type conversions before invoking a method. This method
335      * is called before a method or a constructor is invoked. The passed in
336      * parameter types are the actual parameters of the method to be invoked,
337      * the arguments are the ones passed by the caller. They may require a type
338      * conversion. This implementation performs this conversion if necessary.
339      *
340      * @param parameterTypes the array with the parameter types
341      * @param args the array with the call arguments
342      * @return an array with the converted arguments
343      * @throws InjectionException if a conversion fails
344      */
345     protected Object[] convertArguments(Class<?>[] parameterTypes, Object[] args)
346     {
347         assert arrayLength(parameterTypes) == arrayLength(args) : "Different array lengths!";
348         Object[] results = null;
349 
350         for (int i = 0; i < parameterTypes.length; i++)
351         {
352             Object convValue =
353                     getConversionHelper().convert(parameterTypes[i], args[i]);
354 
355             if (convValue != args[i])
356             {
357                 if (results == null)
358                 {
359                     // lazy create results array
360                     results = new Object[args.length];
361                     System.arraycopy(args, 0, results, 0, args.length);
362                 }
363                 results[i] = convValue;
364             }
365         }
366 
367         return (results != null) ? results : args;
368     }
369 
370     /**
371      * Helper method for finding methods that match given criteria. This method
372      * delegates to {@link ReflectionUtils} for doing the lookup. Then it sorts
373      * out the hits that are incompatible with the passed in parameters.
374      * Finally, it deals with methods with identical signatures but different
375      * return types.
376      *
377      * @param targetClass the target class
378      * @param methodName the name of the method
379      * @param parameterTypes an array with parameter types
380      * @param args concrete parameters to be passed to the method
381      * @param exactMatch a flag whether an exact match is to be performed
382      * @return the list with found methods
383      */
384     private static List<Method> findMethods(Class<?> targetClass,
385             String methodName, Class<?>[] parameterTypes, Object[] args,
386             boolean exactMatch)
387     {
388         List<Method> methods =
389                 ReflectionUtils.findMethods(targetClass, methodName,
390                         parameterTypes, exactMatch);
391 
392         for (Iterator<Method> it = methods.iterator(); it.hasNext();)
393         {
394             Method m = it.next();
395             if (!checkArgumentsForCompatibility(m.getParameterTypes(), args))
396             {
397                 it.remove();
398             }
399         }
400 
401         return ReflectionUtils.removeCovariantDuplicates(methods);
402     }
403 
404     /**
405      * Helper method for finding constructors that match given criteria. Works
406      * analogously to
407      * {@link #findMethods(Class, String, Class[], Object[], boolean)}, but
408      * operates on constructors.
409      *
410      * @param <T> the type of the target class
411      * @param targetClass the target class
412      * @param parameterTypes an array with parameter types
413      * @param args concrete parameters to be passed to the constructor
414      * @param exactMatch a flag whether an exact match is to be performed
415      * @return the list with found constructors
416      */
417     private static <T> List<Constructor<T>> findConstructors(
418             Class<T> targetClass, Class<?>[] parameterTypes, Object[] args,
419             boolean exactMatch)
420     {
421         List<Constructor<T>> constrs =
422                 ReflectionUtils.findConstructors(targetClass, parameterTypes,
423                         exactMatch);
424 
425         for (Iterator<Constructor<T>> it = constrs.iterator(); it.hasNext();)
426         {
427             Constructor<T> c = it.next();
428             if (!checkArgumentsForCompatibility(c.getParameterTypes(), args))
429             {
430                 it.remove();
431             }
432         }
433 
434         return constrs;
435     }
436 
437     /**
438      * Tests whether the specified arguments are compatible with the given
439      * method signature. This method mainly checks whether a null value is
440      * assigned to a parameter of primitive type. Further type checks are not
441      * performed because there may be type conversions later.
442      *
443      * @param parameterTypes the parameter types of the method
444      * @param args the arguments to be passed to the method
445      * @return a flag whether these arguments are compatible with the method
446      */
447     private static boolean checkArgumentsForCompatibility(
448             Class<?>[] parameterTypes, Object[] args)
449     {
450         if (arrayLength(parameterTypes) != arrayLength(args))
451         {
452             // number of parameters does not match
453             return false;
454         }
455 
456         for (int i = 0; i < arrayLength(parameterTypes); i++)
457         {
458             if (args[i] == null && parameterTypes[i].isPrimitive())
459             {
460                 return false;
461             }
462         }
463 
464         return true;
465     }
466 
467     /**
468      * Derives parameter types from the concrete parameters. For each parameter
469      * type whose class is not specified the corresponding type from the
470      * argument is set - unless it is undefined, too.
471      *
472      * @param parameterTypes the array with parameter types
473      * @param args the concrete call arguments
474      * @return an array with enhanced parameter type information
475      */
476     private static Class<?>[] parameterTypesFromArguments(
477             Class<?>[] parameterTypes, Object[] args)
478     {
479         if (arrayLength(args) == 0)
480         {
481             return parameterTypes;
482         }
483 
484         Class<?>[] newTypes = new Class<?>[arrayLength(args)];
485         System.arraycopy(parameterTypes, 0, newTypes, 0, newTypes.length);
486         for (int i = 0; i < newTypes.length; i++)
487         {
488             if (newTypes[i] == null && args[i] != null)
489             {
490                 newTypes[i] = args[i].getClass();
491             }
492         }
493 
494         return newTypes;
495     }
496 
497     /**
498      * Obtains the class to be invoked from the parameters of a method
499      * invocation. If both a target class and an instance are provided, they are
500      * checked for compatibility. If a target class was specified, it is used;
501      * otherwise the class from the instance is obtained.
502      *
503      * @param targetClass the target class
504      * @param instance the object instance
505      * @return the class to be invoked
506      * @throws IllegalArgumentException if the parameters are invalid
507      */
508     private static Class<?> getClassToInvoke(Class<?> targetClass,
509             Object instance)
510     {
511         if (targetClass == null && instance == null)
512         {
513             throw new IllegalArgumentException(
514                     "Neither target class nor instance provided!");
515         }
516 
517         Class<?> clsToInvoke;
518         if (targetClass != null)
519         {
520             if (instance != null
521                     && !ClassUtils.isAssignable(instance.getClass(),
522                             targetClass))
523             {
524                 throw new IllegalArgumentException("Target class "
525                         + targetClass + " is not compatible with instance "
526                         + instance);
527             }
528             clsToInvoke = targetClass;
529         }
530         else
531         {
532             clsToInvoke = instance.getClass();
533         }
534         return clsToInvoke;
535     }
536 
537     /**
538      * Determines the type of the specified property.
539      *
540      * @param bean the bean
541      * @param property the name of the property
542      * @return the data type of this property
543      * @throws IllegalArgumentException if parameters are invalid
544      * @throws InjectionException if an error occurs
545      */
546     private static Class<?> getPropertyType(Object bean, String property)
547     {
548         Class<?> propertyType;
549         try
550         {
551             propertyType = PropertyUtils.getPropertyType(bean, property);
552         }
553         catch (IllegalArgumentException iex)
554         {
555             throw iex;
556         }
557         catch (Exception ex)
558         {
559             // any other exception related to reflection is redirected
560             throw new InjectionException(
561                     "Error when determining type of property " + property
562                             + " on bean " + bean, ex);
563         }
564 
565         if (propertyType == null)
566         {
567             throw new InjectionException("Cannot determine type of property "
568                     + property + " on bean " + bean);
569         }
570         return propertyType;
571     }
572 
573     /**
574      * Helper method for generating a string representation for a method
575      * signature.
576      *
577      * @param targetClass the class the method belongs to
578      * @param methodName the name of the method
579      * @param paramTypes the array with parameter types
580      * @param args the array with call arguments
581      * @return a string representation of the method signature
582      */
583     private static String methodSignature(Class<?> targetClass,
584             String methodName, Class<?>[] paramTypes, Object[] args)
585     {
586         return String.format(METHOD_SIGNATURE, targetClass.getName(),
587                 methodName, Arrays.toString(paramTypes), Arrays.toString(args));
588     }
589 
590     /**
591      * Helper method for generating a meaningful exception message if no unique
592      * match for a method to be called can be found. This method checks whether
593      * 0 or more than 1 matches were found and produces a corresponding error
594      * message. The exception returned by this method also contains the
595      * signature of the desired method.
596      *
597      * @param matches a list with the found matches
598      * @param targetClass the class the method belongs to
599      * @param methodName the name of the method
600      * @param paramTypes the array with parameter types
601      * @param args the array with call arguments
602      * @return an exception reporting the error condition
603      */
604     private static InjectionException nonUniqueMethodException(
605             List<? extends AccessibleObject> matches, Class<?> targetClass,
606             String methodName, Class<?>[] paramTypes, Object[] args)
607     {
608         String msg =
609                 matches.isEmpty() ? "No match found for method %s"
610                         : String.format(
611                                 "Multiple matches found for method %%s. Matches: %s",
612                                 matches.toString());
613         return new InjectionException(String.format(msg,
614                 methodSignature(targetClass, methodName, paramTypes, args)));
615     }
616 
617     /**
618      * Null-safe method for determining the length of an array.
619      *
620      * @param ar the array in question
621      * @return the length of this array
622      */
623     private static int arrayLength(Object[] ar)
624     {
625         return (ar != null) ? ar.length : 0;
626     }
627 }