001/**
002 * Copyright 2005-2018 The Kuali Foundation
003 *
004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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.kuali.rice.krad.uif.util;
017
018import java.beans.BeanInfo;
019import java.beans.IntrospectionException;
020import java.beans.Introspector;
021import java.beans.PropertyDescriptor;
022import java.beans.PropertyEditor;
023import java.beans.PropertyEditorManager;
024import java.lang.annotation.Annotation;
025import java.lang.reflect.Field;
026import java.lang.reflect.Method;
027import java.lang.reflect.Modifier;
028import java.lang.reflect.ParameterizedType;
029import java.lang.reflect.Type;
030import java.lang.reflect.WildcardType;
031import java.util.ArrayList;
032import java.util.Collection;
033import java.util.Collections;
034import java.util.HashSet;
035import java.util.LinkedHashMap;
036import java.util.LinkedHashSet;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.Map;
040import java.util.Map.Entry;
041import java.util.Queue;
042import java.util.Set;
043import java.util.WeakHashMap;
044
045import org.apache.commons.lang.StringUtils;
046import org.apache.log4j.Logger;
047import org.kuali.rice.krad.service.DataDictionaryService;
048import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
049import org.kuali.rice.krad.uif.UifConstants;
050import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
051import org.kuali.rice.krad.uif.util.ObjectPathExpressionParser.PathEntry;
052import org.kuali.rice.krad.uif.view.ViewModel;
053import org.springframework.beans.BeanWrapper;
054import org.springframework.beans.PropertyEditorRegistry;
055import org.springframework.web.context.request.RequestAttributes;
056import org.springframework.web.context.request.RequestContextHolder;
057
058/**
059 * Utility methods to get/set property values and working with objects.
060 * 
061 * @author Kuali Rice Team (rice.collab@kuali.org)
062 */
063public final class ObjectPropertyUtils {
064    private static final Logger LOG = Logger.getLogger(ObjectPropertyUtils.class);
065
066    /**
067     * Internal metadata cache.
068     * 
069     * <p>
070     * NOTE: WeakHashMap is used as the internal cache representation. Since class objects are used
071     * as the keys, this allows property descriptors to stay in cache until the class loader is
072     * unloaded, but will not prevent the class loader itself from unloading. PropertyDescriptor
073     * instances do not hold hard references back to the classes they refer to, so weak value
074     * maintenance is not necessary.
075     * </p>
076     */
077    private static final Map<Class<?>, ObjectPropertyMetadata> METADATA_CACHE = Collections
078            .synchronizedMap(new WeakHashMap<Class<?>, ObjectPropertyMetadata>(2048));
079
080    /**
081     * Get a mapping of property descriptors by property name for a bean class.
082     * 
083     * @param beanClass The bean class.
084     * @return A mapping of all property descriptors for the bean class, by property name.
085     */
086    public static Map<String, PropertyDescriptor> getPropertyDescriptors(Class<?> beanClass) {
087        return getMetadata(beanClass).propertyDescriptors;
088    }
089    
090    /**
091     * Get a property descriptor from a class by property name.
092     * 
093     * @param beanClass The bean class.
094     * @param propertyName The bean property name.
095     * @return The property descriptor named on the bean class.
096     */
097    public static PropertyDescriptor getPropertyDescriptor(Class<?> beanClass, String propertyName) {
098        if (propertyName == null) {
099            throw new IllegalArgumentException("Null property name");
100        }
101
102        PropertyDescriptor propertyDescriptor = getPropertyDescriptors(beanClass).get(propertyName);
103        if (propertyDescriptor != null) {
104            return propertyDescriptor;
105        } else {
106            throw new IllegalArgumentException("Property " + propertyName + " not found for bean " + beanClass);
107        }
108    }
109
110    /**
111     * Registers a default set of property editors for use with KRAD in a given property editor registry.
112     *
113     * @param registry property editor registry
114     */
115    public static void registerPropertyEditors(PropertyEditorRegistry registry) {
116        DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
117        Map<Class<?>, String> propertyEditorMap = dataDictionaryService.getPropertyEditorMap();
118
119        if (propertyEditorMap == null) {
120            LOG.warn("No propertyEditorMap defined in data dictionary");
121            return;
122        }
123
124        for (Entry<Class<?>, String> propertyEditorEntry : propertyEditorMap.entrySet()) {
125            PropertyEditor editor = (PropertyEditor) dataDictionaryService.getDataDictionary().getDictionaryPrototype(
126                    propertyEditorEntry.getValue());
127            registry.registerCustomEditor(propertyEditorEntry.getKey(), editor);
128
129            if (LOG.isDebugEnabled()) {
130                LOG.debug("registered " + propertyEditorEntry);
131            }
132        }
133    }
134
135    /**
136     * Gets the names of all readable properties for the bean class.
137     * 
138     * @param beanClass The bean class.
139     * @return set of property names
140     */
141    public static Set<String> getReadablePropertyNames(Class<?> beanClass) {
142        return getMetadata(beanClass).readMethods.keySet();
143    }
144
145    /**
146     * Get the read method for a specific property on a bean class.
147     * 
148     * @param beanClass The bean class.
149     * @param propertyName The property name.
150     * @return The read method for the property.
151     */
152    public static Method getReadMethod(Class<?> beanClass, String propertyName) {
153        return getMetadata(beanClass).readMethods.get(propertyName);
154    }
155
156    /**
157     * Get the read method for a specific property on a bean class.
158     * 
159     * @param beanClass The bean class.
160     * @param propertyName The property name.
161     * @return The read method for the property.
162     */
163    public static Method getWriteMethod(Class<?> beanClass, String propertyName) {
164        return getMetadata(beanClass).writeMethods.get(propertyName);
165    }
166
167    /**
168     * Copy properties from a string map to an object.
169     * 
170     * @param properties The string map. The keys of this map must be valid property path
171     *        expressions in the context of the target object. The values are the string
172     *        representations of the target bean properties.
173     * @param object The target object, to copy the property values to.
174     * @see ObjectPathExpressionParser
175     */
176    public static void copyPropertiesToObject(Map<String, String> properties, Object object) {
177        for (Map.Entry<String, String> property : properties.entrySet()) {
178            setPropertyValue(object, property.getKey(), property.getValue());
179        }
180    }
181
182    /**
183     * Get the type of a bean property.
184     * 
185     * <p>
186     * Note that this method does not instantiate the bean class before performing introspection, so
187     * will not dynamic initialization behavior into account. When dynamic initialization is needed
188     * to accurate inspect the inferred property type, use {@link #getPropertyType(Object, String)}
189     * instead of this method. This method is, however, intended for use on the implementation
190     * class; to avoid instantiation simply to infer the property type, consider overriding the
191     * return type on the property read method.
192     * </p>
193     * 
194     * @param beanClass The bean class.
195     * @param propertyPath A valid property path expression in the context of the bean class.
196     * @return The property type referred to by the provided bean class and property path.
197     * @see ObjectPathExpressionParser
198     */
199    public static Class<?> getPropertyType(Class<?> beanClass, String propertyPath) {
200        try {
201            ObjectPropertyReference.setWarning(true);
202            return ObjectPropertyReference.resolvePath(null, beanClass, propertyPath, false).getPropertyType();
203        } finally {
204            ObjectPropertyReference.setWarning(false);
205        }
206    }
207
208    /**
209     * Get the type of a bean property.
210     * 
211     * @param object The bean instance. Use {@link #getPropertyType(Class, String)} to look up
212     *        property types when an instance is not available.
213     * @param propertyPath A valid property path expression in the context of the bean.
214     * @return The property type referred to by the provided bean and property path.
215     * @see ObjectPathExpressionParser
216     */
217    public static Class<?> getPropertyType(Object object, String propertyPath) {
218        try {
219            ObjectPropertyReference.setWarning(true);
220            return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false)
221                    .getPropertyType();
222        } finally {
223            ObjectPropertyReference.setWarning(false);
224        }
225    }
226
227    /**
228     * Gets the property names by property type, based on the read methods.
229     * 
230     * @param bean The bean.
231     * @param propertyType The return type of the read method on the property.
232     * @return list of property names
233     */
234    public static Set<String> getReadablePropertyNamesByType(Object bean, Class<?> propertyType) {
235        return getReadablePropertyNamesByType(bean.getClass(), propertyType);
236    }
237
238    /**
239     * Gets the property names by property type, based on the read methods.
240     * 
241     * @param beanClass The bean class.
242     * @param propertyType The return type of the read method on the property.
243     * @return list of property names
244     */
245    public static Set<String> getReadablePropertyNamesByType(Class<?> beanClass, Class<?> propertyType) {
246        return getMetadata(beanClass).getReadablePropertyNamesByType(propertyType);
247    }
248
249    /**
250     * Gets the property names by annotation type, based on the read methods.
251     * 
252     * @param bean The bean.
253     * @param annotationType The type of an annotation on the return type.
254     * @return list of property names
255     */
256    public static Set<String> getReadablePropertyNamesByAnnotationType(Object bean,
257            Class<? extends Annotation> annotationType) {
258        return getReadablePropertyNamesByAnnotationType(bean.getClass(), annotationType);
259    }
260
261    /**
262     * Gets the property names by annotation type, based on the read methods.
263     * 
264     * @param beanClass The bean class.
265     * @param annotationType The type of an annotation on the return type.
266     * @return list of property names
267     */
268    public static Set<String> getReadablePropertyNamesByAnnotationType(
269            Class<?> beanClass, Class<? extends Annotation> annotationType) {
270        return getMetadata(beanClass).getReadablePropertyNamesByAnnotationType(annotationType);
271    }
272
273    /**
274     * Gets the property names by collection type, based on the read methods.
275     * 
276     * @param bean The bean.
277     * @param collectionType The type of elements in a collection or array.
278     * @return list of property names
279     */
280    public static Set<String> getReadablePropertyNamesByCollectionType(Object bean, Class<?> collectionType) {
281        return getReadablePropertyNamesByCollectionType(bean.getClass(), collectionType);
282    }
283
284    /**
285     * Gets the property names for the given object that are writable
286     *
287     * @param bean object to get writable property names for
288     * @return set of property names
289     */
290    public static Set<String> getWritablePropertyNames(Object bean) {
291        return getMetadata(bean.getClass()).getWritablePropertyNames();
292    }
293
294    /**
295     * Gets the property names by collection type, based on the read methods.
296     * 
297     * @param beanClass The bean class.
298     * @param collectionType The type of elements in a collection or array.
299     * @return list of property names
300     */
301    public static Set<String> getReadablePropertyNamesByCollectionType(Class<?> beanClass, Class<?> collectionType) {
302        return getMetadata(beanClass).getReadablePropertyNamesByCollectionType(collectionType);
303    }
304
305    /**
306     * Look up a property value.
307     * 
308     * @param <T> property type
309     * @param object The bean instance to look up a property value for.
310     * @param propertyPath A valid property path expression in the context of the bean.
311     * @return The value of the property referred to by the provided bean and property path.
312     * @see ObjectPathExpressionParser
313     */
314    @SuppressWarnings("unchecked")
315    public static <T extends Object> T getPropertyValue(Object object, String propertyPath) {
316        boolean trace = ProcessLogger.isTraceActive() && object != null; 
317        if (trace) {
318            // May be uncommented for debugging high execution count
319            // ProcessLogger.ntrace(object.getClass().getSimpleName() + ":r:" + propertyPath, "", 1000);
320            ProcessLogger.countBegin("bean-property-read");
321        }
322
323        try {
324            ObjectPropertyReference.setWarning(true);
325
326            return (T) ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).get();
327
328        } catch (RuntimeException e) {
329            throw new IllegalArgumentException("Error getting property '" + propertyPath + "' from " + object, e);
330        } finally {
331            ObjectPropertyReference.setWarning(false);
332            if (trace) {
333                ProcessLogger.countEnd("bean-property-read", object.getClass().getSimpleName() + ":" + propertyPath);
334            }
335        }
336
337    }
338
339    /**
340     * Looks up a property value, then convert to text using a registered property editor.
341     *
342     * @param bean bean instance to look up a property value for
343     * @param path property path relative to the bean
344     * @return The property value, converted to text using a registered property editor.
345     */
346    public static String getPropertyValueAsText(Object bean, String path) {
347        Object propertyValue = getPropertyValue(bean, path);
348        PropertyEditor editor = getPropertyEditor(bean, path);
349
350        if (editor == null) {
351            return propertyValue == null ? null : propertyValue.toString();
352        } else {
353            editor.setValue(propertyValue);
354            return editor.getAsText();
355        }
356    }
357    
358    /**
359     * Gets the property editor registry configured for the active request.
360     */
361    public static PropertyEditorRegistry getPropertyEditorRegistry() {
362        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
363
364        PropertyEditorRegistry registry = null;
365        if (attributes != null) {
366            registry = (PropertyEditorRegistry) attributes
367                    .getAttribute(UifConstants.PROPERTY_EDITOR_REGISTRY, RequestAttributes.SCOPE_REQUEST);
368        }
369
370        return registry;
371    }
372
373    /**
374     * Convert to a primitive type if available.
375     * 
376     * @param type The type to convert.
377     * @return A primitive type, if available, that corresponds to the type.
378     */
379    public static Class<?> getPrimitiveType(Class<?> type) {
380        if (Byte.class.equals(type)) {
381            return Byte.TYPE;
382        } else if (Short.class.equals(type)) {
383            return Short.TYPE;
384        } else if (Integer.class.equals(type)) {
385            return Integer.TYPE;
386        } else if (Long.class.equals(type)) {
387            return Long.TYPE;
388        } else if (Boolean.class.equals(type)) {
389            return Boolean.TYPE;
390        } else if (Float.class.equals(type)) {
391            return Float.TYPE;
392        } else if (Double.class.equals(type)) {
393            return Double.TYPE;
394        }
395
396        return type;
397    }
398
399    /**
400     * Gets a property editor given a specific bean and property path.
401     * 
402     * @param bean The bean instance.
403     * @param path The property path.
404     * @return property editor
405     */
406    public static PropertyEditor getPropertyEditor(Object bean, String path) {
407        Class<?> propertyType = getPrimitiveType(getPropertyType(bean, path));
408        
409        PropertyEditor editor = null;
410
411        PropertyEditorRegistry registry = getPropertyEditorRegistry();
412        if (registry != null) {
413            editor = registry.findCustomEditor(propertyType, path);
414            
415            if (editor != null && editor != registry.findCustomEditor(propertyType, null)) {
416                return editor;
417            }
418            
419            if (registry instanceof BeanWrapper
420                    && bean == ((BeanWrapper) registry).getWrappedInstance()
421                    && (bean instanceof ViewModel)) {
422                
423                ViewModel viewModel = (ViewModel) bean;
424                ViewPostMetadata viewPostMetadata = viewModel.getViewPostMetadata();
425                PropertyEditor editorFromView = viewPostMetadata == null ? null : viewPostMetadata.getFieldEditor(path);
426
427                if (editorFromView != null) {
428                    registry.registerCustomEditor(propertyType, path, editorFromView);
429                    editor = registry.findCustomEditor(propertyType, path);
430                }
431            }
432        }
433
434        if (editor != null) {
435            return editor;
436        }
437        
438        return getPropertyEditor(propertyType);
439    }
440    
441    /**
442     * Get a property editor given a property type.
443     *
444     * @param propertyType The property type to look up an editor for.
445     * @param path The property path, if applicable.
446     * @return property editor
447     */
448    public static PropertyEditor getPropertyEditor(Class<?> propertyType) {
449        PropertyEditorRegistry registry = getPropertyEditorRegistry();
450        PropertyEditor editor = null;
451        
452        if (registry != null) {
453            editor = registry.findCustomEditor(propertyType, null);
454        } else {
455            
456            DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
457            Map<Class<?>, String> editorMap = dataDictionaryService.getPropertyEditorMap();
458            String editorPrototypeName = editorMap == null ? null : editorMap.get(propertyType);
459            
460            if (editorPrototypeName != null) {
461                editor = (PropertyEditor) dataDictionaryService.getDataDictionary().getDictionaryPrototype(editorPrototypeName);
462            }
463        }
464
465        if (editor == null && propertyType != null) {
466            // Fall back to default beans lookup
467            editor = PropertyEditorManager.findEditor(propertyType);
468        }
469
470        return editor;
471    }
472
473    /**
474     * Initialize a property value.
475     * 
476     * <p>
477     * Upon returning from this method, the property referred to by the provided bean and property
478     * path will have been initialized with a default instance of the indicated property type.
479     * </p>
480     * 
481     * @param object The bean instance to initialize a property value for.
482     * @param propertyPath A valid property path expression in the context of the bean.
483     * @see #getPropertyType(Object, String)
484     * @see #setPropertyValue(Object, String, Object)
485     * @see ObjectPathExpressionParser
486     */
487    public static void initializeProperty(Object object, String propertyPath) {
488        Class<?> propertyType = getPropertyType(object, propertyPath);
489        try {
490            setPropertyValue(object, propertyPath, propertyType.newInstance());
491        } catch (InstantiationException e) {
492            // just set the value to null
493            setPropertyValue(object, propertyPath, null);
494        } catch (IllegalAccessException e) {
495            throw new IllegalArgumentException("Unable to set new instance for property: " + propertyPath, e);
496        }
497    }
498
499    /**
500     * Modify a property value.
501     * 
502     * <p>
503     * Upon returning from this method, the property referred to by the provided bean and property
504     * path will have been populated with property value provided. If the propertyValue does not
505     * match the type of the indicated property, then type conversion will be attempted using
506     * {@link PropertyEditorManager}.
507     * </p>
508     * 
509     * @param object The bean instance to initialize a property value for.
510     * @param propertyPath A valid property path expression in the context of the bean.
511     * @param propertyValue The value to populate value in the property referred to by the provided
512     *        bean and property path.
513     * @see ObjectPathExpressionParser
514     * @throws RuntimeException If the property path is not valid in the context of the bean
515     *         provided.
516     */
517    public static void setPropertyValue(Object object, String propertyPath, Object propertyValue) {
518        if (ProcessLogger.isTraceActive() && object != null) {
519            // May be uncommented for debugging high execution count
520            // ProcessLogger.ntrace(object.getClass().getSimpleName() + ":w:" + propertyPath + ":", "", 1000);
521            ProcessLogger.countBegin("bean-property-write");
522        }
523
524        try {
525            ObjectPropertyReference.setWarning(true);
526
527            ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, true).set(propertyValue);
528
529        } catch (RuntimeException e) {
530            throw new IllegalArgumentException(
531                    "Error setting property '" + propertyPath + "' on " + object + " with " + propertyValue, e);
532        } finally {
533            ObjectPropertyReference.setWarning(false);
534
535            if (ProcessLogger.isTraceActive() && object != null) {
536                ProcessLogger.countEnd("bean-property-write", object.getClass().getSimpleName() + ":" + propertyPath);
537            }
538        }
539
540    }
541
542    /**
543     * Modify a property value.
544     * 
545     * <p>
546     * Upon returning from this method, the property referred to by the provided bean and property
547     * path will have been populated with property value provided. If the propertyValue does not
548     * match the type of the indicated property, then type conversion will be attempted using
549     * {@link PropertyEditorManager}.
550     * </p>
551     * 
552     * @param object The bean instance to initialize a property value for.
553     * @param propertyPath A property path expression in the context of the bean.
554     * @param propertyValue The value to populate value in the property referred to by the provided
555     *        bean and property path.
556     * @param ignoreUnknown True if invalid property values should be ignored, false to throw a
557     *        RuntimeException if the property reference is invalid.
558     * @see ObjectPathExpressionParser
559     */
560    public static void setPropertyValue(Object object, String propertyPath, Object propertyValue, boolean ignoreUnknown) {
561        try {
562            setPropertyValue(object, propertyPath, propertyValue);
563        } catch (RuntimeException e) {
564            // only throw exception if they have indicated to not ignore unknown
565            if (!ignoreUnknown) {
566                throw e;
567            }
568            if (LOG.isTraceEnabled()) {
569                LOG.trace("Ignoring exception thrown during setting of property '" + propertyPath + "': "
570                        + e.getLocalizedMessage());
571            }
572        }
573    }
574
575    /**
576     * Determine if a property is readable.
577     * 
578     * @param object The bean instance to initialize a property value for.
579     * @param propertyPath A property path expression in the context of the bean.
580     * @return True if the path expression resolves to a valid readable property reference in the
581     *         context of the bean provided.
582     */
583    public static boolean isReadableProperty(Object object, String propertyPath) {
584        return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).canRead();
585    }
586
587    /**
588     * Determine if a property is writable.
589     * 
590     * @param object The bean instance to initialize a property value for.
591     * @param propertyPath A property path expression in the context of the bean.
592     * @return True if the path expression resolves to a valid writable property reference in the
593     *         context of the bean provided.
594     */
595    public static boolean isWritableProperty(Object object, String propertyPath) {
596        return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).canWrite();
597    }
598
599    /**
600     * Returns an List of {@code Field} objects reflecting all the fields
601     * declared by the class or interface represented by this
602     * {@code Class} object. This includes public, protected, default
603     * (package) access, and private fields, and includes inherited fields.
604     *
605     * @param fields A list of {@code Field} objects which gets returned.
606     * @param type Type of class or interface for which fields are returned.
607     * @param stopAt The Superclass upto which the inherited fields are to be included
608     * @return List of all fields
609     */
610    public static List<Field> getAllFields(List<Field> fields, Class<?> type, Class<?> stopAt) {
611        for (Field field : type.getDeclaredFields()) {
612            fields.add(field);
613        }
614
615        if (type.getSuperclass() != null && !type.getName().equals(stopAt.getName())) {
616            fields = getAllFields(fields, type.getSuperclass(), stopAt);
617        }
618
619        return fields;
620    }
621
622    /**
623     * Get the best known component type for a generic type.
624     * 
625     * <p>
626     * When the type is not parameterized or has no explicitly defined parameters, {@link Object} is
627     * returned.
628     * </p>
629     * 
630     * <p>
631     * When the type has multiple parameters, the right-most parameter is considered the component
632     * type. This facilitates identifying the value type of a Map.
633     * </p>
634     * 
635     * @param type The generic collection or map type.
636     * @return component or value type, resolved from the generic type
637     */
638    public static Type getComponentType(Type type) {
639        if (!(type instanceof ParameterizedType)) {
640            return Object.class;
641        }
642
643        ParameterizedType parameterizedType = (ParameterizedType) type;
644        Type[] params = parameterizedType.getActualTypeArguments();
645        if (params.length == 0) {
646            return Object.class;
647        }
648
649        Type valueType = params[params.length - 1];
650        return valueType;
651    }
652
653    /**
654     * Get the upper bound of a generic type.
655     * 
656     * <p>
657     * When the type is a class, the class is returned.
658     * </p>
659     * 
660     * <p>
661     * When the type is a wildcard, and the upper bound is a class, the upper bound of the wildcard
662     * is returned.
663     * </p>
664     * 
665     * <p>
666     * If the type has not been explicitly defined at compile time, {@link Object} is returned.
667     * </p>
668     * 
669     * @param valueType The generic collection or map type.
670     * @return component or value type, resolved from the generic type
671     */
672    public static Class<?> getUpperBound(Type valueType) {
673        if (valueType instanceof WildcardType) {
674            Type[] upperBounds = ((WildcardType) valueType).getUpperBounds();
675
676            if (upperBounds.length >= 1) {
677                valueType = upperBounds[0];
678            }
679        }
680
681        if (valueType instanceof ParameterizedType) {
682            valueType = ((ParameterizedType) valueType).getRawType();
683        }
684
685        if (valueType instanceof Class) {
686            return (Class<?>) valueType;
687        }
688
689        return Object.class;
690    }
691
692    /**
693     * Locate the generic type declaration for a given target class in the generic type hierarchy of
694     * the source class.
695     * 
696     * @param sourceClass The class representing the generic type hierarchy to scan.
697     * @param targetClass The class representing the generic type declaration to locate within the
698     *        source class' hierarchy.
699     * @return The generic type representing the target class within the source class' generic
700     *         hierarchy.
701     */
702    public static Type findGenericType(Class<?> sourceClass, Class<?> targetClass) {
703        if (!targetClass.isAssignableFrom(sourceClass)) {
704            throw new IllegalArgumentException(targetClass + " is not assignable from " + sourceClass);
705        }
706
707        if (sourceClass.equals(targetClass)) {
708            return sourceClass;
709        }
710        
711        @SuppressWarnings("unchecked")
712        Queue<Type> typeQueue = RecycleUtils.getInstance(LinkedList.class);
713        typeQueue.offer(sourceClass);
714        while (!typeQueue.isEmpty()) {
715            Type type = typeQueue.poll();
716            
717            Class<?> upperBound = getUpperBound(type);
718            if (targetClass.equals(upperBound)) {
719                return type;
720            }
721
722            Type genericSuper = upperBound.getGenericSuperclass();
723            if (genericSuper != null) {
724                typeQueue.offer(genericSuper);
725            }
726
727            Type[] genericInterfaces = upperBound.getGenericInterfaces();
728            for (int i=0; i<genericInterfaces.length; i++) {
729                if (genericInterfaces[i] != null) {
730                    typeQueue.offer(genericInterfaces[i]);
731                }
732            }
733        }
734        
735        throw new IllegalStateException(targetClass + " is assignable from " + sourceClass
736                + " but could not be found in the generic type hierarchy");
737    }
738
739    /**
740     * Split parse path entry for supporting {@link ObjectPropertyUtils#splitPropertyPath(String)}. 
741     * 
742     * @author Kuali Rice Team (rice.collab@kuali.org)
743     */
744    private static class SplitPropertyPathEntry implements PathEntry {
745
746        /**
747         * Invoked at each path separator on the path.
748         * 
749         * <p>
750         * Note that {@link ObjectPathExpressionParser} strips quotes and brackets then treats
751         * list/map references as property names. However
752         * {@link ObjectPropertyUtils#splitPropertyPath(String)} expects that a list/map reference
753         * will be part of the path expression, as a reference to a specific element in a list or
754         * map related to the bean. Therefore, this method needs to rejoin these list/map references
755         * before returning.
756         * </p>
757         * 
758         * @param parentPath The portion of the path leading up to the current node.
759         * @param node The list of property names in the path.
760         * @param next The next property name being parsed.
761         * 
762         * {@inheritDoc}
763         */
764        @Override
765        public List<String> parse(String parentPath, Object node, String next) {
766            if (next == null) {
767                return new ArrayList<String>();
768            }
769
770            @SuppressWarnings("unchecked")
771            List<String> rv = (List<String>) node;
772            // First node, or no path separator in parent path.
773            if (rv.isEmpty()) {
774                rv.add(next);
775                return rv;
776            }
777            
778            rejoinTrailingIndexReference(rv, parentPath);
779            rv.add(next);
780            
781            return rv;
782        }
783    }
784    
785    private static final SplitPropertyPathEntry SPLIT_PROPERTY_PATH_ENTRY = new SplitPropertyPathEntry();
786    private static final String[] EMPTY_STRING_ARRAY = new String[0];
787
788    /**
789     * Helper method for splitting property paths with bracketed index references.
790     * 
791     * <p>
792     * Since the parser treats index references as separate tokens, they need to be rejoined in order
793     * to be maintained as a single indexed property reference.  This method handles that operation.
794     * </p>
795     * 
796     * @param tokenList The list of tokens being parsed.
797     * @param path The portion of the path expression represented by the token list.
798     */
799    private static void rejoinTrailingIndexReference(List<String> tokenList, String path) {
800        int lastIndex = tokenList.size() - 1;
801        String lastToken = tokenList.get(lastIndex);
802        String lastParentToken = path.substring(path.lastIndexOf('.') + 1);
803        
804        if (!lastToken.equals(lastParentToken) && lastIndex > 0) {
805
806            // read back one more token and "concatenate" by
807            // recreating the subexpression as a substring of
808            // the parent path
809            String prevToken = tokenList.get(--lastIndex);
810            
811            // parent path index of last prevToken.
812            int iopt = path.lastIndexOf(prevToken, path.lastIndexOf(lastToken));
813            
814            String fullToken = path.substring(iopt);
815            tokenList.remove(lastIndex); // remove first entry
816            tokenList.set(lastIndex, fullToken); // replace send with concatenation
817        }
818    }
819    
820    /**
821     * Splits the given property path into a string of property names that make up the path.
822     *
823     * @param path property path to split
824     * @return string array of names, starting from the top parent
825     * @see SplitPropertyPathEntry#parse(String, Object, String)
826     */
827    public static String[] splitPropertyPath(String path) {
828        List<String> split = ObjectPathExpressionParser.parsePathExpression(null, path, SPLIT_PROPERTY_PATH_ENTRY);
829        if (split == null || split.isEmpty()) {
830            return EMPTY_STRING_ARRAY;
831        }
832        
833        rejoinTrailingIndexReference(split, path);
834
835        return split.toArray(new String[split.size()]);
836    }
837
838    /**
839     * Returns the tail of a given property path (if nested, the nested path).
840     *
841     * <p>For example, if path is "nested1.foo", this will return "foo". If path is just "foo", "foo" will be
842     * returned.</p>
843     *
844     * @param path path to return tail for
845     * @return String tail of path
846     */
847    public static String getPathTail(String path) {
848        String[] propertyPaths = splitPropertyPath(path);
849
850        return propertyPaths[propertyPaths.length - 1];
851    }
852
853    /**
854     * Removes the tail of the path from the return path.
855     *
856     * <p>For example, if path is "nested1.foo", this will return "nested1". If path is just "foo", "" will be
857     * returned.</p>
858     *
859     * @param path path to remove tail from
860     * @return String path with tail removed (may be empty string)
861     */
862    public static String removePathTail(String path) {
863        String[] propertyPaths = splitPropertyPath(path);
864
865        return StringUtils.join(propertyPaths, ".", 0, propertyPaths.length - 1);
866    }
867    
868    /**
869     * Removes any collection references from a property path, making it more useful for referring
870     * to metadata related to the property.
871     * @param path A property path expression.
872     * @return The path, with collection references removed.
873     */
874    public static String getCanonicalPath(String path) {
875        if (path == null || path.indexOf('[') == -1) {
876            return path;
877        }
878
879        // The path has at least one left bracket, so will need to be modified
880        // copy it to a mutable StringBuilder
881        StringBuilder pathBuilder = new StringBuilder(path);
882
883        int bracketCount = 0;
884        int leftBracketPos = -1;
885        for (int i = 0; i < pathBuilder.length(); i++) {
886            char c = pathBuilder.charAt(i);
887
888            if (c == '[') {
889                bracketCount++;
890                if (bracketCount == 1)
891                    leftBracketPos = i;
892            }
893
894            if (c == ']') {
895                bracketCount--;
896
897                if (bracketCount < 0) {
898                    throw new IllegalArgumentException("Unmatched ']' at " + i + " " + pathBuilder);
899                }
900
901                if (bracketCount == 0) {
902                    pathBuilder.delete(leftBracketPos, i + 1);
903                    i -= i + 1 - leftBracketPos;
904                    leftBracketPos = -1;
905                }
906            }
907        }
908
909        if (bracketCount > 0) {
910            throw new IllegalArgumentException("Unmatched '[' at " + leftBracketPos + " " + pathBuilder);
911        }
912
913        return pathBuilder.toString();
914    }
915    
916    /**
917     * Private constructor - utility class only.
918     */
919    private ObjectPropertyUtils() {}
920
921    /**
922     * Infer the read method based on method name.
923     * 
924     * @param beanClass The bean class.
925     * @param propertyName The property name.
926     * @return The read method for the property.
927     */
928    private static Method getReadMethodByName(Class<?> beanClass, String propertyName) {
929
930        try {
931            return beanClass.getMethod("get" + Character.toUpperCase(propertyName.charAt(0))
932                    + propertyName.substring(1));
933        } catch (SecurityException e) {
934            // Ignore
935        } catch (NoSuchMethodException e) {
936            // Ignore
937        }
938
939        try {
940            Method readMethod = beanClass.getMethod("is"
941                    + Character.toUpperCase(propertyName.charAt(0))
942                    + propertyName.substring(1));
943            
944            if (readMethod.getReturnType() == Boolean.class || readMethod.getReturnType() == Boolean.TYPE) {
945                return readMethod;
946            }
947        } catch (SecurityException e) {
948            // Ignore
949        } catch (NoSuchMethodException e) {
950            // Ignore
951        }
952        
953        return null;
954    }
955    
956    /**
957     * Get the cached metadata for a bean class.
958     * 
959     * @param beanClass The bean class.
960     * @return cached metadata for beanClass
961     */
962    private static ObjectPropertyMetadata getMetadata(Class<?> beanClass) {
963        ObjectPropertyMetadata metadata = METADATA_CACHE.get(beanClass);
964
965        if (metadata == null) {
966            metadata = new ObjectPropertyMetadata(beanClass);
967            METADATA_CACHE.put(beanClass, metadata);
968        }
969
970        return metadata;
971    }
972    
973    /**
974     * Stores property metadata related to a bean class, for reducing introspection and reflection
975     * overhead.
976     * 
977     * @author Kuali Rice Team (rice.collab@kuali.org)
978     */
979    private static class ObjectPropertyMetadata {
980
981        private final Map<String, PropertyDescriptor> propertyDescriptors;
982        private final Map<String, Method> readMethods;
983        private final Map<String, Method> writeMethods;
984        private final Map<Class<?>, Set<String>> readablePropertyNamesByPropertyType =
985                Collections.synchronizedMap(new WeakHashMap<Class<?>, Set<String>>());
986        private final Map<Class<?>, Set<String>> readablePropertyNamesByAnnotationType =
987                Collections.synchronizedMap(new WeakHashMap<Class<?>, Set<String>>());
988        private final Map<Class<?>, Set<String>> readablePropertyNamesByCollectionType =
989                Collections.synchronizedMap(new WeakHashMap<Class<?>, Set<String>>());
990        
991        /**
992         * Gets the property names by type, based on the read methods.
993         * 
994         * @param propertyType The return type of the read method on the property.
995         * @return list of property names
996         */
997        private Set<String> getReadablePropertyNamesByType(Class<?> propertyType) {
998            Set<String> propertyNames = readablePropertyNamesByPropertyType.get(propertyType);
999            if (propertyNames != null) {
1000                return propertyNames;
1001            }
1002            
1003            propertyNames = new LinkedHashSet<String>();
1004            for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) {
1005                Method readMethod = readMethodEntry.getValue();
1006                if (readMethod != null && propertyType.isAssignableFrom(readMethod.getReturnType())) {
1007                    propertyNames.add(readMethodEntry.getKey());
1008                }
1009            }
1010            
1011            propertyNames = Collections.unmodifiableSet(propertyNames);
1012            readablePropertyNamesByPropertyType.put(propertyType, propertyNames);
1013            
1014            return propertyNames;
1015        }
1016
1017        /**
1018         * Gets the property names by annotation type, based on the read methods.
1019         * 
1020         * @param annotationType The type of an annotation on the return type.
1021         * @return list of property names
1022         */
1023        private Set<String> getReadablePropertyNamesByAnnotationType(Class<? extends Annotation> annotationType) {
1024            Set<String> propertyNames = readablePropertyNamesByAnnotationType.get(annotationType);
1025            if (propertyNames != null) {
1026                return propertyNames;
1027            }
1028            
1029            propertyNames = new LinkedHashSet<String>();
1030            for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) {
1031                Method readMethod = readMethodEntry.getValue();
1032                if (readMethod != null && readMethod.isAnnotationPresent(annotationType)) {
1033                    propertyNames.add(readMethodEntry.getKey());
1034                }
1035            }
1036            
1037            propertyNames = Collections.unmodifiableSet(propertyNames);
1038            readablePropertyNamesByPropertyType.put(annotationType, propertyNames);
1039            
1040            return propertyNames;
1041        }
1042
1043        /**
1044         * Gets the property names by collection type, based on the read methods.
1045         * 
1046         * @param collectionType The type of elements in a collection or array.
1047         * @return list of property names
1048         */
1049        private Set<String> getReadablePropertyNamesByCollectionType(Class<?> collectionType) {
1050            Set<String> propertyNames = readablePropertyNamesByCollectionType.get(collectionType);
1051            if (propertyNames != null) {
1052                return propertyNames;
1053            }
1054            
1055            propertyNames = new LinkedHashSet<String>();
1056            for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) {
1057                Method readMethod = readMethodEntry.getValue();
1058                if (readMethod == null) {
1059                    continue;
1060                }
1061                
1062                Class<?> propertyClass = readMethod.getReturnType();
1063                if (propertyClass.isArray() && collectionType.isAssignableFrom(propertyClass.getComponentType())) {
1064                    propertyNames.add(readMethodEntry.getKey());
1065                    continue;
1066                }
1067                
1068                boolean isCollection = Collection.class.isAssignableFrom(propertyClass);
1069                boolean isMap = Map.class.isAssignableFrom(propertyClass);
1070                if (!isCollection && !isMap) {
1071                    continue;
1072                }
1073                
1074                if (collectionType.equals(Object.class)) {
1075                    propertyNames.add(readMethodEntry.getKey());
1076                    continue;
1077                }
1078                
1079                Type propertyType = readMethodEntry.getValue().getGenericReturnType();
1080                if (propertyType instanceof ParameterizedType) {
1081                    ParameterizedType parameterizedType = (ParameterizedType) propertyType;
1082                    Type valueType = parameterizedType.getActualTypeArguments()[isCollection ? 0 : 1];
1083
1084                    if (valueType instanceof WildcardType) {
1085                        Type[] upperBounds = ((WildcardType) valueType).getUpperBounds(); 
1086                        
1087                        if (upperBounds.length >= 1) {
1088                            valueType = upperBounds[0];
1089                        }
1090                    }
1091                    
1092                    if (valueType instanceof Class &&
1093                            collectionType.isAssignableFrom((Class<?>) valueType)) {
1094                        propertyNames.add(readMethodEntry.getKey());
1095                    }
1096                }
1097            }
1098            
1099            propertyNames = Collections.unmodifiableSet(propertyNames);
1100            readablePropertyNamesByCollectionType.put(collectionType, propertyNames);
1101            
1102            return propertyNames;
1103        }
1104
1105        /**
1106         * Gets the property names that are writable for the metadata class.
1107         *
1108         * @return set of writable property names
1109         */
1110        private Set<String> getWritablePropertyNames() {
1111            Set<String> writablePropertyNames = new HashSet<String>();
1112
1113            for (Entry<String, Method> writeMethodEntry : writeMethods.entrySet()) {
1114                writablePropertyNames.add(writeMethodEntry.getKey());
1115            }
1116
1117            return writablePropertyNames;
1118        }
1119
1120        /**
1121         * Creates a new metadata wrapper for a bean class.
1122         * 
1123         * @param beanClass The bean class.
1124         */
1125        private ObjectPropertyMetadata(Class<?> beanClass) {
1126            if (beanClass == null) {
1127                throw new RuntimeException("Class to retrieve property from was null");
1128            }
1129
1130            BeanInfo beanInfo;
1131            try {
1132                beanInfo = Introspector.getBeanInfo(beanClass);
1133            } catch (IntrospectionException e) {
1134                LOG.warn("Bean Info not found for bean " + beanClass, e);
1135                beanInfo = null;
1136            }
1137
1138            Map<String, PropertyDescriptor> mutablePropertyDescriptorMap = new LinkedHashMap<String, PropertyDescriptor>();
1139            Map<String, Method> mutableReadMethodMap = new LinkedHashMap<String, Method>();
1140            Map<String, Method> mutableWriteMethodMap = new LinkedHashMap<String, Method>();
1141
1142            if (beanInfo != null) {
1143                for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
1144                    String propertyName = propertyDescriptor.getName();
1145
1146                    mutablePropertyDescriptorMap.put(propertyName, propertyDescriptor);
1147                    Method readMethod = propertyDescriptor.getReadMethod();
1148                    if (readMethod == null) {
1149                        readMethod = getReadMethodByName(beanClass, propertyName);
1150                    }
1151
1152                    // checking for bridge methods http://www.znetdevelopment.com/blogs/2012/04/11/java-bean-introspector-and-covariantgeneric-returns/
1153                    if (readMethod != null && readMethod.isBridge()) {
1154                        readMethod = getCorrectedReadMethod(beanClass, readMethod);
1155                    }
1156
1157                    mutableReadMethodMap.put(propertyName, readMethod);
1158
1159                    Method writeMethod = propertyDescriptor.getWriteMethod();
1160                    assert writeMethod == null
1161                            || (writeMethod.getParameterTypes().length == 1 && writeMethod.getParameterTypes()[0] != null) : writeMethod;
1162                    mutableWriteMethodMap.put(propertyName, writeMethod);
1163                }
1164            }
1165
1166            propertyDescriptors = Collections.unmodifiableMap(mutablePropertyDescriptorMap);
1167            readMethods = Collections.unmodifiableMap(mutableReadMethodMap);
1168            writeMethods = Collections.unmodifiableMap(mutableWriteMethodMap);
1169        }
1170
1171        /**
1172         * Workaround for a JDK6 Introspector issue (see KULRICE-12334) that results in getters for interface types
1173         * being returned instead of same named getters for concrete implementation types (depending on the Method order
1174         * returned by reflection on the beanClass.
1175         *
1176         * <p>Note that this doesn't cover all cases, see ObjectPropertyUtilsTest.testGetterInInterfaceOrSuperHasWiderType
1177         * for details.</p>
1178         *
1179         * @param beanClass the class of the bean being inspected
1180         * @param readMethod the read method being double-checked
1181         * @return the corrected read Method
1182         */
1183        private Method getCorrectedReadMethod(Class<?> beanClass, Method readMethod) {
1184            if (readMethod != null
1185                    && !readMethod.getReturnType().isPrimitive()
1186                    && isAbstractClassOrInterface(readMethod.getReturnType())) {
1187
1188                Method implReadMethod = null;
1189
1190                try {
1191                    implReadMethod = beanClass.getMethod(readMethod.getName(), readMethod.getParameterTypes());
1192                } catch (NoSuchMethodException e) {
1193                    // if readMethod != null, this should not happen according to the javadocs for Class.getMethod()
1194                }
1195
1196                if (implReadMethod != null && isSubClass(implReadMethod.getReturnType(), readMethod.getReturnType())) {
1197                    return implReadMethod;
1198                }
1199            }
1200
1201            return readMethod;
1202        }
1203
1204        // we assume a non-null arg
1205        private boolean isAbstractClassOrInterface(Class<?> clazz) {
1206            return clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers());
1207        }
1208
1209        // we assume non-null args
1210        private boolean isSubClass(Class<?> childClassCandidate, Class<?> parentClassCandidate) {
1211            // if A != B and A >= B then A > B
1212            return parentClassCandidate != childClassCandidate && parentClassCandidate.isAssignableFrom(childClassCandidate);
1213        }
1214    }
1215}