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.Constructor;
19  import java.lang.reflect.Method;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.HashMap;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.Map;
26  
27  import org.apache.commons.beanutils.PropertyUtils;
28  import org.apache.commons.lang.ClassUtils;
29  
30  /**
31   * <p>
32   * An utility class that provides some functionality related to reflection and
33   * dependency injection.
34   * </p>
35   * <p>
36   * This class implements some basic functionality that is needed by other parts
37   * of the dependency injection package. It especially deals with row reflection
38   * calls and exception handling. It is not intended to be used directly by
39   * applications using this framework. It will be called under the hood.
40   * </p>
41   *
42   * @author Oliver Heger
43   * @version $Id: ReflectionUtils.java 205 2012-01-29 18:29:57Z oheger $
44   */
45  public final class ReflectionUtils
46  {
47      /**
48       * A private constructor, so that no instances can be created.
49       */
50      private ReflectionUtils()
51      {
52          // empty
53      }
54  
55      /**
56       * Loads the class with the specified name using the given class loader.
57       * This is a thin wrapper over the <code>Class.forName()</code> method.
58       * <code>ClassNotFoundException</code> exceptions are caught and re-thrown
59       * as <code>InjectionException</code> exceptions.
60       *
61       * @param className the name of the class to be loaded
62       * @param loader the class loader to use
63       * @return the loaded class
64       * @throws IllegalArgumentException if the class name or the class loader is
65       * undefined
66       * @throws InjectionException if the class cannot be resolved
67       */
68      public static Class<?> loadClass(String className, ClassLoader loader)
69      {
70          if (className == null)
71          {
72              throw new IllegalArgumentException("Class name must not be null!");
73          }
74          if (loader == null)
75          {
76              throw new IllegalArgumentException("Class loader must not be null!");
77          }
78  
79          try
80          {
81              return ClassUtils.getClass(loader, className);
82          }
83          catch (ClassNotFoundException cnfex)
84          {
85              throw new InjectionException(cnfex);
86          }
87      }
88  
89      /**
90       * Helper method for invoking a method using reflection. This method catches
91       * the variety of possible exceptions and re-throws them as runtime
92       * exceptions.
93       *
94       * @param method the method to be invoked
95       * @param target the target object
96       * @param args the arguments of the method
97       * @return the return value of the method
98       * @throws InjectionException if invoking the method causes an error
99       * @throws IllegalArgumentException if the method object is <b>null</b>
100      */
101     public static Object invokeMethod(Method method, Object target,
102             Object... args)
103     {
104         if (method == null)
105         {
106             throw new IllegalArgumentException("Method must not be null!");
107         }
108 
109         try
110         {
111             return method.invoke(target, args);
112         }
113         catch (Exception ex)
114         {
115             // catch all related exceptions
116             throw new InjectionException("Exception when invoking method "
117                     + method.getName(), ex);
118         }
119     }
120 
121     /**
122      * Creates an object by invoking the specified constructor with the given
123      * arguments. Like <code>invokeMethod()</code>, this is a helper method
124      * that deals with all possible exceptions and redirects them as
125      * <code>InjectionException</code>s.
126      *
127      * @param <T> the type of the constructor
128      * @param ctor the constructor to be invoked (must not be <b>null</b>)
129      * @param args the arguments to be passed to the constructor
130      * @return the newly created instance
131      * @throws InjectionException if construction of the object fails
132      * @throws IllegalArgumentException if the constructor object is <b>null</b>
133      */
134     public static <T> T invokeConstructor(Constructor<T> ctor, Object... args)
135     {
136         if (ctor == null)
137         {
138             throw new IllegalArgumentException("Constructor must not be null!");
139         }
140 
141         try
142         {
143             return ctor.newInstance(args);
144         }
145         catch (Exception ex)
146         {
147             // redirect all exceptions
148             throw new InjectionException("Exception when invoking constructor "
149                     + ctor, ex);
150         }
151     }
152 
153     /**
154      * Finds all methods matching the given search criteria. With this method a
155      * list of the methods of the specified target class can be obtained that
156      * have the given name and are compatible with the given parameter types.
157      * Wild cards are supported as follows:
158      * <ul>
159      * <li>The method name can be <b>null</b>, then only parameter types are
160      * compared.</li>
161      * <li>Each element of the {@code paramTypes} can be <b>null</b>, then
162      * arbitrary parameter types are accepted at this place.</li>
163      * <li>The whole {@code paramTypes} array can be <b>null</b>, then all
164      * methods with the given name and arbitrary signature are accepted.</li>
165      * </ul>
166      * The {@code exactTypeMatch} parameter controls how parameter types are
167      * compared: If set to <b>true</b> the parameter types must match exactly;
168      * otherwise, the parameters of the method in the target class can be super
169      * classes of the provided parameter types. So this method allows searching
170      * for methods of a given class in a flexible way including the following
171      * use cases:
172      * <ul>
173      * <li>Search for all methods with a given name: just pass in <b>null</b>
174      * for the parameter types array.</li>
175      * <li>Search for all methods with a given signature: pass in <b>null</b>
176      * for the method name and specify a corresponding array with parameter
177      * types.</li>
178      * <li>Search for methods with a given name and a partly known signature:
179      * here the name of the method and a type array with all known types set has
180      * to be passed in.</li>
181      * </ul>
182      *
183      * @param targetClass the target class (must not be <b>null</b>)
184      * @param methodName the name of the method to be searched for
185      * @param paramTypes an array with the parameter types
186      * @param exactTypeMatch a flag whether parameter types should be matched
187      *        exactly
188      * @return a list with methods matching the search criteria
189      * @throws IllegalArgumentException if the target class is <b>null</b>
190      */
191     public static List<Method> findMethods(Class<?> targetClass,
192             String methodName, Class<?>[] paramTypes, boolean exactTypeMatch)
193     {
194         checkTargetClass(targetClass);
195         List<Method> methods = new LinkedList<Method>();
196 
197         for (Method m : targetClass.getMethods())
198         {
199             if (methodName == null || methodName.equals(m.getName()))
200             {
201                 if (isSignatureCompatible(m.getParameterTypes(), paramTypes,
202                         exactTypeMatch))
203                 {
204                     methods.add(m);
205                 }
206             }
207         }
208 
209         return methods;
210     }
211 
212     /**
213      * Removes duplicate methods from the specified list. When calling
214      * {@link #findMethods(Class, String, Class[], boolean)} it is possible that
215      * the resulting list contains multiple methods with the same name and
216      * signature, but with a different result type. This is the case for
217      * instance if a class overrides a super class method with a covariant
218      * return type. With this method such duplicates can be eliminated. It
219      * searches for methods with the same name and signature and drops all of
220      * them except for the one with the most specific return type. The result of
221      * the method depends on the operations performed: If no duplicates have
222      * been found, the same list is returned without changes. Otherwise, a new
223      * list is created and returned.
224      *
225      * @param methods the list with methods to be checked (must not be
226      *        <b>null</b>
227      * @return a list with duplicates removed
228      * @throws IllegalArgumentException if the passed in list is <b>null</b> or
229      *         contain <b>null</b> elements
230      */
231     public static List<Method> removeCovariantDuplicates(List<Method> methods)
232     {
233         if (methods == null)
234         {
235             throw new IllegalArgumentException("Method list must not be null!");
236         }
237         if (methods.size() < 1)
238         {
239             // there can't be duplicates
240             return methods;
241         }
242 
243         Map<Signature, Method> methodMap = new HashMap<Signature, Method>();
244         for (Method m : methods)
245         {
246             Signature sig = new Signature(m);
247             Method m2 = methodMap.get(sig);
248             if (m2 == null
249                     || m2.getReturnType().isAssignableFrom(m.getReturnType()))
250             {
251                 methodMap.put(sig, m);
252             }
253         }
254 
255         if (methodMap.size() == methods.size())
256         {
257             // no changes, return input list
258             return methods;
259         }
260         else
261         {
262             return new ArrayList<Method>(methodMap.values());
263         }
264     }
265 
266     /**
267      * Finds all constructors matching the specified search criteria. This
268      * method works like {@link #findMethods(Class, String, Class[], boolean)},
269      * but deals with constructors of the target class.
270      *
271      * @param <T> the type of the constructor
272      * @param targetClass the target class (must not be <b>null</b>)
273      * @param paramTypes an array with the parameter types
274      * @param exactTypeMatch a flag whether parameter types should be matched
275      *        exactly
276      * @return a list with constructors matching the search criteria
277      * @throws IllegalArgumentException if the target class is <b>null</b>
278      */
279     public static <T> List<Constructor<T>> findConstructors(
280             Class<T> targetClass, Class<?>[] paramTypes, boolean exactTypeMatch)
281     {
282         checkTargetClass(targetClass);
283         List<Constructor<T>> constr = new LinkedList<Constructor<T>>();
284 
285         for (Constructor<?> c : targetClass.getConstructors())
286         {
287             if (isSignatureCompatible(c.getParameterTypes(), paramTypes,
288                     exactTypeMatch))
289             {
290                 // should be a constructor of the target class
291                 @SuppressWarnings("unchecked")
292                 Constructor<T> ctor = (Constructor<T>) c;
293                 constr.add(ctor);
294             }
295         }
296 
297         return constr;
298     }
299 
300     /**
301      * Returns the value of the specified property from the given bean.
302      *
303      * @param bean the bean
304      * @param name the name of the property to retrieve
305      * @return the value of this property
306      * @throws InjectionException if accessing the property fails
307      * @throws IllegalArgumentException if invalid parameters are specified
308      */
309     public static Object getProperty(Object bean, String name)
310     {
311         try
312         {
313             return PropertyUtils.getProperty(bean, name);
314         }
315         catch (IllegalArgumentException iex)
316         {
317             throw iex;
318         }
319         catch (Exception ex)
320         {
321             // redirect all reflection exceptions
322             throw new InjectionException("Error when accessing property "
323                     + name, ex);
324         }
325     }
326 
327     /**
328      * Sets the value of a property for the specified bean. The given value will
329      * be directly written into the property, without performing any type
330      * conversions. Occurring exceptions will be re-thrown as
331      * <code>InjectionException</code>s.
332      *
333      * @param bean the bean, on which to set the property
334      * @param name the name of the property to be set
335      * @param value the new value of the property
336      * @throws InjectionException if an error occurs when setting the property
337      * @throws IllegalArgumentException if invalid parameters are passed in
338      */
339     public static void setProperty(Object bean, String name, Object value)
340     {
341         try
342         {
343             PropertyUtils.setProperty(bean, name, value);
344         }
345         catch (IllegalArgumentException iex)
346         {
347             throw iex;
348         }
349         catch (Exception ex)
350         {
351             // handle all reflection exceptions the same way
352             throw new InjectionException("Error when setting property " + name,
353                     ex);
354         }
355     }
356 
357     /**
358      * Tests whether the specified actual parameter value can be assigned to a
359      * parameter of the given type.
360      *
361      * @param paramClass the parameter type
362      * @param param the current parameter value
363      * @return a flag whether this assignment is possible
364      */
365     static boolean isParamAssignable(Class<?> paramClass, Object param)
366     {
367         if (paramClass == null)
368         {
369             throw new IllegalArgumentException(
370                     "Parameter class must not be null!");
371         }
372 
373         if (param == null && paramClass.isPrimitive())
374         {
375             // cannot assign null to a primitive parameter
376             return false;
377         }
378         Class<?> valueClass = (param != null) ? param.getClass() : null;
379         return isParameterCompatible(paramClass, valueClass, false);
380     }
381 
382     /**
383      * Tests whether the specified value objects match the given parameter
384      * types. With this method the signature of a method can be tested for
385      * compatibility with a set of parameter objects.
386      *
387      * @param parameterTypes the parameter types to be checked
388      * @param definedTypes an array with defined parameter types; all defined
389      * elements in this array must be exactly the same as in the parameterTypes
390      * array
391      * @param values an array with the current parameter values
392      * @return a flag whether the method signature is compatible with the
393      * parameter values
394      */
395     static boolean matchParameterTypes(Class<?>[] parameterTypes,
396             Class<?>[] definedTypes, Object[] values)
397     {
398         assert parameterTypes != null && definedTypes != null && values != null
399             : "Input parameters are null";
400         assert definedTypes.length == values.length : "Invalid array lengths";
401 
402         if (parameterTypes.length != values.length)
403         {
404             return false;
405         }
406         for (int i = 0; i < values.length; i++)
407         {
408             if (definedTypes[i] != null)
409             {
410                 if (!definedTypes[i].equals(parameterTypes[i]))
411                 {
412                     return false;
413                 }
414             }
415             else
416             {
417                 if (!isParamAssignable(parameterTypes[i], values[i]))
418                 {
419                     return false;
420                 }
421             }
422         }
423 
424         return true;
425     }
426 
427     /**
428      * Tests whether a method signature matches search criteria. This method is
429      * used by methods for finding specific methods or constructors. All wild
430      * cards supported by search criteria for signatures are supported.
431      *
432      * @param methodTypes the parameter types of a method in question
433      * @param callTypes the search criteria for method types
434      * @param exactMatch the exact match flag
435      * @return a flag whether the parameters match the criteria
436      */
437     private static boolean isSignatureCompatible(Class<?>[] methodTypes,
438             Class<?>[] callTypes, boolean exactMatch)
439     {
440         if (callTypes == null)
441         {
442             // wild card for whole signature
443             return true;
444         }
445 
446         if (methodTypes.length != callTypes.length)
447         {
448             return false;
449         }
450 
451         for (int i = 0; i < methodTypes.length; i++)
452         {
453             if (!isParameterCompatible(methodTypes[i], callTypes[i], exactMatch))
454             {
455                 return false;
456             }
457         }
458 
459         return true;
460     }
461 
462     /**
463      * Tests whether an argument class is compatible with the parameter class of
464      * a method. This method compares the classes either exactly or checks
465      * whether they are assignment compatible. Primitive types and wrappers are
466      * also handled correctly.
467      *
468      * @param methodParamCls the class of the method parameter
469      * @param argCls the class of the argument
470      * @param exactMatch the exact match flag
471      * @return a flag whether the parameter is compatible
472      */
473     private static boolean isParameterCompatible(Class<?> methodParamCls,
474             Class<?> argCls, boolean exactMatch)
475     {
476         if (argCls == null)
477         {
478             return true;
479         }
480 
481         if (compareParameterClasses(methodParamCls, argCls, exactMatch))
482         {
483             return true;
484         }
485 
486         // Check for unboxing conversions of wrapper types
487         if (methodParamCls.isPrimitive())
488         {
489             Class<?> unboxedCls = wrapperToPrimitive(argCls);
490             if (unboxedCls != null
491                     && ClassUtils.isAssignable(unboxedCls, methodParamCls))
492             {
493                 return true;
494             }
495         }
496 
497         return false;
498     }
499 
500     /**
501      * Helper method for comparing the classes of a method parameter. Depending
502      * on the exact match flag either a strict comparison or a test for
503      * assignment compatibility is performed.
504      *
505      * @param methodParamCls the class of the method parameter
506      * @param argCls the class of the argument
507      * @param exactMatch the exact match flag
508      * @return a flag whether the classes are compatible
509      */
510     private static boolean compareParameterClasses(Class<?> methodParamCls,
511             Class<?> argCls, boolean exactMatch)
512     {
513         return exactMatch ? methodParamCls.equals(argCls) : ClassUtils
514                 .isAssignable(argCls, methodParamCls);
515     }
516 
517     /**
518      * Tests whether the target class parameter is valid. Throws an exception if
519      * not.
520      *
521      * @param targetClass the target class
522      */
523     private static void checkTargetClass(Class<?> targetClass)
524     {
525         if (targetClass == null)
526         {
527             throw new IllegalArgumentException("Target class must not be null!");
528         }
529     }
530 
531     /**
532      * Returns the corresponding primitive type to a wrapper class. If the
533      * passed in class is not a wrapper class, <b>null</b> is returned.
534      *
535      * @param wrapperCls the wrapper class
536      * @return the corresponding primitive type
537      */
538     private static Class<?> wrapperToPrimitive(Class<?> wrapperCls)
539     {
540         return ClassUtils.wrapperToPrimitive(wrapperCls);
541     }
542 
543     /**
544      * An internally used helper class representing a method signature.
545      */
546     private static class Signature
547     {
548         /** The name of the method. */
549         private final String methodName;
550 
551         /** The parameter types of the method. */
552         private final Class<?>[] parameterTypes;
553 
554         /**
555          * Creates a new instance of {@code Signature} and initializes it from
556          * the given {@code Method} instance.
557          *
558          * @param m the represented method instance
559          * @throws IllegalArgumentException if the method is <b>null</b>
560          */
561         public Signature(Method m)
562         {
563             if (m == null)
564             {
565                 throw new IllegalArgumentException("Method must not be null!");
566             }
567             methodName = m.getName();
568             parameterTypes = m.getParameterTypes();
569         }
570 
571         /**
572          * Returns a hash code for this object. The result is based on the
573          * method name and the parameter types.
574          *
575          * @return a hash code for this object
576          */
577         @Override
578         public int hashCode()
579         {
580             final int factor = 31;
581             final int seed = 17;
582 
583             int result = seed;
584             result = result * factor + methodName.hashCode();
585             result = result * factor + Arrays.hashCode(parameterTypes);
586             return result;
587         }
588 
589         /**
590          * Checks whether this object equals another one. Two instances are
591          * considered equals if the method name and the parameter types are
592          * equal.
593          *
594          * @param obj the object to compare to
595          * @return a flag whether these objects are equal
596          */
597         @Override
598         public boolean equals(Object obj)
599         {
600             if (this == obj)
601             {
602                 return true;
603             }
604             if (!(obj instanceof Signature))
605             {
606                 return false;
607             }
608 
609             Signature c = (Signature) obj;
610             return methodName.equals(c.methodName)
611                     && Arrays.equals(parameterTypes, c.parameterTypes);
612         }
613     }
614 }