001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 019package org.apache.commons.beanutils; 020 021 022import java.beans.IndexedPropertyDescriptor; 023import java.beans.PropertyDescriptor; 024import java.lang.reflect.Array; 025import java.lang.reflect.InvocationTargetException; 026import java.lang.reflect.Method; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032 033import org.apache.commons.beanutils.expression.Resolver; 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036 037 038/** 039 * <p>JavaBean property population methods.</p> 040 * 041 * <p>This class provides implementations for the utility methods in 042 * {@link BeanUtils}. 043 * Different instances can be used to isolate caches between classloaders 044 * and to vary the value converters registered.</p> 045 * 046 * @version $Id$ 047 * @see BeanUtils 048 * @since 1.7 049 */ 050 051public class BeanUtilsBean { 052 053 054 // ------------------------------------------------------ Private Class Variables 055 056 /** 057 * Contains <code>BeanUtilsBean</code> instances indexed by context classloader. 058 */ 059 private static final ContextClassLoaderLocal<BeanUtilsBean> 060 BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() { 061 // Creates the default instance used when the context classloader is unavailable 062 @Override 063 protected BeanUtilsBean initialValue() { 064 return new BeanUtilsBean(); 065 } 066 }; 067 068 /** 069 * Gets the instance which provides the functionality for {@link BeanUtils}. 070 * This is a pseudo-singleton - an single instance is provided per (thread) context classloader. 071 * This mechanism provides isolation for web apps deployed in the same container. 072 * 073 * @return The (pseudo-singleton) BeanUtils bean instance 074 */ 075 public static BeanUtilsBean getInstance() { 076 return BEANS_BY_CLASSLOADER.get(); 077 } 078 079 /** 080 * Sets the instance which provides the functionality for {@link BeanUtils}. 081 * This is a pseudo-singleton - an single instance is provided per (thread) context classloader. 082 * This mechanism provides isolation for web apps deployed in the same container. 083 * 084 * @param newInstance The (pseudo-singleton) BeanUtils bean instance 085 */ 086 public static void setInstance(final BeanUtilsBean newInstance) { 087 BEANS_BY_CLASSLOADER.set(newInstance); 088 } 089 090 // --------------------------------------------------------- Attributes 091 092 /** 093 * Logging for this instance 094 */ 095 private final Log log = LogFactory.getLog(BeanUtils.class); 096 097 /** Used to perform conversions between object types when setting properties */ 098 private final ConvertUtilsBean convertUtilsBean; 099 100 /** Used to access properties*/ 101 private final PropertyUtilsBean propertyUtilsBean; 102 103 /** A reference to Throwable's initCause method, or null if it's not there in this JVM */ 104 private static final Method INIT_CAUSE_METHOD = getInitCauseMethod(); 105 106 // --------------------------------------------------------- Constuctors 107 108 /** 109 * <p>Constructs an instance using new property 110 * and conversion instances.</p> 111 */ 112 public BeanUtilsBean() { 113 this(new ConvertUtilsBean(), new PropertyUtilsBean()); 114 } 115 116 /** 117 * <p>Constructs an instance using given conversion instances 118 * and new {@link PropertyUtilsBean} instance.</p> 119 * 120 * @param convertUtilsBean use this <code>ConvertUtilsBean</code> 121 * to perform conversions from one object to another 122 * 123 * @since 1.8.0 124 */ 125 public BeanUtilsBean(final ConvertUtilsBean convertUtilsBean) { 126 this(convertUtilsBean, new PropertyUtilsBean()); 127 } 128 129 /** 130 * <p>Constructs an instance using given property and conversion instances.</p> 131 * 132 * @param convertUtilsBean use this <code>ConvertUtilsBean</code> 133 * to perform conversions from one object to another 134 * @param propertyUtilsBean use this <code>PropertyUtilsBean</code> 135 * to access properties 136 */ 137 public BeanUtilsBean( 138 final ConvertUtilsBean convertUtilsBean, 139 final PropertyUtilsBean propertyUtilsBean) { 140 141 this.convertUtilsBean = convertUtilsBean; 142 this.propertyUtilsBean = propertyUtilsBean; 143 } 144 145 // --------------------------------------------------------- Public Methods 146 147 /** 148 * <p>Clone a bean based on the available property getters and setters, 149 * even if the bean class itself does not implement Cloneable.</p> 150 * 151 * <p> 152 * <strong>Note:</strong> this method creates a <strong>shallow</strong> clone. 153 * In other words, any objects referred to by the bean are shared with the clone 154 * rather than being cloned in turn. 155 * </p> 156 * 157 * @param bean Bean to be cloned 158 * @return the cloned bean 159 * 160 * @throws IllegalAccessException if the caller does not have 161 * access to the property accessor method 162 * @throws InstantiationException if a new instance of the bean's 163 * class cannot be instantiated 164 * @throws InvocationTargetException if the property accessor method 165 * throws an exception 166 * @throws NoSuchMethodException if an accessor method for this 167 * property cannot be found 168 */ 169 public Object cloneBean(final Object bean) 170 throws IllegalAccessException, InstantiationException, 171 InvocationTargetException, NoSuchMethodException { 172 173 if (log.isDebugEnabled()) { 174 log.debug("Cloning bean: " + bean.getClass().getName()); 175 } 176 Object newBean = null; 177 if (bean instanceof DynaBean) { 178 newBean = ((DynaBean) bean).getDynaClass().newInstance(); 179 } else { 180 newBean = bean.getClass().newInstance(); 181 } 182 getPropertyUtils().copyProperties(newBean, bean); 183 return (newBean); 184 185 } 186 187 188 /** 189 * <p>Copy property values from the origin bean to the destination bean 190 * for all cases where the property names are the same. For each 191 * property, a conversion is attempted as necessary. All combinations of 192 * standard JavaBeans and DynaBeans as origin and destination are 193 * supported. Properties that exist in the origin bean, but do not exist 194 * in the destination bean (or are read-only in the destination bean) are 195 * silently ignored.</p> 196 * 197 * <p>If the origin "bean" is actually a <code>Map</code>, it is assumed 198 * to contain String-valued <strong>simple</strong> property names as the keys, pointing at 199 * the corresponding property values that will be converted (if necessary) 200 * and set in the destination bean. <strong>Note</strong> that this method 201 * is intended to perform a "shallow copy" of the properties and so complex 202 * properties (for example, nested ones) will not be copied.</p> 203 * 204 * <p>This method differs from <code>populate()</code>, which 205 * was primarily designed for populating JavaBeans from the map of request 206 * parameters retrieved on an HTTP request, is that no scalar->indexed 207 * or indexed->scalar manipulations are performed. If the origin property 208 * is indexed, the destination property must be also.</p> 209 * 210 * <p>If you know that no type conversions are required, the 211 * <code>copyProperties()</code> method in {@link PropertyUtils} will 212 * execute faster than this method.</p> 213 * 214 * <p><strong>FIXME</strong> - Indexed and mapped properties that do not 215 * have getter and setter methods for the underlying array or Map are not 216 * copied by this method.</p> 217 * 218 * @param dest Destination bean whose properties are modified 219 * @param orig Origin bean whose properties are retrieved 220 * 221 * @throws IllegalAccessException if the caller does not have 222 * access to the property accessor method 223 * @throws IllegalArgumentException if the <code>dest</code> or 224 * <code>orig</code> argument is null or if the <code>dest</code> 225 * property type is different from the source type and the relevant 226 * converter has not been registered. 227 * @throws InvocationTargetException if the property accessor method 228 * throws an exception 229 */ 230 public void copyProperties(final Object dest, final Object orig) 231 throws IllegalAccessException, InvocationTargetException { 232 233 // Validate existence of the specified beans 234 if (dest == null) { 235 throw new IllegalArgumentException 236 ("No destination bean specified"); 237 } 238 if (orig == null) { 239 throw new IllegalArgumentException("No origin bean specified"); 240 } 241 if (log.isDebugEnabled()) { 242 log.debug("BeanUtils.copyProperties(" + dest + ", " + 243 orig + ")"); 244 } 245 246 // Copy the properties, converting as necessary 247 if (orig instanceof DynaBean) { 248 final DynaProperty[] origDescriptors = 249 ((DynaBean) orig).getDynaClass().getDynaProperties(); 250 for (DynaProperty origDescriptor : origDescriptors) { 251 final String name = origDescriptor.getName(); 252 // Need to check isReadable() for WrapDynaBean 253 // (see Jira issue# BEANUTILS-61) 254 if (getPropertyUtils().isReadable(orig, name) && 255 getPropertyUtils().isWriteable(dest, name)) { 256 final Object value = ((DynaBean) orig).get(name); 257 copyProperty(dest, name, value); 258 } 259 } 260 } else if (orig instanceof Map) { 261 @SuppressWarnings("unchecked") 262 final 263 // Map properties are always of type <String, Object> 264 Map<String, Object> propMap = (Map<String, Object>) orig; 265 for (final Map.Entry<String, Object> entry : propMap.entrySet()) { 266 final String name = entry.getKey(); 267 if (getPropertyUtils().isWriteable(dest, name)) { 268 copyProperty(dest, name, entry.getValue()); 269 } 270 } 271 } else /* if (orig is a standard JavaBean) */ { 272 final PropertyDescriptor[] origDescriptors = 273 getPropertyUtils().getPropertyDescriptors(orig); 274 for (PropertyDescriptor origDescriptor : origDescriptors) { 275 final String name = origDescriptor.getName(); 276 if ("class".equals(name)) { 277 continue; // No point in trying to set an object's class 278 } 279 if (getPropertyUtils().isReadable(orig, name) && 280 getPropertyUtils().isWriteable(dest, name)) { 281 try { 282 final Object value = 283 getPropertyUtils().getSimpleProperty(orig, name); 284 copyProperty(dest, name, value); 285 } catch (final NoSuchMethodException e) { 286 // Should not happen 287 } 288 } 289 } 290 } 291 292 } 293 294 295 /** 296 * <p>Copy the specified property value to the specified destination bean, 297 * performing any type conversion that is required. If the specified 298 * bean does not have a property of the specified name, or the property 299 * is read only on the destination bean, return without 300 * doing anything. If you have custom destination property types, register 301 * {@link Converter}s for them by calling the <code>register()</code> 302 * method of {@link ConvertUtils}.</p> 303 * 304 * <p><strong>IMPLEMENTATION RESTRICTIONS</strong>:</p> 305 * <ul> 306 * <li>Does not support destination properties that are indexed, 307 * but only an indexed setter (as opposed to an array setter) 308 * is available.</li> 309 * <li>Does not support destination properties that are mapped, 310 * but only a keyed setter (as opposed to a Map setter) 311 * is available.</li> 312 * <li>The desired property type of a mapped setter cannot be 313 * determined (since Maps support any data type), so no conversion 314 * will be performed.</li> 315 * </ul> 316 * 317 * @param bean Bean on which setting is to be performed 318 * @param name Property name (can be nested/indexed/mapped/combo) 319 * @param value Value to be set 320 * 321 * @throws IllegalAccessException if the caller does not have 322 * access to the property accessor method 323 * @throws InvocationTargetException if the property accessor method 324 * throws an exception 325 */ 326 public void copyProperty(final Object bean, String name, Object value) 327 throws IllegalAccessException, InvocationTargetException { 328 329 // Trace logging (if enabled) 330 if (log.isTraceEnabled()) { 331 final StringBuilder sb = new StringBuilder(" copyProperty("); 332 sb.append(bean); 333 sb.append(", "); 334 sb.append(name); 335 sb.append(", "); 336 if (value == null) { 337 sb.append("<NULL>"); 338 } else if (value instanceof String) { 339 sb.append((String) value); 340 } else if (value instanceof String[]) { 341 final String[] values = (String[]) value; 342 sb.append('['); 343 for (int i = 0; i < values.length; i++) { 344 if (i > 0) { 345 sb.append(','); 346 } 347 sb.append(values[i]); 348 } 349 sb.append(']'); 350 } else { 351 sb.append(value.toString()); 352 } 353 sb.append(')'); 354 log.trace(sb.toString()); 355 } 356 357 // Resolve any nested expression to get the actual target bean 358 Object target = bean; 359 final Resolver resolver = getPropertyUtils().getResolver(); 360 while (resolver.hasNested(name)) { 361 try { 362 target = getPropertyUtils().getProperty(target, resolver.next(name)); 363 name = resolver.remove(name); 364 } catch (final NoSuchMethodException e) { 365 return; // Skip this property setter 366 } 367 } 368 if (log.isTraceEnabled()) { 369 log.trace(" Target bean = " + target); 370 log.trace(" Target name = " + name); 371 } 372 373 // Declare local variables we will require 374 final String propName = resolver.getProperty(name); // Simple name of target property 375 Class<?> type = null; // Java type of target property 376 final int index = resolver.getIndex(name); // Indexed subscript value (if any) 377 final String key = resolver.getKey(name); // Mapped key value (if any) 378 379 // Calculate the target property type 380 if (target instanceof DynaBean) { 381 final DynaClass dynaClass = ((DynaBean) target).getDynaClass(); 382 final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName); 383 if (dynaProperty == null) { 384 return; // Skip this property setter 385 } 386 type = dynaPropertyType(dynaProperty, value); 387 } else { 388 PropertyDescriptor descriptor = null; 389 try { 390 descriptor = 391 getPropertyUtils().getPropertyDescriptor(target, name); 392 if (descriptor == null) { 393 return; // Skip this property setter 394 } 395 } catch (final NoSuchMethodException e) { 396 return; // Skip this property setter 397 } 398 type = descriptor.getPropertyType(); 399 if (type == null) { 400 // Most likely an indexed setter on a POJB only 401 if (log.isTraceEnabled()) { 402 log.trace(" target type for property '" + 403 propName + "' is null, so skipping ths setter"); 404 } 405 return; 406 } 407 } 408 if (log.isTraceEnabled()) { 409 log.trace(" target propName=" + propName + ", type=" + 410 type + ", index=" + index + ", key=" + key); 411 } 412 413 // Convert the specified value to the required type and store it 414 if (index >= 0) { // Destination must be indexed 415 value = convertForCopy(value, type.getComponentType()); 416 try { 417 getPropertyUtils().setIndexedProperty(target, propName, 418 index, value); 419 } catch (final NoSuchMethodException e) { 420 throw new InvocationTargetException 421 (e, "Cannot set " + propName); 422 } 423 } else if (key != null) { // Destination must be mapped 424 // Maps do not know what the preferred data type is, 425 // so perform no conversions at all 426 // FIXME - should we create or support a TypedMap? 427 try { 428 getPropertyUtils().setMappedProperty(target, propName, 429 key, value); 430 } catch (final NoSuchMethodException e) { 431 throw new InvocationTargetException 432 (e, "Cannot set " + propName); 433 } 434 } else { // Destination must be simple 435 value = convertForCopy(value, type); 436 try { 437 getPropertyUtils().setSimpleProperty(target, propName, value); 438 } catch (final NoSuchMethodException e) { 439 throw new InvocationTargetException 440 (e, "Cannot set " + propName); 441 } 442 } 443 444 } 445 446 447 /** 448 * <p>Return the entire set of properties for which the specified bean 449 * provides a read method. This map contains the to <code>String</code> 450 * converted property values for all properties for which a read method 451 * is provided (i.e. where the getReadMethod() returns non-null).</p> 452 * 453 * <p>This map can be fed back to a call to 454 * <code>BeanUtils.populate()</code> to reconsitute the same set of 455 * properties, modulo differences for read-only and write-only 456 * properties, but only if there are no indexed properties.</p> 457 * 458 * <p><strong>Warning:</strong> if any of the bean property implementations 459 * contain (directly or indirectly) a call to this method then 460 * a stack overflow may result. For example:</p> 461 * <pre><code> 462 * class MyBean 463 * { 464 * public Map getParameterMap() 465 * { 466 * BeanUtils.describe(this); 467 * } 468 * } 469 * </code></pre> 470 * <p> 471 * will result in an infinite regression when <code>getParametersMap</code> 472 * is called. It is recommended that such methods are given alternative 473 * names (for example, <code>parametersMap</code>). 474 * </p> 475 * @param bean Bean whose properties are to be extracted 476 * @return Map of property descriptors 477 * 478 * @throws IllegalAccessException if the caller does not have 479 * access to the property accessor method 480 * @throws InvocationTargetException if the property accessor method 481 * throws an exception 482 * @throws NoSuchMethodException if an accessor method for this 483 * property cannot be found 484 */ 485 public Map<String, String> describe(final Object bean) 486 throws IllegalAccessException, InvocationTargetException, 487 NoSuchMethodException { 488 489 if (bean == null) { 490 // return (Collections.EMPTY_MAP); 491 return (new java.util.HashMap<String, String>()); 492 } 493 494 if (log.isDebugEnabled()) { 495 log.debug("Describing bean: " + bean.getClass().getName()); 496 } 497 498 final Map<String, String> description = new HashMap<String, String>(); 499 if (bean instanceof DynaBean) { 500 final DynaProperty[] descriptors = 501 ((DynaBean) bean).getDynaClass().getDynaProperties(); 502 for (DynaProperty descriptor : descriptors) { 503 final String name = descriptor.getName(); 504 description.put(name, getProperty(bean, name)); 505 } 506 } else { 507 final PropertyDescriptor[] descriptors = 508 getPropertyUtils().getPropertyDescriptors(bean); 509 final Class<?> clazz = bean.getClass(); 510 for (PropertyDescriptor descriptor : descriptors) { 511 final String name = descriptor.getName(); 512 if (getPropertyUtils().getReadMethod(clazz, descriptor) != null) { 513 description.put(name, getProperty(bean, name)); 514 } 515 } 516 } 517 return (description); 518 519 } 520 521 522 /** 523 * Return the value of the specified array property of the specified 524 * bean, as a String array. 525 * 526 * @param bean Bean whose property is to be extracted 527 * @param name Name of the property to be extracted 528 * @return The array property value 529 * 530 * @throws IllegalAccessException if the caller does not have 531 * access to the property accessor method 532 * @throws InvocationTargetException if the property accessor method 533 * throws an exception 534 * @throws NoSuchMethodException if an accessor method for this 535 * property cannot be found 536 */ 537 public String[] getArrayProperty(final Object bean, final String name) 538 throws IllegalAccessException, InvocationTargetException, 539 NoSuchMethodException { 540 541 final Object value = getPropertyUtils().getProperty(bean, name); 542 if (value == null) { 543 return (null); 544 } else if (value instanceof Collection) { 545 final ArrayList<String> values = new ArrayList<String>(); 546 for (final Object item : (Collection<?>) value) { 547 if (item == null) { 548 values.add(null); 549 } else { 550 // convert to string using convert utils 551 values.add(getConvertUtils().convert(item)); 552 } 553 } 554 return (values.toArray(new String[values.size()])); 555 } else if (value.getClass().isArray()) { 556 final int n = Array.getLength(value); 557 final String[] results = new String[n]; 558 for (int i = 0; i < n; i++) { 559 final Object item = Array.get(value, i); 560 if (item == null) { 561 results[i] = null; 562 } else { 563 // convert to string using convert utils 564 results[i] = getConvertUtils().convert(item); 565 } 566 } 567 return (results); 568 } else { 569 final String[] results = new String[1]; 570 results[0] = getConvertUtils().convert(value); 571 return (results); 572 } 573 574 } 575 576 577 /** 578 * Return the value of the specified indexed property of the specified 579 * bean, as a String. The zero-relative index of the 580 * required value must be included (in square brackets) as a suffix to 581 * the property name, or <code>IllegalArgumentException</code> will be 582 * thrown. 583 * 584 * @param bean Bean whose property is to be extracted 585 * @param name <code>propertyname[index]</code> of the property value 586 * to be extracted 587 * @return The indexed property's value, converted to a String 588 * 589 * @throws IllegalAccessException if the caller does not have 590 * access to the property accessor method 591 * @throws InvocationTargetException if the property accessor method 592 * throws an exception 593 * @throws NoSuchMethodException if an accessor method for this 594 * property cannot be found 595 */ 596 public String getIndexedProperty(final Object bean, final String name) 597 throws IllegalAccessException, InvocationTargetException, 598 NoSuchMethodException { 599 600 final Object value = getPropertyUtils().getIndexedProperty(bean, name); 601 return (getConvertUtils().convert(value)); 602 603 } 604 605 606 /** 607 * Return the value of the specified indexed property of the specified 608 * bean, as a String. The index is specified as a method parameter and 609 * must *not* be included in the property name expression 610 * 611 * @param bean Bean whose property is to be extracted 612 * @param name Simple property name of the property value to be extracted 613 * @param index Index of the property value to be extracted 614 * @return The indexed property's value, converted to a String 615 * 616 * @throws IllegalAccessException if the caller does not have 617 * access to the property accessor method 618 * @throws InvocationTargetException if the property accessor method 619 * throws an exception 620 * @throws NoSuchMethodException if an accessor method for this 621 * property cannot be found 622 */ 623 public String getIndexedProperty(final Object bean, 624 final String name, final int index) 625 throws IllegalAccessException, InvocationTargetException, 626 NoSuchMethodException { 627 628 final Object value = getPropertyUtils().getIndexedProperty(bean, name, index); 629 return (getConvertUtils().convert(value)); 630 631 } 632 633 634 /** 635 * Return the value of the specified indexed property of the specified 636 * bean, as a String. The String-valued key of the required value 637 * must be included (in parentheses) as a suffix to 638 * the property name, or <code>IllegalArgumentException</code> will be 639 * thrown. 640 * 641 * @param bean Bean whose property is to be extracted 642 * @param name <code>propertyname(index)</code> of the property value 643 * to be extracted 644 * @return The mapped property's value, converted to a String 645 * 646 * @throws IllegalAccessException if the caller does not have 647 * access to the property accessor method 648 * @throws InvocationTargetException if the property accessor method 649 * throws an exception 650 * @throws NoSuchMethodException if an accessor method for this 651 * property cannot be found 652 */ 653 public String getMappedProperty(final Object bean, final String name) 654 throws IllegalAccessException, InvocationTargetException, 655 NoSuchMethodException { 656 657 final Object value = getPropertyUtils().getMappedProperty(bean, name); 658 return (getConvertUtils().convert(value)); 659 660 } 661 662 663 /** 664 * Return the value of the specified mapped property of the specified 665 * bean, as a String. The key is specified as a method parameter and 666 * must *not* be included in the property name expression 667 * 668 * @param bean Bean whose property is to be extracted 669 * @param name Simple property name of the property value to be extracted 670 * @param key Lookup key of the property value to be extracted 671 * @return The mapped property's value, converted to a String 672 * 673 * @throws IllegalAccessException if the caller does not have 674 * access to the property accessor method 675 * @throws InvocationTargetException if the property accessor method 676 * throws an exception 677 * @throws NoSuchMethodException if an accessor method for this 678 * property cannot be found 679 */ 680 public String getMappedProperty(final Object bean, 681 final String name, final String key) 682 throws IllegalAccessException, InvocationTargetException, 683 NoSuchMethodException { 684 685 final Object value = getPropertyUtils().getMappedProperty(bean, name, key); 686 return (getConvertUtils().convert(value)); 687 688 } 689 690 691 /** 692 * Return the value of the (possibly nested) property of the specified 693 * name, for the specified bean, as a String. 694 * 695 * @param bean Bean whose property is to be extracted 696 * @param name Possibly nested name of the property to be extracted 697 * @return The nested property's value, converted to a String 698 * 699 * @throws IllegalAccessException if the caller does not have 700 * access to the property accessor method 701 * @throws IllegalArgumentException if a nested reference to a 702 * property returns null 703 * @throws InvocationTargetException if the property accessor method 704 * throws an exception 705 * @throws NoSuchMethodException if an accessor method for this 706 * property cannot be found 707 */ 708 public String getNestedProperty(final Object bean, final String name) 709 throws IllegalAccessException, InvocationTargetException, 710 NoSuchMethodException { 711 712 final Object value = getPropertyUtils().getNestedProperty(bean, name); 713 return (getConvertUtils().convert(value)); 714 715 } 716 717 718 /** 719 * Return the value of the specified property of the specified bean, 720 * no matter which property reference format is used, as a String. 721 * 722 * @param bean Bean whose property is to be extracted 723 * @param name Possibly indexed and/or nested name of the property 724 * to be extracted 725 * @return The property's value, converted to a String 726 * 727 * @throws IllegalAccessException if the caller does not have 728 * access to the property accessor method 729 * @throws InvocationTargetException if the property accessor method 730 * throws an exception 731 * @throws NoSuchMethodException if an accessor method for this 732 * property cannot be found 733 */ 734 public String getProperty(final Object bean, final String name) 735 throws IllegalAccessException, InvocationTargetException, 736 NoSuchMethodException { 737 738 return (getNestedProperty(bean, name)); 739 740 } 741 742 743 /** 744 * Return the value of the specified simple property of the specified 745 * bean, converted to a String. 746 * 747 * @param bean Bean whose property is to be extracted 748 * @param name Name of the property to be extracted 749 * @return The property's value, converted to a String 750 * 751 * @throws IllegalAccessException if the caller does not have 752 * access to the property accessor method 753 * @throws InvocationTargetException if the property accessor method 754 * throws an exception 755 * @throws NoSuchMethodException if an accessor method for this 756 * property cannot be found 757 */ 758 public String getSimpleProperty(final Object bean, final String name) 759 throws IllegalAccessException, InvocationTargetException, 760 NoSuchMethodException { 761 762 final Object value = getPropertyUtils().getSimpleProperty(bean, name); 763 return (getConvertUtils().convert(value)); 764 765 } 766 767 768 /** 769 * <p>Populate the JavaBeans properties of the specified bean, based on 770 * the specified name/value pairs. This method uses Java reflection APIs 771 * to identify corresponding "property setter" method names, and deals 772 * with setter arguments of type <code>String</code>, <code>boolean</code>, 773 * <code>int</code>, <code>long</code>, <code>float</code>, and 774 * <code>double</code>. In addition, array setters for these types (or the 775 * corresponding primitive types) can also be identified.</p> 776 * 777 * <p>The particular setter method to be called for each property is 778 * determined using the usual JavaBeans introspection mechanisms. Thus, 779 * you may identify custom setter methods using a BeanInfo class that is 780 * associated with the class of the bean itself. If no such BeanInfo 781 * class is available, the standard method name conversion ("set" plus 782 * the capitalized name of the property in question) is used.</p> 783 * 784 * <p><strong>NOTE</strong>: It is contrary to the JavaBeans Specification 785 * to have more than one setter method (with different argument 786 * signatures) for the same property.</p> 787 * 788 * <p><strong>WARNING</strong> - The logic of this method is customized 789 * for extracting String-based request parameters from an HTTP request. 790 * It is probably not what you want for general property copying with 791 * type conversion. For that purpose, check out the 792 * <code>copyProperties()</code> method instead.</p> 793 * 794 * @param bean JavaBean whose properties are being populated 795 * @param properties Map keyed by property name, with the 796 * corresponding (String or String[]) value(s) to be set 797 * 798 * @throws IllegalAccessException if the caller does not have 799 * access to the property accessor method 800 * @throws InvocationTargetException if the property accessor method 801 * throws an exception 802 */ 803 public void populate(final Object bean, final Map<String, ? extends Object> properties) 804 throws IllegalAccessException, InvocationTargetException { 805 806 // Do nothing unless both arguments have been specified 807 if ((bean == null) || (properties == null)) { 808 return; 809 } 810 if (log.isDebugEnabled()) { 811 log.debug("BeanUtils.populate(" + bean + ", " + 812 properties + ")"); 813 } 814 815 // Loop through the property name/value pairs to be set 816 for(final Map.Entry<String, ? extends Object> entry : properties.entrySet()) { 817 // Identify the property name and value(s) to be assigned 818 final String name = entry.getKey(); 819 if (name == null) { 820 continue; 821 } 822 823 // Perform the assignment for this property 824 setProperty(bean, name, entry.getValue()); 825 826 } 827 828 } 829 830 831 /** 832 * <p>Set the specified property value, performing type conversions as 833 * required to conform to the type of the destination property.</p> 834 * 835 * <p>If the property is read only then the method returns 836 * without throwing an exception.</p> 837 * 838 * <p>If <code>null</code> is passed into a property expecting a primitive value, 839 * then this will be converted as if it were a <code>null</code> string.</p> 840 * 841 * <p><strong>WARNING</strong> - The logic of this method is customized 842 * to meet the needs of <code>populate()</code>, and is probably not what 843 * you want for general property copying with type conversion. For that 844 * purpose, check out the <code>copyProperty()</code> method instead.</p> 845 * 846 * <p><strong>WARNING</strong> - PLEASE do not modify the behavior of this 847 * method without consulting with the Struts developer community. There 848 * are some subtleties to its functionality that are not documented in the 849 * Javadoc description above, yet are vital to the way that Struts utilizes 850 * this method.</p> 851 * 852 * @param bean Bean on which setting is to be performed 853 * @param name Property name (can be nested/indexed/mapped/combo) 854 * @param value Value to be set 855 * 856 * @throws IllegalAccessException if the caller does not have 857 * access to the property accessor method 858 * @throws InvocationTargetException if the property accessor method 859 * throws an exception 860 */ 861 public void setProperty(final Object bean, String name, final Object value) 862 throws IllegalAccessException, InvocationTargetException { 863 864 // Trace logging (if enabled) 865 if (log.isTraceEnabled()) { 866 final StringBuilder sb = new StringBuilder(" setProperty("); 867 sb.append(bean); 868 sb.append(", "); 869 sb.append(name); 870 sb.append(", "); 871 if (value == null) { 872 sb.append("<NULL>"); 873 } else if (value instanceof String) { 874 sb.append((String) value); 875 } else if (value instanceof String[]) { 876 final String[] values = (String[]) value; 877 sb.append('['); 878 for (int i = 0; i < values.length; i++) { 879 if (i > 0) { 880 sb.append(','); 881 } 882 sb.append(values[i]); 883 } 884 sb.append(']'); 885 } else { 886 sb.append(value.toString()); 887 } 888 sb.append(')'); 889 log.trace(sb.toString()); 890 } 891 892 // Resolve any nested expression to get the actual target bean 893 Object target = bean; 894 final Resolver resolver = getPropertyUtils().getResolver(); 895 while (resolver.hasNested(name)) { 896 try { 897 target = getPropertyUtils().getProperty(target, resolver.next(name)); 898 if (target == null) { // the value of a nested property is null 899 return; 900 } 901 name = resolver.remove(name); 902 } catch (final NoSuchMethodException e) { 903 return; // Skip this property setter 904 } 905 } 906 if (log.isTraceEnabled()) { 907 log.trace(" Target bean = " + target); 908 log.trace(" Target name = " + name); 909 } 910 911 // Declare local variables we will require 912 final String propName = resolver.getProperty(name); // Simple name of target property 913 Class<?> type = null; // Java type of target property 914 final int index = resolver.getIndex(name); // Indexed subscript value (if any) 915 final String key = resolver.getKey(name); // Mapped key value (if any) 916 917 // Calculate the property type 918 if (target instanceof DynaBean) { 919 final DynaClass dynaClass = ((DynaBean) target).getDynaClass(); 920 final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName); 921 if (dynaProperty == null) { 922 return; // Skip this property setter 923 } 924 type = dynaPropertyType(dynaProperty, value); 925 if (index >= 0 && List.class.isAssignableFrom(type)) { 926 type = Object.class; 927 } 928 } else if (target instanceof Map) { 929 type = Object.class; 930 } else if (target != null && target.getClass().isArray() && index >= 0) { 931 type = Array.get(target, index).getClass(); 932 } else { 933 PropertyDescriptor descriptor = null; 934 try { 935 descriptor = 936 getPropertyUtils().getPropertyDescriptor(target, name); 937 if (descriptor == null) { 938 return; // Skip this property setter 939 } 940 } catch (final NoSuchMethodException e) { 941 return; // Skip this property setter 942 } 943 if (descriptor instanceof MappedPropertyDescriptor) { 944 if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) { 945 if (log.isDebugEnabled()) { 946 log.debug("Skipping read-only property"); 947 } 948 return; // Read-only, skip this property setter 949 } 950 type = ((MappedPropertyDescriptor) descriptor). 951 getMappedPropertyType(); 952 } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) { 953 if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) { 954 if (log.isDebugEnabled()) { 955 log.debug("Skipping read-only property"); 956 } 957 return; // Read-only, skip this property setter 958 } 959 type = ((IndexedPropertyDescriptor) descriptor). 960 getIndexedPropertyType(); 961 } else if (index >= 0 && List.class.isAssignableFrom(descriptor.getPropertyType())) { 962 type = Object.class; 963 } else if (key != null) { 964 if (descriptor.getReadMethod() == null) { 965 if (log.isDebugEnabled()) { 966 log.debug("Skipping read-only property"); 967 } 968 return; // Read-only, skip this property setter 969 } 970 type = (value == null) ? Object.class : value.getClass(); 971 } else { 972 if (descriptor.getWriteMethod() == null) { 973 if (log.isDebugEnabled()) { 974 log.debug("Skipping read-only property"); 975 } 976 return; // Read-only, skip this property setter 977 } 978 type = descriptor.getPropertyType(); 979 } 980 } 981 982 // Convert the specified value to the required type 983 Object newValue = null; 984 if (type.isArray() && (index < 0)) { // Scalar value into array 985 if (value == null) { 986 final String[] values = new String[1]; 987 values[0] = null; 988 newValue = getConvertUtils().convert(values, type); 989 } else if (value instanceof String) { 990 newValue = getConvertUtils().convert(value, type); 991 } else if (value instanceof String[]) { 992 newValue = getConvertUtils().convert((String[]) value, type); 993 } else { 994 newValue = convert(value, type); 995 } 996 } else if (type.isArray()) { // Indexed value into array 997 if (value instanceof String || value == null) { 998 newValue = getConvertUtils().convert((String) value, 999 type.getComponentType()); 1000 } else if (value instanceof String[]) { 1001 newValue = getConvertUtils().convert(((String[]) value)[0], 1002 type.getComponentType()); 1003 } else { 1004 newValue = convert(value, type.getComponentType()); 1005 } 1006 } else { // Value into scalar 1007 if (value instanceof String) { 1008 newValue = getConvertUtils().convert((String) value, type); 1009 } else if (value instanceof String[]) { 1010 newValue = getConvertUtils().convert(((String[]) value)[0], 1011 type); 1012 } else { 1013 newValue = convert(value, type); 1014 } 1015 } 1016 1017 // Invoke the setter method 1018 try { 1019 getPropertyUtils().setProperty(target, name, newValue); 1020 } catch (final NoSuchMethodException e) { 1021 throw new InvocationTargetException 1022 (e, "Cannot set " + propName); 1023 } 1024 1025 } 1026 1027 /** 1028 * Gets the <code>ConvertUtilsBean</code> instance used to perform the conversions. 1029 * 1030 * @return The ConvertUtils bean instance 1031 */ 1032 public ConvertUtilsBean getConvertUtils() { 1033 return convertUtilsBean; 1034 } 1035 1036 /** 1037 * Gets the <code>PropertyUtilsBean</code> instance used to access properties. 1038 * 1039 * @return The ConvertUtils bean instance 1040 */ 1041 public PropertyUtilsBean getPropertyUtils() { 1042 return propertyUtilsBean; 1043 } 1044 1045 /** 1046 * If we're running on JDK 1.4 or later, initialize the cause for the given throwable. 1047 * 1048 * @param throwable The throwable. 1049 * @param cause The cause of the throwable. 1050 * @return true if the cause was initialized, otherwise false. 1051 * @since 1.8.0 1052 */ 1053 public boolean initCause(final Throwable throwable, final Throwable cause) { 1054 if (INIT_CAUSE_METHOD != null && cause != null) { 1055 try { 1056 INIT_CAUSE_METHOD.invoke(throwable, new Object[] { cause }); 1057 return true; 1058 } catch (final Throwable e) { 1059 return false; // can't initialize cause 1060 } 1061 } 1062 return false; 1063 } 1064 1065 /** 1066 * <p>Convert the value to an object of the specified class (if 1067 * possible).</p> 1068 * 1069 * @param value Value to be converted (may be null) 1070 * @param type Class of the value to be converted to 1071 * @return The converted value 1072 * 1073 * @throws ConversionException if thrown by an underlying Converter 1074 * @since 1.8.0 1075 */ 1076 protected Object convert(final Object value, final Class<?> type) { 1077 final Converter converter = getConvertUtils().lookup(type); 1078 if (converter != null) { 1079 log.trace(" USING CONVERTER " + converter); 1080 return converter.convert(type, value); 1081 } else { 1082 return value; 1083 } 1084 } 1085 1086 /** 1087 * Performs a type conversion of a property value before it is copied to a target 1088 * bean. This method delegates to {@link #convert(Object, Class)}, but <b>null</b> 1089 * values are not converted. This causes <b>null</b> values to be copied verbatim. 1090 * 1091 * @param value the value to be converted and copied 1092 * @param type the target type of the conversion 1093 * @return the converted value 1094 */ 1095 private Object convertForCopy(final Object value, final Class<?> type) { 1096 return (value != null) ? convert(value, type) : value; 1097 } 1098 1099 /** 1100 * Returns a <code>Method<code> allowing access to 1101 * {@link Throwable#initCause(Throwable)} method of {@link Throwable}, 1102 * or <code>null</code> if the method 1103 * does not exist. 1104 * 1105 * @return A <code>Method<code> for <code>Throwable.initCause</code>, or 1106 * <code>null</code> if unavailable. 1107 */ 1108 private static Method getInitCauseMethod() { 1109 try { 1110 final Class<?>[] paramsClasses = new Class<?>[] { Throwable.class }; 1111 return Throwable.class.getMethod("initCause", paramsClasses); 1112 } catch (final NoSuchMethodException e) { 1113 final Log log = LogFactory.getLog(BeanUtils.class); 1114 if (log.isWarnEnabled()) { 1115 log.warn("Throwable does not have initCause() method in JDK 1.3"); 1116 } 1117 return null; 1118 } catch (final Throwable e) { 1119 final Log log = LogFactory.getLog(BeanUtils.class); 1120 if (log.isWarnEnabled()) { 1121 log.warn("Error getting the Throwable initCause() method", e); 1122 } 1123 return null; 1124 } 1125 } 1126 1127 /** 1128 * Determines the type of a {@code DynaProperty}. Here a special treatment 1129 * is needed for mapped properties. 1130 * 1131 * @param dynaProperty the property descriptor 1132 * @param value the value object to be set for this property 1133 * @return the type of this property 1134 */ 1135 private static Class<?> dynaPropertyType(final DynaProperty dynaProperty, 1136 final Object value) { 1137 if (!dynaProperty.isMapped()) { 1138 return dynaProperty.getType(); 1139 } 1140 return (value == null) ? String.class : value.getClass(); 1141 } 1142}