1 package org.andromda.core.common;
2
3 import java.beans.PropertyDescriptor;
4 import java.lang.reflect.Method;
5 import java.util.Collection;
6 import java.util.HashMap;
7 import java.util.Map;
8
9 import org.apache.commons.lang.StringUtils;
10 import org.apache.commons.lang.exception.ExceptionUtils;
11
12
13 /***
14 * A simple class providing the ability to manipulate properties on java bean objects.
15 *
16 * @author Chad Brandon
17 */
18 public final class Introspector
19 {
20 /***
21 * The shared instance.
22 */
23 private static Introspector instance = null;
24
25 /***
26 * Gets the shared instance.
27 *
28 * @return the shared introspector instance.
29 */
30 public static Introspector instance()
31 {
32 if (instance == null)
33 {
34 instance = new Introspector();
35 }
36 return instance;
37 }
38
39 /***
40 * <p> Indicates whether or not the given <code>object</code> contains a
41 * valid property with the given <code>name</code> and <code>value</code>.
42 * </p>
43 * <p>
44 * A valid property means the following:
45 * <ul>
46 * <li>It exists on the object</li>
47 * <li>It is not null on the object</li>
48 * <li>If its a boolean value, then it evaluates to <code>true</code></li>
49 * <li>If value is not null, then the property matches the given </code>.value</code></li>
50 * </ul>
51 * All other possibilities return <code>false</code>
52 * </p>
53 *
54 * @param object the object to test for the valid property.
55 * @param name the name of the propery for which to test.
56 * @param value the value to evaluate against.
57 * @return true/false
58 */
59 public boolean containsValidProperty(
60 final Object object,
61 final String name,
62 final String value)
63 {
64 boolean valid;
65
66 try
67 {
68 final Object propertyValue = this.getProperty(
69 object,
70 name);
71 valid = propertyValue != null;
72
73
74
75 if (valid)
76 {
77
78
79 if (propertyValue instanceof Collection)
80 {
81 valid = !((Collection)propertyValue).isEmpty();
82 }
83 else
84 {
85 final String valueAsString = String.valueOf(propertyValue);
86 if (StringUtils.isNotEmpty(value))
87 {
88 valid = valueAsString.equals(value);
89 }
90 else if (propertyValue instanceof Boolean)
91 {
92 valid = Boolean.valueOf(valueAsString).booleanValue();
93 }
94 }
95 }
96 }
97 catch (final Throwable throwable)
98 {
99 valid = false;
100 }
101 return valid;
102 }
103
104 /***
105 * Sets the property having the given <code>name</code> on the <code>object</code>
106 * with the given <code>value</code>.
107 *
108 * @param object the object on which to set the property.
109 * @param name the name of the property to populate.
110 * @param value the value to give the property.
111 */
112 public void setProperty(
113 final Object object,
114 final String name,
115 final Object value)
116 {
117 this.setNestedProperty(
118 object,
119 name,
120 value);
121 }
122
123 /***
124 * The delimiter used for seperating nested properties.
125 */
126 private static final char NESTED_DELIMITER = '.';
127
128 /***
129 * Attempts to set the nested property with the given
130 * name of the given object.
131 * @param object the object on which to populate the property.
132 * @param name the name of the object.
133 * @param value the value to populate.
134 */
135 private void setNestedProperty(
136 final Object object,
137 String name,
138 final Object value)
139 {
140 if (object != null && name != null && name.length() > 0)
141 {
142 final int dotIndex = name.indexOf(NESTED_DELIMITER);
143 if (dotIndex >= name.length())
144 {
145 throw new IntrospectorException("Invalid property call --> '" + name + "'");
146 }
147 String[] names = name.split("//" + NESTED_DELIMITER);
148 Object objectToPopulate = object;
149 for (int ctr = 0; ctr < names.length; ctr++)
150 {
151 name = names[ctr];
152 if (ctr == names.length - 1)
153 {
154 break;
155 }
156 objectToPopulate = this.internalGetProperty(
157 objectToPopulate,
158 name);
159 }
160 this.internalSetProperty(
161 objectToPopulate,
162 name,
163 value);
164 }
165 }
166
167 /***
168 * Attempts to retrieve the property with the given <code>name</code> on the <code>object</code>.
169 *
170 * @param object the object to which the property belongs.
171 * @param name the name of the property
172 * @return the value of the property.
173 */
174 public final Object getProperty(
175 final Object object,
176 final String name)
177 {
178 Object result;
179
180 try
181 {
182 result = this.getNestedProperty(
183 object,
184 name);
185 }
186 catch (final IntrospectorException throwable)
187 {
188
189
190
191 throw throwable;
192 }
193 catch (Throwable throwable)
194 {
195 throwable = ExceptionUtils.getRootCause(throwable);
196
197
198
199 if (throwable instanceof IntrospectorException)
200 {
201 throw (IntrospectorException)throwable;
202 }
203 throw new IntrospectorException(throwable);
204 }
205 return result;
206 }
207
208 /***
209 * Gets a nested property, that is it gets the properties
210 * seperated by '.'.
211 *
212 * @param object the object from which to retrieve the nested property.
213 * @param name the name of the property
214 * @return the property value or null if one couldn't be retrieved.
215 */
216 private Object getNestedProperty(
217 final Object object,
218 final String name)
219 {
220 Object property = null;
221 if (object != null && name != null && name.length() > 0)
222 {
223 int dotIndex = name.indexOf(NESTED_DELIMITER);
224 if (dotIndex == -1)
225 {
226 property = this.internalGetProperty(
227 object,
228 name);
229 }
230 else
231 {
232 if (dotIndex >= name.length())
233 {
234 throw new IntrospectorException("Invalid property call --> '" + name + "'");
235 }
236 final Object nextInstance = this.internalGetProperty(
237 object,
238 name.substring(
239 0,
240 dotIndex));
241 property = getNestedProperty(
242 nextInstance,
243 name.substring(dotIndex + 1));
244 }
245 }
246 return property;
247 }
248
249 /***
250 * Cache for a class's write methods.
251 */
252 private final Map writeMethodsCache = new HashMap();
253
254 /***
255 * Gets the writable method for the property.
256 *
257 * @param object the object from which to retrieve the property method.
258 * @param name the name of the property.
259 * @return the property method or null if one wasn't found.
260 */
261 private Method getWriteMethod(
262 final Object object,
263 final String name)
264 {
265 Method writeMethod = null;
266 final Class objectClass = object.getClass();
267 Map classWriteMethods = (Map)this.writeMethodsCache.get(objectClass);
268 if (classWriteMethods == null)
269 {
270 classWriteMethods = new HashMap();
271 }
272 else
273 {
274 writeMethod = (Method)classWriteMethods.get(name);
275 }
276 if (writeMethod == null)
277 {
278 final PropertyDescriptor descriptor = this.getPropertyDescriptor(
279 object.getClass(),
280 name);
281 writeMethod = descriptor != null ? descriptor.getWriteMethod() : null;
282 if (writeMethod != null)
283 {
284 classWriteMethods.put(
285 name,
286 writeMethod);
287 this.writeMethodsCache.put(
288 objectClass,
289 classWriteMethods);
290 }
291 }
292 return writeMethod;
293 }
294
295 /***
296 * Indicates if the <code>object</code> has a property that
297 * is <em>readable</em> with the given <code>name</code>.
298 *
299 * @param object the object to check.
300 * @param name the property to check for.
301 */
302 public boolean isReadable(
303 final Object object,
304 final String name)
305 {
306 return this.getReadMethod(
307 object,
308 name) != null;
309 }
310
311 /***
312 * Indicates if the <code>object</code> has a property that
313 * is <em>writable</em> with the given <code>name</code>.
314 *
315 * @param object the object to check.
316 * @param name the property to check for.
317 */
318 public boolean isWritable(
319 final Object object,
320 final String name)
321 {
322 return this.getWriteMethod(
323 object,
324 name) != null;
325 }
326
327 /***
328 * Cache for a class's read methods.
329 */
330 private final Map readMethodsCache = new HashMap();
331
332 /***
333 * Gets the readable method for the property.
334 *
335 * @param object the object from which to retrieve the property method.
336 * @param name the name of the property.
337 * @return the property method or null if one wasn't found.
338 */
339 private Method getReadMethod(
340 final Object object,
341 final String name)
342 {
343 Method readMethod = null;
344 final Class objectClass = object.getClass();
345 Map classReadMethods = (Map)this.readMethodsCache.get(objectClass);
346 if (classReadMethods == null)
347 {
348 classReadMethods = new HashMap();
349 }
350 else
351 {
352 readMethod = (Method)classReadMethods.get(name);
353 }
354 if (readMethod == null)
355 {
356 final PropertyDescriptor descriptor = this.getPropertyDescriptor(
357 object.getClass(),
358 name);
359 readMethod = descriptor != null ? descriptor.getReadMethod() : null;
360 if (readMethod != null)
361 {
362 classReadMethods.put(
363 name,
364 readMethod);
365 this.readMethodsCache.put(
366 objectClass,
367 classReadMethods);
368 }
369 }
370 return readMethod;
371 }
372
373 /***
374 * The cache of property descriptors.
375 */
376 private final Map propertyDescriptorsCache = new HashMap();
377
378 /***
379 * Retrives the property descriptor for the given type and name of
380 * the property.
381 *
382 * @param type the Class of which we'll attempt to retrieve the property
383 * @param name the name of the property.
384 * @return the found property descriptor
385 */
386 private PropertyDescriptor getPropertyDescriptor(
387 final Class type,
388 final String name)
389 {
390 PropertyDescriptor propertyDescriptor = null;
391 Map classPropertyDescriptors = (Map)this.propertyDescriptorsCache.get(type);
392 if (classPropertyDescriptors == null)
393 {
394 classPropertyDescriptors = new HashMap();
395 }
396 else
397 {
398 propertyDescriptor = (PropertyDescriptor)classPropertyDescriptors.get(name);
399 }
400
401 if (propertyDescriptor == null)
402 {
403 try
404 {
405 final PropertyDescriptor[] descriptors =
406 java.beans.Introspector.getBeanInfo(type).getPropertyDescriptors();
407 final int descriptorNumber = descriptors.length;
408 for (int ctr = 0; ctr < descriptorNumber; ctr++)
409 {
410 final PropertyDescriptor descriptor = descriptors[ctr];
411
412
413 final String compareName =
414 name.matches("//p{Lower}//p{Upper}.*") ? StringUtils.capitalize(name) : name;
415 if (descriptor.getName().equals(compareName))
416 {
417 propertyDescriptor = descriptor;
418 break;
419 }
420 }
421 if (propertyDescriptor == null && name.indexOf(NESTED_DELIMITER) != -1)
422 {
423 int dotIndex = name.indexOf(NESTED_DELIMITER);
424 if (dotIndex >= name.length())
425 {
426 throw new IntrospectorException("Invalid property call --> '" + name + "'");
427 }
428 final PropertyDescriptor nextInstance =
429 this.getPropertyDescriptor(
430 type,
431 name.substring(
432 0,
433 dotIndex));
434 propertyDescriptor =
435 this.getPropertyDescriptor(
436 nextInstance.getPropertyType(),
437 name.substring(dotIndex + 1));
438 }
439 }
440 catch (final java.beans.IntrospectionException exception)
441 {
442 throw new IntrospectorException(exception);
443 }
444 classPropertyDescriptors.put(
445 name,
446 propertyDescriptor);
447 this.propertyDescriptorsCache.put(
448 type,
449 classPropertyDescriptors);
450 }
451 return propertyDescriptor;
452 }
453
454 /***
455 * Prevents stack-over-flows by storing the objects that
456 * are currently being evaluted within {@link #internalGetProperty(Object, String)}.
457 */
458 private final Map evaluatingObjects = new HashMap();
459
460 /***
461 * Attempts to get the value of the property with <code>name</code> on the
462 * given <code>object</code> (throws an exception if the property
463 * is not readable on the object).
464 *
465 * @param object the object from which to retrieve the property.
466 * @param name the name of the property
467 * @return the resulting property value
468 */
469 private Object internalGetProperty(
470 final Object object,
471 final String name)
472 {
473 Object property = null;
474
475
476
477 final Object value = this.evaluatingObjects.get(object);
478 if (value == null || !value.equals(name))
479 {
480 this.evaluatingObjects.put(
481 object,
482 name);
483 if (object != null || name != null || name.length() > 0)
484 {
485 final Method method = this.getReadMethod(
486 object,
487 name);
488 if (method == null)
489 {
490 throw new IntrospectorException("No readable property named '" + name + "', exists on object '" +
491 object + "'");
492 }
493 try
494 {
495 property = method.invoke(
496 object,
497 (Object[])null);
498 }
499 catch (final Throwable throwable)
500 {
501 throw new IntrospectorException(throwable);
502 }
503 }
504 this.evaluatingObjects.remove(object);
505 }
506 return property;
507 }
508
509 /***
510 * Attempts to sets the value of the property with <code>name</code> on the
511 * given <code>object</code> (throws an exception if the property
512 * is not writable on the object).
513 *
514 * @param object the object from which to retrieve the property.
515 * @param name the name of the property to set.
516 * @param value the value of the property to set.
517 */
518 private void internalSetProperty(
519 final Object object,
520 final String name,
521 Object value)
522 {
523 if (object != null || name != null || name.length() > 0)
524 {
525 Class expectedType = null;
526 if (value != null)
527 {
528 final PropertyDescriptor descriptor = this.getPropertyDescriptor(
529 object.getClass(),
530 name);
531 if (descriptor != null)
532 {
533 expectedType = this.getPropertyDescriptor(
534 object.getClass(),
535 name).getPropertyType();
536 value = Converter.convert(
537 value,
538 expectedType);
539 }
540 }
541 final Method method = this.getWriteMethod(
542 object,
543 name);
544 if (method == null)
545 {
546 throw new IntrospectorException("No writeable property named '" + name + "', exists on object '" +
547 object + "'");
548 }
549 try
550 {
551 method.invoke(
552 object,
553 new Object[] {value});
554 }
555 catch (final Throwable throwable)
556 {
557 throw new IntrospectorException(throwable);
558 }
559 }
560 }
561
562 /***
563 * Shuts this instance down and reclaims
564 * any resouces used by this instance.
565 */
566 public void shutdown()
567 {
568 this.propertyDescriptorsCache.clear();
569 this.writeMethodsCache.clear();
570 this.readMethodsCache.clear();
571 this.evaluatingObjects.clear();
572 instance = null;
573 }
574 }