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 }