org.kuali.rice.krad.uif.util.ObjectPropertyUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.rice.krad.uif.util.ObjectPropertyUtils.java

Source

/**
 * Copyright 2005-2014 The Kuali Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.opensource.org/licenses/ecl2.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kuali.rice.krad.uif.util;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.rice.krad.service.DataDictionaryService;
import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
import org.kuali.rice.krad.uif.UifConstants;
import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
import org.kuali.rice.krad.uif.util.ObjectPathExpressionParser.PathEntry;
import org.kuali.rice.krad.uif.view.ViewModel;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

/**
 * Utility methods to get/set property values and working with objects.
 * 
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public final class ObjectPropertyUtils {
    private static final Logger LOG = Logger.getLogger(ObjectPropertyUtils.class);

    // enables a work-around that attempts to correct a platform difference
    private static final boolean isJdk6 = System.getProperty("java.version").startsWith("1.6.");

    /**
     * Internal metadata cache.
     * 
     * <p>
     * NOTE: WeakHashMap is used as the internal cache representation. Since class objects are used
     * as the keys, this allows property descriptors to stay in cache until the class loader is
     * unloaded, but will not prevent the class loader itself from unloading. PropertyDescriptor
     * instances do not hold hard references back to the classes they refer to, so weak value
     * maintenance is not necessary.
     * </p>
     */
    private static final Map<Class<?>, ObjectPropertyMetadata> METADATA_CACHE = Collections
            .synchronizedMap(new WeakHashMap<Class<?>, ObjectPropertyMetadata>(2048));

    /**
     * Get a mapping of property descriptors by property name for a bean class.
     * 
     * @param beanClass The bean class.
     * @return A mapping of all property descriptors for the bean class, by property name.
     */
    public static Map<String, PropertyDescriptor> getPropertyDescriptors(Class<?> beanClass) {
        return getMetadata(beanClass).propertyDescriptors;
    }

    /**
     * Get a property descriptor from a class by property name.
     * 
     * @param beanClass The bean class.
     * @param propertyName The bean property name.
     * @return The property descriptor named on the bean class.
     */
    public static PropertyDescriptor getPropertyDescriptor(Class<?> beanClass, String propertyName) {
        if (propertyName == null) {
            throw new IllegalArgumentException("Null property name");
        }

        PropertyDescriptor propertyDescriptor = getPropertyDescriptors(beanClass).get(propertyName);
        if (propertyDescriptor != null) {
            return propertyDescriptor;
        } else {
            throw new IllegalArgumentException("Property " + propertyName + " not found for bean " + beanClass);
        }
    }

    /**
     * Registers a default set of property editors for use with KRAD in a given property editor registry.
     *
     * @param registry property editor registry
     */
    public static void registerPropertyEditors(PropertyEditorRegistry registry) {
        DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
        Map<Class<?>, String> propertyEditorMap = dataDictionaryService.getPropertyEditorMap();

        if (propertyEditorMap == null) {
            LOG.warn("No propertyEditorMap defined in data dictionary");
            return;
        }

        for (Entry<Class<?>, String> propertyEditorEntry : propertyEditorMap.entrySet()) {
            PropertyEditor editor = (PropertyEditor) dataDictionaryService.getDataDictionary()
                    .getDictionaryPrototype(propertyEditorEntry.getValue());
            registry.registerCustomEditor(propertyEditorEntry.getKey(), editor);

            if (LOG.isDebugEnabled()) {
                LOG.debug("registered " + propertyEditorEntry);
            }
        }
    }

    /**
     * Gets the names of all readable properties for the bean class.
     * 
     * @param beanClass The bean class.
     * @return set of property names
     */
    public static Set<String> getReadablePropertyNames(Class<?> beanClass) {
        return getMetadata(beanClass).readMethods.keySet();
    }

    /**
     * Get the read method for a specific property on a bean class.
     * 
     * @param beanClass The bean class.
     * @param propertyName The property name.
     * @return The read method for the property.
     */
    public static Method getReadMethod(Class<?> beanClass, String propertyName) {
        return getMetadata(beanClass).readMethods.get(propertyName);
    }

    /**
     * Get the read method for a specific property on a bean class.
     * 
     * @param beanClass The bean class.
     * @param propertyName The property name.
     * @return The read method for the property.
     */
    public static Method getWriteMethod(Class<?> beanClass, String propertyName) {
        return getMetadata(beanClass).writeMethods.get(propertyName);
    }

    /**
     * Copy properties from a string map to an object.
     * 
     * @param properties The string map. The keys of this map must be valid property path
     *        expressions in the context of the target object. The values are the string
     *        representations of the target bean properties.
     * @param object The target object, to copy the property values to.
     * @see ObjectPathExpressionParser
     */
    public static void copyPropertiesToObject(Map<String, String> properties, Object object) {
        for (Map.Entry<String, String> property : properties.entrySet()) {
            setPropertyValue(object, property.getKey(), property.getValue());
        }
    }

    /**
     * Get the type of a bean property.
     * 
     * <p>
     * Note that this method does not instantiate the bean class before performing introspection, so
     * will not dynamic initialization behavior into account. When dynamic initialization is needed
     * to accurate inspect the inferred property type, use {@link #getPropertyType(Object, String)}
     * instead of this method. This method is, however, intended for use on the implementation
     * class; to avoid instantiation simply to infer the property type, consider overriding the
     * return type on the property read method.
     * </p>
     * 
     * @param beanClass The bean class.
     * @param propertyPath A valid property path expression in the context of the bean class.
     * @return The property type referred to by the provided bean class and property path.
     * @see ObjectPathExpressionParser
     */
    public static Class<?> getPropertyType(Class<?> beanClass, String propertyPath) {
        try {
            ObjectPropertyReference.setWarning(true);
            return ObjectPropertyReference.resolvePath(null, beanClass, propertyPath, false).getPropertyType();
        } finally {
            ObjectPropertyReference.setWarning(false);
        }
    }

    /**
     * Get the type of a bean property.
     * 
     * @param object The bean instance. Use {@link #getPropertyType(Class, String)} to look up
     *        property types when an instance is not available.
     * @param propertyPath A valid property path expression in the context of the bean.
     * @return The property type referred to by the provided bean and property path.
     * @see ObjectPathExpressionParser
     */
    public static Class<?> getPropertyType(Object object, String propertyPath) {
        try {
            ObjectPropertyReference.setWarning(true);
            return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false)
                    .getPropertyType();
        } finally {
            ObjectPropertyReference.setWarning(false);
        }
    }

    /**
     * Gets the property names by property type, based on the read methods.
     * 
     * @param bean The bean.
     * @param propertyType The return type of the read method on the property.
     * @return list of property names
     */
    public static Set<String> getReadablePropertyNamesByType(Object bean, Class<?> propertyType) {
        return getReadablePropertyNamesByType(bean.getClass(), propertyType);
    }

    /**
     * Gets the property names by property type, based on the read methods.
     * 
     * @param beanClass The bean class.
     * @param propertyType The return type of the read method on the property.
     * @return list of property names
     */
    public static Set<String> getReadablePropertyNamesByType(Class<?> beanClass, Class<?> propertyType) {
        return getMetadata(beanClass).getReadablePropertyNamesByType(propertyType);
    }

    /**
     * Gets the property names by annotation type, based on the read methods.
     * 
     * @param bean The bean.
     * @param annotationType The type of an annotation on the return type.
     * @return list of property names
     */
    public static Set<String> getReadablePropertyNamesByAnnotationType(Object bean,
            Class<? extends Annotation> annotationType) {
        return getReadablePropertyNamesByAnnotationType(bean.getClass(), annotationType);
    }

    /**
     * Gets the property names by annotation type, based on the read methods.
     * 
     * @param beanClass The bean class.
     * @param annotationType The type of an annotation on the return type.
     * @return list of property names
     */
    public static Set<String> getReadablePropertyNamesByAnnotationType(Class<?> beanClass,
            Class<? extends Annotation> annotationType) {
        return getMetadata(beanClass).getReadablePropertyNamesByAnnotationType(annotationType);
    }

    /**
     * Gets the property names by collection type, based on the read methods.
     * 
     * @param bean The bean.
     * @param collectionType The type of elements in a collection or array.
     * @return list of property names
     */
    public static Set<String> getReadablePropertyNamesByCollectionType(Object bean, Class<?> collectionType) {
        return getReadablePropertyNamesByCollectionType(bean.getClass(), collectionType);
    }

    /**
     * Gets the property names for the given object that are writable
     *
     * @param bean object to get writable property names for
     * @return set of property names
     */
    public static Set<String> getWritablePropertyNames(Object bean) {
        return getMetadata(bean.getClass()).getWritablePropertyNames();
    }

    /**
     * Gets the property names by collection type, based on the read methods.
     * 
     * @param beanClass The bean class.
     * @param collectionType The type of elements in a collection or array.
     * @return list of property names
     */
    public static Set<String> getReadablePropertyNamesByCollectionType(Class<?> beanClass,
            Class<?> collectionType) {
        return getMetadata(beanClass).getReadablePropertyNamesByCollectionType(collectionType);
    }

    /**
     * Look up a property value.
     * 
     * @param <T> property type
     * @param object The bean instance to look up a property value for.
     * @param propertyPath A valid property path expression in the context of the bean.
     * @return The value of the property referred to by the provided bean and property path.
     * @see ObjectPathExpressionParser
     */
    @SuppressWarnings("unchecked")
    public static <T extends Object> T getPropertyValue(Object object, String propertyPath) {
        boolean trace = ProcessLogger.isTraceActive() && object != null;
        if (trace) {
            // May be uncommented for debugging high execution count
            // ProcessLogger.ntrace(object.getClass().getSimpleName() + ":r:" + propertyPath, "", 1000);
            ProcessLogger.countBegin("bean-property-read");
        }

        try {
            ObjectPropertyReference.setWarning(true);

            return (T) ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).get();

        } catch (RuntimeException e) {
            throw new IllegalArgumentException("Error getting property '" + propertyPath + "' from " + object, e);
        } finally {
            ObjectPropertyReference.setWarning(false);
            if (trace) {
                ProcessLogger.countEnd("bean-property-read",
                        object.getClass().getSimpleName() + ":" + propertyPath);
            }
        }

    }

    /**
     * Looks up a property value, then convert to text using a registered property editor.
     *
     * @param bean bean instance to look up a property value for
     * @param path property path relative to the bean
     * @return The property value, converted to text using a registered property editor.
     */
    public static String getPropertyValueAsText(Object bean, String path) {
        Object propertyValue = getPropertyValue(bean, path);
        PropertyEditor editor = getPropertyEditor(bean, path);

        if (editor == null) {
            return propertyValue == null ? null : propertyValue.toString();
        } else {
            editor.setValue(propertyValue);
            return editor.getAsText();
        }
    }

    /**
     * Gets the property editor registry configured for the active request.
     */
    public static PropertyEditorRegistry getPropertyEditorRegistry() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();

        PropertyEditorRegistry registry = null;
        if (attributes != null) {
            registry = (PropertyEditorRegistry) attributes.getAttribute(UifConstants.PROPERTY_EDITOR_REGISTRY,
                    RequestAttributes.SCOPE_REQUEST);
        }

        return registry;
    }

    /**
     * Convert to a primitive type if available.
     * 
     * @param type The type to convert.
     * @return A primitive type, if available, that corresponds to the type.
     */
    public static Class<?> getPrimitiveType(Class<?> type) {
        if (Byte.class.equals(type)) {
            return Byte.TYPE;

        } else if (Short.class.equals(type)) {
            return Short.TYPE;

        } else if (Integer.class.equals(type)) {
            return Integer.TYPE;

        } else if (Long.class.equals(type)) {
            return Long.TYPE;

        } else if (Boolean.class.equals(type)) {
            return Boolean.TYPE;

        } else if (Float.class.equals(type)) {
            return Float.TYPE;

        } else if (Double.class.equals(type)) {
            return Double.TYPE;
        }

        return type;
    }

    /**
     * Gets a property editor given a specific bean and property path.
     * 
     * @param bean The bean instance.
     * @param path The property path.
     * @return property editor
     */
    public static PropertyEditor getPropertyEditor(Object bean, String path) {
        Class<?> propertyType = getPrimitiveType(getPropertyType(bean, path));

        PropertyEditor editor = null;

        PropertyEditorRegistry registry = getPropertyEditorRegistry();
        if (registry != null) {
            editor = registry.findCustomEditor(propertyType, path);

            if (editor != null && editor != registry.findCustomEditor(propertyType, null)) {
                return editor;
            }

            if (registry instanceof BeanWrapper && bean == ((BeanWrapper) registry).getWrappedInstance()
                    && (bean instanceof ViewModel)) {

                ViewModel viewModel = (ViewModel) bean;
                ViewPostMetadata viewPostMetadata = viewModel.getViewPostMetadata();
                PropertyEditor editorFromView = viewPostMetadata == null ? null
                        : viewPostMetadata.getFieldEditor(path);

                if (editorFromView != null) {
                    registry.registerCustomEditor(propertyType, path, editorFromView);
                    editor = registry.findCustomEditor(propertyType, path);
                }
            }
        }

        if (editor != null) {
            return editor;
        }

        return getPropertyEditor(propertyType);
    }

    /**
     * Get a property editor given a property type.
     *
     * @param propertyType The property type to look up an editor for.
     * @param path The property path, if applicable.
     * @return property editor
     */
    public static PropertyEditor getPropertyEditor(Class<?> propertyType) {
        PropertyEditorRegistry registry = getPropertyEditorRegistry();
        PropertyEditor editor = null;

        if (registry != null) {
            editor = registry.findCustomEditor(propertyType, null);
        } else {

            DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
            Map<Class<?>, String> editorMap = dataDictionaryService.getPropertyEditorMap();
            String editorPrototypeName = editorMap == null ? null : editorMap.get(propertyType);

            if (editorPrototypeName != null) {
                editor = (PropertyEditor) dataDictionaryService.getDataDictionary()
                        .getDictionaryPrototype(editorPrototypeName);
            }
        }

        if (editor == null && propertyType != null) {
            // Fall back to default beans lookup
            editor = PropertyEditorManager.findEditor(propertyType);
        }

        return editor;
    }

    /**
     * Initialize a property value.
     * 
     * <p>
     * Upon returning from this method, the property referred to by the provided bean and property
     * path will have been initialized with a default instance of the indicated property type.
     * </p>
     * 
     * @param object The bean instance to initialize a property value for.
     * @param propertyPath A valid property path expression in the context of the bean.
     * @see #getPropertyType(Object, String)
     * @see #setPropertyValue(Object, String, Object)
     * @see ObjectPathExpressionParser
     */
    public static void initializeProperty(Object object, String propertyPath) {
        Class<?> propertyType = getPropertyType(object, propertyPath);
        try {
            setPropertyValue(object, propertyPath, propertyType.newInstance());
        } catch (InstantiationException e) {
            // just set the value to null
            setPropertyValue(object, propertyPath, null);
        } catch (IllegalAccessException e) {
            throw new IllegalArgumentException("Unable to set new instance for property: " + propertyPath, e);
        }
    }

    /**
     * Modify a property value.
     * 
     * <p>
     * Upon returning from this method, the property referred to by the provided bean and property
     * path will have been populated with property value provided. If the propertyValue does not
     * match the type of the indicated property, then type conversion will be attempted using
     * {@link PropertyEditorManager}.
     * </p>
     * 
     * @param object The bean instance to initialize a property value for.
     * @param propertyPath A valid property path expression in the context of the bean.
     * @param propertyValue The value to populate value in the property referred to by the provided
     *        bean and property path.
     * @see ObjectPathExpressionParser
     * @throws RuntimeException If the property path is not valid in the context of the bean
     *         provided.
     */
    public static void setPropertyValue(Object object, String propertyPath, Object propertyValue) {
        if (ProcessLogger.isTraceActive() && object != null) {
            // May be uncommented for debugging high execution count
            // ProcessLogger.ntrace(object.getClass().getSimpleName() + ":w:" + propertyPath + ":", "", 1000);
            ProcessLogger.countBegin("bean-property-write");
        }

        try {
            ObjectPropertyReference.setWarning(true);

            ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, true).set(propertyValue);

        } catch (RuntimeException e) {
            throw new IllegalArgumentException(
                    "Error setting property '" + propertyPath + "' on " + object + " with " + propertyValue, e);
        } finally {
            ObjectPropertyReference.setWarning(false);

            if (ProcessLogger.isTraceActive() && object != null) {
                ProcessLogger.countEnd("bean-property-write",
                        object.getClass().getSimpleName() + ":" + propertyPath);
            }
        }

    }

    /**
     * Modify a property value.
     * 
     * <p>
     * Upon returning from this method, the property referred to by the provided bean and property
     * path will have been populated with property value provided. If the propertyValue does not
     * match the type of the indicated property, then type conversion will be attempted using
     * {@link PropertyEditorManager}.
     * </p>
     * 
     * @param object The bean instance to initialize a property value for.
     * @param propertyPath A property path expression in the context of the bean.
     * @param propertyValue The value to populate value in the property referred to by the provided
     *        bean and property path.
     * @param ignoreUnknown True if invalid property values should be ignored, false to throw a
     *        RuntimeException if the property reference is invalid.
     * @see ObjectPathExpressionParser
     */
    public static void setPropertyValue(Object object, String propertyPath, Object propertyValue,
            boolean ignoreUnknown) {
        try {
            setPropertyValue(object, propertyPath, propertyValue);
        } catch (RuntimeException e) {
            // only throw exception if they have indicated to not ignore unknown
            if (!ignoreUnknown) {
                throw e;
            }
            if (LOG.isTraceEnabled()) {
                LOG.trace("Ignoring exception thrown during setting of property '" + propertyPath + "': "
                        + e.getLocalizedMessage());
            }
        }
    }

    /**
     * Determine if a property is readable.
     * 
     * @param object The bean instance to initialize a property value for.
     * @param propertyPath A property path expression in the context of the bean.
     * @return True if the path expression resolves to a valid readable property reference in the
     *         context of the bean provided.
     */
    public static boolean isReadableProperty(Object object, String propertyPath) {
        return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).canRead();
    }

    /**
     * Determine if a property is writable.
     * 
     * @param object The bean instance to initialize a property value for.
     * @param propertyPath A property path expression in the context of the bean.
     * @return True if the path expression resolves to a valid writable property reference in the
     *         context of the bean provided.
     */
    public static boolean isWritableProperty(Object object, String propertyPath) {
        return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).canWrite();
    }

    /**
     * Returns an List of {@code Field} objects reflecting all the fields
     * declared by the class or interface represented by this
     * {@code Class} object. This includes public, protected, default
     * (package) access, and private fields, and includes inherited fields.
     *
     * @param fields A list of {@code Field} objects which gets returned.
     * @param type Type of class or interface for which fields are returned.
     * @param stopAt The Superclass upto which the inherited fields are to be included
     * @return List of all fields
     */
    public static List<Field> getAllFields(List<Field> fields, Class<?> type, Class<?> stopAt) {
        for (Field field : type.getDeclaredFields()) {
            fields.add(field);
        }

        if (type.getSuperclass() != null && !type.getName().equals(stopAt.getName())) {
            fields = getAllFields(fields, type.getSuperclass(), stopAt);
        }

        return fields;
    }

    /**
     * Get the best known component type for a generic type.
     * 
     * <p>
     * When the type is not parameterized or has no explicitly defined parameters, {@link Object} is
     * returned.
     * </p>
     * 
     * <p>
     * When the type has multiple parameters, the right-most parameter is considered the component
     * type. This facilitates identifying the value type of a Map.
     * </p>
     * 
     * @param type The generic collection or map type.
     * @return component or value type, resolved from the generic type
     */
    public static Type getComponentType(Type type) {
        if (!(type instanceof ParameterizedType)) {
            return Object.class;
        }

        ParameterizedType parameterizedType = (ParameterizedType) type;
        Type[] params = parameterizedType.getActualTypeArguments();
        if (params.length == 0) {
            return Object.class;
        }

        Type valueType = params[params.length - 1];
        return valueType;
    }

    /**
     * Get the upper bound of a generic type.
     * 
     * <p>
     * When the type is a class, the class is returned.
     * </p>
     * 
     * <p>
     * When the type is a wildcard, and the upper bound is a class, the upper bound of the wildcard
     * is returned.
     * </p>
     * 
     * <p>
     * If the type has not been explicitly defined at compile time, {@link Object} is returned.
     * </p>
     * 
     * @param valueType The generic collection or map type.
     * @return component or value type, resolved from the generic type
     */
    public static Class<?> getUpperBound(Type valueType) {
        if (valueType instanceof WildcardType) {
            Type[] upperBounds = ((WildcardType) valueType).getUpperBounds();

            if (upperBounds.length >= 1) {
                valueType = upperBounds[0];
            }
        }

        if (valueType instanceof ParameterizedType) {
            valueType = ((ParameterizedType) valueType).getRawType();
        }

        if (valueType instanceof Class) {
            return (Class<?>) valueType;
        }

        return Object.class;
    }

    /**
     * Locate the generic type declaration for a given target class in the generic type hierarchy of
     * the source class.
     * 
     * @param sourceClass The class representing the generic type hierarchy to scan.
     * @param targetClass The class representing the generic type declaration to locate within the
     *        source class' hierarchy.
     * @return The generic type representing the target class within the source class' generic
     *         hierarchy.
     */
    public static Type findGenericType(Class<?> sourceClass, Class<?> targetClass) {
        if (!targetClass.isAssignableFrom(sourceClass)) {
            throw new IllegalArgumentException(targetClass + " is not assignable from " + sourceClass);
        }

        if (sourceClass.equals(targetClass)) {
            return sourceClass;
        }

        @SuppressWarnings("unchecked")
        Queue<Type> typeQueue = RecycleUtils.getInstance(LinkedList.class);
        typeQueue.offer(sourceClass);
        while (!typeQueue.isEmpty()) {
            Type type = typeQueue.poll();

            Class<?> upperBound = getUpperBound(type);
            if (targetClass.equals(upperBound)) {
                return type;
            }

            Type genericSuper = upperBound.getGenericSuperclass();
            if (genericSuper != null) {
                typeQueue.offer(genericSuper);
            }

            Type[] genericInterfaces = upperBound.getGenericInterfaces();
            for (int i = 0; i < genericInterfaces.length; i++) {
                if (genericInterfaces[i] != null) {
                    typeQueue.offer(genericInterfaces[i]);
                }
            }
        }

        throw new IllegalStateException(targetClass + " is assignable from " + sourceClass
                + " but could not be found in the generic type hierarchy");
    }

    /**
     * Split parse path entry for supporting {@link ObjectPropertyUtils#splitPropertyPath(String)}. 
     * 
     * @author Kuali Rice Team (rice.collab@kuali.org)
     */
    private static class SplitPropertyPathEntry implements PathEntry {

        /**
         * Invoked at each path separator on the path.
         * 
         * <p>
         * Note that {@link ObjectPathExpressionParser} strips quotes and brackets then treats
         * list/map references as property names. However
         * {@link ObjectPropertyUtils#splitPropertyPath(String)} expects that a list/map reference
         * will be part of the path expression, as a reference to a specific element in a list or
         * map related to the bean. Therefore, this method needs to rejoin these list/map references
         * before returning.
         * </p>
         * 
         * @param parentPath The portion of the path leading up to the current node.
         * @param node The list of property names in the path.
         * @param next The next property name being parsed.
         * 
         * {@inheritDoc}
         */
        @Override
        public List<String> parse(String parentPath, Object node, String next) {
            if (next == null) {
                return new ArrayList<String>();
            }

            @SuppressWarnings("unchecked")
            List<String> rv = (List<String>) node;
            // First node, or no path separator in parent path.
            if (rv.isEmpty()) {
                rv.add(next);
                return rv;
            }

            rejoinTrailingIndexReference(rv, parentPath);
            rv.add(next);

            return rv;
        }
    }

    private static final SplitPropertyPathEntry SPLIT_PROPERTY_PATH_ENTRY = new SplitPropertyPathEntry();
    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    /**
     * Helper method for splitting property paths with bracketed index references.
     * 
     * <p>
     * Since the parser treats index references as separate tokens, they need to be rejoined in order
     * to be maintained as a single indexed property reference.  This method handles that operation.
     * </p>
     * 
     * @param tokenList The list of tokens being parsed.
     * @param path The portion of the path expression represented by the token list.
     */
    private static void rejoinTrailingIndexReference(List<String> tokenList, String path) {
        int lastIndex = tokenList.size() - 1;
        String lastToken = tokenList.get(lastIndex);
        String lastParentToken = path.substring(path.lastIndexOf('.') + 1);

        if (!lastToken.equals(lastParentToken) && lastIndex > 0) {

            // read back one more token and "concatenate" by
            // recreating the subexpression as a substring of
            // the parent path
            String prevToken = tokenList.get(--lastIndex);

            // parent path index of last prevToken.
            int iopt = path.lastIndexOf(prevToken, path.lastIndexOf(lastToken));

            String fullToken = path.substring(iopt);
            tokenList.remove(lastIndex); // remove first entry
            tokenList.set(lastIndex, fullToken); // replace send with concatenation
        }
    }

    /**
     * Splits the given property path into a string of property names that make up the path.
     *
     * @param path property path to split
     * @return string array of names, starting from the top parent
     * @see SplitPropertyPathEntry#parse(String, Object, String)
     */
    public static String[] splitPropertyPath(String path) {
        List<String> split = ObjectPathExpressionParser.parsePathExpression(null, path, SPLIT_PROPERTY_PATH_ENTRY);
        if (split == null || split.isEmpty()) {
            return EMPTY_STRING_ARRAY;
        }

        rejoinTrailingIndexReference(split, path);

        return split.toArray(new String[split.size()]);
    }

    /**
     * Returns the tail of a given property path (if nested, the nested path).
     *
     * <p>For example, if path is "nested1.foo", this will return "foo". If path is just "foo", "foo" will be
     * returned.</p>
     *
     * @param path path to return tail for
     * @return String tail of path
     */
    public static String getPathTail(String path) {
        String[] propertyPaths = splitPropertyPath(path);

        return propertyPaths[propertyPaths.length - 1];
    }

    /**
     * Removes the tail of the path from the return path.
     *
     * <p>For example, if path is "nested1.foo", this will return "nested1". If path is just "foo", "" will be
     * returned.</p>
     *
     * @param path path to remove tail from
     * @return String path with tail removed (may be empty string)
     */
    public static String removePathTail(String path) {
        String[] propertyPaths = splitPropertyPath(path);

        return StringUtils.join(propertyPaths, ".", 0, propertyPaths.length - 1);
    }

    /**
     * Removes any collection references from a property path, making it more useful for referring
     * to metadata related to the property.
     * @param path A property path expression.
     * @return The path, with collection references removed.
     */
    public static String getCanonicalPath(String path) {
        if (path == null || path.indexOf('[') == -1) {
            return path;
        }

        // The path has at least one left bracket, so will need to be modified
        // copy it to a mutable StringBuilder
        StringBuilder pathBuilder = new StringBuilder(path);

        int bracketCount = 0;
        int leftBracketPos = -1;
        for (int i = 0; i < pathBuilder.length(); i++) {
            char c = pathBuilder.charAt(i);

            if (c == '[') {
                bracketCount++;
                if (bracketCount == 1)
                    leftBracketPos = i;
            }

            if (c == ']') {
                bracketCount--;

                if (bracketCount < 0) {
                    throw new IllegalArgumentException("Unmatched ']' at " + i + " " + pathBuilder);
                }

                if (bracketCount == 0) {
                    pathBuilder.delete(leftBracketPos, i + 1);
                    i -= i + 1 - leftBracketPos;
                    leftBracketPos = -1;
                }
            }
        }

        if (bracketCount > 0) {
            throw new IllegalArgumentException("Unmatched '[' at " + leftBracketPos + " " + pathBuilder);
        }

        return pathBuilder.toString();
    }

    /**
     * Private constructor - utility class only.
     */
    private ObjectPropertyUtils() {
    }

    /**
     * Infer the read method based on method name.
     * 
     * @param beanClass The bean class.
     * @param propertyName The property name.
     * @return The read method for the property.
     */
    private static Method getReadMethodByName(Class<?> beanClass, String propertyName) {

        try {
            return beanClass
                    .getMethod("get" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1));
        } catch (SecurityException e) {
            // Ignore
        } catch (NoSuchMethodException e) {
            // Ignore
        }

        try {
            Method readMethod = beanClass
                    .getMethod("is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1));

            if (readMethod.getReturnType() == Boolean.class || readMethod.getReturnType() == Boolean.TYPE) {
                return readMethod;
            }
        } catch (SecurityException e) {
            // Ignore
        } catch (NoSuchMethodException e) {
            // Ignore
        }

        return null;
    }

    /**
     * Get the cached metadata for a bean class.
     * 
     * @param beanClass The bean class.
     * @return cached metadata for beanClass
     */
    private static ObjectPropertyMetadata getMetadata(Class<?> beanClass) {
        ObjectPropertyMetadata metadata = METADATA_CACHE.get(beanClass);

        if (metadata == null) {
            metadata = new ObjectPropertyMetadata(beanClass);
            METADATA_CACHE.put(beanClass, metadata);
        }

        return metadata;
    }

    /**
     * Stores property metadata related to a bean class, for reducing introspection and reflection
     * overhead.
     * 
     * @author Kuali Rice Team (rice.collab@kuali.org)
     */
    private static class ObjectPropertyMetadata {

        private final Map<String, PropertyDescriptor> propertyDescriptors;
        private final Map<String, Method> readMethods;
        private final Map<String, Method> writeMethods;
        private final Map<Class<?>, Set<String>> readablePropertyNamesByPropertyType = Collections
                .synchronizedMap(new WeakHashMap<Class<?>, Set<String>>());
        private final Map<Class<?>, Set<String>> readablePropertyNamesByAnnotationType = Collections
                .synchronizedMap(new WeakHashMap<Class<?>, Set<String>>());
        private final Map<Class<?>, Set<String>> readablePropertyNamesByCollectionType = Collections
                .synchronizedMap(new WeakHashMap<Class<?>, Set<String>>());

        /**
         * Gets the property names by type, based on the read methods.
         * 
         * @param propertyType The return type of the read method on the property.
         * @return list of property names
         */
        private Set<String> getReadablePropertyNamesByType(Class<?> propertyType) {
            Set<String> propertyNames = readablePropertyNamesByPropertyType.get(propertyType);
            if (propertyNames != null) {
                return propertyNames;
            }

            propertyNames = new LinkedHashSet<String>();
            for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) {
                Method readMethod = readMethodEntry.getValue();
                if (readMethod != null && propertyType.isAssignableFrom(readMethod.getReturnType())) {
                    propertyNames.add(readMethodEntry.getKey());
                }
            }

            propertyNames = Collections.unmodifiableSet(propertyNames);
            readablePropertyNamesByPropertyType.put(propertyType, propertyNames);

            return propertyNames;
        }

        /**
         * Gets the property names by annotation type, based on the read methods.
         * 
         * @param annotationType The type of an annotation on the return type.
         * @return list of property names
         */
        private Set<String> getReadablePropertyNamesByAnnotationType(Class<? extends Annotation> annotationType) {
            Set<String> propertyNames = readablePropertyNamesByAnnotationType.get(annotationType);
            if (propertyNames != null) {
                return propertyNames;
            }

            propertyNames = new LinkedHashSet<String>();
            for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) {
                Method readMethod = readMethodEntry.getValue();
                if (readMethod != null && readMethod.isAnnotationPresent(annotationType)) {
                    propertyNames.add(readMethodEntry.getKey());
                }
            }

            propertyNames = Collections.unmodifiableSet(propertyNames);
            readablePropertyNamesByPropertyType.put(annotationType, propertyNames);

            return propertyNames;
        }

        /**
         * Gets the property names by collection type, based on the read methods.
         * 
         * @param collectionType The type of elements in a collection or array.
         * @return list of property names
         */
        private Set<String> getReadablePropertyNamesByCollectionType(Class<?> collectionType) {
            Set<String> propertyNames = readablePropertyNamesByCollectionType.get(collectionType);
            if (propertyNames != null) {
                return propertyNames;
            }

            propertyNames = new LinkedHashSet<String>();
            for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) {
                Method readMethod = readMethodEntry.getValue();
                if (readMethod == null) {
                    continue;
                }

                Class<?> propertyClass = readMethod.getReturnType();
                if (propertyClass.isArray() && collectionType.isAssignableFrom(propertyClass.getComponentType())) {
                    propertyNames.add(readMethodEntry.getKey());
                    continue;
                }

                boolean isCollection = Collection.class.isAssignableFrom(propertyClass);
                boolean isMap = Map.class.isAssignableFrom(propertyClass);
                if (!isCollection && !isMap) {
                    continue;
                }

                if (collectionType.equals(Object.class)) {
                    propertyNames.add(readMethodEntry.getKey());
                    continue;
                }

                Type propertyType = readMethodEntry.getValue().getGenericReturnType();
                if (propertyType instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) propertyType;
                    Type valueType = parameterizedType.getActualTypeArguments()[isCollection ? 0 : 1];

                    if (valueType instanceof WildcardType) {
                        Type[] upperBounds = ((WildcardType) valueType).getUpperBounds();

                        if (upperBounds.length >= 1) {
                            valueType = upperBounds[0];
                        }
                    }

                    if (valueType instanceof Class && collectionType.isAssignableFrom((Class<?>) valueType)) {
                        propertyNames.add(readMethodEntry.getKey());
                    }
                }
            }

            propertyNames = Collections.unmodifiableSet(propertyNames);
            readablePropertyNamesByCollectionType.put(collectionType, propertyNames);

            return propertyNames;
        }

        /**
         * Gets the property names that are writable for the metadata class.
         *
         * @return set of writable property names
         */
        private Set<String> getWritablePropertyNames() {
            Set<String> writablePropertyNames = new HashSet<String>();

            for (Entry<String, Method> writeMethodEntry : writeMethods.entrySet()) {
                writablePropertyNames.add(writeMethodEntry.getKey());
            }

            return writablePropertyNames;
        }

        /**
         * Creates a new metadata wrapper for a bean class.
         * 
         * @param beanClass The bean class.
         */
        private ObjectPropertyMetadata(Class<?> beanClass) {
            if (beanClass == null) {
                throw new RuntimeException("Class to retrieve property from was null");
            }

            BeanInfo beanInfo;
            try {
                beanInfo = Introspector.getBeanInfo(beanClass);
            } catch (IntrospectionException e) {
                LOG.warn("Bean Info not found for bean " + beanClass, e);
                beanInfo = null;
            }

            Map<String, PropertyDescriptor> mutablePropertyDescriptorMap = new LinkedHashMap<String, PropertyDescriptor>();
            Map<String, Method> mutableReadMethodMap = new LinkedHashMap<String, Method>();
            Map<String, Method> mutableWriteMethodMap = new LinkedHashMap<String, Method>();

            if (beanInfo != null) {
                for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
                    String propertyName = propertyDescriptor.getName();

                    mutablePropertyDescriptorMap.put(propertyName, propertyDescriptor);
                    Method readMethod = propertyDescriptor.getReadMethod();
                    if (readMethod == null) {
                        readMethod = getReadMethodByName(beanClass, propertyName);
                    }

                    // working around a JDK6 Introspector bug WRT covariance, see KULRICE-12334
                    if (isJdk6) {
                        readMethod = getCorrectedReadMethod(beanClass, readMethod);
                    }

                    mutableReadMethodMap.put(propertyName, readMethod);

                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    assert writeMethod == null || (writeMethod.getParameterTypes().length == 1
                            && writeMethod.getParameterTypes()[0] != null) : writeMethod;
                    mutableWriteMethodMap.put(propertyName, writeMethod);
                }
            }

            propertyDescriptors = Collections.unmodifiableMap(mutablePropertyDescriptorMap);
            readMethods = Collections.unmodifiableMap(mutableReadMethodMap);
            writeMethods = Collections.unmodifiableMap(mutableWriteMethodMap);
        }

        /**
         * Workaround for a JDK6 Introspector issue (see KULRICE-12334) that results in getters for interface types
         * being returned instead of same named getters for concrete implementation types (depending on the Method order
         * returned by reflection on the beanClass.
         *
         * <p>Note that this doesn't cover all cases, see ObjectPropertyUtilsTest.testGetterInInterfaceOrSuperHasWiderType
         * for details.</p>
         *
         * @param beanClass the class of the bean being inspected
         * @param readMethod the read method being double-checked
         * @return the corrected read Method
         */
        private Method getCorrectedReadMethod(Class<?> beanClass, Method readMethod) {
            if (readMethod != null && !readMethod.getReturnType().isPrimitive()
                    && isAbstractClassOrInterface(readMethod.getReturnType())) {

                Method implReadMethod = null;

                try {
                    implReadMethod = beanClass.getMethod(readMethod.getName(), readMethod.getParameterTypes());
                } catch (NoSuchMethodException e) {
                    // if readMethod != null, this should not happen according to the javadocs for Class.getMethod()
                }

                if (implReadMethod != null
                        && isSubClass(implReadMethod.getReturnType(), readMethod.getReturnType())) {
                    return implReadMethod;
                }
            }

            return readMethod;
        }

        // we assume a non-null arg
        private boolean isAbstractClassOrInterface(Class<?> clazz) {
            return clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers());
        }

        // we assume non-null args
        private boolean isSubClass(Class<?> childClassCandidate, Class<?> parentClassCandidate) {
            // if A != B and A >= B then A > B
            return parentClassCandidate != childClassCandidate
                    && parentClassCandidate.isAssignableFrom(childClassCandidate);
        }
    }
}