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}