001package org.andromda.core.common;
002
003import java.beans.IntrospectionException;
004import java.beans.PropertyDescriptor;
005import java.lang.reflect.Method;
006import java.util.Collection;
007import java.util.HashMap;
008import java.util.Map;
009import java.util.regex.Pattern;
010import org.apache.commons.lang.StringUtils;
011import org.apache.commons.lang.exception.ExceptionUtils;
012import org.apache.log4j.Logger;
013
014/**
015 * A simple class providing the ability to manipulate properties on java bean objects.
016 *
017 * @author Chad Brandon
018 * @author Bob Fields
019 */
020public final class Introspector
021{
022    /**
023     * The shared instance.
024     */
025    private static Introspector instance = null;
026
027    /**
028     * Gets the shared instance.
029     *
030     * @return the shared introspector instance.
031     */
032    public static Introspector instance()
033    {
034        if (instance == null)
035        {
036            instance = new Introspector();
037        }
038        return instance;
039    }
040
041    /**
042     * The logger instance.
043     */
044    private static final Logger logger = Logger.getLogger(Introspector.class);
045
046    /**
047     * <p> Indicates whether or not the given <code>object</code> contains a
048     * valid property with the given <code>name</code> and <code>value</code>.
049     * </p>
050     * <p>
051     * A valid property means the following:
052     * <ul>
053     * <li>It exists on the object</li>
054     * <li>It is not null on the object</li>
055     * <li>If its a boolean value, then it evaluates to <code>true</code></li>
056     * <li>If value is not null, then the property matches the given </code>.value</code></li>
057     * </ul>
058     * All other possibilities return <code>false</code>
059     * </p>
060     *
061     * @param object the object to test for the valid property.
062     * @param name the name of the property for which to test.
063     * @param value the value to evaluate against.
064     * @return true/false
065     */
066    public boolean containsValidProperty(
067        final Object object,
068        final String name,
069        final String value)
070    {
071        boolean valid;
072
073        try
074        {
075            final Object propertyValue = this.getProperty(
076                    object,
077                    name);
078            valid = propertyValue != null;
079
080            // if valid is still true, and the propertyValue
081            // is not null
082            if (valid)
083            {
084                // if it's a collection then we check to see if the
085                // collection is not empty
086                if (propertyValue instanceof Collection)
087                {
088                    valid = !((Collection)propertyValue).isEmpty();
089                }
090                else
091                {
092                    final String valueAsString = String.valueOf(propertyValue);
093                    if (StringUtils.isNotBlank(value))
094                    {
095                        valid = valueAsString.equals(value);
096                    }
097                    else if (propertyValue instanceof Boolean)
098                    {
099                        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}