View Javadoc

1   /**
2    * Copyright 2005-2013 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.PropertyEditorManager;
23  import java.lang.reflect.Method;
24  import java.util.Collections;
25  import java.util.Map;
26  import java.util.WeakHashMap;
27  
28  import org.apache.log4j.Logger;
29  
30  /**
31   * Utility methods to get/set property values and working with objects.
32   * 
33   * @author Kuali Rice Team (rice.collab@kuali.org)
34   */
35  public class ObjectPropertyUtils {
36  
37      private static final Logger LOG = Logger.getLogger(ObjectPropertyUtils.class);
38  
39      /**
40       * Internal property descriptor cache.
41       * 
42       * <p>
43       * NOTE: WeakHashMap is used as the internal cache representation. Since class objects are used
44       * as the keys, this allows property descriptors to stay in cache until the class loader is
45       * unloaded, but will not prevent the class loader itself from unloading. PropertyDescriptor
46       * instances do not hold hard references back to the classes they refer to, so weak value
47       * maintenance is not necessary.
48       * </p>
49       */
50      private static Map<Class<?>, Map<String, PropertyDescriptor>> PROPERTY_DESCRIPTOR_CACHE = Collections
51              .synchronizedMap(new WeakHashMap<Class<?>, Map<String, PropertyDescriptor>>(2048));
52  
53      /**
54       * Get a mapping of property descriptors by property name for a bean class.
55       * 
56       * @param beanClass The bean class.
57       * @return A mapping of all property descriptors for the bean class, by property name.
58       */
59      public static Map<String, PropertyDescriptor> getPropertyDescriptors(Class<?> beanClass) {
60          Map<String, PropertyDescriptor> propertyDescriptors = PROPERTY_DESCRIPTOR_CACHE.get(beanClass);
61  
62          if (propertyDescriptors == null) {
63              BeanInfo beanInfo;
64              try {
65                  beanInfo = Introspector.getBeanInfo(beanClass);
66              } catch (IntrospectionException e) {
67                  LOG.warn(
68                          "Bean Info not found for bean " + beanClass, e);
69                  beanInfo = null;
70              }
71  
72              Map<String, PropertyDescriptor> unsynchronizedPropertyDescriptorMap = new java.util.LinkedHashMap<String, PropertyDescriptor>();
73  
74              if (beanInfo != null) {
75                  for (PropertyDescriptor propertyDescriptor : beanInfo
76                          .getPropertyDescriptors()) {
77                      unsynchronizedPropertyDescriptorMap.put(propertyDescriptor.getName(), propertyDescriptor);
78                  }
79              }
80  
81              PROPERTY_DESCRIPTOR_CACHE.put(beanClass, propertyDescriptors = Collections.unmodifiableMap(Collections
82                      .synchronizedMap(unsynchronizedPropertyDescriptorMap)));
83          }
84  
85          return propertyDescriptors;
86      }
87  
88      /**
89       * Get a property descriptor from a class by property name.
90       * 
91       * @param beanClass The bean class.
92       * @param propertyName The bean property name.
93       * @return The property descriptor named on the bean class.
94       */
95      public static PropertyDescriptor getPropertyDescriptor(Class<?> beanClass, String propertyName) {
96          if (propertyName == null) {
97              throw new IllegalArgumentException("Null property name");
98          }
99  
100         PropertyDescriptor propertyDescriptor = getPropertyDescriptors(beanClass).get(propertyName);
101         if (propertyDescriptor != null) {
102             return propertyDescriptor;
103         } else {
104             throw new IllegalArgumentException("Property " + propertyName
105                     + " not found for bean " + beanClass);
106         }
107     }
108 
109     /**
110      * Get the read method for a specific property on a bean class.
111      * 
112      * @param beanClass The bean class.
113      * @param propertyName The property name.
114      * @return The read method for the property.
115      */
116     public static Method getReadMethod(Class<?> beanClass, String propertyName) {
117         if (propertyName == null || propertyName.length() == 0) {
118             return null;
119         }
120 
121         Method readMethod = null;
122 
123         PropertyDescriptor propertyDescriptor = ObjectPropertyUtils.getPropertyDescriptors(beanClass).get(propertyName);
124         if (propertyDescriptor != null) {
125             readMethod = propertyDescriptor.getReadMethod();
126         }
127 
128         if (readMethod == null) {
129             try {
130                 readMethod = beanClass.getMethod("get" + Character.toUpperCase(propertyName.charAt(0))
131                         + propertyName.substring(1));
132             } catch (SecurityException e) {
133                 // Ignore
134             } catch (NoSuchMethodException e) {
135                 // Ignore
136             }
137         }
138 
139         if (readMethod == null) {
140             try {
141                 Method trm = beanClass.getMethod("is"
142                         + Character.toUpperCase(propertyName.charAt(0))
143                         + propertyName.substring(1));
144                 if (trm.getReturnType() == Boolean.class
145                         || trm.getReturnType() == Boolean.TYPE)
146                     readMethod = trm;
147             } catch (SecurityException e) {
148                 // Ignore
149             } catch (NoSuchMethodException e) {
150                 // Ignore
151             }
152         }
153 
154         return readMethod;
155     }
156 
157     /**
158      * Get the read method for a specific property on a bean class.
159      * 
160      * @param beanClass The bean class.
161      * @param propertyName The property name.
162      * @return The read method for the property.
163      */
164     public static Method getWriteMethod(Class<?> beanClass, String propertyName) {
165         PropertyDescriptor propertyDescriptor = ObjectPropertyUtils.getPropertyDescriptors(beanClass).get(propertyName);
166 
167         if (propertyDescriptor != null) {
168             Method writeMethod = propertyDescriptor.getWriteMethod();
169             assert writeMethod == null
170                     || (writeMethod.getParameterTypes().length == 1 && writeMethod.getParameterTypes()[0] != null) : writeMethod;
171             return writeMethod;
172         } else {
173             return null;
174         }
175     }
176 
177     /**
178      * Copy properties from a string map to an object.
179      * 
180      * @param properties The string map. The keys of this map must be valid property path
181      *        expressions in the context of the target object. The values are the string
182      *        representations of the target bean properties.
183      * @param object The target object, to copy the property values to.
184      * @see ObjectPathExpressionParser
185      */
186     public static void copyPropertiesToObject(Map<String, String> properties, Object object) {
187         for (Map.Entry<String, String> property : properties.entrySet()) {
188             setPropertyValue(object, property.getKey(), property.getValue());
189         }
190     }
191 
192     /**
193      * Get the type of a bean property.
194      * 
195      * @param beanClass The bean class.
196      * @param propertyPath A valid property path expression in the context of the bean class.
197      * @return The property type referred to by the provided bean class and property path.
198      * @see ObjectPathExpressionParser
199      */
200     public static Class<?> getPropertyType(Class<?> beanClass, String propertyPath) {
201         return ObjectPropertyReference.resolvePath(null, beanClass, propertyPath, false).getPropertyType();
202     }
203 
204     /**
205      * Get the type of a bean property.
206      * 
207      * @param object The bean instance. Use {@link #getPropertyType(Class, String)} to look up
208      *        property types when an instance is not available.
209      * @param propertyPath A valid property path expression in the context of the bean.
210      * @return The property type referred to by the provided bean and property path.
211      * @see ObjectPathExpressionParser
212      */
213     public static Class<?> getPropertyType(Object object, String propertyPath) {
214         return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).getPropertyType();
215     }
216 
217     /**
218      * Look up a property value.
219      * 
220      * @param object The bean instance to look up a property value for.
221      * @param propertyPath A valid property path expression in the context of the bean.
222      * @return The value of the property referred to by the provided bean and property path.
223      * @see ObjectPathExpressionParser
224      */
225     @SuppressWarnings("unchecked")
226     public static <T extends Object> T getPropertyValue(Object object, String propertyPath) {
227         if (ProcessLogger.isTraceActive() && object != null) {
228             // May be uncommented for debugging high execution count
229             // ProcessLogger.ntrace(object.getClass().getSimpleName() + ":r:" + propertyPath, "", 1000);
230             ProcessLogger.countBegin("bean-property-read");
231         }
232 
233         try {
234         
235             return (T) ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).get();
236         
237         } catch (RuntimeException e) {
238             throw new RuntimeException("Error getting property '" + propertyPath + "' from " + object, e);
239         } finally {
240             if (ProcessLogger.isTraceActive() && object != null) {
241                 ProcessLogger.countEnd("bean-property-read", object.getClass().getSimpleName() + ":" + propertyPath);
242             }
243         }
244         
245     }
246 
247     /**
248      * Initialize a property value.
249      * 
250      * <p>
251      * Upon returning from this method, the property referred to by the provided bean and property
252      * path will have been initialized with a default instance of the indicated property type.
253      * </p>
254      * 
255      * @param object The bean instance to initialize a property value for.
256      * @param propertyPath A valid property path expression in the context of the bean.
257      * @see #getPropertyType(Object, String)
258      * @see #setPropertyValue(Object, String, Object)
259      * @see ObjectPathExpressionParser
260      */
261     public static void initializeProperty(Object object, String propertyPath) {
262         Class<?> propertyType = getPropertyType(object, propertyPath);
263         try {
264             setPropertyValue(object, propertyPath, propertyType.newInstance());
265         } catch (InstantiationException e) {
266             // just set the value to null
267             setPropertyValue(object, propertyPath, null);
268         } catch (IllegalAccessException e) {
269             throw new RuntimeException("Unable to set new instance for property: " + propertyPath, e);
270         }
271     }
272 
273     /**
274      * Modify a property value.
275      * 
276      * <p>
277      * Upon returning from this method, the property referred to by the provided bean and property
278      * path will have been populated with property value provided. If the propertyValue does not
279      * match the type of the indicated property, then type conversion will be attempted using
280      * {@link PropertyEditorManager}.
281      * </p>
282      * 
283      * @param object The bean instance to initialize a property value for.
284      * @param propertyPath A valid property path expression in the context of the bean.
285      * @param propertyValue The value to populate value in the property referred to by the provided
286      *        bean and property path.
287      * @see ObjectPathExpressionParser
288      * @throws RuntimeException If the property path is not valid in the context of the bean
289      *         provided.
290      */
291     public static void setPropertyValue(Object object, String propertyPath, Object propertyValue) {
292         if (ProcessLogger.isTraceActive() && object != null) {
293             // May be uncommented for debugging high execution count
294             // ProcessLogger.ntrace(object.getClass().getSimpleName() + ":w:" + propertyPath + ":", "", 1000);
295             ProcessLogger.countBegin("bean-property-write");
296         }
297         
298         try {
299 
300             ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, true).set(propertyValue);
301         
302         } catch (RuntimeException e) {
303             throw new RuntimeException("Error setting property '" + propertyPath + "' on " + object + " with "
304                     + propertyValue, e);
305         } finally {
306             if (ProcessLogger.isTraceActive() && object != null) {
307                 ProcessLogger.countEnd("bean-property-write", object.getClass().getSimpleName() + ":" + propertyPath);
308             }
309         }
310         
311     }
312 
313     /**
314      * Modify a property value.
315      * 
316      * <p>
317      * Upon returning from this method, the property referred to by the provided bean and property
318      * path will have been populated with property value provided. If the propertyValue does not
319      * match the type of the indicated property, then type conversion will be attempted using
320      * {@link PropertyEditorManager}.
321      * </p>
322      * 
323      * @param object The bean instance to initialize a property value for.
324      * @param propertyPath A property path expression in the context of the bean.
325      * @param propertyValue The value to populate value in the property referred to by the provided
326      *        bean and property path.
327      * @param ignore True if invalid property values should be ignored, false to throw a
328      *        RuntimeException if the property refernce is invalid.
329      * @see ObjectPathExpressionParser
330      */
331     public static void setPropertyValue(Object object, String propertyPath, Object propertyValue, boolean ignoreUnknown) {
332         try {
333             setPropertyValue(object, propertyPath, propertyValue);
334         } catch (RuntimeException e) {
335             // only throw exception if they have indicated to not ignore unknown
336             if (!ignoreUnknown) {
337                 throw e;
338             }
339             if (LOG.isTraceEnabled()) {
340                 LOG.trace("Ignoring exception thrown during setting of property '" + propertyPath + "': "
341                         + e.getLocalizedMessage());
342             }
343         }
344     }
345 
346     /**
347      * Determine if a property is readable.
348      * 
349      * @param object The bean instance to initialize a property value for.
350      * @param propertyPath A property path expression in the context of the bean.
351      * @return True if the path expression resolves to a valid readable property reference in the
352      *         context of the bean provided.
353      */
354     public static boolean isReadableProperty(Object object, String propertyPath) {
355         return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).canRead();
356     }
357 
358     /**
359      * Determine if a property is writable.
360      * 
361      * @param object The bean instance to initialize a property value for.
362      * @param propertyPath A property path expression in the context of the bean.
363      * @return True if the path expression resolves to a valid writable property reference in the
364      *         context of the bean provided.
365      */
366     public static boolean isWritableProperty(Object object, String propertyPath) {
367         return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).canWrite();
368     }
369 
370     /**
371      * Private constructor - utility class only.
372      */
373     private ObjectPropertyUtils() {}
374 
375 }