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