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}