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 018package org.apache.commons.beanutils2; 019 020import java.beans.IndexedPropertyDescriptor; 021import java.beans.PropertyDescriptor; 022import java.lang.reflect.Array; 023import java.lang.reflect.InvocationTargetException; 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Objects; 029 030import org.apache.commons.beanutils2.expression.Resolver; 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033 034/** 035 * TODO docs 036 * 037 * <p> 038 * 2.0 039 * </p> 040 * 041 * <p> 042 * {@link BeanUtilsBean} implementation that creates a {@link ConvertUtilsBean} and delegates conversion to {@link ConvertUtilsBean#convert(Object, Class)}. 043 * </p> 044 * 045 * <p> 046 * To configure this implementation for the current context ClassLoader invoke {@code BeanUtilsBean.setInstance(new BeanUtilsBean2());} 047 * </p> 048 * 049 * <p> 050 * BeanUtils 1.7.0 delegated all conversion to String to the converter registered for the {@code String.class}. One of the improvements in BeanUtils 1.8.0 was 051 * to upgrade the {@link Converter} implementations so that they could handle conversion to String for their type (for example IntegerConverter now handles 052 * conversion from an Integer to a String as well as String to Integer). 053 * </p> 054 * 055 * <p> 056 * In order to take advantage of these improvements BeanUtils needs to change how it gets the appropriate {@link Converter}. This functionality has been 057 * implemented in the new {@link ConvertUtilsBean#lookup(Class, Class)} and {@link ConvertUtilsBean#convert(Object, Class)} methods. However changing 058 * {@link BeanUtilsBean} to use these methods could create compatibility issues for existing users. In order to avoid that, this new {@link BeanUtilsBean} 059 * implementation has been created (and the associated {@link ConvertUtilsBean}). 060 * </p> 061 * 062 * <p> 063 * Pre-2.0 064 * </p> 065 * 066 * <p> 067 * JavaBean property population methods. 068 * </p> 069 * 070 * <p> 071 * This class provides implementations for the utility methods in {@link BeanUtils}. Different instances can be used to isolate caches between class loaders and 072 * to vary the value converters registered. 073 * </p> 074 * 075 * @see BeanUtils 076 * @since 1.7 077 */ 078public class BeanUtilsBean { 079 080 /** 081 * Contains {@code BeanUtilsBean} instances indexed by context classloader. 082 */ 083 private static final ContextClassLoaderLocal<BeanUtilsBean> BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() { 084 // Creates the default instance used when the context classloader is unavailable 085 @Override 086 protected BeanUtilsBean initialValue() { 087 return new BeanUtilsBean(); 088 } 089 }; 090 091 /** 092 * Logging for this instance 093 */ 094 private static final Log LOG = LogFactory.getLog(BeanUtilsBean.class); 095 096 /** 097 * Determines the type of a {@code DynaProperty}. Here a special treatment is needed for mapped properties. 098 * 099 * @param dynaProperty the property descriptor 100 * @param value the value object to be set for this property 101 * @return the type of this property 102 */ 103 private static Class<?> dynaPropertyType(final DynaProperty dynaProperty, final Object value) { 104 if (!dynaProperty.isMapped()) { 105 return dynaProperty.getType(); 106 } 107 return value == null ? String.class : value.getClass(); 108 } 109 110 /** 111 * Gets the instance which provides the functionality for {@link BeanUtils}. This is a pseudo-singleton - an single instance is provided per (thread) 112 * context classloader. This mechanism provides isolation for web apps deployed in the same container. 113 * 114 * @return The (pseudo-singleton) BeanUtils bean instance 115 */ 116 public static BeanUtilsBean getInstance() { 117 return BEANS_BY_CLASSLOADER.get(); 118 } 119 120 /** 121 * Sets the instance which provides the functionality for {@link BeanUtils}. This is a pseudo-singleton - an single instance is provided per (thread) 122 * context classloader. This mechanism provides isolation for web apps deployed in the same container. 123 * 124 * @param newInstance The (pseudo-singleton) BeanUtils bean instance 125 */ 126 public static void setInstance(final BeanUtilsBean newInstance) { 127 BEANS_BY_CLASSLOADER.set(newInstance); 128 } 129 130 /** Used to perform conversions between object types when setting properties */ 131 private final ConvertUtilsBean convertUtilsBean; 132 133 /** Used to access properties */ 134 private final PropertyUtilsBean propertyUtilsBean; 135 136 /** 137 * <p> 138 * Constructs an instance using new property and conversion instances. 139 * </p> 140 */ 141 public BeanUtilsBean() { 142 this(new ConvertUtilsBean(), new PropertyUtilsBean()); 143 } 144 145 /** 146 * <p> 147 * Constructs an instance using given conversion instances and new {@link PropertyUtilsBean} instance. 148 * </p> 149 * 150 * @param todoRemove use this {@code ConvertUtilsBean} to perform conversions from one object to another 151 * @since 1.8.0 152 */ 153 public BeanUtilsBean(final ConvertUtilsBean todoRemove) { 154 this(new ConvertUtilsBean(), new PropertyUtilsBean()); 155 } 156 157 /** 158 * <p> 159 * Constructs an instance using given property and conversion instances. 160 * </p> 161 * 162 * @param convertUtilsBean use this {@code ConvertUtilsBean} to perform conversions from one object to another 163 * @param propertyUtilsBean use this {@code PropertyUtilsBean} to access properties 164 */ 165 public BeanUtilsBean(final ConvertUtilsBean convertUtilsBean, final PropertyUtilsBean propertyUtilsBean) { 166 this.convertUtilsBean = convertUtilsBean; 167 this.propertyUtilsBean = propertyUtilsBean; 168 } 169 170 /** 171 * <p> 172 * Clone a bean based on the available property getters and setters, even if the bean class itself does not implement Cloneable. 173 * </p> 174 * 175 * <p> 176 * <strong>Note:</strong> this method creates a <strong>shallow</strong> clone. In other words, any objects referred to by the bean are shared with the 177 * clone rather than being cloned in turn. 178 * </p> 179 * 180 * @param bean Bean to be cloned 181 * @return the cloned bean 182 * @throws IllegalAccessException if the caller does not have access to the property accessor method 183 * @throws InstantiationException if a new instance of the bean's class cannot be instantiated 184 * @throws InvocationTargetException if the property accessor method throws an exception 185 * @throws NoSuchMethodException if an accessor method for this property cannot be found 186 */ 187 public Object cloneBean(final Object bean) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException { 188 if (LOG.isDebugEnabled()) { 189 LOG.debug("Cloning bean: " + bean.getClass().getName()); 190 } 191 Object newBean = null; 192 if (bean instanceof DynaBean) { 193 newBean = ((DynaBean) bean).getDynaClass().newInstance(); 194 } else { 195 newBean = bean.getClass().newInstance(); 196 } 197 getPropertyUtils().copyProperties(newBean, bean); 198 return newBean; 199 } 200 201 /** 202 * <p> 203 * Converts the value to an object of the specified class (if possible). 204 * </p> 205 * 206 * @param <R> the type of the class for the return value. 207 * @param value Value to be converted (may be null) 208 * @param type Class of the value to be converted to 209 * @return The converted value 210 */ 211 protected <R> Object convert(final Object value, final Class<R> type) { 212 return getConvertUtils().convert(value, type); 213 } 214 215 /** 216 * Performs a type conversion of a property value before it is copied to a target bean. This method delegates to {@link #convert(Object, Class)}, but 217 * <strong>null</strong> values are not converted. This causes <strong>null</strong> values to be copied verbatim. 218 * 219 * @param value the value to be converted and copied 220 * @param type the target type of the conversion 221 * @return the converted value 222 */ 223 private Object convertForCopy(final Object value, final Class<?> type) { 224 return value != null ? convert(value, type) : value; 225 } 226 227 /** 228 * <p> 229 * Copy property values from the origin bean to the destination bean for all cases where the property names are the same. For each property, a conversion is 230 * attempted as necessary. All combinations of standard JavaBeans and DynaBeans as origin and destination are supported. Properties that exist in the origin 231 * bean, but do not exist in the destination bean (or are read-only in the destination bean) are silently ignored. 232 * </p> 233 * 234 * <p> 235 * If the origin "bean" is actually a {@code Map}, it is assumed to contain String-valued <strong>simple</strong> property names as the keys, pointing at 236 * the corresponding property values that will be converted (if necessary) and set in the destination bean. <strong>Note</strong> that this method is 237 * intended to perform a "shallow copy" of the properties and so complex properties (for example, nested ones) will not be copied. 238 * </p> 239 * 240 * <p> 241 * This method differs from {@code populate()}, which was primarily designed for populating JavaBeans from the map of request parameters retrieved on an 242 * HTTP request, is that no scalar->indexed or indexed->scalar manipulations are performed. If the origin property is indexed, the destination 243 * property must be also. 244 * </p> 245 * 246 * <p> 247 * If you know that no type conversions are required, the {@code copyProperties()} method in {@link PropertyUtils} will execute faster than this method. 248 * </p> 249 * 250 * <p> 251 * <strong>FIXME</strong> - Indexed and mapped properties that do not have getter and setter methods for the underlying array or Map are not copied by this 252 * method. 253 * </p> 254 * 255 * @param dest Destination bean whose properties are modified 256 * @param orig Origin bean whose properties are retrieved 257 * @throws IllegalAccessException if the caller does not have access to the property accessor method 258 * @throws IllegalArgumentException if the {@code dest} or {@code orig</code> argument is null or if the <code>dest} property type is different from the 259 * source type and the relevant converter has not been registered. 260 * @throws InvocationTargetException if the property accessor method throws an exception 261 */ 262 public void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException { 263 // Validate existence of the specified beans 264 Objects.requireNonNull(dest, "dest"); 265 Objects.requireNonNull(orig, "orig"); 266 if (LOG.isDebugEnabled()) { 267 LOG.debug("BeanUtils.copyProperties(" + dest + ", " + orig + ")"); 268 } 269 // Copy the properties, converting as necessary 270 if (orig instanceof DynaBean) { 271 final DynaProperty[] origDescriptors = ((DynaBean) orig).getDynaClass().getDynaProperties(); 272 for (final DynaProperty origDescriptor : origDescriptors) { 273 final String name = origDescriptor.getName(); 274 // Need to check isReadable() for WrapDynaBean 275 // (see Jira issue# BEANUTILS-61) 276 if (getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)) { 277 final Object value = ((DynaBean) orig).get(name); 278 copyProperty(dest, name, value); 279 } 280 } 281 } else if (orig instanceof Map) { 282 @SuppressWarnings("unchecked") 283 final 284 // Map properties are always of type <String, Object> 285 Map<String, Object> propMap = (Map<String, Object>) orig; 286 for (final Map.Entry<String, Object> entry : propMap.entrySet()) { 287 final String k = entry.getKey(); 288 if (getPropertyUtils().isWriteable(dest, k)) { 289 copyProperty(dest, k, entry.getValue()); 290 } 291 } 292 } else /* if (orig is a standard JavaBean) */ { 293 final PropertyDescriptor[] origDescriptors = getPropertyUtils().getPropertyDescriptors(orig); 294 for (final PropertyDescriptor origDescriptor : origDescriptors) { 295 final String name = origDescriptor.getName(); 296 if ("class".equals(name)) { 297 continue; // No point in trying to set an object's class 298 } 299 if (getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)) { 300 try { 301 final Object value = getPropertyUtils().getSimpleProperty(orig, name); 302 copyProperty(dest, name, value); 303 } catch (final NoSuchMethodException e) { 304 // Should not happen 305 } 306 } 307 } 308 } 309 } 310 311 /** 312 * <p> 313 * Copy the specified property value to the specified destination bean, performing any type conversion that is required. If the specified bean does not have 314 * a property of the specified name, or the property is read only on the destination bean, return without doing anything. If you have custom destination 315 * property types, register {@link Converter}s for them by calling the {@code register()} method of {@link ConvertUtils}. 316 * </p> 317 * 318 * <p> 319 * <strong>IMPLEMENTATION RESTRICTIONS</strong>: 320 * </p> 321 * <ul> 322 * <li>Does not support destination properties that are indexed, but only an indexed setter (as opposed to an array setter) is available.</li> 323 * <li>Does not support destination properties that are mapped, but only a keyed setter (as opposed to a Map setter) is available.</li> 324 * <li>The desired property type of a mapped setter cannot be determined (since Maps support any data type), so no conversion will be performed.</li> 325 * </ul> 326 * 327 * @param bean Bean on which setting is to be performed 328 * @param name Property name (can be nested/indexed/mapped/combo) 329 * @param value Value to be set 330 * @throws IllegalAccessException if the caller does not have access to the property accessor method 331 * @throws InvocationTargetException if the property accessor method throws an exception 332 */ 333 public void copyProperty(final Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException { 334 // Trace logging (if enabled) 335 if (LOG.isTraceEnabled()) { 336 final StringBuilder sb = new StringBuilder(" copyProperty("); 337 sb.append(bean); 338 sb.append(", "); 339 sb.append(name); 340 sb.append(", "); 341 if (value == null) { 342 sb.append("<NULL>"); 343 } else if (value instanceof String) { 344 sb.append((String) value); 345 } else if (value instanceof String[]) { 346 final String[] values = (String[]) value; 347 sb.append('['); 348 for (int i = 0; i < values.length; i++) { 349 if (i > 0) { 350 sb.append(','); 351 } 352 sb.append(values[i]); 353 } 354 sb.append(']'); 355 } else { 356 sb.append(value.toString()); 357 } 358 sb.append(')'); 359 LOG.trace(sb.toString()); 360 } 361 362 // Resolve any nested expression to get the actual target bean 363 Object target = bean; 364 final Resolver resolver = getPropertyUtils().getResolver(); 365 while (resolver.hasNested(name)) { 366 try { 367 target = getPropertyUtils().getProperty(target, resolver.next(name)); 368 name = resolver.remove(name); 369 } catch (final NoSuchMethodException e) { 370 return; // Skip this property setter 371 } 372 } 373 if (LOG.isTraceEnabled()) { 374 LOG.trace(" Target bean = " + target); 375 LOG.trace(" Target name = " + name); 376 } 377 378 // Declare local variables we will require 379 final String propName = resolver.getProperty(name); // Simple name of target property 380 Class<?> type = null; // Java type of target property 381 final int index = resolver.getIndex(name); // Indexed subscript value (if any) 382 final String key = resolver.getKey(name); // Mapped key value (if any) 383 384 // Calculate the target property type 385 if (target instanceof DynaBean) { 386 final DynaClass dynaClass = ((DynaBean) target).getDynaClass(); 387 final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName); 388 if (dynaProperty == null) { 389 return; // Skip this property setter 390 } 391 type = dynaPropertyType(dynaProperty, value); 392 } else { 393 PropertyDescriptor descriptor = null; 394 try { 395 descriptor = getPropertyUtils().getPropertyDescriptor(target, name); 396 if (descriptor == null) { 397 return; // Skip this property setter 398 } 399 } catch (final NoSuchMethodException e) { 400 return; // Skip this property setter 401 } 402 type = descriptor.getPropertyType(); 403 if (type == null) { 404 // Most likely an indexed setter on a POJB only 405 if (LOG.isTraceEnabled()) { 406 LOG.trace(" target type for property '" + propName + "' is null, so skipping the setter"); 407 } 408 return; 409 } 410 } 411 if (LOG.isTraceEnabled()) { 412 LOG.trace(" target propName=" + propName + ", type=" + type + ", index=" + index + ", key=" + key); 413 } 414 415 // Convert the specified value to the required type and store it 416 if (index >= 0) { // Destination must be indexed 417 value = convertForCopy(value, type.getComponentType()); 418 try { 419 getPropertyUtils().setIndexedProperty(target, propName, index, value); 420 } catch (final NoSuchMethodException e) { 421 throw new InvocationTargetException(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, key, value); 429 } catch (final NoSuchMethodException e) { 430 throw new InvocationTargetException(e, "Cannot set " + propName); 431 } 432 } else { // Destination must be simple 433 value = convertForCopy(value, type); 434 try { 435 getPropertyUtils().setSimpleProperty(target, propName, value); 436 } catch (final NoSuchMethodException e) { 437 throw new InvocationTargetException(e, "Cannot set " + propName); 438 } 439 } 440 } 441 442 /** 443 * <p> 444 * Return the entire set of properties for which the specified bean provides a read method. This map contains the to {@code String} converted property 445 * values for all properties for which a read method is provided (i.e. where the getReadMethod() returns non-null). 446 * </p> 447 * 448 * <p> 449 * This map can be fed back to a call to {@code BeanUtils.populate()} to re-constitute the same set of properties, modulo differences for read-only and 450 * write-only properties, but only if there are no indexed properties. 451 * </p> 452 * 453 * <p> 454 * <strong>Warning:</strong> if any of the bean property implementations contain (directly or indirectly) a call to this method then a stack overflow may 455 * result. For example: 456 * </p> 457 * 458 * <pre> 459 * <code> 460 * class MyBean 461 * { 462 * public Map getParameterMap() 463 * { 464 * BeanUtils.describe(this); 465 * } 466 * } 467 * </code> 468 * </pre> 469 * <p> 470 * will result in an infinite regression when {@code getParametersMap} is called. It is recommended that such methods are given alternative names (for 471 * example, {@code parametersMap}). 472 * </p> 473 * 474 * @param bean Bean whose properties are to be extracted 475 * @return Map of property descriptors 476 * @throws IllegalAccessException if the caller does not have access to the property accessor method 477 * @throws InvocationTargetException if the property accessor method throws an exception 478 * @throws NoSuchMethodException if an accessor method for this property cannot be found 479 */ 480 public Map<String, String> describe(final Object bean) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 481 if (bean == null) { 482 // return (Collections.EMPTY_MAP); 483 return new java.util.HashMap<>(); 484 } 485 486 if (LOG.isDebugEnabled()) { 487 LOG.debug("Describing bean: " + bean.getClass().getName()); 488 } 489 490 final Map<String, String> description = new HashMap<>(); 491 if (bean instanceof DynaBean) { 492 final DynaProperty[] descriptors = ((DynaBean) bean).getDynaClass().getDynaProperties(); 493 for (final DynaProperty descriptor : descriptors) { 494 final String name = descriptor.getName(); 495 description.put(name, getProperty(bean, name)); 496 } 497 } else { 498 final PropertyDescriptor[] descriptors = getPropertyUtils().getPropertyDescriptors(bean); 499 final Class<?> clazz = bean.getClass(); 500 for (final PropertyDescriptor descriptor : descriptors) { 501 final String name = descriptor.getName(); 502 if (getPropertyUtils().getReadMethod(clazz, descriptor) != null) { 503 description.put(name, getProperty(bean, name)); 504 } 505 } 506 } 507 return description; 508 } 509 510 /** 511 * Gets the value of the specified array property of the specified bean, as a String array. 512 * 513 * @param bean Bean whose property is to be extracted 514 * @param name Name of the property to be extracted 515 * @return The array property value 516 * @throws IllegalAccessException if the caller does not have access to the property accessor method 517 * @throws InvocationTargetException if the property accessor method throws an exception 518 * @throws NoSuchMethodException if an accessor method for this property cannot be found 519 */ 520 public String[] getArrayProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 521 final Object value = getPropertyUtils().getProperty(bean, name); 522 if (value == null) { 523 return null; 524 } 525 if (value instanceof Collection) { 526 return ((Collection<?>) value).stream().map(item -> item != null ? getConvertUtils().convert(item) : null).toArray(String[]::new); 527 } 528 if (!value.getClass().isArray()) { 529 final String[] results = new String[1]; 530 results[0] = getConvertUtils().convert(value); 531 return results; 532 } 533 final int n = Array.getLength(value); 534 final String[] results = new String[n]; 535 for (int i = 0; i < n; i++) { 536 final Object item = Array.get(value, i); 537 if (item == null) { 538 results[i] = null; 539 } else { 540 // convert to string using convert utils 541 results[i] = getConvertUtils().convert(item); 542 } 543 } 544 return results; 545 } 546 547 /** 548 * Gets the {@code ConvertUtilsBean} instance used to perform the conversions. 549 * 550 * @return The ConvertUtils bean instance 551 */ 552 public ConvertUtilsBean getConvertUtils() { 553 return convertUtilsBean; 554 } 555 556 /** 557 * Gets the value of the specified indexed property of the specified bean, as a String. The zero-relative index of the required value must be included (in 558 * square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. 559 * 560 * @param bean Bean whose property is to be extracted 561 * @param name {@code propertyname[index]} of the property value to be extracted 562 * @return The indexed property's value, converted to a String 563 * @throws IllegalAccessException if the caller does not have access to the property accessor method 564 * @throws InvocationTargetException if the property accessor method throws an exception 565 * @throws NoSuchMethodException if an accessor method for this property cannot be found 566 */ 567 public String getIndexedProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 568 final Object value = getPropertyUtils().getIndexedProperty(bean, name); 569 return getConvertUtils().convert(value); 570 } 571 572 /** 573 * Gets the value of the specified indexed property of the specified bean, as a String. The index is specified as a method parameter and must *not* be 574 * included in the property name expression 575 * 576 * @param bean Bean whose property is to be extracted 577 * @param name Simple property name of the property value to be extracted 578 * @param index Index of the property value to be extracted 579 * @return The indexed property's value, converted to a String 580 * @throws IllegalAccessException if the caller does not have access to the property accessor method 581 * @throws InvocationTargetException if the property accessor method throws an exception 582 * @throws NoSuchMethodException if an accessor method for this property cannot be found 583 */ 584 public String getIndexedProperty(final Object bean, final String name, final int index) 585 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 586 final Object value = getPropertyUtils().getIndexedProperty(bean, name, index); 587 return getConvertUtils().convert(value); 588 } 589 590 /** 591 * Gets the value of the specified indexed property of the specified bean, as a String. The String-valued key of the required value must be included (in 592 * parentheses) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. 593 * 594 * @param bean Bean whose property is to be extracted 595 * @param name {@code propertyname(index)} of the property value to be extracted 596 * @return The mapped property's value, converted to a String 597 * @throws IllegalAccessException if the caller does not have access to the property accessor method 598 * @throws InvocationTargetException if the property accessor method throws an exception 599 * @throws NoSuchMethodException if an accessor method for this property cannot be found 600 */ 601 public String getMappedProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 602 final Object value = getPropertyUtils().getMappedProperty(bean, name); 603 return getConvertUtils().convert(value); 604 } 605 606 /** 607 * Gets the value of the specified mapped property of the specified bean, as a String. The key is specified as a method parameter and must *not* be included 608 * in the property name expression 609 * 610 * @param bean Bean whose property is to be extracted 611 * @param name Simple property name of the property value to be extracted 612 * @param key Lookup key of the property value to be extracted 613 * @return The mapped property's value, converted to a String 614 * @throws IllegalAccessException if the caller does not have access to the property accessor method 615 * @throws InvocationTargetException if the property accessor method throws an exception 616 * @throws NoSuchMethodException if an accessor method for this property cannot be found 617 */ 618 public String getMappedProperty(final Object bean, final String name, final String key) 619 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 620 final Object value = getPropertyUtils().getMappedProperty(bean, name, key); 621 return getConvertUtils().convert(value); 622 } 623 624 /** 625 * Gets the value of the (possibly nested) property of the specified name, for the specified bean, as a String. 626 * 627 * @param bean Bean whose property is to be extracted 628 * @param name Possibly nested name of the property to be extracted 629 * @return The nested property's value, converted to a String 630 * @throws IllegalAccessException if the caller does not have access to the property accessor method 631 * @throws IllegalArgumentException if a nested reference to a property returns null 632 * @throws InvocationTargetException if the property accessor method throws an exception 633 * @throws NoSuchMethodException if an accessor method for this property cannot be found 634 */ 635 public String getNestedProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 636 final Object value = getPropertyUtils().getNestedProperty(bean, name); 637 return getConvertUtils().convert(value); 638 } 639 640 /** 641 * Gets the value of the specified property of the specified bean, no matter which property reference format is used, as a String. 642 * 643 * @param bean Bean whose property is to be extracted 644 * @param name Possibly indexed and/or nested name of the property to be extracted 645 * @return The property's value, converted to a String 646 * @throws IllegalAccessException if the caller does not have access to the property accessor method 647 * @throws InvocationTargetException if the property accessor method throws an exception 648 * @throws NoSuchMethodException if an accessor method for this property cannot be found 649 */ 650 public String getProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 651 return getNestedProperty(bean, name); 652 } 653 654 /** 655 * Gets the {@code PropertyUtilsBean} instance used to access properties. 656 * 657 * @return The ConvertUtils bean instance 658 */ 659 public PropertyUtilsBean getPropertyUtils() { 660 return propertyUtilsBean; 661 } 662 663 /** 664 * Gets the value of the specified simple property of the specified bean, converted to a String. 665 * 666 * @param bean Bean whose property is to be extracted 667 * @param name Name of the property to be extracted 668 * @return The property's value, converted to a String 669 * @throws IllegalAccessException if the caller does not have access to the property accessor method 670 * @throws InvocationTargetException if the property accessor method throws an exception 671 * @throws NoSuchMethodException if an accessor method for this property cannot be found 672 */ 673 public String getSimpleProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 674 final Object value = getPropertyUtils().getSimpleProperty(bean, name); 675 return getConvertUtils().convert(value); 676 } 677 678 /** 679 * <p> 680 * Populate the JavaBeans properties of the specified bean, based on the specified name/value pairs. This method uses Java reflection APIs to identify 681 * corresponding "property setter" method names, and deals with setter arguments of type {@code String</code>, <code>boolean}, {@code int}, {@code long}, 682 * {@code float}, and {@code double}. In addition, array setters for these types (or the corresponding primitive types) can also be identified. 683 * </p> 684 * 685 * <p> 686 * The particular setter method to be called for each property is determined using the usual JavaBeans introspection mechanisms. Thus, you may identify 687 * custom setter methods using a BeanInfo class that is associated with the class of the bean itself. If no such BeanInfo class is available, the standard 688 * method name conversion ("set" plus the capitalized name of the property in question) is used. 689 * </p> 690 * 691 * <p> 692 * <strong>NOTE</strong>: It is contrary to the JavaBeans Specification to have more than one setter method (with different argument signatures) for the 693 * same property. 694 * </p> 695 * 696 * <p> 697 * <strong>WARNING</strong> - The logic of this method is customized for extracting String-based request parameters from an HTTP request. It is probably not 698 * what you want for general property copying with type conversion. For that purpose, check out the {@code copyProperties()} method instead. 699 * </p> 700 * 701 * @param bean JavaBean whose properties are being populated 702 * @param properties Map keyed by property name, with the corresponding (String or String[]) value(s) to be set 703 * @throws IllegalAccessException if the caller does not have access to the property accessor method 704 * @throws InvocationTargetException if the property accessor method throws an exception 705 */ 706 public void populate(final Object bean, final Map<String, ? extends Object> properties) throws IllegalAccessException, InvocationTargetException { 707 // Do nothing unless both arguments have been specified 708 if (bean == null || properties == null) { 709 return; 710 } 711 if (LOG.isDebugEnabled()) { 712 LOG.debug("BeanUtils.populate(" + bean + ", " + properties + ")"); 713 } 714 715 // Loop through the property name/value pairs to be set 716 for (final Map.Entry<String, ? extends Object> entry : properties.entrySet()) { 717 // Identify the property name and value(s) to be assigned 718 final String name = entry.getKey(); 719 if (name == null) { 720 continue; 721 } 722 723 // Perform the assignment for this property 724 setProperty(bean, name, entry.getValue()); 725 } 726 } 727 728 /** 729 * <p> 730 * Set the specified property value, performing type conversions as required to conform to the type of the destination property. 731 * </p> 732 * 733 * <p> 734 * If the property is read only then the method returns without throwing an exception. 735 * </p> 736 * 737 * <p> 738 * If {@code null} is passed into a property expecting a primitive value, then this will be converted as if it were a {@code null} string. 739 * </p> 740 * 741 * <p> 742 * <strong>WARNING</strong> - The logic of this method is customized to meet the needs of {@code populate()}, and is probably not what you want for general 743 * property copying with type conversion. For that purpose, check out the {@code copyProperty()} method instead. 744 * </p> 745 * 746 * <p> 747 * <strong>WARNING</strong> - PLEASE do not modify the behavior of this method without consulting with the Struts developer community. There are some 748 * subtleties to its functionality that are not documented in the Javadoc description above, yet are vital to the way that Struts utilizes this method. 749 * </p> 750 * 751 * @param bean Bean on which setting is to be performed 752 * @param name Property name (can be nested/indexed/mapped/combo) 753 * @param value Value to be set 754 * @throws IllegalAccessException if the caller does not have access to the property accessor method 755 * @throws InvocationTargetException if the property accessor method throws an exception 756 */ 757 public void setProperty(final Object bean, String name, final Object value) throws IllegalAccessException, InvocationTargetException { 758 // Trace logging (if enabled) 759 if (LOG.isTraceEnabled()) { 760 final StringBuilder sb = new StringBuilder(" setProperty("); 761 sb.append(bean); 762 sb.append(", "); 763 sb.append(name); 764 sb.append(", "); 765 if (value == null) { 766 sb.append("<NULL>"); 767 } else if (value instanceof String) { 768 sb.append((String) value); 769 } else if (value instanceof String[]) { 770 final String[] values = (String[]) value; 771 sb.append('['); 772 for (int i = 0; i < values.length; i++) { 773 if (i > 0) { 774 sb.append(','); 775 } 776 sb.append(values[i]); 777 } 778 sb.append(']'); 779 } else { 780 sb.append(value.toString()); 781 } 782 sb.append(')'); 783 LOG.trace(sb.toString()); 784 } 785 786 // Resolve any nested expression to get the actual target bean 787 Object target = bean; 788 final Resolver resolver = getPropertyUtils().getResolver(); 789 while (resolver.hasNested(name)) { 790 try { 791 target = getPropertyUtils().getProperty(target, resolver.next(name)); 792 if (target == null) { // the value of a nested property is null 793 return; 794 } 795 name = resolver.remove(name); 796 } catch (final NoSuchMethodException e) { 797 return; // Skip this property setter 798 } 799 } 800 if (LOG.isTraceEnabled()) { 801 LOG.trace(" Target bean = " + target); 802 LOG.trace(" Target name = " + name); 803 } 804 805 // Declare local variables we will require 806 final String propName = resolver.getProperty(name); // Simple name of target property 807 Class<?> type = null; // Java type of target property 808 final int index = resolver.getIndex(name); // Indexed subscript value (if any) 809 final String key = resolver.getKey(name); // Mapped key value (if any) 810 811 // Calculate the property type 812 if (target instanceof DynaBean) { 813 final DynaClass dynaClass = ((DynaBean) target).getDynaClass(); 814 final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName); 815 if (dynaProperty == null) { 816 return; // Skip this property setter 817 } 818 type = dynaPropertyType(dynaProperty, value); 819 if (index >= 0 && List.class.isAssignableFrom(type)) { 820 type = Object.class; 821 } 822 } else if (target instanceof Map) { 823 type = Object.class; 824 } else if (target != null && target.getClass().isArray() && index >= 0) { 825 type = Array.get(target, index).getClass(); 826 } else { 827 PropertyDescriptor descriptor = null; 828 try { 829 descriptor = getPropertyUtils().getPropertyDescriptor(target, name); 830 if (descriptor == null) { 831 return; // Skip this property setter 832 } 833 } catch (final NoSuchMethodException e) { 834 return; // Skip this property setter 835 } 836 if (descriptor instanceof MappedPropertyDescriptor) { 837 if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) { 838 if (LOG.isDebugEnabled()) { 839 LOG.debug("Skipping read-only property"); 840 } 841 return; // Read-only, skip this property setter 842 } 843 type = ((MappedPropertyDescriptor) descriptor).getMappedPropertyType(); 844 } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) { 845 if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) { 846 if (LOG.isDebugEnabled()) { 847 LOG.debug("Skipping read-only property"); 848 } 849 return; // Read-only, skip this property setter 850 } 851 type = ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType(); 852 } else if (index >= 0 && List.class.isAssignableFrom(descriptor.getPropertyType())) { 853 type = Object.class; 854 } else if (key != null) { 855 if (descriptor.getReadMethod() == null) { 856 if (LOG.isDebugEnabled()) { 857 LOG.debug("Skipping read-only property"); 858 } 859 return; // Read-only, skip this property setter 860 } 861 type = value == null ? Object.class : value.getClass(); 862 } else { 863 if (descriptor.getWriteMethod() == null) { 864 if (LOG.isDebugEnabled()) { 865 LOG.debug("Skipping read-only property"); 866 } 867 return; // Read-only, skip this property setter 868 } 869 type = descriptor.getPropertyType(); 870 } 871 } 872 873 // Convert the specified value to the required type 874 Object newValue = null; 875 if (type.isArray() && index < 0) { // Scalar value into array 876 if (value == null) { 877 final String[] values = new String[1]; 878 values[0] = null; 879 newValue = getConvertUtils().convert(values, type); 880 } else if (value instanceof String) { 881 newValue = getConvertUtils().convert(value, type); 882 } else if (value instanceof String[]) { 883 newValue = getConvertUtils().convert((String[]) value, type); 884 } else { 885 newValue = convert(value, type); 886 } 887 } else if (type.isArray()) { // Indexed value into array 888 if (value instanceof String || value == null) { 889 newValue = getConvertUtils().convert((String) value, type.getComponentType()); 890 } else if (value instanceof String[]) { 891 newValue = getConvertUtils().convert(((String[]) value)[0], type.getComponentType()); 892 } else { 893 newValue = convert(value, type.getComponentType()); 894 } 895 } else if (value instanceof String) { 896 newValue = getConvertUtils().convert((String) value, type); 897 } else if (value instanceof String[]) { 898 newValue = getConvertUtils().convert(((String[]) value)[0], type); 899 } else { 900 newValue = convert(value, type); 901 } 902 903 // Invoke the setter method 904 try { 905 getPropertyUtils().setProperty(target, name, newValue); 906 } catch (final NoSuchMethodException e) { 907 throw new InvocationTargetException(e, "Cannot set " + propName); 908 } 909 } 910}