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