001: package org.andromda.core.common;
002:
003: import java.beans.PropertyDescriptor;
004: import java.lang.reflect.Method;
005: import java.util.Collection;
006: import java.util.HashMap;
007: import java.util.Map;
008:
009: import org.apache.commons.lang.StringUtils;
010: import org.apache.commons.lang.exception.ExceptionUtils;
011:
012: /**
013: * A simple class providing the ability to manipulate properties on java bean objects.
014: *
015: * @author Chad Brandon
016: */
017: public final class Introspector {
018: /**
019: * The shared instance.
020: */
021: private static Introspector instance = null;
022:
023: /**
024: * Gets the shared instance.
025: *
026: * @return the shared introspector instance.
027: */
028: public static Introspector instance() {
029: if (instance == null) {
030: instance = new Introspector();
031: }
032: return instance;
033: }
034:
035: /**
036: * <p> Indicates whether or not the given <code>object</code> contains a
037: * valid property with the given <code>name</code> and <code>value</code>.
038: * </p>
039: * <p>
040: * A valid property means the following:
041: * <ul>
042: * <li>It exists on the object</li>
043: * <li>It is not null on the object</li>
044: * <li>If its a boolean value, then it evaluates to <code>true</code></li>
045: * <li>If value is not null, then the property matches the given </code>.value</code></li>
046: * </ul>
047: * All other possibilities return <code>false</code>
048: * </p>
049: *
050: * @param object the object to test for the valid property.
051: * @param name the name of the propery for which to test.
052: * @param value the value to evaluate against.
053: * @return true/false
054: */
055: public boolean containsValidProperty(final Object object,
056: final String name, final String value) {
057: boolean valid;
058:
059: try {
060: final Object propertyValue = this .getProperty(object, name);
061: valid = propertyValue != null;
062:
063: // if valid is still true, and the propertyValue
064: // is not null
065: if (valid) {
066: // if it's a collection then we check to see if the
067: // collection is not empty
068: if (propertyValue instanceof Collection) {
069: valid = !((Collection) propertyValue).isEmpty();
070: } else {
071: final String valueAsString = String
072: .valueOf(propertyValue);
073: if (StringUtils.isNotEmpty(value)) {
074: valid = valueAsString.equals(value);
075: } else if (propertyValue instanceof Boolean) {
076: valid = Boolean.valueOf(valueAsString)
077: .booleanValue();
078: }
079: }
080: }
081: } catch (final Throwable throwable) {
082: valid = false;
083: }
084: return valid;
085: }
086:
087: /**
088: * Sets the property having the given <code>name</code> on the <code>object</code>
089: * with the given <code>value</code>.
090: *
091: * @param object the object on which to set the property.
092: * @param name the name of the property to populate.
093: * @param value the value to give the property.
094: */
095: public void setProperty(final Object object, final String name,
096: final Object value) {
097: this .setNestedProperty(object, name, value);
098: }
099:
100: /**
101: * The delimiter used for seperating nested properties.
102: */
103: private static final char NESTED_DELIMITER = '.';
104:
105: /**
106: * Attempts to set the nested property with the given
107: * name of the given object.
108: * @param object the object on which to populate the property.
109: * @param name the name of the object.
110: * @param value the value to populate.
111: */
112: private void setNestedProperty(final Object object, String name,
113: final Object value) {
114: if (object != null && name != null && name.length() > 0) {
115: final int dotIndex = name.indexOf(NESTED_DELIMITER);
116: if (dotIndex >= name.length()) {
117: throw new IntrospectorException(
118: "Invalid property call --> '" + name + "'");
119: }
120: String[] names = name.split("\\" + NESTED_DELIMITER);
121: Object objectToPopulate = object;
122: for (int ctr = 0; ctr < names.length; ctr++) {
123: name = names[ctr];
124: if (ctr == names.length - 1) {
125: break;
126: }
127: objectToPopulate = this .internalGetProperty(
128: objectToPopulate, name);
129: }
130: this .internalSetProperty(objectToPopulate, name, value);
131: }
132: }
133:
134: /**
135: * Attempts to retrieve the property with the given <code>name</code> on the <code>object</code>.
136: *
137: * @param object the object to which the property belongs.
138: * @param name the name of the property
139: * @return the value of the property.
140: */
141: public final Object getProperty(final Object object,
142: final String name) {
143: Object result;
144:
145: try {
146: result = this .getNestedProperty(object, name);
147: } catch (final IntrospectorException throwable) {
148: // Dont catch our own exceptions.
149: // Otherwise get Exception/Cause chain which
150: // can hide the original exception.
151: throw throwable;
152: } catch (Throwable throwable) {
153: throwable = ExceptionUtils.getRootCause(throwable);
154:
155: // If cause is an IntrospectorException re-throw that exception
156: // rather than creating a new one.
157: if (throwable instanceof IntrospectorException) {
158: throw (IntrospectorException) throwable;
159: }
160: throw new IntrospectorException(throwable);
161: }
162: return result;
163: }
164:
165: /**
166: * Gets a nested property, that is it gets the properties
167: * seperated by '.'.
168: *
169: * @param object the object from which to retrieve the nested property.
170: * @param name the name of the property
171: * @return the property value or null if one couldn't be retrieved.
172: */
173: private Object getNestedProperty(final Object object,
174: final String name) {
175: Object property = null;
176: if (object != null && name != null && name.length() > 0) {
177: int dotIndex = name.indexOf(NESTED_DELIMITER);
178: if (dotIndex == -1) {
179: property = this .internalGetProperty(object, name);
180: } else {
181: if (dotIndex >= name.length()) {
182: throw new IntrospectorException(
183: "Invalid property call --> '" + name + "'");
184: }
185: final Object nextInstance = this .internalGetProperty(
186: object, name.substring(0, dotIndex));
187: property = getNestedProperty(nextInstance, name
188: .substring(dotIndex + 1));
189: }
190: }
191: return property;
192: }
193:
194: /**
195: * Cache for a class's write methods.
196: */
197: private final Map writeMethodsCache = new HashMap();
198:
199: /**
200: * Gets the writable method for the property.
201: *
202: * @param object the object from which to retrieve the property method.
203: * @param name the name of the property.
204: * @return the property method or null if one wasn't found.
205: */
206: private Method getWriteMethod(final Object object, final String name) {
207: Method writeMethod = null;
208: final Class objectClass = object.getClass();
209: Map classWriteMethods = (Map) this .writeMethodsCache
210: .get(objectClass);
211: if (classWriteMethods == null) {
212: classWriteMethods = new HashMap();
213: } else {
214: writeMethod = (Method) classWriteMethods.get(name);
215: }
216: if (writeMethod == null) {
217: final PropertyDescriptor descriptor = this
218: .getPropertyDescriptor(object.getClass(), name);
219: writeMethod = descriptor != null ? descriptor
220: .getWriteMethod() : null;
221: if (writeMethod != null) {
222: classWriteMethods.put(name, writeMethod);
223: this .writeMethodsCache.put(objectClass,
224: classWriteMethods);
225: }
226: }
227: return writeMethod;
228: }
229:
230: /**
231: * Indicates if the <code>object</code> has a property that
232: * is <em>readable</em> with the given <code>name</code>.
233: *
234: * @param object the object to check.
235: * @param name the property to check for.
236: */
237: public boolean isReadable(final Object object, final String name) {
238: return this .getReadMethod(object, name) != null;
239: }
240:
241: /**
242: * Indicates if the <code>object</code> has a property that
243: * is <em>writable</em> with the given <code>name</code>.
244: *
245: * @param object the object to check.
246: * @param name the property to check for.
247: */
248: public boolean isWritable(final Object object, final String name) {
249: return this .getWriteMethod(object, name) != null;
250: }
251:
252: /**
253: * Cache for a class's read methods.
254: */
255: private final Map readMethodsCache = new HashMap();
256:
257: /**
258: * Gets the readable method for the property.
259: *
260: * @param object the object from which to retrieve the property method.
261: * @param name the name of the property.
262: * @return the property method or null if one wasn't found.
263: */
264: private Method getReadMethod(final Object object, final String name) {
265: Method readMethod = null;
266: final Class objectClass = object.getClass();
267: Map classReadMethods = (Map) this .readMethodsCache
268: .get(objectClass);
269: if (classReadMethods == null) {
270: classReadMethods = new HashMap();
271: } else {
272: readMethod = (Method) classReadMethods.get(name);
273: }
274: if (readMethod == null) {
275: final PropertyDescriptor descriptor = this
276: .getPropertyDescriptor(object.getClass(), name);
277: readMethod = descriptor != null ? descriptor
278: .getReadMethod() : null;
279: if (readMethod != null) {
280: classReadMethods.put(name, readMethod);
281: this .readMethodsCache
282: .put(objectClass, classReadMethods);
283: }
284: }
285: return readMethod;
286: }
287:
288: /**
289: * The cache of property descriptors.
290: */
291: private final Map propertyDescriptorsCache = new HashMap();
292:
293: /**
294: * Retrives the property descriptor for the given type and name of
295: * the property.
296: *
297: * @param type the Class of which we'll attempt to retrieve the property
298: * @param name the name of the property.
299: * @return the found property descriptor
300: */
301: private PropertyDescriptor getPropertyDescriptor(final Class type,
302: final String name) {
303: PropertyDescriptor propertyDescriptor = null;
304: Map classPropertyDescriptors = (Map) this .propertyDescriptorsCache
305: .get(type);
306: if (classPropertyDescriptors == null) {
307: classPropertyDescriptors = new HashMap();
308: } else {
309: propertyDescriptor = (PropertyDescriptor) classPropertyDescriptors
310: .get(name);
311: }
312:
313: if (propertyDescriptor == null) {
314: try {
315: final PropertyDescriptor[] descriptors = java.beans.Introspector
316: .getBeanInfo(type).getPropertyDescriptors();
317: final int descriptorNumber = descriptors.length;
318: for (int ctr = 0; ctr < descriptorNumber; ctr++) {
319: final PropertyDescriptor descriptor = descriptors[ctr];
320:
321: // - handle names that start with a lowercased letter and have an uppercase as the second letter
322: final String compareName = name
323: .matches("\\p{Lower}\\p{Upper}.*") ? StringUtils
324: .capitalize(name)
325: : name;
326: if (descriptor.getName().equals(compareName)) {
327: propertyDescriptor = descriptor;
328: break;
329: }
330: }
331: if (propertyDescriptor == null
332: && name.indexOf(NESTED_DELIMITER) != -1) {
333: int dotIndex = name.indexOf(NESTED_DELIMITER);
334: if (dotIndex >= name.length()) {
335: throw new IntrospectorException(
336: "Invalid property call --> '" + name
337: + "'");
338: }
339: final PropertyDescriptor nextInstance = this
340: .getPropertyDescriptor(type, name
341: .substring(0, dotIndex));
342: propertyDescriptor = this .getPropertyDescriptor(
343: nextInstance.getPropertyType(), name
344: .substring(dotIndex + 1));
345: }
346: } catch (final java.beans.IntrospectionException exception) {
347: throw new IntrospectorException(exception);
348: }
349: classPropertyDescriptors.put(name, propertyDescriptor);
350: this .propertyDescriptorsCache.put(type,
351: classPropertyDescriptors);
352: }
353: return propertyDescriptor;
354: }
355:
356: /**
357: * Prevents stack-over-flows by storing the objects that
358: * are currently being evaluted within {@link #internalGetProperty(Object, String)}.
359: */
360: private final Map evaluatingObjects = new HashMap();
361:
362: /**
363: * Attempts to get the value of the property with <code>name</code> on the
364: * given <code>object</code> (throws an exception if the property
365: * is not readable on the object).
366: *
367: * @param object the object from which to retrieve the property.
368: * @param name the name of the property
369: * @return the resulting property value
370: */
371: private Object internalGetProperty(final Object object,
372: final String name) {
373: Object property = null;
374:
375: // - prevent stack-over-flows by checking to make sure
376: // we aren't entering any circular evalutions
377: final Object value = this .evaluatingObjects.get(object);
378: if (value == null || !value.equals(name)) {
379: this .evaluatingObjects.put(object, name);
380: if (object != null || name != null || name.length() > 0) {
381: final Method method = this .getReadMethod(object, name);
382: if (method == null) {
383: throw new IntrospectorException(
384: "No readable property named '" + name
385: + "', exists on object '" + object
386: + "'");
387: }
388: try {
389: property = method.invoke(object, (Object[]) null);
390: } catch (final Throwable throwable) {
391: throw new IntrospectorException(throwable);
392: }
393: }
394: this .evaluatingObjects.remove(object);
395: }
396: return property;
397: }
398:
399: /**
400: * Attempts to sets the value of the property with <code>name</code> on the
401: * given <code>object</code> (throws an exception if the property
402: * is not writable on the object).
403: *
404: * @param object the object from which to retrieve the property.
405: * @param name the name of the property to set.
406: * @param value the value of the property to set.
407: */
408: private void internalSetProperty(final Object object,
409: final String name, Object value) {
410: if (object != null || name != null || name.length() > 0) {
411: Class expectedType = null;
412: if (value != null) {
413: final PropertyDescriptor descriptor = this
414: .getPropertyDescriptor(object.getClass(), name);
415: if (descriptor != null) {
416: expectedType = this .getPropertyDescriptor(
417: object.getClass(), name).getPropertyType();
418: value = Converter.convert(value, expectedType);
419: }
420: }
421: final Method method = this .getWriteMethod(object, name);
422: if (method == null) {
423: throw new IntrospectorException(
424: "No writeable property named '" + name
425: + "', exists on object '" + object
426: + "'");
427: }
428: try {
429: method.invoke(object, new Object[] { value });
430: } catch (final Throwable throwable) {
431: throw new IntrospectorException(throwable);
432: }
433: }
434: }
435:
436: /**
437: * Shuts this instance down and reclaims
438: * any resouces used by this instance.
439: */
440: public void shutdown() {
441: this.propertyDescriptorsCache.clear();
442: this.writeMethodsCache.clear();
443: this.readMethodsCache.clear();
444: this.evaluatingObjects.clear();
445: instance = null;
446: }
447: }
|