001/* 002 * ModeShape (http://www.modeshape.org) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.modeshape.common.util; 017 018import java.io.Serializable; 019import java.lang.annotation.Annotation; 020import java.lang.reflect.Array; 021import java.lang.reflect.Field; 022import java.lang.reflect.InvocationTargetException; 023import java.lang.reflect.Method; 024import java.lang.reflect.Modifier; 025import java.security.AccessController; 026import java.security.PrivilegedActionException; 027import java.security.PrivilegedExceptionAction; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.HashSet; 034import java.util.Iterator; 035import java.util.LinkedList; 036import java.util.List; 037import java.util.Map; 038import java.util.Set; 039import java.util.regex.Pattern; 040import org.modeshape.common.annotation.Category; 041import org.modeshape.common.annotation.Description; 042import org.modeshape.common.annotation.Immutable; 043import org.modeshape.common.annotation.Label; 044import org.modeshape.common.annotation.ReadOnly; 045import org.modeshape.common.i18n.I18n; 046import org.modeshape.common.text.Inflector; 047 048/** 049 * Utility class for working reflectively with objects. 050 */ 051@Immutable 052public class Reflection { 053 054 /** 055 * Build the list of classes that correspond to the list of argument objects. 056 * 057 * @param arguments the list of argument objects. 058 * @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null 059 * element for each null argument. 060 */ 061 public static Class<?>[] buildArgumentClasses( Object... arguments ) { 062 if (arguments == null || arguments.length == 0) return EMPTY_CLASS_ARRAY; 063 Class<?>[] result = new Class<?>[arguments.length]; 064 int i = 0; 065 for (Object argument : arguments) { 066 if (argument != null) { 067 result[i] = argument.getClass(); 068 } else { 069 result[i] = null; 070 } 071 } 072 return result; 073 } 074 075 /** 076 * Build the list of classes that correspond to the list of argument objects. 077 * 078 * @param arguments the list of argument objects. 079 * @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null 080 * element for each null argument. 081 */ 082 public static List<Class<?>> buildArgumentClassList( Object... arguments ) { 083 if (arguments == null || arguments.length == 0) return Collections.emptyList(); 084 List<Class<?>> result = new ArrayList<Class<?>>(arguments.length); 085 for (Object argument : arguments) { 086 if (argument != null) { 087 result.add(argument.getClass()); 088 } else { 089 result.add(null); 090 } 091 } 092 return result; 093 } 094 095 /** 096 * Convert any argument classes to primitives. 097 * 098 * @param arguments the list of argument classes. 099 * @return the list of Class instances in which any classes that could be represented by primitives (e.g., Boolean) were 100 * replaced with the primitive classes (e.g., Boolean.TYPE). 101 */ 102 public static List<Class<?>> convertArgumentClassesToPrimitives( Class<?>... arguments ) { 103 if (arguments == null || arguments.length == 0) return Collections.emptyList(); 104 List<Class<?>> result = new ArrayList<Class<?>>(arguments.length); 105 for (Class<?> clazz : arguments) { 106 if (clazz == Boolean.class) clazz = Boolean.TYPE; 107 else if (clazz == Character.class) clazz = Character.TYPE; 108 else if (clazz == Byte.class) clazz = Byte.TYPE; 109 else if (clazz == Short.class) clazz = Short.TYPE; 110 else if (clazz == Integer.class) clazz = Integer.TYPE; 111 else if (clazz == Long.class) clazz = Long.TYPE; 112 else if (clazz == Float.class) clazz = Float.TYPE; 113 else if (clazz == Double.class) clazz = Double.TYPE; 114 else if (clazz == Void.class) clazz = Void.TYPE; 115 result.add(clazz); 116 } 117 118 return result; 119 } 120 121 /** 122 * Returns the name of the class. The result will be the fully-qualified class name, or the readable form for arrays and 123 * primitive types. 124 * 125 * @param clazz the class for which the class name is to be returned. 126 * @return the readable name of the class. 127 */ 128 public static String getClassName( final Class<?> clazz ) { 129 final String fullName = clazz.getName(); 130 final int fullNameLength = fullName.length(); 131 132 // Check for array ('[') or the class/interface marker ('L') ... 133 int numArrayDimensions = 0; 134 while (numArrayDimensions < fullNameLength) { 135 final char c = fullName.charAt(numArrayDimensions); 136 if (c != '[') { 137 String name = null; 138 // Not an array, so it must be one of the other markers ... 139 switch (c) { 140 case 'L': { 141 name = fullName.subSequence(numArrayDimensions + 1, fullNameLength).toString(); 142 break; 143 } 144 case 'B': { 145 name = "byte"; 146 break; 147 } 148 case 'C': { 149 name = "char"; 150 break; 151 } 152 case 'D': { 153 name = "double"; 154 break; 155 } 156 case 'F': { 157 name = "float"; 158 break; 159 } 160 case 'I': { 161 name = "int"; 162 break; 163 } 164 case 'J': { 165 name = "long"; 166 break; 167 } 168 case 'S': { 169 name = "short"; 170 break; 171 } 172 case 'Z': { 173 name = "boolean"; 174 break; 175 } 176 case 'V': { 177 name = "void"; 178 break; 179 } 180 default: { 181 name = fullName.subSequence(numArrayDimensions, fullNameLength).toString(); 182 } 183 } 184 if (numArrayDimensions == 0) { 185 // No array markers, so just return the name ... 186 return name; 187 } 188 // Otherwise, add the array markers and the name ... 189 if (numArrayDimensions < BRACKETS_PAIR.length) { 190 name = name + BRACKETS_PAIR[numArrayDimensions]; 191 } else { 192 for (int i = 0; i < numArrayDimensions; i++) { 193 name = name + BRACKETS_PAIR[1]; 194 } 195 } 196 return name; 197 } 198 ++numArrayDimensions; 199 } 200 201 return fullName; 202 } 203 204 /** 205 * Sets the value of a field of an object instance via reflection 206 * 207 * @param instance to inspect 208 * @param fieldName name of field to set 209 * @param value the value to set 210 */ 211 public static void setValue(Object instance, String fieldName, Object value) { 212 try { 213 Field f = findFieldRecursively(instance.getClass(), fieldName); 214 if (f == null) 215 throw new NoSuchMethodException("Cannot find field " + fieldName + " on " + instance.getClass() + " or superclasses"); 216 f.setAccessible(true); 217 f.set(instance, value); 218 } catch (Exception e) { 219 throw new RuntimeException(e); 220 } 221 } 222 223 /** 224 * Retrieves the field with the given name from a class 225 * 226 * @param fieldName the field to retrieve 227 * @param objectClass the class from which to retrieve the field 228 * @return either a {@link Field} instance or {@code null} if no such field exists. 229 */ 230 public static Field getField(String fieldName, Class<?> objectClass) { 231 try { 232 return objectClass.getDeclaredField(fieldName); 233 } catch (NoSuchFieldException e) { 234 if (!objectClass.equals(Object.class)) { 235 return getField(fieldName, objectClass.getSuperclass()); 236 } else { 237 return null; 238 } 239 } 240 } 241 242 /** 243 * Searches for a method with a given name in a class. 244 * 245 * @param type a {@link Class} instance; never null 246 * @param methodName the name of the method to search for; never null 247 * @return a {@link Method} instance if the method is found 248 */ 249 public static Method findMethod(Class<?> type, String methodName) { 250 try { 251 return type.getDeclaredMethod(methodName); 252 } catch (NoSuchMethodException e) { 253 if (type.equals(Object.class) || type.isInterface()) { 254 throw new RuntimeException(e); 255 } 256 return findMethod(type.getSuperclass(), methodName); 257 } 258 } 259 260 /** 261 * Searches for a given field recursively under a particular class 262 * 263 * @param c a {@link Class} instance, never null 264 * @param fieldName the name of the field, never null 265 * @return a {@link Field} instance if the field is located anywhere in the hierarchy or {@code null} if no such field exists 266 */ 267 public static Field findFieldRecursively(Class<?> c, String fieldName) { 268 Field f = null; 269 try { 270 f = c.getDeclaredField(fieldName); 271 } catch (NoSuchFieldException e) { 272 if (!c.equals(Object.class)) f = findFieldRecursively(c.getSuperclass(), fieldName); 273 } 274 return f; 275 } 276 277 /** 278 * Instantiates a class based on the class name provided. Instantiation is attempted via an appropriate, static 279 * factory method named <tt>getInstance()</tt> first, and failing the existence of an appropriate factory, falls 280 * back to an empty constructor. 281 * <p /> 282 * 283 * @param classname class to instantiate 284 * @return an instance of classname 285 */ 286 @SuppressWarnings( "unchecked" ) 287 public static <T> T getInstance(String classname, ClassLoader cl) { 288 if (classname == null) throw new IllegalArgumentException("Cannot load null class!"); 289 Class<T> clazz = null; 290 try { 291 clazz = (Class<T>)Class.forName(classname, true, cl); 292 // first look for a getInstance() constructor 293 T instance = null; 294 try { 295 Method factoryMethod = getFactoryMethod(clazz); 296 if (factoryMethod != null) instance = (T) factoryMethod.invoke(null); 297 } 298 catch (Exception e) { 299 // no factory method or factory method failed. Try a constructor. 300 instance = null; 301 } 302 if (instance == null) { 303 instance = clazz.newInstance(); 304 } 305 return instance; 306 } catch (Exception e) { 307 throw new RuntimeException(e); 308 } 309 } 310 311 private static Method getFactoryMethod(Class<?> c) { 312 for (Method m : c.getMethods()) { 313 if (m.getName().equals("getInstance") && m.getParameterTypes().length == 0 && Modifier.isStatic(m.getModifiers())) 314 return m; 315 } 316 return null; 317 } 318 319 /** 320 * Invokes a method using reflection, in an accessible manner (by using {@link Method#setAccessible(boolean)} 321 * 322 * @param instance instance on which to execute the method 323 * @param method method to execute 324 * @param parameters parameters 325 */ 326 public static Object invokeAccessibly(Object instance, Method method, Object[] parameters) { 327 try { 328 method.setAccessible(true); 329 return method.invoke(instance, parameters); 330 } catch (Exception e) { 331 throw new RuntimeException("Unable to invoke method " + method + " on object of type " + (instance == null ? 332 "null" : 333 instance.getClass().getSimpleName()) + 334 (parameters != null ? " with parameters " + Arrays.asList(parameters) : ""), e); 335 } 336 } 337 338 private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[] {}; 339 private static final String[] BRACKETS_PAIR = new String[] {"", "[]", "[][]", "[][][]", "[][][][]", "[][][][][]"}; 340 341 private final Class<?> targetClass; 342 private Map<String, LinkedList<Method>> methodMap = null; // used for the brute-force method finder 343 344 /** 345 * Construct a Reflection instance that cache's some information about the target class. The target class is the Class object 346 * upon which the methods will be found. 347 * 348 * @param targetClass the target class 349 * @throws IllegalArgumentException if the target class is null 350 */ 351 public Reflection( Class<?> targetClass ) { 352 CheckArg.isNotNull(targetClass, "targetClass"); 353 this.targetClass = targetClass; 354 } 355 356 /** 357 * Return the class that is the target for the reflection methods. 358 * 359 * @return the target class 360 */ 361 public Class<?> getTargetClass() { 362 return this.targetClass; 363 } 364 365 /** 366 * Find the method on the target class that matches the supplied method name. 367 * 368 * @param methodName the name of the method that is to be found. 369 * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter 370 * @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name. 371 */ 372 public Method[] findMethods( String methodName, 373 boolean caseSensitive ) { 374 Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE); 375 return findMethods(pattern); 376 } 377 378 /** 379 * Find the methods on the target class that matches the supplied method name. 380 * 381 * @param methodNamePattern the regular expression pattern for the name of the method that is to be found. 382 * @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name. 383 */ 384 public Method[] findMethods( Pattern methodNamePattern ) { 385 final Method[] allMethods = this.targetClass.getMethods(); 386 final List<Method> result = new ArrayList<Method>(); 387 for (int i = 0; i < allMethods.length; i++) { 388 final Method m = allMethods[i]; 389 if (methodNamePattern.matcher(m.getName()).matches()) { 390 result.add(m); 391 } 392 } 393 return result.toArray(new Method[result.size()]); 394 } 395 396 /** 397 * Find the getter methods on the target class that begin with "get" or "is", that have no parameters, and that return 398 * something other than void. This method skips the {@link Object#getClass()} method. 399 * 400 * @return the Method objects for the getters; never null but possibly empty 401 */ 402 public Method[] findGetterMethods() { 403 final Method[] allMethods = this.targetClass.getMethods(); 404 final List<Method> result = new ArrayList<Method>(); 405 for (int i = 0; i < allMethods.length; i++) { 406 final Method m = allMethods[i]; 407 int numParams = m.getParameterTypes().length; 408 if (numParams != 0) continue; 409 String name = m.getName(); 410 if ("getClass()".equals(name)) continue; 411 if (m.getReturnType() == Void.TYPE) continue; 412 if (name.startsWith("get") || name.startsWith("is") || name.startsWith("are")) { 413 result.add(m); 414 } 415 } 416 return result.toArray(new Method[result.size()]); 417 } 418 419 /** 420 * Find the property names with getter methods on the target class. This method returns the property names for the methods 421 * returned by {@link #findGetterMethods()}. 422 * 423 * @return the Java Bean property names for the getters; never null but possibly empty 424 */ 425 public String[] findGetterPropertyNames() { 426 final Method[] getters = findGetterMethods(); 427 final List<String> result = new ArrayList<String>(); 428 for (int i = 0; i < getters.length; i++) { 429 final Method m = getters[i]; 430 String name = m.getName(); 431 String propertyName = null; 432 if (name.startsWith("get") && name.length() > 3) { 433 propertyName = name.substring(3); 434 } else if (name.startsWith("is") && name.length() > 2) { 435 propertyName = name.substring(2); 436 } else if (name.startsWith("are") && name.length() > 3) { 437 propertyName = name.substring(3); 438 } 439 if (propertyName != null) { 440 propertyName = INFLECTOR.camelCase(INFLECTOR.underscore(propertyName), false); 441 result.add(propertyName); 442 } 443 } 444 return result.toArray(new String[result.size()]); 445 } 446 447 /** 448 * Find the method on the target class that matches the supplied method name. 449 * 450 * @param methodName the name of the method that is to be found. 451 * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter 452 * @return the first Method object found that has a matching name, or null if there are no methods that have a matching name. 453 */ 454 public Method findFirstMethod( String methodName, 455 boolean caseSensitive ) { 456 Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE); 457 return findFirstMethod(pattern); 458 } 459 460 /** 461 * Find the method on the target class that matches the supplied method name. 462 * 463 * @param methodNamePattern the regular expression pattern for the name of the method that is to be found. 464 * @return the first Method object found that has a matching name, or null if there are no methods that have a matching name. 465 */ 466 public Method findFirstMethod( Pattern methodNamePattern ) { 467 final Method[] allMethods = this.targetClass.getMethods(); 468 for (int i = 0; i < allMethods.length; i++) { 469 final Method m = allMethods[i]; 470 if (methodNamePattern.matcher(m.getName()).matches()) { 471 return m; 472 } 473 } 474 return null; 475 } 476 477 /** 478 * Finds the methods on the target class that match the supplied method name. 479 * 480 * @param methodName the name of the method that is to be found. 481 * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter 482 * @return the Method objects that have a matching name, or empty if there are no methods that have a matching name. 483 */ 484 public Iterable<Method> findAllMethods( String methodName, 485 boolean caseSensitive ) { 486 Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE); 487 return findAllMethods(pattern); 488 } 489 490 /** 491 * Finds the methods on the target class that match the supplied method name. 492 * 493 * @param methodNamePattern the regular expression pattern for the name of the method that is to be found. 494 * @return the Method objects that have a matching name, or empty if there are no methods that have a matching name. 495 */ 496 public Iterable<Method> findAllMethods( Pattern methodNamePattern ) { 497 LinkedList<Method> methods = new LinkedList<Method>(); 498 499 final Method[] allMethods = this.targetClass.getMethods(); 500 for (int i = 0; i < allMethods.length; i++) { 501 final Method m = allMethods[i]; 502 if (methodNamePattern.matcher(m.getName()).matches()) { 503 methods.add(m); 504 } 505 } 506 return methods; 507 } 508 509 /** 510 * Find and execute the best method on the target class that matches the signature specified with one of the specified names 511 * and the list of arguments. If no such method is found, a NoSuchMethodException is thrown. 512 * <P> 513 * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are 514 * instances of <code>Number</code> or its subclasses. 515 * </p> 516 * 517 * @param methodNames the names of the methods that are to be invoked, in the order they are to be tried 518 * @param target the object on which the method is to be invoked 519 * @param arguments the array of Object instances that correspond to the arguments passed to the method. 520 * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method 521 * could be found. 522 * @throws NoSuchMethodException if a matching method is not found. 523 * @throws SecurityException if access to the information is denied. 524 * @throws InvocationTargetException 525 * @throws IllegalAccessException 526 * @throws IllegalArgumentException 527 */ 528 public Object invokeBestMethodOnTarget( String[] methodNames, 529 final Object target, 530 final Object... arguments ) 531 throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException, 532 InvocationTargetException { 533 Class<?>[] argumentClasses = buildArgumentClasses(arguments); 534 int remaining = methodNames.length; 535 Object result = null; 536 for (String methodName : methodNames) { 537 --remaining; 538 try { 539 final Method method = findBestMethodWithSignature(methodName, argumentClasses); 540 result = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { 541 @Override 542 public Object run() throws Exception { 543 return method.invoke(target, arguments); 544 } 545 }); 546 break; 547 } catch (PrivilegedActionException pae) { 548 // pae will be wrapping one of IllegalAccessException, IllegalArgumentException, InvocationTargetException 549 if (pae.getException() instanceof IllegalAccessException) { 550 throw (IllegalAccessException)pae.getException(); 551 } 552 553 if (pae.getException() instanceof IllegalArgumentException) { 554 throw (IllegalArgumentException)pae.getException(); 555 } 556 557 if (pae.getException() instanceof InvocationTargetException) { 558 throw (InvocationTargetException)pae.getException(); 559 } 560 561 } catch (NoSuchMethodException e) { 562 if (remaining == 0) throw e; 563 } 564 } 565 return result; 566 } 567 568 /** 569 * Find and execute the best setter method on the target class for the supplied property name and the supplied list of 570 * arguments. If no such method is found, a NoSuchMethodException is thrown. 571 * <P> 572 * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are 573 * instances of <code>Number</code> or its subclasses. 574 * </p> 575 * 576 * @param javaPropertyName the name of the property whose setter is to be invoked, in the order they are to be tried 577 * @param target the object on which the method is to be invoked 578 * @param argument the new value for the property 579 * @return the result of the setter method, which is typically null (void) 580 * @throws NoSuchMethodException if a matching method is not found. 581 * @throws SecurityException if access to the information is denied. 582 * @throws InvocationTargetException 583 * @throws IllegalAccessException 584 * @throws IllegalArgumentException 585 */ 586 public Object invokeSetterMethodOnTarget( String javaPropertyName, 587 Object target, 588 Object argument ) 589 throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException, 590 InvocationTargetException { 591 String[] methodNamesArray = findMethodNames("set" + javaPropertyName); 592 try { 593 return invokeBestMethodOnTarget(methodNamesArray, target, argument); 594 } catch (NoSuchMethodException e) { 595 // If the argument is an Object[], see if it works with an array of whatever type the actual value is ... 596 if (argument instanceof Object[]) { 597 Object[] arrayArg = (Object[])argument; 598 for (Object arrayValue : arrayArg) { 599 if (arrayValue == null) continue; 600 Class<?> arrayValueType = arrayValue.getClass(); 601 // Create an array of this type ... 602 Object typedArray = Array.newInstance(arrayValueType, arrayArg.length); 603 Object[] newArray = (Object[])typedArray; 604 for (int i = 0; i != arrayArg.length; ++i) { 605 newArray[i] = arrayArg[i]; 606 } 607 // Try to execute again ... 608 try { 609 return invokeBestMethodOnTarget(methodNamesArray, target, typedArray); 610 } catch (NoSuchMethodException e2) { 611 // Throw the original exception ... 612 throw e; 613 } 614 } 615 } 616 throw e; 617 } 618 } 619 620 /** 621 * Find and execute the getter method on the target class for the supplied property name. If no such method is found, a 622 * NoSuchMethodException is thrown. 623 * 624 * @param javaPropertyName the name of the property whose getter is to be invoked, in the order they are to be tried 625 * @param target the object on which the method is to be invoked 626 * @return the property value (the result of the getter method call) 627 * @throws NoSuchMethodException if a matching method is not found. 628 * @throws SecurityException if access to the information is denied. 629 * @throws InvocationTargetException 630 * @throws IllegalAccessException 631 * @throws IllegalArgumentException 632 */ 633 public Object invokeGetterMethodOnTarget( String javaPropertyName, 634 Object target ) 635 throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException, 636 InvocationTargetException { 637 String[] methodNamesArray = findMethodNames("get" + javaPropertyName); 638 if (methodNamesArray.length <= 0) { 639 // Try 'is' getter ... 640 methodNamesArray = findMethodNames("is" + javaPropertyName); 641 } 642 if (methodNamesArray.length <= 0) { 643 // Try 'are' getter ... 644 methodNamesArray = findMethodNames("are" + javaPropertyName); 645 } 646 return invokeBestMethodOnTarget(methodNamesArray, target); 647 } 648 649 protected String[] findMethodNames( String methodName ) { 650 Method[] methods = findMethods(methodName, false); 651 Set<String> methodNames = new HashSet<String>(); 652 for (Method method : methods) { 653 String actualMethodName = method.getName(); 654 methodNames.add(actualMethodName); 655 } 656 return methodNames.toArray(new String[methodNames.size()]); 657 } 658 659 /** 660 * Find the best method on the target class that matches the signature specified with the specified name and the list of 661 * arguments. This method first attempts to find the method with the specified arguments; if no such method is found, a 662 * NoSuchMethodException is thrown. 663 * <P> 664 * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are 665 * instances of <code>Number</code> or its subclasses. 666 * 667 * @param methodName the name of the method that is to be invoked. 668 * @param arguments the array of Object instances that correspond to the arguments passed to the method. 669 * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method 670 * could be found. 671 * @throws NoSuchMethodException if a matching method is not found. 672 * @throws SecurityException if access to the information is denied. 673 */ 674 public Method findBestMethodOnTarget( String methodName, 675 Object... arguments ) throws NoSuchMethodException, SecurityException { 676 Class<?>[] argumentClasses = buildArgumentClasses(arguments); 677 return findBestMethodWithSignature(methodName, argumentClasses); 678 } 679 680 /** 681 * Find the best method on the target class that matches the signature specified with the specified name and the list of 682 * argument classes. This method first attempts to find the method with the specified argument classes; if no such method is 683 * found, a NoSuchMethodException is thrown. 684 * 685 * @param methodName the name of the method that is to be invoked. 686 * @param argumentsClasses the list of Class instances that correspond to the classes for each argument passed to the method. 687 * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method 688 * could be found. 689 * @throws NoSuchMethodException if a matching method is not found. 690 * @throws SecurityException if access to the information is denied. 691 */ 692 public Method findBestMethodWithSignature( String methodName, 693 Class<?>... argumentsClasses ) throws NoSuchMethodException, SecurityException { 694 695 return findBestMethodWithSignature(methodName, true, argumentsClasses); 696 } 697 698 /** 699 * Find the best method on the target class that matches the signature specified with the specified name and the list of 700 * argument classes. This method first attempts to find the method with the specified argument classes; if no such method is 701 * found, a NoSuchMethodException is thrown. 702 * 703 * @param methodName the name of the method that is to be invoked. 704 * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter 705 * @param argumentsClasses the list of Class instances that correspond to the classes for each argument passed to the method. 706 * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method 707 * could be found. 708 * @throws NoSuchMethodException if a matching method is not found. 709 * @throws SecurityException if access to the information is denied. 710 */ 711 public Method findBestMethodWithSignature( String methodName, 712 boolean caseSensitive, 713 Class<?>... argumentsClasses ) throws NoSuchMethodException, SecurityException { 714 715 // Attempt to find the method 716 Method result; 717 718 // ------------------------------------------------------------------------------- 719 // First try to find the method with EXACTLY the argument classes as specified ... 720 // ------------------------------------------------------------------------------- 721 Class<?>[] classArgs = null; 722 try { 723 classArgs = argumentsClasses != null ? argumentsClasses : new Class[] {}; 724 result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found 725 return result; 726 } catch (NoSuchMethodException e) { 727 // No method found, so continue ... 728 } 729 730 // --------------------------------------------------------------------------------------------- 731 // Then try to find a method with the argument classes converted to a primitive, if possible ... 732 // --------------------------------------------------------------------------------------------- 733 List<Class<?>> argumentsClassList = convertArgumentClassesToPrimitives(argumentsClasses); 734 try { 735 classArgs = argumentsClassList.toArray(new Class[argumentsClassList.size()]); 736 result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found 737 return result; 738 } catch (NoSuchMethodException e) { 739 // No method found, so continue ... 740 } 741 742 // --------------------------------------------------------------------------------------------- 743 // Still haven't found anything. So far, the "getMethod" logic only finds methods that EXACTLY 744 // match the argument classes (i.e., not methods declared with superclasses or interfaces of 745 // the arguments). There is no canned algorithm in Java to do this, so we have to brute-force it. 746 // The following algorithm will find the first method that matches by doing "instanceof", so it 747 // may not be the best method. Since there is some overhead to this algorithm, the first time 748 // caches some information in class members. 749 // --------------------------------------------------------------------------------------------- 750 Method method; 751 LinkedList<Method> methodsWithSameName; 752 if (this.methodMap == null) { 753 // This is idempotent, so no need to lock or synchronize ... 754 this.methodMap = new HashMap<String, LinkedList<Method>>(); 755 Method[] methods = this.targetClass.getMethods(); 756 for (int i = 0; i != methods.length; ++i) { 757 method = methods[i]; 758 methodsWithSameName = this.methodMap.get(method.getName()); 759 if (methodsWithSameName == null) { 760 methodsWithSameName = new LinkedList<Method>(); 761 this.methodMap.put(method.getName(), methodsWithSameName); 762 } 763 methodsWithSameName.addFirst(method); // add lower methods first 764 } 765 } 766 767 // ------------------------------------------------------------------------ 768 // Find the set of methods with the same name (do this twice, once with the 769 // original methods and once with the primitives) ... 770 // ------------------------------------------------------------------------ 771 // List argClass = argumentsClasses; 772 for (int j = 0; j != 2; ++j) { 773 774 if (caseSensitive) { 775 methodsWithSameName = this.methodMap.get(methodName); 776 } else { 777 methodsWithSameName = new LinkedList<Method>(); 778 Pattern pattern = Pattern.compile(methodName, Pattern.CASE_INSENSITIVE); 779 780 for (Map.Entry<String, LinkedList<Method>> entry : this.methodMap.entrySet()) { 781 // entry.getKey() is the method name 782 if (pattern.matcher(entry.getKey()).matches()) { 783 methodsWithSameName.addAll(entry.getValue()); 784 } 785 } 786 } 787 788 if (methodsWithSameName == null) { 789 throw new NoSuchMethodException(methodName); 790 } 791 Iterator<Method> iter = methodsWithSameName.iterator(); 792 Class<?>[] args; 793 Class<?> clazz; 794 boolean allMatch; 795 while (iter.hasNext()) { 796 method = iter.next(); 797 args = method.getParameterTypes(); 798 if (args.length == argumentsClassList.size()) { 799 allMatch = true; // assume they all match 800 for (int i = 0; i < args.length; ++i) { 801 clazz = argumentsClassList.get(i); 802 if (clazz != null) { 803 Class<?> argClass = args[i]; 804 if (argClass.isAssignableFrom(clazz)) { 805 // It's a match 806 } else if (argClass.isArray() && clazz.isArray() 807 && argClass.getComponentType().isAssignableFrom(clazz.getComponentType())) { 808 // They're both arrays, and they're castable, so we're good ... 809 } else { 810 allMatch = false; // found one that doesn't match 811 i = args.length; // force completion 812 } 813 } else { 814 // a null is assignable for everything except a primitive 815 if (args[i].isPrimitive()) { 816 allMatch = false; // found one that doesn't match 817 i = args.length; // force completion 818 } 819 } 820 } 821 if (allMatch) { 822 return method; 823 } 824 } 825 } 826 } 827 828 throw new NoSuchMethodException(methodName); 829 } 830 831 /** 832 * Get the representation of the named property (with the supplied labe, category, description, and allowed values) on the 833 * target object. 834 * <p> 835 * If the label is not provided, this method looks for the {@link Label} annotation on the property's field and sets the label 836 * to the annotation's literal value, or if the {@link Label#i18n()} class is referenced, the localized value of the 837 * referenced {@link I18n}. 838 * </p> 839 * If the description is not provided, this method looks for the {@link Description} annotation on the property's field and 840 * sets the label to the annotation's literal value, or if the {@link Description#i18n()} class is referenced, the localized 841 * value of the referenced {@link I18n}. </p> 842 * <p> 843 * And if the category is not provided, this method looks for the {@link Category} annotation on the property's field and sets 844 * the label to the annotation's literal value, or if the {@link Category#i18n()} class is referenced, the localized value of 845 * the referenced {@link I18n}. 846 * </p> 847 * 848 * @param target the target on which the setter is to be called; may not be null 849 * @param propertyName the name of the Java object property; may not be null 850 * @param label the new label for the property; may be null 851 * @param category the category for this property; may be null 852 * @param description the description for the property; may be null if this is not known 853 * @param allowedValues the of allowed values, or null or empty if the values are not constrained 854 * @return the representation of the Java property; never null 855 * @throws NoSuchMethodException if a matching method is not found. 856 * @throws SecurityException if access to the information is denied. 857 * @throws IllegalAccessException if the setter method could not be accessed 858 * @throws InvocationTargetException if there was an error invoking the setter method on the target 859 * @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty 860 */ 861 public Property getProperty( Object target, 862 String propertyName, 863 String label, 864 String category, 865 String description, 866 Object... allowedValues ) 867 throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, 868 InvocationTargetException { 869 CheckArg.isNotNull(target, "target"); 870 CheckArg.isNotEmpty(propertyName, "propertyName"); 871 Method[] setters = findMethods("set" + propertyName, false); 872 boolean readOnly = setters.length < 1; 873 Class<?> type = Object.class; 874 Method[] getters = findMethods("get" + propertyName, false); 875 if (getters.length == 0) { 876 getters = findMethods("is" + propertyName, false); 877 } 878 if (getters.length == 0) { 879 getters = findMethods("are" + propertyName, false); 880 } 881 if (getters.length > 0) { 882 type = getters[0].getReturnType(); 883 } 884 boolean inferred = true; 885 Field field = null; 886 try { 887 // Find the corresponding field ... 888 field = getField(targetClass, propertyName); 889 } catch (NoSuchFieldException e) { 890 // Nothing to do here 891 892 } 893 if (description == null) { 894 Description desc = getAnnotation(Description.class, field, getters, setters); 895 if (desc != null) { 896 description = localizedString(desc.i18n(), desc.value()); 897 inferred = false; 898 } 899 } 900 if (label == null) { 901 Label labelAnnotation = getAnnotation(Label.class, field, getters, setters); 902 if (labelAnnotation != null) { 903 label = localizedString(labelAnnotation.i18n(), labelAnnotation.value()); 904 inferred = false; 905 } 906 } 907 if (category == null) { 908 Category cat = getAnnotation(Category.class, field, getters, setters); 909 if (cat != null) { 910 category = localizedString(cat.i18n(), cat.value()); 911 inferred = false; 912 } 913 } 914 if (!readOnly) { 915 ReadOnly readOnlyAnnotation = getAnnotation(ReadOnly.class, field, getters, setters); 916 if (readOnlyAnnotation != null) { 917 readOnly = true; 918 inferred = false; 919 } 920 } 921 922 Property property = new Property(propertyName, label, description, category, readOnly, type, allowedValues); 923 property.setInferred(inferred); 924 return property; 925 } 926 927 /** 928 * Get a Field intance for a given class and property. Iterate over super classes of a class when a <@link 929 * NoSuchFieldException> occurs until no more super classes are found then re-throw the <@link NoSuchFieldException>. 930 * 931 * @param targetClass 932 * @param propertyName 933 * @return Field 934 * @throws NoSuchFieldException 935 */ 936 protected Field getField( Class<?> targetClass, 937 String propertyName ) throws NoSuchFieldException { 938 Field field = null; 939 940 try { 941 field = targetClass.getDeclaredField(Inflector.getInstance().lowerCamelCase(propertyName)); 942 } catch (NoSuchFieldException e) { 943 Class<?> clazz = targetClass.getSuperclass(); 944 if (clazz != null) { 945 field = getField(clazz, propertyName); 946 } else { 947 throw e; 948 } 949 } 950 951 return field; 952 } 953 954 protected static <AnnotationType extends Annotation> AnnotationType getAnnotation( Class<AnnotationType> annotationType, 955 Field field, 956 Method[] getters, 957 Method[] setters ) { 958 AnnotationType annotation = null; 959 if (field != null) { 960 annotation = field.getAnnotation(annotationType); 961 } 962 if (annotation == null && getters != null) { 963 for (Method getter : getters) { 964 annotation = getter.getAnnotation(annotationType); 965 if (annotation != null) break; 966 } 967 } 968 if (annotation == null && setters != null) { 969 for (Method setter : setters) { 970 annotation = setter.getAnnotation(annotationType); 971 if (annotation != null) break; 972 } 973 } 974 return annotation; 975 } 976 977 protected static String localizedString( Class<?> i18nClass, 978 String id ) { 979 if (i18nClass != null && !Object.class.equals(i18nClass) && id != null) { 980 try { 981 // Look up the I18n field ... 982 Field i18nMsg = i18nClass.getDeclaredField(id); 983 I18n msg = (I18n)i18nMsg.get(null); 984 if (msg != null) { 985 return msg.text(); 986 } 987 } catch (SecurityException err) { 988 // ignore 989 } catch (NoSuchFieldException err) { 990 // ignore 991 } catch (IllegalArgumentException err) { 992 // ignore 993 } catch (IllegalAccessException err) { 994 // ignore 995 } 996 } 997 return id; 998 } 999 1000 /** 1001 * Get the representation of the named property (with the supplied description) on the target object. 1002 * 1003 * @param target the target on which the setter is to be called; may not be null 1004 * @param propertyName the name of the Java object property; may not be null 1005 * @param description the description for the property; may be null if this is not known 1006 * @return the representation of the Java property; never null 1007 * @throws NoSuchMethodException if a matching method is not found. 1008 * @throws SecurityException if access to the information is denied. 1009 * @throws IllegalAccessException if the setter method could not be accessed 1010 * @throws InvocationTargetException if there was an error invoking the setter method on the target 1011 * @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty 1012 */ 1013 public Property getProperty( Object target, 1014 String propertyName, 1015 String description ) 1016 throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, 1017 InvocationTargetException { 1018 CheckArg.isNotNull(target, "target"); 1019 CheckArg.isNotEmpty(propertyName, "propertyName"); 1020 return getProperty(target, propertyName, null, null, description); 1021 } 1022 1023 /** 1024 * Get the representation of the named property on the target object. 1025 * 1026 * @param target the target on which the setter is to be called; may not be null 1027 * @param propertyName the name of the Java object property; may not be null 1028 * @return the representation of the Java property; never null 1029 * @throws NoSuchMethodException if a matching method is not found. 1030 * @throws SecurityException if access to the information is denied. 1031 * @throws IllegalAccessException if the setter method could not be accessed 1032 * @throws InvocationTargetException if there was an error invoking the setter method on the target 1033 * @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty 1034 */ 1035 public Property getProperty( Object target, 1036 String propertyName ) 1037 throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, 1038 InvocationTargetException { 1039 CheckArg.isNotNull(target, "target"); 1040 CheckArg.isNotEmpty(propertyName, "propertyName"); 1041 return getProperty(target, propertyName, null, null, null); 1042 } 1043 1044 /** 1045 * Get representations for all of the Java properties on the supplied object. 1046 * 1047 * @param target the target on which the setter is to be called; may not be null 1048 * @return the list of all properties; never null 1049 * @throws NoSuchMethodException if a matching method is not found. 1050 * @throws SecurityException if access to the information is denied. 1051 * @throws IllegalAccessException if the setter method could not be accessed 1052 * @throws InvocationTargetException if there was an error invoking the setter method on the target 1053 * @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty 1054 */ 1055 public List<Property> getAllPropertiesOn( Object target ) 1056 throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, 1057 InvocationTargetException { 1058 String[] propertyNames = findGetterPropertyNames(); 1059 List<Property> results = new ArrayList<Property>(propertyNames.length); 1060 for (String propertyName : propertyNames) { 1061 if ("class".equals(propertyName)) continue; 1062 Property prop = getProperty(target, propertyName); 1063 results.add(prop); 1064 } 1065 Collections.sort(results); 1066 return results; 1067 } 1068 1069 /** 1070 * Get representations for all of the Java properties on the supplied object. 1071 * 1072 * @param target the target on which the setter is to be called; may not be null 1073 * @return the map of all properties keyed by their name; never null 1074 * @throws NoSuchMethodException if a matching method is not found. 1075 * @throws SecurityException if access to the information is denied. 1076 * @throws IllegalAccessException if the setter method could not be accessed 1077 * @throws InvocationTargetException if there was an error invoking the setter method on the target 1078 * @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty 1079 */ 1080 public Map<String, Property> getAllPropertiesByNameOn( Object target ) 1081 throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, 1082 InvocationTargetException { 1083 String[] propertyNames = findGetterPropertyNames(); 1084 Map<String, Property> results = new HashMap<String, Property>(); 1085 for (String propertyName : propertyNames) { 1086 if ("class".equals(propertyName)) continue; 1087 Property prop = getProperty(target, propertyName); 1088 results.put(prop.getName(), prop); 1089 } 1090 return results; 1091 } 1092 1093 /** 1094 * Set the property on the supplied target object to the specified value. 1095 * 1096 * @param target the target on which the setter is to be called; may not be null 1097 * @param property the property that is to be set on the target 1098 * @param value the new value for the property 1099 * @throws NoSuchMethodException if a matching method is not found. 1100 * @throws SecurityException if access to the information is denied. 1101 * @throws IllegalAccessException if the setter method could not be accessed 1102 * @throws InvocationTargetException if there was an error invoking the setter method on the target 1103 * @throws IllegalArgumentException if 'target' is null, 'property' is null, or 'property.getName()' is null 1104 */ 1105 public void setProperty( Object target, 1106 Property property, 1107 Object value ) 1108 throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, 1109 InvocationTargetException { 1110 CheckArg.isNotNull(target, "target"); 1111 CheckArg.isNotNull(property, "property"); 1112 CheckArg.isNotNull(property.getName(), "property.getName()"); 1113 invokeSetterMethodOnTarget(property.getName(), target, value); 1114 } 1115 1116 /** 1117 * Get current value for the property on the supplied target object. 1118 * 1119 * @param target the target on which the setter is to be called; may not be null 1120 * @param property the property that is to be set on the target 1121 * @return the current value for the property; may be null 1122 * @throws NoSuchMethodException if a matching method is not found. 1123 * @throws SecurityException if access to the information is denied. 1124 * @throws IllegalAccessException if the setter method could not be accessed 1125 * @throws InvocationTargetException if there was an error invoking the setter method on the target 1126 * @throws IllegalArgumentException if 'target' is null, 'property' is null, or 'property.getName()' is null 1127 */ 1128 public Object getProperty( Object target, 1129 Property property ) 1130 throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, 1131 InvocationTargetException { 1132 CheckArg.isNotNull(target, "target"); 1133 CheckArg.isNotNull(property, "property"); 1134 CheckArg.isNotNull(property.getName(), "property.getName()"); 1135 return invokeGetterMethodOnTarget(property.getName(), target); 1136 } 1137 1138 /** 1139 * Get current value represented as a string for the property on the supplied target object. 1140 * 1141 * @param target the target on which the setter is to be called; may not be null 1142 * @param property the property that is to be set on the target 1143 * @return the current value for the property; may be null 1144 * @throws NoSuchMethodException if a matching method is not found. 1145 * @throws SecurityException if access to the information is denied. 1146 * @throws IllegalAccessException if the setter method could not be accessed 1147 * @throws InvocationTargetException if there was an error invoking the setter method on the target 1148 * @throws IllegalArgumentException if 'target' is null, 'property' is null, or 'property.getName()' is null 1149 */ 1150 public String getPropertyAsString( Object target, 1151 Property property ) 1152 throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, 1153 InvocationTargetException { 1154 Object value = getProperty(target, property); 1155 StringBuilder sb = new StringBuilder(); 1156 writeObjectAsString(value, sb, false); 1157 return sb.toString(); 1158 } 1159 1160 protected void writeObjectAsString( Object obj, 1161 StringBuilder sb, 1162 boolean wrapWithBrackets ) { 1163 if (obj == null) { 1164 sb.append("null"); 1165 return; 1166 } 1167 if (obj.getClass().isArray()) { 1168 Object[] array = (Object[])obj; 1169 boolean first = true; 1170 if (wrapWithBrackets) sb.append("["); 1171 for (Object value : array) { 1172 if (first) first = false; 1173 else sb.append(", "); 1174 writeObjectAsString(value, sb, true); 1175 } 1176 if (wrapWithBrackets) sb.append("]"); 1177 return; 1178 } 1179 sb.append(obj); 1180 } 1181 1182 protected static final Inflector INFLECTOR = Inflector.getInstance(); 1183 1184 /** 1185 * A representation of a property on a Java object. 1186 */ 1187 public static class Property implements Comparable<Property>, Serializable { 1188 1189 private static final long serialVersionUID = 1L; 1190 1191 private String name; 1192 private String label; 1193 private String description; 1194 private Object value; 1195 private Collection<?> allowedValues; 1196 private Class<?> type; 1197 private boolean readOnly; 1198 private String category; 1199 private boolean inferred; 1200 1201 /** 1202 * Create a new object property that has no fields initialized. 1203 */ 1204 public Property() { 1205 } 1206 1207 /** 1208 * Create a new object property with the supplied parameters set. 1209 * 1210 * @param name the property name; may be null 1211 * @param label the human-readable property label; may be null 1212 * @param description the description for this property; may be null 1213 * @param readOnly true if the property is read-only, or false otherwise 1214 */ 1215 public Property( String name, 1216 String label, 1217 String description, 1218 boolean readOnly ) { 1219 this(name, label, description, null, readOnly, null); 1220 } 1221 1222 /** 1223 * Create a new object property with the supplied parameters set. 1224 * 1225 * @param name the property name; may be null 1226 * @param label the human-readable property label; may be null 1227 * @param description the description for this property; may be null 1228 * @param category the category for this property; may be null 1229 * @param readOnly true if the property is read-only, or false otherwise 1230 * @param type the value class; may be null 1231 * @param allowedValues the array of allowed values, or null or empty if the values are not constrained 1232 */ 1233 public Property( String name, 1234 String label, 1235 String description, 1236 String category, 1237 boolean readOnly, 1238 Class<?> type, 1239 Object... allowedValues ) { 1240 setName(name); 1241 if (label != null) setLabel(label); 1242 if (description != null) setDescription(description); 1243 setCategory(category); 1244 setReadOnly(readOnly); 1245 setType(type); 1246 setAllowedValues(allowedValues); 1247 } 1248 1249 /** 1250 * Get the property name in camel case. The getter method is simply "get" followed by the name of the property (with the 1251 * first character of the property converted to uppercase). The setter method is "set" (or "is" for boolean properties) 1252 * followed by the name of the property (with the first character of the property converted to uppercase). 1253 * 1254 * @return the property name; never null, but possibly empty 1255 */ 1256 public String getName() { 1257 return name != null ? name : ""; 1258 } 1259 1260 /** 1261 * Set the property name in camel case. The getter method is simply "get" followed by the name of the property (with the 1262 * first character of the property converted to uppercase). The setter method is "set" (or "is" for boolean properties) 1263 * followed by the name of the property (with the first character of the property converted to uppercase). 1264 * 1265 * @param name the nwe property name; may be null 1266 */ 1267 public void setName( String name ) { 1268 this.name = name; 1269 if (this.label == null) setLabel(null); 1270 } 1271 1272 /** 1273 * Get the human-readable label for the property. This is often just a {@link Inflector#humanize(String, String...) 1274 * humanized} form of the {@link #getName() property name}. 1275 * 1276 * @return label the human-readable property label; never null, but possibly empty 1277 */ 1278 public String getLabel() { 1279 return label != null ? label : ""; 1280 } 1281 1282 /** 1283 * Set the human-readable label for the property. If null, this will be set to the 1284 * {@link Inflector#humanize(String, String...) humanized} form of the {@link #getName() property name}. 1285 * 1286 * @param label the new label for the property; may be null 1287 */ 1288 public void setLabel( String label ) { 1289 if (label == null && name != null) { 1290 label = INFLECTOR.titleCase(INFLECTOR.humanize(INFLECTOR.underscore(name))); 1291 } 1292 this.label = label; 1293 } 1294 1295 /** 1296 * Get the description for this property. 1297 * 1298 * @return the description; never null, but possibly empty 1299 */ 1300 public String getDescription() { 1301 return description != null ? description : ""; 1302 } 1303 1304 /** 1305 * Set the description for this property. 1306 * 1307 * @param description the new description for this property; may be null 1308 */ 1309 public void setDescription( String description ) { 1310 this.description = description; 1311 } 1312 1313 /** 1314 * Return whether this property is read-only. 1315 * 1316 * @return true if the property is read-only, or false otherwise 1317 */ 1318 public boolean isReadOnly() { 1319 return readOnly; 1320 } 1321 1322 /** 1323 * Set whether this property is read-only. 1324 * 1325 * @param readOnly true if the property is read-only, or false otherwise 1326 */ 1327 public void setReadOnly( boolean readOnly ) { 1328 this.readOnly = readOnly; 1329 } 1330 1331 /** 1332 * Get the name of the category in which this property belongs. 1333 * 1334 * @return the category name; never null, but possibly empty 1335 */ 1336 public String getCategory() { 1337 return category != null ? category : ""; 1338 } 1339 1340 /** 1341 * Set the name of the category in which this property belongs. 1342 * 1343 * @param category the category name; may be null 1344 */ 1345 public void setCategory( String category ) { 1346 this.category = category; 1347 } 1348 1349 /** 1350 * Get the class to which the value must belong (excluding null values). 1351 * 1352 * @return the value class; never null, but may be {@link Object Object.class} 1353 */ 1354 public Class<?> getType() { 1355 return type; 1356 } 1357 1358 /** 1359 * Set the class to which the value must belong (excluding null values). 1360 * 1361 * @param type the value class; may be null 1362 */ 1363 public void setType( Class<?> type ) { 1364 this.type = type != null ? type : Object.class; 1365 } 1366 1367 /** 1368 * Determine if this is a boolean property (the {@link #getType() type} is a {@link Boolean} or boolean). 1369 * 1370 * @return true if this is a boolean property, or false otherwise 1371 */ 1372 public boolean isBooleanType() { 1373 return Boolean.class.equals(type) || Boolean.TYPE.equals(type); 1374 } 1375 1376 /** 1377 * Determine if this is property's (the {@link #getType() type} is a primitive. 1378 * 1379 * @return true if this property's type is a primitive, or false otherwise 1380 */ 1381 public boolean isPrimitive() { 1382 return type.isPrimitive(); 1383 } 1384 1385 /** 1386 * Determine if this is property's (the {@link #getType() type} is an array. 1387 * 1388 * @return true if this property's type is an array, or false otherwise 1389 */ 1390 public boolean isArrayType() { 1391 return type.isArray(); 1392 } 1393 1394 /** 1395 * Get the allowed values for this property. If this is non-null and non-empty, the value must be one of these values. 1396 * 1397 * @return collection of allowed values, or the empty set if the values are not constrained 1398 */ 1399 public Collection<?> getAllowedValues() { 1400 return allowedValues != null ? allowedValues : Collections.emptySet(); 1401 } 1402 1403 /** 1404 * Set the allowed values for this property. If this is non-null and non-empty, the value is expected to be one of these 1405 * values. 1406 * 1407 * @param allowedValues the collection of allowed values, or null or empty if the values are not constrained 1408 */ 1409 public void setAllowedValues( Collection<?> allowedValues ) { 1410 this.allowedValues = allowedValues; 1411 } 1412 1413 /** 1414 * Set the allowed values for this property. If this is non-null and non-empty, the value is expected to be one of these 1415 * values. 1416 * 1417 * @param allowedValues the array of allowed values, or null or empty if the values are not constrained 1418 */ 1419 public void setAllowedValues( Object... allowedValues ) { 1420 if (allowedValues != null && allowedValues.length != 0) { 1421 this.allowedValues = new ArrayList<Object>(Arrays.asList(allowedValues)); 1422 } else { 1423 this.allowedValues = null; 1424 } 1425 } 1426 1427 /** 1428 * Return whether this property was inferred purely by reflection, or whether annotations were used for its definition. 1429 * 1430 * @return true if it was inferred only by reflection, or false if at least one annotation was found and used 1431 */ 1432 public boolean isInferred() { 1433 return inferred; 1434 } 1435 1436 /** 1437 * Set whether this property was inferred purely by reflection. 1438 * 1439 * @param inferred true if it was inferred only by reflection, or false if at least one annotation was found and used 1440 */ 1441 public void setInferred( boolean inferred ) { 1442 this.inferred = inferred; 1443 } 1444 1445 /** 1446 * {@inheritDoc} 1447 * 1448 * @see java.lang.Comparable#compareTo(java.lang.Object) 1449 */ 1450 @Override 1451 public int compareTo( Property that ) { 1452 if (this == that) return 0; 1453 if (that == null) return 1; 1454 int diff = ObjectUtil.compareWithNulls(this.category, that.category); 1455 if (diff != 0) return diff; 1456 diff = ObjectUtil.compareWithNulls(this.label, that.label); 1457 if (diff != 0) return diff; 1458 diff = ObjectUtil.compareWithNulls(this.name, that.name); 1459 if (diff != 0) return diff; 1460 return 0; 1461 } 1462 1463 /** 1464 * {@inheritDoc} 1465 * 1466 * @see java.lang.Object#hashCode() 1467 */ 1468 @Override 1469 public int hashCode() { 1470 return HashCode.compute(this.category, this.name, this.label); 1471 } 1472 1473 /** 1474 * {@inheritDoc} 1475 * 1476 * @see java.lang.Object#equals(java.lang.Object) 1477 */ 1478 @Override 1479 public boolean equals( Object obj ) { 1480 if (obj == this) return true; 1481 if (obj instanceof Property) { 1482 Property that = (Property)obj; 1483 if (!ObjectUtil.isEqualWithNulls(this.category, that.category)) return false; 1484 if (!ObjectUtil.isEqualWithNulls(this.label, that.label)) return false; 1485 if (!ObjectUtil.isEqualWithNulls(this.name, that.name)) return false; 1486 if (!ObjectUtil.isEqualWithNulls(this.value, that.value)) return false; 1487 if (!ObjectUtil.isEqualWithNulls(this.readOnly, that.readOnly)) return false; 1488 return true; 1489 } 1490 return false; 1491 } 1492 1493 /** 1494 * {@inheritDoc} 1495 * 1496 * @see java.lang.Object#toString() 1497 */ 1498 @Override 1499 public String toString() { 1500 StringBuilder sb = new StringBuilder(); 1501 if (name != null) sb.append(name).append(" = "); 1502 sb.append(value); 1503 sb.append(" ( "); 1504 sb.append(readOnly ? "readonly " : "writable "); 1505 if (category != null) sb.append("category=\"").append(category).append("\" "); 1506 if (label != null) sb.append("label=\"").append(label).append("\" "); 1507 if (description != null) sb.append("description=\"").append(description).append("\" "); 1508 sb.append(")"); 1509 return sb.toString(); 1510 } 1511 } 1512}