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