View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.common.util;
25  
26  import java.lang.reflect.Array;
27  import java.lang.reflect.InvocationTargetException;
28  import java.lang.reflect.Method;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.LinkedList;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.regex.Pattern;
39  import net.jcip.annotations.Immutable;
40  
41  /**
42   * Utility class for working reflectively with objects.
43   */
44  @Immutable
45  public class Reflection {
46  
47      /**
48       * Build the list of classes that correspond to the list of argument objects.
49       * 
50       * @param arguments the list of argument objects.
51       * @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null
52       *         element for each null argument.
53       */
54      public static Class<?>[] buildArgumentClasses( Object... arguments ) {
55          if (arguments == null || arguments.length == 0) return EMPTY_CLASS_ARRAY;
56          Class<?>[] result = new Class<?>[arguments.length];
57          int i = 0;
58          for (Object argument : arguments) {
59              if (argument != null) {
60                  result[i] = argument.getClass();
61              } else {
62                  result[i] = null;
63              }
64          }
65          return result;
66      }
67  
68      /**
69       * Build the list of classes that correspond to the list of argument objects.
70       * 
71       * @param arguments the list of argument objects.
72       * @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null
73       *         element for each null argument.
74       */
75      public static List<Class<?>> buildArgumentClassList( Object... arguments ) {
76          if (arguments == null || arguments.length == 0) return Collections.emptyList();
77          List<Class<?>> result = new ArrayList<Class<?>>(arguments.length);
78          for (Object argument : arguments) {
79              if (argument != null) {
80                  result.add(argument.getClass());
81              } else {
82                  result.add(null);
83              }
84          }
85          return result;
86      }
87  
88      /**
89       * Convert any argument classes to primitives.
90       * 
91       * @param arguments the list of argument classes.
92       * @return the list of Class instances in which any classes that could be represented by primitives (e.g., Boolean) were
93       *         replaced with the primitive classes (e.g., Boolean.TYPE).
94       */
95      public static List<Class<?>> convertArgumentClassesToPrimitives( Class<?>... arguments ) {
96          if (arguments == null || arguments.length == 0) return Collections.emptyList();
97          List<Class<?>> result = new ArrayList<Class<?>>(arguments.length);
98          for (Class<?> clazz : arguments) {
99              if (clazz == Boolean.class) clazz = Boolean.TYPE;
100             else if (clazz == Character.class) clazz = Character.TYPE;
101             else if (clazz == Byte.class) clazz = Byte.TYPE;
102             else if (clazz == Short.class) clazz = Short.TYPE;
103             else if (clazz == Integer.class) clazz = Integer.TYPE;
104             else if (clazz == Long.class) clazz = Long.TYPE;
105             else if (clazz == Float.class) clazz = Float.TYPE;
106             else if (clazz == Double.class) clazz = Double.TYPE;
107             else if (clazz == Void.class) clazz = Void.TYPE;
108             result.add(clazz);
109         }
110 
111         return result;
112     }
113 
114     /**
115      * Returns the name of the class. The result will be the fully-qualified class name, or the readable form for arrays and
116      * primitive types.
117      * 
118      * @param clazz the class for which the class name is to be returned.
119      * @return the readable name of the class.
120      */
121     public static String getClassName( final Class<?> clazz ) {
122         final String fullName = clazz.getName();
123         final int fullNameLength = fullName.length();
124 
125         // Check for array ('[') or the class/interface marker ('L') ...
126         int numArrayDimensions = 0;
127         while (numArrayDimensions < fullNameLength) {
128             final char c = fullName.charAt(numArrayDimensions);
129             if (c != '[') {
130                 String name = null;
131                 // Not an array, so it must be one of the other markers ...
132                 switch (c) {
133                     case 'L': {
134                         name = fullName.subSequence(numArrayDimensions + 1, fullNameLength).toString();
135                         break;
136                     }
137                     case 'B': {
138                         name = "byte";
139                         break;
140                     }
141                     case 'C': {
142                         name = "char";
143                         break;
144                     }
145                     case 'D': {
146                         name = "double";
147                         break;
148                     }
149                     case 'F': {
150                         name = "float";
151                         break;
152                     }
153                     case 'I': {
154                         name = "int";
155                         break;
156                     }
157                     case 'J': {
158                         name = "long";
159                         break;
160                     }
161                     case 'S': {
162                         name = "short";
163                         break;
164                     }
165                     case 'Z': {
166                         name = "boolean";
167                         break;
168                     }
169                     case 'V': {
170                         name = "void";
171                         break;
172                     }
173                     default: {
174                         name = fullName.subSequence(numArrayDimensions, fullNameLength).toString();
175                     }
176                 }
177                 if (numArrayDimensions == 0) {
178                     // No array markers, so just return the name ...
179                     return name;
180                 }
181                 // Otherwise, add the array markers and the name ...
182                 if (numArrayDimensions < BRACKETS_PAIR.length) {
183                     name = name + BRACKETS_PAIR[numArrayDimensions];
184                 } else {
185                     for (int i = 0; i < numArrayDimensions; i++) {
186                         name = name + BRACKETS_PAIR[1];
187                     }
188                 }
189                 return name;
190             }
191             ++numArrayDimensions;
192         }
193 
194         return fullName;
195     }
196 
197     private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[] {};
198     private static final String[] BRACKETS_PAIR = new String[] {"", "[]", "[][]", "[][][]", "[][][][]", "[][][][][]"};
199 
200     private final Class<?> targetClass;
201     private Map<String, LinkedList<Method>> methodMap = null; // used for the brute-force method finder
202 
203     /**
204      * Construct a Reflection instance that cache's some information about the target class. The target class is the Class object
205      * upon which the methods will be found.
206      * 
207      * @param targetClass the target class
208      * @throws IllegalArgumentException if the target class is null
209      */
210     public Reflection( Class<?> targetClass ) {
211         CheckArg.isNotNull(targetClass, "targetClass");
212         this.targetClass = targetClass;
213     }
214 
215     /**
216      * Return the class that is the target for the reflection methods.
217      * 
218      * @return the target class
219      */
220     public Class<?> getTargetClass() {
221         return this.targetClass;
222     }
223 
224     /**
225      * Find the method on the target class that matches the supplied method name.
226      * 
227      * @param methodName the name of the method that is to be found.
228      * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
229      * @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name.
230      */
231     public Method[] findMethods( String methodName,
232                                  boolean caseSensitive ) {
233         Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
234         return findMethods(pattern);
235     }
236 
237     /**
238      * Find the methods on the target class that matches the supplied method name.
239      * 
240      * @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
241      * @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name.
242      */
243     public Method[] findMethods( Pattern methodNamePattern ) {
244         final Method[] allMethods = this.targetClass.getMethods();
245         final List<Method> result = new ArrayList<Method>();
246         for (int i = 0; i < allMethods.length; i++) {
247             final Method m = allMethods[i];
248             if (methodNamePattern.matcher(m.getName()).matches()) {
249                 result.add(m);
250             }
251         }
252         return result.toArray(new Method[result.size()]);
253     }
254 
255     /**
256      * Find the getter methods on the target class that begin with "get" or "is", that have no parameters, and that return
257      * something other than void. This method skips the {@link Object#getClass()} method.
258      * 
259      * @return the Method objects for the getters; never null but possibly empty
260      */
261     public Method[] findGetterMethods() {
262         final Method[] allMethods = this.targetClass.getMethods();
263         final List<Method> result = new ArrayList<Method>();
264         for (int i = 0; i < allMethods.length; i++) {
265             final Method m = allMethods[i];
266             int numParams = m.getParameterTypes().length;
267             if (numParams != 0) continue;
268             String name = m.getName();
269             if (name.equals("getClass")) continue;
270             if (m.getReturnType() == Void.TYPE) continue;
271             if (name.startsWith("get") || name.startsWith("is")) {
272                 result.add(m);
273             }
274         }
275         return result.toArray(new Method[result.size()]);
276     }
277 
278     /**
279      * Find the property names with getter methods on the target class. This method returns the property names for the methods
280      * returned by {@link #findGetterMethods()}.
281      * 
282      * @return the Java Bean property names for the getters; never null but possibly empty
283      */
284     public String[] findGetterPropertyNames() {
285         final Method[] getters = findGetterMethods();
286         final List<String> result = new ArrayList<String>();
287         for (int i = 0; i < getters.length; i++) {
288             final Method m = getters[i];
289             String name = m.getName();
290             if (name.startsWith("get") && name.length() > 3) {
291                 result.add(name.substring(3));
292             } else if (name.startsWith("is") && name.length() > 2) {
293                 result.add(name.substring(2));
294             }
295         }
296         return result.toArray(new String[result.size()]);
297     }
298 
299     /**
300      * Find the method on the target class that matches the supplied method name.
301      * 
302      * @param methodName the name of the method that is to be found.
303      * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
304      * @return the first Method object found that has a matching name, or null if there are no methods that have a matching name.
305      */
306     public Method findFirstMethod( String methodName,
307                                    boolean caseSensitive ) {
308         Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
309         return findFirstMethod(pattern);
310     }
311 
312     /**
313      * Find the method on the target class that matches the supplied method name.
314      * 
315      * @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
316      * @return the first Method object found that has a matching name, or null if there are no methods that have a matching name.
317      */
318     public Method findFirstMethod( Pattern methodNamePattern ) {
319         final Method[] allMethods = this.targetClass.getMethods();
320         for (int i = 0; i < allMethods.length; i++) {
321             final Method m = allMethods[i];
322             if (methodNamePattern.matcher(m.getName()).matches()) {
323                 return m;
324             }
325         }
326         return null;
327     }
328 
329     /**
330      * Find and execute the best method on the target class that matches the signature specified with one of the specified names
331      * and the list of arguments. If no such method is found, a NoSuchMethodException is thrown.
332      * <P>
333      * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
334      * instances of <code>Number</code> or its subclasses.
335      * </p>
336      * 
337      * @param methodNames the names of the methods that are to be invoked, in the order they are to be tried
338      * @param target the object on which the method is to be invoked
339      * @param arguments the array of Object instances that correspond to the arguments passed to the method.
340      * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
341      *         could be found.
342      * @throws NoSuchMethodException if a matching method is not found.
343      * @throws SecurityException if access to the information is denied.
344      * @throws InvocationTargetException
345      * @throws IllegalAccessException
346      * @throws IllegalArgumentException
347      */
348     public Object invokeBestMethodOnTarget( String[] methodNames,
349                                             Object target,
350                                             Object... arguments )
351         throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
352         InvocationTargetException {
353         Class<?>[] argumentClasses = buildArgumentClasses(arguments);
354         int remaining = methodNames.length;
355         Object result = null;
356         for (String methodName : methodNames) {
357             --remaining;
358             try {
359                 Method method = findBestMethodWithSignature(methodName, argumentClasses);
360                 result = method.invoke(target, arguments);
361                 break;
362             } catch (NoSuchMethodException e) {
363                 if (remaining == 0) throw e;
364             }
365         }
366         return result;
367     }
368 
369     /**
370      * Find and execute the best setter method on the target class for the supplied property name and the supplied list of
371      * arguments. If no such method is found, a NoSuchMethodException is thrown.
372      * <P>
373      * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
374      * instances of <code>Number</code> or its subclasses.
375      * </p>
376      * 
377      * @param javaPropertyName the name of the property whose setter is to be invoked, in the order they are to be tried
378      * @param target the object on which the method is to be invoked
379      * @param argument the new value for the property
380      * @return the result of the setter method, which is typically null (void)
381      * @throws NoSuchMethodException if a matching method is not found.
382      * @throws SecurityException if access to the information is denied.
383      * @throws InvocationTargetException
384      * @throws IllegalAccessException
385      * @throws IllegalArgumentException
386      */
387     public Object invokeSetterMethodOnTarget( String javaPropertyName,
388                                               Object target,
389                                               Object argument )
390         throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
391         InvocationTargetException {
392         String[] methodNamesArray = findMethodNames("set" + javaPropertyName);
393         try {
394             return invokeBestMethodOnTarget(methodNamesArray, target, argument);
395         } catch (NoSuchMethodException e) {
396             // If the argument is an Object[], see if it works with an array of whatever type the actual value is ...
397             if (argument instanceof Object[]) {
398                 Object[] arrayArg = (Object[])argument;
399                 for (Object arrayValue : arrayArg) {
400                     if (arrayValue == null) continue;
401                     Class<?> arrayValueType = arrayValue.getClass();
402                     // Create an array of this type ...
403                     Object typedArray = Array.newInstance(arrayValueType, arrayArg.length);
404                     Object[] newArray = (Object[])typedArray;
405                     for (int i = 0; i != arrayArg.length; ++i) {
406                         newArray[i] = arrayArg[i];
407                     }
408                     // Try to execute again ...
409                     try {
410                         return invokeBestMethodOnTarget(methodNamesArray, target, typedArray);
411                     } catch (NoSuchMethodException e2) {
412                         // Throw the original exception ...
413                         throw e;
414                     }
415                 }
416             }
417             throw e;
418         }
419     }
420 
421     /**
422      * Find and execute the getter method on the target class for the supplied property name. If no such method is found, a
423      * NoSuchMethodException is thrown.
424      * 
425      * @param javaPropertyName the name of the property whose getter is to be invoked, in the order they are to be tried
426      * @param target the object on which the method is to be invoked
427      * @return the property value (the result of the getter method call)
428      * @throws NoSuchMethodException if a matching method is not found.
429      * @throws SecurityException if access to the information is denied.
430      * @throws InvocationTargetException
431      * @throws IllegalAccessException
432      * @throws IllegalArgumentException
433      */
434     public Object invokeGetterMethodOnTarget( String javaPropertyName,
435                                               Object target )
436         throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
437         InvocationTargetException {
438         String[] methodNamesArray = findMethodNames("get" + javaPropertyName);
439         return invokeBestMethodOnTarget(methodNamesArray, target);
440     }
441 
442     protected String[] findMethodNames( String methodName ) {
443         Method[] methods = findMethods(methodName, false);
444         Set<String> methodNames = new HashSet<String>();
445         for (Method method : methods) {
446             String actualMethodName = method.getName();
447             methodNames.add(actualMethodName);
448         }
449         return methodNames.toArray(new String[methodNames.size()]);
450     }
451 
452     /**
453      * Find the best method on the target class that matches the signature specified with the specified name and the list of
454      * arguments. This method first attempts to find the method with the specified arguments; if no such method is found, a
455      * NoSuchMethodException is thrown.
456      * <P>
457      * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
458      * instances of <code>Number</code> or its subclasses.
459      * 
460      * @param methodName the name of the method that is to be invoked.
461      * @param arguments the array of Object instances that correspond to the arguments passed to the method.
462      * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
463      *         could be found.
464      * @throws NoSuchMethodException if a matching method is not found.
465      * @throws SecurityException if access to the information is denied.
466      */
467     public Method findBestMethodOnTarget( String methodName,
468                                           Object... arguments ) throws NoSuchMethodException, SecurityException {
469         Class<?>[] argumentClasses = buildArgumentClasses(arguments);
470         return findBestMethodWithSignature(methodName, argumentClasses);
471     }
472 
473     /**
474      * Find the best method on the target class that matches the signature specified with the specified name and the list of
475      * argument classes. This method first attempts to find the method with the specified argument classes; if no such method is
476      * found, a NoSuchMethodException is thrown.
477      * 
478      * @param methodName the name of the method that is to be invoked.
479      * @param argumentsClasses the list of Class instances that correspond to the classes for each argument passed to the method.
480      * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
481      *         could be found.
482      * @throws NoSuchMethodException if a matching method is not found.
483      * @throws SecurityException if access to the information is denied.
484      */
485     public Method findBestMethodWithSignature( String methodName,
486                                                Class<?>... argumentsClasses ) throws NoSuchMethodException, SecurityException {
487 
488         // Attempt to find the method
489         Method result;
490 
491         // -------------------------------------------------------------------------------
492         // First try to find the method with EXACTLY the argument classes as specified ...
493         // -------------------------------------------------------------------------------
494         Class<?>[] classArgs = null;
495         try {
496             classArgs = argumentsClasses != null ? argumentsClasses : new Class[] {};
497             result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found
498             return result;
499         } catch (NoSuchMethodException e) {
500             // No method found, so continue ...
501         }
502 
503         // ---------------------------------------------------------------------------------------------
504         // Then try to find a method with the argument classes converted to a primitive, if possible ...
505         // ---------------------------------------------------------------------------------------------
506         List<Class<?>> argumentsClassList = convertArgumentClassesToPrimitives(argumentsClasses);
507         try {
508             classArgs = argumentsClassList.toArray(new Class[argumentsClassList.size()]);
509             result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found
510             return result;
511         } catch (NoSuchMethodException e) {
512             // No method found, so continue ...
513         }
514 
515         // ---------------------------------------------------------------------------------------------
516         // Still haven't found anything. So far, the "getMethod" logic only finds methods that EXACTLY
517         // match the argument classes (i.e., not methods declared with superclasses or interfaces of
518         // the arguments). There is no canned algorithm in Java to do this, so we have to brute-force it.
519         // The following algorithm will find the first method that matches by doing "instanceof", so it
520         // may not be the best method. Since there is some overhead to this algorithm, the first time
521         // caches some information in class members.
522         // ---------------------------------------------------------------------------------------------
523         Method method;
524         LinkedList<Method> methodsWithSameName;
525         if (this.methodMap == null) {
526             // This is idempotent, so no need to lock or synchronize ...
527             this.methodMap = new HashMap<String, LinkedList<Method>>();
528             Method[] methods = this.targetClass.getMethods();
529             for (int i = 0; i != methods.length; ++i) {
530                 method = methods[i];
531                 methodsWithSameName = this.methodMap.get(method.getName());
532                 if (methodsWithSameName == null) {
533                     methodsWithSameName = new LinkedList<Method>();
534                     this.methodMap.put(method.getName(), methodsWithSameName);
535                 }
536                 methodsWithSameName.addFirst(method); // add lower methods first
537             }
538         }
539 
540         // ------------------------------------------------------------------------
541         // Find the set of methods with the same name (do this twice, once with the
542         // original methods and once with the primitives) ...
543         // ------------------------------------------------------------------------
544         // List argClass = argumentsClasses;
545         for (int j = 0; j != 2; ++j) {
546             methodsWithSameName = this.methodMap.get(methodName);
547             if (methodsWithSameName == null) {
548                 throw new NoSuchMethodException(methodName);
549             }
550             Iterator<Method> iter = methodsWithSameName.iterator();
551             Class<?>[] args;
552             Class<?> clazz;
553             boolean allMatch;
554             while (iter.hasNext()) {
555                 method = iter.next();
556                 args = method.getParameterTypes();
557                 if (args.length == argumentsClassList.size()) {
558                     allMatch = true; // assume they all match
559                     for (int i = 0; i < args.length; ++i) {
560                         clazz = argumentsClassList.get(i);
561                         if (clazz != null) {
562                             Class<?> argClass = args[i];
563                             if (argClass.isAssignableFrom(clazz)) {
564                                 // It's a match
565                             } else if (argClass.isArray() && clazz.isArray()
566                                        && argClass.getComponentType().isAssignableFrom(clazz.getComponentType())) {
567                                 // They're both arrays, and they're castable, so we're good ...
568                             } else {
569                                 allMatch = false; // found one that doesn't match
570                                 i = args.length; // force completion
571                             }
572                         } else {
573                             // a null is assignable for everything except a primitive
574                             if (args[i].isPrimitive()) {
575                                 allMatch = false; // found one that doesn't match
576                                 i = args.length; // force completion
577                             }
578                         }
579                     }
580                     if (allMatch) {
581                         return method;
582                     }
583                 }
584             }
585         }
586 
587         throw new NoSuchMethodException(methodName);
588     }
589 
590 }