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 }