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 }