1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.beanutils2;
19
20 import java.beans.IndexedPropertyDescriptor;
21 import java.beans.PropertyDescriptor;
22 import java.lang.reflect.Array;
23 import java.lang.reflect.InvocationTargetException;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29
30 import org.apache.commons.beanutils2.expression.Resolver;
31 import org.apache.commons.logging.Log;
32 import org.apache.commons.logging.LogFactory;
33
34 /**
35 * TODO docs
36 *
37 * <p>
38 * 2.0
39 * </p>
40 *
41 * <p>
42 * {@link BeanUtilsBean} implementation that creates a {@link ConvertUtilsBean} and delegates conversion to {@link ConvertUtilsBean#convert(Object, Class)}.
43 * </p>
44 *
45 * <p>
46 * To configure this implementation for the current context ClassLoader invoke {@code BeanUtilsBean.setInstance(new BeanUtilsBean2());}
47 * </p>
48 *
49 * <p>
50 * 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
51 * to upgrade the {@link Converter} implementations so that they could handle conversion to String for their type (for example IntegerConverter now handles
52 * conversion from an Integer to a String as well as String to Integer).
53 * </p>
54 *
55 * <p>
56 * In order to take advantage of these improvements BeanUtils needs to change how it gets the appropriate {@link Converter}. This functionality has been
57 * implemented in the new {@link ConvertUtilsBean#lookup(Class, Class)} and {@link ConvertUtilsBean#convert(Object, Class)} methods. However changing
58 * {@link BeanUtilsBean} to use these methods could create compatibility issues for existing users. In order to avoid that, this new {@link BeanUtilsBean}
59 * implementation has been created (and the associated {@link ConvertUtilsBean}).
60 * </p>
61 *
62 * <p>
63 * Pre-2.0
64 * </p>
65 *
66 * <p>
67 * JavaBean property population methods.
68 * </p>
69 *
70 * <p>
71 * This class provides implementations for the utility methods in {@link BeanUtils}. Different instances can be used to isolate caches between class loaders and
72 * to vary the value converters registered.
73 * </p>
74 *
75 * @see BeanUtils
76 * @since 1.7
77 */
78 public class BeanUtilsBean {
79
80 /**
81 * Contains {@code BeanUtilsBean} instances indexed by context classloader.
82 */
83 private static final ContextClassLoaderLocal<BeanUtilsBean> BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
84 // Creates the default instance used when the context classloader is unavailable
85 @Override
86 protected BeanUtilsBean initialValue() {
87 return new BeanUtilsBean();
88 }
89 };
90
91 /**
92 * Logging for this instance
93 */
94 private static final Log LOG = LogFactory.getLog(BeanUtilsBean.class);
95
96 /**
97 * Determines the type of a {@code DynaProperty}. Here a special treatment is needed for mapped properties.
98 *
99 * @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 }