001 /*
002 * JBoss DNA (http://www.jboss.org/dna)
003 * See the COPYRIGHT.txt file distributed with this work for information
004 * regarding copyright ownership. Some portions may be licensed
005 * to Red Hat, Inc. under one or more contributor license agreements.
006 * See the AUTHORS.txt file in the distribution for a full listing of
007 * individual contributors.
008 *
009 * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010 * is licensed to you under the terms of the GNU Lesser General Public License as
011 * published by the Free Software Foundation; either version 2.1 of
012 * the License, or (at your option) any later version.
013 *
014 * JBoss DNA is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017 * Lesser General Public License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this software; if not, write to the Free
021 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023 */
024 package org.jboss.dna.common.util;
025
026 import java.lang.reflect.InvocationTargetException;
027 import java.lang.reflect.Method;
028 import java.util.ArrayList;
029 import java.util.Collections;
030 import java.util.HashMap;
031 import java.util.HashSet;
032 import java.util.Iterator;
033 import java.util.LinkedList;
034 import java.util.List;
035 import java.util.Map;
036 import java.util.Set;
037 import java.util.regex.Pattern;
038
039 /**
040 * Utility class for working reflectively with objects.
041 *
042 * @author Randall Hauch
043 */
044 public class Reflection {
045
046 /**
047 * Build the list of classes that correspond to the list of argument objects.
048 *
049 * @param arguments the list of argument objects.
050 * @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null
051 * element for each null argument.
052 */
053 public static Class<?>[] buildArgumentClasses( Object... arguments ) {
054 if (arguments == null || arguments.length == 0) return EMPTY_CLASS_ARRAY;
055 Class<?>[] result = new Class<?>[arguments.length];
056 int i = 0;
057 for (Object argument : arguments) {
058 if (argument != null) {
059 result[i] = argument.getClass();
060 } else {
061 result[i] = null;
062 }
063 }
064 return result;
065 }
066
067 /**
068 * Build the list of classes that correspond to the list of argument objects.
069 *
070 * @param arguments the list of argument objects.
071 * @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null
072 * element for each null argument.
073 */
074 public static List<Class<?>> buildArgumentClassList( Object... arguments ) {
075 if (arguments == null || arguments.length == 0) return Collections.emptyList();
076 List<Class<?>> result = new ArrayList<Class<?>>(arguments.length);
077 for (Object argument : arguments) {
078 if (argument != null) {
079 result.add(argument.getClass());
080 } else {
081 result.add(null);
082 }
083 }
084 return result;
085 }
086
087 /**
088 * Convert any argument classes to primitives.
089 *
090 * @param arguments the list of argument classes.
091 * @return the list of Class instances in which any classes that could be represented by primitives (e.g., Boolean) were
092 * replaced with the primitive classes (e.g., Boolean.TYPE).
093 */
094 public static List<Class<?>> convertArgumentClassesToPrimitives( Class<?>... arguments ) {
095 if (arguments == null || arguments.length == 0) return Collections.emptyList();
096 List<Class<?>> result = new ArrayList<Class<?>>(arguments.length);
097 for (Class<?> clazz : arguments) {
098 if (clazz == Boolean.class) clazz = Boolean.TYPE;
099 else if (clazz == Character.class) clazz = Character.TYPE;
100 else if (clazz == Byte.class) clazz = Byte.TYPE;
101 else if (clazz == Short.class) clazz = Short.TYPE;
102 else if (clazz == Integer.class) clazz = Integer.TYPE;
103 else if (clazz == Long.class) clazz = Long.TYPE;
104 else if (clazz == Float.class) clazz = Float.TYPE;
105 else if (clazz == Double.class) clazz = Double.TYPE;
106 else if (clazz == Void.class) clazz = Void.TYPE;
107 result.add(clazz);
108 }
109
110 return result;
111 }
112
113 /**
114 * Returns the name of the class. The result will be the fully-qualified class name, or the readable form for arrays and
115 * primitive types.
116 *
117 * @param clazz the class for which the class name is to be returned.
118 * @return the readable name of the class.
119 */
120 public static String getClassName( final Class<?> clazz ) {
121 final String fullName = clazz.getName();
122 final int fullNameLength = fullName.length();
123
124 // Check for array ('[') or the class/interface marker ('L') ...
125 int numArrayDimensions = 0;
126 while (numArrayDimensions < fullNameLength) {
127 final char c = fullName.charAt(numArrayDimensions);
128 if (c != '[') {
129 String name = null;
130 // Not an array, so it must be one of the other markers ...
131 switch (c) {
132 case 'L': {
133 name = fullName.subSequence(numArrayDimensions + 1, fullNameLength).toString();
134 break;
135 }
136 case 'B': {
137 name = "byte";
138 break;
139 }
140 case 'C': {
141 name = "char";
142 break;
143 }
144 case 'D': {
145 name = "double";
146 break;
147 }
148 case 'F': {
149 name = "float";
150 break;
151 }
152 case 'I': {
153 name = "int";
154 break;
155 }
156 case 'J': {
157 name = "long";
158 break;
159 }
160 case 'S': {
161 name = "short";
162 break;
163 }
164 case 'Z': {
165 name = "boolean";
166 break;
167 }
168 case 'V': {
169 name = "void";
170 break;
171 }
172 default: {
173 name = fullName.subSequence(numArrayDimensions, fullNameLength).toString();
174 }
175 }
176 if (numArrayDimensions == 0) {
177 // No array markers, so just return the name ...
178 return name;
179 }
180 // Otherwise, add the array markers and the name ...
181 if (numArrayDimensions < BRACKETS_PAIR.length) {
182 name = name + BRACKETS_PAIR[numArrayDimensions];
183 } else {
184 for (int i = 0; i < numArrayDimensions; i++) {
185 name = name + BRACKETS_PAIR[1];
186 }
187 }
188 return name;
189 }
190 ++numArrayDimensions;
191 }
192
193 return fullName;
194 }
195
196 private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[] {};
197 private static final String[] BRACKETS_PAIR = new String[] {"", "[]", "[][]", "[][][]", "[][][][]", "[][][][][]"};
198
199 private Class<?> targetClass;
200 private Map<String, LinkedList<Method>> methodMap = null; // used for the brute-force method finder
201
202 /**
203 * Construct a Reflection instance that cache's some information about the target class. The target class is the Class object
204 * upon which the methods will be found.
205 *
206 * @param targetClass the target class
207 * @throws IllegalArgumentException if the target class is null
208 */
209 public Reflection( Class<?> targetClass ) {
210 CheckArg.isNotNull(targetClass, "targetClass");
211 this.targetClass = targetClass;
212 }
213
214 /**
215 * Return the class that is the target for the reflection methods.
216 *
217 * @return the target class
218 */
219 public Class<?> getTargetClass() {
220 return this.targetClass;
221 }
222
223 /**
224 * Find the method on the target class that matches the supplied method name.
225 *
226 * @param methodName the name of the method that is to be found.
227 * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
228 * @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name.
229 */
230 public Method[] findMethods( String methodName,
231 boolean caseSensitive ) {
232 Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
233 return findMethods(pattern);
234 }
235
236 /**
237 * Find the methods on the target class that matches the supplied method name.
238 *
239 * @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
240 * @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name.
241 */
242 public Method[] findMethods( Pattern methodNamePattern ) {
243 final Method[] allMethods = this.targetClass.getMethods();
244 final List<Method> result = new ArrayList<Method>();
245 for (int i = 0; i < allMethods.length; i++) {
246 final Method m = allMethods[i];
247 if (methodNamePattern.matcher(m.getName()).matches()) {
248 result.add(m);
249 }
250 }
251 return result.toArray(new Method[result.size()]);
252 }
253
254 /**
255 * Find the getter methods on the target class that begin with "get" or "is", that have no parameters, and that return
256 * something other than void. This method skips the {@link Object#getClass()} method.
257 *
258 * @return the Method objects for the getters; never null but possibly empty
259 */
260 public Method[] findGetterMethods() {
261 final Method[] allMethods = this.targetClass.getMethods();
262 final List<Method> result = new ArrayList<Method>();
263 for (int i = 0; i < allMethods.length; i++) {
264 final Method m = allMethods[i];
265 int numParams = m.getParameterTypes().length;
266 if (numParams != 0) continue;
267 String name = m.getName();
268 if (name.equals("getClass")) continue;
269 if (m.getReturnType() == Void.TYPE) continue;
270 if (name.startsWith("get") || name.startsWith("is")) {
271 result.add(m);
272 }
273 }
274 return result.toArray(new Method[result.size()]);
275 }
276
277 /**
278 * Find the property names with getter methods on the target class. This method returns the property names for the methods
279 * returned by {@link #findGetterMethods()}.
280 *
281 * @return the Java Bean property names for the getters; never null but possibly empty
282 */
283 public String[] findGetterPropertyNames() {
284 final Method[] getters = findGetterMethods();
285 final List<String> result = new ArrayList<String>();
286 for (int i = 0; i < getters.length; i++) {
287 final Method m = getters[i];
288 String name = m.getName();
289 if (name.startsWith("get") && name.length() > 3) {
290 result.add(name.substring(3));
291 } else if (name.startsWith("is") && name.length() > 2) {
292 result.add(name.substring(2));
293 }
294 }
295 return result.toArray(new String[result.size()]);
296 }
297
298 /**
299 * Find the method on the target class that matches the supplied method name.
300 *
301 * @param methodName the name of the method that is to be found.
302 * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
303 * @return the first Method object found that has a matching name, or null if there are no methods that have a matching name.
304 */
305 public Method findFirstMethod( String methodName,
306 boolean caseSensitive ) {
307 Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
308 return findFirstMethod(pattern);
309 }
310
311 /**
312 * Find the method on the target class that matches the supplied method name.
313 *
314 * @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
315 * @return the first Method object found that has a matching name, or null if there are no methods that have a matching name.
316 */
317 public Method findFirstMethod( Pattern methodNamePattern ) {
318 final Method[] allMethods = this.targetClass.getMethods();
319 for (int i = 0; i < allMethods.length; i++) {
320 final Method m = allMethods[i];
321 if (methodNamePattern.matcher(m.getName()).matches()) {
322 return m;
323 }
324 }
325 return null;
326 }
327
328 /**
329 * Find and execute the best method on the target class that matches the signature specified with one of the specified names
330 * and the list of arguments. If no such method is found, a NoSuchMethodException is thrown.
331 * <P>
332 * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
333 * instances of <code>Number</code> or its subclasses.
334 * </p>
335 *
336 * @param methodNames the names of the methods that are to be invoked, in the order they are to be tried
337 * @param target the object on which the method is to be invoked
338 * @param arguments the array of Object instances that correspond to the arguments passed to the method.
339 * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
340 * could be found.
341 * @throws NoSuchMethodException if a matching method is not found.
342 * @throws SecurityException if access to the information is denied.
343 * @throws InvocationTargetException
344 * @throws IllegalAccessException
345 * @throws IllegalArgumentException
346 */
347 public Object invokeBestMethodOnTarget( String[] methodNames,
348 Object target,
349 Object... arguments )
350 throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
351 InvocationTargetException {
352 Class<?>[] argumentClasses = buildArgumentClasses(arguments);
353 int remaining = methodNames.length;
354 Object result = null;
355 for (String methodName : methodNames) {
356 --remaining;
357 try {
358 Method method = findBestMethodWithSignature(methodName, argumentClasses);
359 result = method.invoke(target, arguments);
360 break;
361 } catch (NoSuchMethodException e) {
362 if (remaining == 0) throw e;
363 }
364 }
365 return result;
366 }
367
368 /**
369 * Find and execute the best setter method on the target class for the supplied property name and the supplied list of
370 * arguments. If no such method is found, a NoSuchMethodException is thrown.
371 * <P>
372 * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
373 * instances of <code>Number</code> or its subclasses.
374 * </p>
375 *
376 * @param javaPropertyName the name of the property whose setter is to be invoked, in the order they are to be tried
377 * @param target the object on which the method is to be invoked
378 * @param argument the new value for the property
379 * @return the result of the setter method, which is typically null (void)
380 * @throws NoSuchMethodException if a matching method is not found.
381 * @throws SecurityException if access to the information is denied.
382 * @throws InvocationTargetException
383 * @throws IllegalAccessException
384 * @throws IllegalArgumentException
385 */
386 public Object invokeSetterMethodOnTarget( String javaPropertyName,
387 Object target,
388 Object argument )
389 throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
390 InvocationTargetException {
391 String[] methodNamesArray = findMethodNames("set" + javaPropertyName);
392 return invokeBestMethodOnTarget(methodNamesArray, target, argument);
393 }
394
395 /**
396 * Find and execute the getter method on the target class for the supplied property name. If no such method is found, a
397 * NoSuchMethodException is thrown.
398 *
399 * @param javaPropertyName the name of the property whose getter is to be invoked, in the order they are to be tried
400 * @param target the object on which the method is to be invoked
401 * @return the property value (the result of the getter method call)
402 * @throws NoSuchMethodException if a matching method is not found.
403 * @throws SecurityException if access to the information is denied.
404 * @throws InvocationTargetException
405 * @throws IllegalAccessException
406 * @throws IllegalArgumentException
407 */
408 public Object invokeGetterMethodOnTarget( String javaPropertyName,
409 Object target )
410 throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
411 InvocationTargetException {
412 String[] methodNamesArray = findMethodNames("get" + javaPropertyName);
413 return invokeBestMethodOnTarget(methodNamesArray, target);
414 }
415
416 protected String[] findMethodNames( String methodName ) {
417 Method[] methods = findMethods(methodName, false);
418 Set<String> methodNames = new HashSet<String>();
419 for (Method method : methods) {
420 String actualMethodName = method.getName();
421 methodNames.add(actualMethodName);
422 }
423 return methodNames.toArray(new String[methodNames.size()]);
424 }
425
426 /**
427 * Find the best method on the target class that matches the signature specified with the specified name and the list of
428 * arguments. This method first attempts to find the method with the specified arguments; if no such method is found, a
429 * NoSuchMethodException is thrown.
430 * <P>
431 * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
432 * instances of <code>Number</code> or its subclasses.
433 *
434 * @param methodName the name of the method that is to be invoked.
435 * @param arguments the array of Object instances that correspond to the arguments passed to the method.
436 * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
437 * could be found.
438 * @throws NoSuchMethodException if a matching method is not found.
439 * @throws SecurityException if access to the information is denied.
440 */
441 public Method findBestMethodOnTarget( String methodName,
442 Object... arguments ) throws NoSuchMethodException, SecurityException {
443 Class<?>[] argumentClasses = buildArgumentClasses(arguments);
444 return findBestMethodWithSignature(methodName, argumentClasses);
445 }
446
447 /**
448 * Find the best method on the target class that matches the signature specified with the specified name and the list of
449 * argument classes. This method first attempts to find the method with the specified argument classes; if no such method is
450 * found, a NoSuchMethodException is thrown.
451 *
452 * @param methodName the name of the method that is to be invoked.
453 * @param argumentsClasses the list of Class instances that correspond to the classes for each argument passed to the method.
454 * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
455 * could be found.
456 * @throws NoSuchMethodException if a matching method is not found.
457 * @throws SecurityException if access to the information is denied.
458 */
459 public Method findBestMethodWithSignature( String methodName,
460 Class<?>... argumentsClasses ) throws NoSuchMethodException, SecurityException {
461
462 // Attempt to find the method
463 Method result;
464
465 // -------------------------------------------------------------------------------
466 // First try to find the method with EXACTLY the argument classes as specified ...
467 // -------------------------------------------------------------------------------
468 Class<?>[] classArgs = null;
469 try {
470 classArgs = argumentsClasses != null ? argumentsClasses : new Class[] {};
471 result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found
472 return result;
473 } catch (NoSuchMethodException e) {
474 // No method found, so continue ...
475 }
476
477 // ---------------------------------------------------------------------------------------------
478 // Then try to find a method with the argument classes converted to a primitive, if possible ...
479 // ---------------------------------------------------------------------------------------------
480 List<Class<?>> argumentsClassList = convertArgumentClassesToPrimitives(argumentsClasses);
481 try {
482 classArgs = argumentsClassList.toArray(new Class[argumentsClassList.size()]);
483 result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found
484 return result;
485 } catch (NoSuchMethodException e) {
486 // No method found, so continue ...
487 }
488
489 // ---------------------------------------------------------------------------------------------
490 // Still haven't found anything. So far, the "getMethod" logic only finds methods that EXACTLY
491 // match the argument classes (i.e., not methods declared with superclasses or interfaces of
492 // the arguments). There is no canned algorithm in Java to do this, so we have to brute-force it.
493 // The following algorithm will find the first method that matches by doing "instanceof", so it
494 // may not be the best method. Since there is some overhead to this algorithm, the first time
495 // caches some information in class members.
496 // ---------------------------------------------------------------------------------------------
497 Method method;
498 LinkedList<Method> methodsWithSameName;
499 if (this.methodMap == null) {
500 this.methodMap = new HashMap<String, LinkedList<Method>>();
501 Method[] methods = this.targetClass.getMethods();
502 for (int i = 0; i != methods.length; ++i) {
503 method = methods[i];
504 methodsWithSameName = this.methodMap.get(method.getName());
505 if (methodsWithSameName == null) {
506 methodsWithSameName = new LinkedList<Method>();
507 this.methodMap.put(method.getName(), methodsWithSameName);
508 }
509 methodsWithSameName.addFirst(method); // add lower methods first
510 }
511 }
512
513 // ------------------------------------------------------------------------
514 // Find the set of methods with the same name (do this twice, once with the
515 // original methods and once with the primitives) ...
516 // ------------------------------------------------------------------------
517 // List argClass = argumentsClasses;
518 for (int j = 0; j != 2; ++j) {
519 methodsWithSameName = this.methodMap.get(methodName);
520 if (methodsWithSameName == null) {
521 throw new NoSuchMethodException(methodName);
522 }
523 Iterator<Method> iter = methodsWithSameName.iterator();
524 Class<?>[] args;
525 Class<?> clazz;
526 boolean allMatch;
527 while (iter.hasNext()) {
528 method = iter.next();
529 args = method.getParameterTypes();
530 if (args.length == argumentsClassList.size()) {
531 allMatch = true; // assume they all match
532 for (int i = 0; i < args.length; ++i) {
533 clazz = argumentsClassList.get(i);
534 if (clazz != null) {
535 if (!args[i].isAssignableFrom(clazz)) {
536 allMatch = false; // found one that doesn't match
537 i = args.length; // force completion
538 }
539 } else {
540 // a null is assignable for everything except a primitive
541 if (args[i].isPrimitive()) {
542 allMatch = false; // found one that doesn't match
543 i = args.length; // force completion
544 }
545 }
546 }
547 if (allMatch) {
548 return method;
549 }
550 }
551 }
552 }
553
554 throw new NoSuchMethodException(methodName);
555 }
556
557 }