View Javadoc

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              // if valid is still true, and the propertyValue
74              // is not null
75              if (valid)
76              {
77                  // if it's a collection then we check to see if the
78                  // collection is not empty
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             // Dont catch our own exceptions.
189             // Otherwise get Exception/Cause chain which
190             // can hide the original exception.
191             throw throwable;
192         }
193         catch (Throwable throwable)
194         {
195             throwable = ExceptionUtils.getRootCause(throwable);
196 
197             // If cause is an IntrospectorException re-throw that exception
198             // rather than creating a new one.
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                     // - handle names that start with a lowercased letter and have an uppercase as the second letter
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         // - prevent stack-over-flows by checking to make sure
476         //   we aren't entering any circular evalutions
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 }