1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.beanutils;
19
20
21 import java.beans.IndexedPropertyDescriptor;
22 import java.beans.PropertyDescriptor;
23 import java.lang.reflect.Array;
24 import java.lang.reflect.InvocationTargetException;
25 import java.lang.reflect.Method;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.Map;
31 import java.util.WeakHashMap;
32
33 import org.apache.commons.collections.FastHashMap;
34 import org.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36
37
38 /**
39 * <p>JavaBean property population methods.</p>
40 *
41 * <p>This class provides implementations for the utility methods in
42 * {@link BeanUtils}.
43 * Different instances can be used to isolate caches between classloaders
44 * and to vary the value converters registered.</p>
45 *
46 * @author Craig R. McClanahan
47 * @author Ralph Schaer
48 * @author Chris Audley
49 * @author Rey Fran?ois
50 * @author Gregor Ra?man
51 * @version $Revision: 1.16 $ $Date: 2004/02/28 13:18:33 $
52 * @see BeanUtils
53 * @since 1.7
54 */
55
56 public class BeanUtilsBean {
57
58
59
60
61 /**
62 * Contains <code>BeanUtilsBean</code> instances indexed by context classloader.
63 */
64 private static final ContextClassLoaderLocal
65 beansByClassLoader = new ContextClassLoaderLocal() {
66
67 protected Object initialValue() {
68 return new BeanUtilsBean();
69 }
70 };
71
72 /**
73 * Gets the instance which provides the functionality for {@link BeanUtils}.
74 * This is a pseudo-singleton - an single instance is provided per (thread) context classloader.
75 * This mechanism provides isolation for web apps deployed in the same container.
76 */
77 public synchronized static BeanUtilsBean getInstance() {
78 return (BeanUtilsBean) beansByClassLoader.get();
79 }
80
81 /**
82 * Sets the instance which provides the functionality for {@link BeanUtils}.
83 * This is a pseudo-singleton - an single instance is provided per (thread) context classloader.
84 * This mechanism provides isolation for web apps deployed in the same container.
85 */
86 public synchronized static void setInstance(BeanUtilsBean newInstance) {
87 beansByClassLoader.set(newInstance);
88 }
89
90
91
92 /**
93 * Logging for this instance
94 */
95 private Log log = LogFactory.getLog(BeanUtils.class);
96
97 /** Used to perform conversions between object types when setting properties */
98 private ConvertUtilsBean convertUtilsBean;
99
100 /** Used to access properties*/
101 private PropertyUtilsBean propertyUtilsBean;
102
103
104
105 /**
106 * <p>Constructs an instance using new property
107 * and conversion instances.</p>
108 */
109 public BeanUtilsBean() {
110 this(new ConvertUtilsBean(), new PropertyUtilsBean());
111 }
112
113 /**
114 * <p>Constructs an instance using given property and conversion instances.</p>
115 *
116 * @param convertUtilsBean use this <code>ConvertUtilsBean</code>
117 * to perform conversions from one object to another
118 * @param propertyUtilsBean use this <code>PropertyUtilsBean</code>
119 * to access properties
120 */
121 public BeanUtilsBean(
122 ConvertUtilsBean convertUtilsBean,
123 PropertyUtilsBean propertyUtilsBean) {
124
125 this.convertUtilsBean = convertUtilsBean;
126 this.propertyUtilsBean = propertyUtilsBean;
127 }
128
129
130
131 /**
132 * <p>Clone a bean based on the available property getters and setters,
133 * even if the bean class itself does not implement Cloneable.</p>
134 *
135 * <p>
136 * <strong>Note:</strong> this method creates a <strong>shallow</strong> clone.
137 * In other words, any objects referred to by the bean are shared with the clone
138 * rather than being cloned in turn.
139 * </p>
140 *
141 * @param bean Bean to be cloned
142 *
143 * @exception IllegalAccessException if the caller does not have
144 * access to the property accessor method
145 * @exception InstantiationException if a new instance of the bean's
146 * class cannot be instantiated
147 * @exception InvocationTargetException if the property accessor method
148 * throws an exception
149 * @exception NoSuchMethodException if an accessor method for this
150 * property cannot be found
151 */
152 public Object cloneBean(Object bean)
153 throws IllegalAccessException, InstantiationException,
154 InvocationTargetException, NoSuchMethodException {
155
156 if (log.isDebugEnabled()) {
157 log.debug("Cloning bean: " + bean.getClass().getName());
158 }
159 Class clazz = bean.getClass();
160 Object newBean = null;
161 if (bean instanceof DynaBean) {
162 newBean = ((DynaBean) bean).getDynaClass().newInstance();
163 } else {
164 newBean = bean.getClass().newInstance();
165 }
166 getPropertyUtils().copyProperties(newBean, bean);
167 return (newBean);
168
169 }
170
171
172 /**
173 * <p>Copy property values from the origin bean to the destination bean
174 * for all cases where the property names are the same. For each
175 * property, a conversion is attempted as necessary. All combinations of
176 * standard JavaBeans and DynaBeans as origin and destination are
177 * supported. Properties that exist in the origin bean, but do not exist
178 * in the destination bean (or are read-only in the destination bean) are
179 * silently ignored.</p>
180 *
181 * <p>If the origin "bean" is actually a <code>Map</code>, it is assumed
182 * to contain String-valued <strong>simple</strong> property names as the keys, pointing at
183 * the corresponding property values that will be converted (if necessary)
184 * and set in the destination bean. <strong>Note</strong> that this method
185 * is intended to perform a "shallow copy" of the properties and so complex
186 * properties (for example, nested ones) will not be copied.</p>
187 *
188 * <p>This method differs from <code>populate()</code>, which
189 * was primarily designed for populating JavaBeans from the map of request
190 * parameters retrieved on an HTTP request, is that no scalar->indexed
191 * or indexed->scalar manipulations are performed. If the origin property
192 * is indexed, the destination property must be also.</p>
193 *
194 * <p>If you know that no type conversions are required, the
195 * <code>copyProperties()</code> method in {@link PropertyUtils} will
196 * execute faster than this method.</p>
197 *
198 * <p><strong>FIXME</strong> - Indexed and mapped properties that do not
199 * have getter and setter methods for the underlying array or Map are not
200 * copied by this method.</p>
201 *
202 * @param dest Destination bean whose properties are modified
203 * @param orig Origin bean whose properties are retrieved
204 *
205 * @exception IllegalAccessException if the caller does not have
206 * access to the property accessor method
207 * @exception IllegalArgumentException if the <code>dest</code> or
208 * <code>orig</code> argument is null
209 * @exception InvocationTargetException if the property accessor method
210 * throws an exception
211 */
212 public void copyProperties(Object dest, Object orig)
213 throws IllegalAccessException, InvocationTargetException {
214
215
216 if (dest == null) {
217 throw new IllegalArgumentException
218 ("No destination bean specified");
219 }
220 if (orig == null) {
221 throw new IllegalArgumentException("No origin bean specified");
222 }
223 if (log.isDebugEnabled()) {
224 log.debug("BeanUtils.copyProperties(" + dest + ", " +
225 orig + ")");
226 }
227
228
229 if (orig instanceof DynaBean) {
230 DynaProperty origDescriptors[] =
231 ((DynaBean) orig).getDynaClass().getDynaProperties();
232 for (int i = 0; i < origDescriptors.length; i++) {
233 String name = origDescriptors[i].getName();
234 if (getPropertyUtils().isWriteable(dest, name)) {
235 Object value = ((DynaBean) orig).get(name);
236 copyProperty(dest, name, value);
237 }
238 }
239 } else if (orig instanceof Map) {
240 Iterator names = ((Map) orig).keySet().iterator();
241 while (names.hasNext()) {
242 String name = (String) names.next();
243 if (getPropertyUtils().isWriteable(dest, name)) {
244 Object value = ((Map) orig).get(name);
245 copyProperty(dest, name, value);
246 }
247 }
248 } else
249 PropertyDescriptor origDescriptors[] =
250 getPropertyUtils().getPropertyDescriptors(orig);
251 for (int i = 0; i < origDescriptors.length; i++) {
252 String name = origDescriptors[i].getName();
253 if ("class".equals(name)) {
254 continue;
255 }
256 if (getPropertyUtils().isReadable(orig, name) &&
257 getPropertyUtils().isWriteable(dest, name)) {
258 try {
259 Object value =
260 getPropertyUtils().getSimpleProperty(orig, name);
261 copyProperty(dest, name, value);
262 } catch (NoSuchMethodException e) {
263 ;
264 }
265 }
266 }
267 }
268
269 }
270
271
272 /**
273 * <p>Copy the specified property value to the specified destination bean,
274 * performing any type conversion that is required. If the specified
275 * bean does not have a property of the specified name, or the property
276 * is read only on the destination bean, return without
277 * doing anything. If you have custom destination property types, register
278 * {@link Converter}s for them by calling the <code>register()</code>
279 * method of {@link ConvertUtils}.</p>
280 *
281 * <p><strong>IMPLEMENTATION RESTRICTIONS</strong>:</p>
282 * <ul>
283 * <li>Does not support destination properties that are indexed,
284 * but only an indexed setter (as opposed to an array setter)
285 * is available.</li>
286 * <li>Does not support destination properties that are mapped,
287 * but only a keyed setter (as opposed to a Map setter)
288 * is available.</li>
289 * <li>The desired property type of a mapped setter cannot be
290 * determined (since Maps support any data type), so no conversion
291 * will be performed.</li>
292 * </ul>
293 *
294 * @param bean Bean on which setting is to be performed
295 * @param name Property name (can be nested/indexed/mapped/combo)
296 * @param value Value to be set
297 *
298 * @exception IllegalAccessException if the caller does not have
299 * access to the property accessor method
300 * @exception InvocationTargetException if the property accessor method
301 * throws an exception
302 */
303 public void copyProperty(Object bean, String name, Object value)
304 throws IllegalAccessException, InvocationTargetException {
305
306
307 if (log.isTraceEnabled()) {
308 StringBuffer sb = new StringBuffer(" copyProperty(");
309 sb.append(bean);
310 sb.append(", ");
311 sb.append(name);
312 sb.append(", ");
313 if (value == null) {
314 sb.append("<NULL>");
315 } else if (value instanceof String) {
316 sb.append((String) value);
317 } else if (value instanceof String[]) {
318 String values[] = (String[]) value;
319 sb.append('[');
320 for (int i = 0; i < values.length; i++) {
321 if (i > 0) {
322 sb.append(',');
323 }
324 sb.append(values[i]);
325 }
326 sb.append(']');
327 } else {
328 sb.append(value.toString());
329 }
330 sb.append(')');
331 log.trace(sb.toString());
332 }
333
334
335 Object target = bean;
336 int delim = name.lastIndexOf(PropertyUtils.NESTED_DELIM);
337 if (delim >= 0) {
338 try {
339 target =
340 getPropertyUtils().getProperty(bean, name.substring(0, delim));
341 } catch (NoSuchMethodException e) {
342 return;
343 }
344 name = name.substring(delim + 1);
345 if (log.isTraceEnabled()) {
346 log.trace(" Target bean = " + target);
347 log.trace(" Target name = " + name);
348 }
349 }
350
351
352 String propName = null;
353 Class type = null;
354 int index = -1;
355 String key = null;
356
357
358 propName = name;
359 int i = propName.indexOf(PropertyUtils.INDEXED_DELIM);
360 if (i >= 0) {
361 int k = propName.indexOf(PropertyUtils.INDEXED_DELIM2);
362 try {
363 index =
364 Integer.parseInt(propName.substring(i + 1, k));
365 } catch (NumberFormatException e) {
366 ;
367 }
368 propName = propName.substring(0, i);
369 }
370 int j = propName.indexOf(PropertyUtils.MAPPED_DELIM);
371 if (j >= 0) {
372 int k = propName.indexOf(PropertyUtils.MAPPED_DELIM2);
373 try {
374 key = propName.substring(j + 1, k);
375 } catch (IndexOutOfBoundsException e) {
376 ;
377 }
378 propName = propName.substring(0, j);
379 }
380
381
382 if (target instanceof DynaBean) {
383 DynaClass dynaClass = ((DynaBean) target).getDynaClass();
384 DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
385 if (dynaProperty == null) {
386 return;
387 }
388 type = dynaProperty.getType();
389 } else {
390 PropertyDescriptor descriptor = null;
391 try {
392 descriptor =
393 getPropertyUtils().getPropertyDescriptor(target, name);
394 if (descriptor == null) {
395 return;
396 }
397 } catch (NoSuchMethodException e) {
398 return;
399 }
400 type = descriptor.getPropertyType();
401 if (type == null) {
402
403 if (log.isTraceEnabled()) {
404 log.trace(" target type for property '" +
405 propName + "' is null, so skipping ths setter");
406 }
407 return;
408 }
409 }
410 if (log.isTraceEnabled()) {
411 log.trace(" target propName=" + propName + ", type=" +
412 type + ", index=" + index + ", key=" + key);
413 }
414
415
416 if (index >= 0) {
417 Converter converter = getConvertUtils().lookup(type.getComponentType());
418 if (converter != null) {
419 log.trace(" USING CONVERTER " + converter);
420 value = converter.convert(type, value);
421 }
422 try {
423 getPropertyUtils().setIndexedProperty(target, propName,
424 index, value);
425 } catch (NoSuchMethodException e) {
426 throw new InvocationTargetException
427 (e, "Cannot set " + propName);
428 }
429 } else if (key != null) {
430
431
432
433 try {
434 getPropertyUtils().setMappedProperty(target, propName,
435 key, value);
436 } catch (NoSuchMethodException e) {
437 throw new InvocationTargetException
438 (e, "Cannot set " + propName);
439 }
440 } else {
441 Converter converter = getConvertUtils().lookup(type);
442 if (converter != null) {
443 log.trace(" USING CONVERTER " + converter);
444 value = converter.convert(type, value);
445 }
446 try {
447 getPropertyUtils().setSimpleProperty(target, propName, value);
448 } catch (NoSuchMethodException e) {
449 throw new InvocationTargetException
450 (e, "Cannot set " + propName);
451 }
452 }
453
454 }
455
456
457 /**
458 * <p>Return the entire set of properties for which the specified bean
459 * provides a read method. This map contains the to <code>String</code>
460 * converted property values for all properties for which a read method
461 * is provided (i.e. where the getReadMethod() returns non-null).</p>
462 *
463 * <p>This map can be fed back to a call to
464 * <code>BeanUtils.populate()</code> to reconsitute the same set of
465 * properties, modulo differences for read-only and write-only
466 * properties, but only if there are no indexed properties.</p>
467 *
468 * @param bean Bean whose properties are to be extracted
469 *
470 * @exception IllegalAccessException if the caller does not have
471 * access to the property accessor method
472 * @exception InvocationTargetException if the property accessor method
473 * throws an exception
474 * @exception NoSuchMethodException if an accessor method for this
475 * property cannot be found
476 */
477 public Map describe(Object bean)
478 throws IllegalAccessException, InvocationTargetException,
479 NoSuchMethodException {
480
481 if (bean == null) {
482
483 return (new java.util.HashMap());
484 }
485
486 if (log.isDebugEnabled()) {
487 log.debug("Describing bean: " + bean.getClass().getName());
488 }
489
490 Map description = new HashMap();
491 if (bean instanceof DynaBean) {
492 DynaProperty descriptors[] =
493 ((DynaBean) bean).getDynaClass().getDynaProperties();
494 for (int i = 0; i < descriptors.length; i++) {
495 String name = descriptors[i].getName();
496 description.put(name, getProperty(bean, name));
497 }
498 } else {
499 PropertyDescriptor descriptors[] =
500 getPropertyUtils().getPropertyDescriptors(bean);
501 for (int i = 0; i < descriptors.length; i++) {
502 String name = descriptors[i].getName();
503 if (descriptors[i].getReadMethod() != null)
504 description.put(name, getProperty(bean, name));
505 }
506 }
507 return (description);
508
509 }
510
511
512 /**
513 * Return the value of the specified array property of the specified
514 * bean, as a String array.
515 *
516 * @param bean Bean whose property is to be extracted
517 * @param name Name of the property to be extracted
518 *
519 * @exception IllegalAccessException if the caller does not have
520 * access to the property accessor method
521 * @exception InvocationTargetException if the property accessor method
522 * throws an exception
523 * @exception NoSuchMethodException if an accessor method for this
524 * property cannot be found
525 */
526 public String[] getArrayProperty(Object bean, String name)
527 throws IllegalAccessException, InvocationTargetException,
528 NoSuchMethodException {
529
530 Object value = getPropertyUtils().getProperty(bean, name);
531 if (value == null) {
532 return (null);
533 } else if (value instanceof Collection) {
534 ArrayList values = new ArrayList();
535 Iterator items = ((Collection) value).iterator();
536 while (items.hasNext()) {
537 Object item = items.next();
538 if (item == null) {
539 values.add((String) null);
540 } else {
541
542 values.add(getConvertUtils().convert(item));
543 }
544 }
545 return ((String[]) values.toArray(new String[values.size()]));
546 } else if (value.getClass().isArray()) {
547 int n = Array.getLength(value);
548 String results[] = new String[n];
549 for (int i = 0; i < n; i++) {
550 Object item = Array.get(value, i);
551 if (item == null) {
552 results[i] = null;
553 } else {
554
555 results[i] = getConvertUtils().convert(item);
556 }
557 }
558 return (results);
559 } else {
560 String results[] = new String[1];
561 results[0] = value.toString();
562 return (results);
563 }
564
565 }
566
567
568 /**
569 * Return the value of the specified indexed property of the specified
570 * bean, as a String. The zero-relative index of the
571 * required value must be included (in square brackets) as a suffix to
572 * the property name, or <code>IllegalArgumentException</code> will be
573 * thrown.
574 *
575 * @param bean Bean whose property is to be extracted
576 * @param name <code>propertyname[index]</code> of the property value
577 * to be extracted
578 *
579 * @exception IllegalAccessException if the caller does not have
580 * access to the property accessor method
581 * @exception InvocationTargetException if the property accessor method
582 * throws an exception
583 * @exception NoSuchMethodException if an accessor method for this
584 * property cannot be found
585 */
586 public String getIndexedProperty(Object bean, String name)
587 throws IllegalAccessException, InvocationTargetException,
588 NoSuchMethodException {
589
590 Object value = getPropertyUtils().getIndexedProperty(bean, name);
591 return (getConvertUtils().convert(value));
592
593 }
594
595
596 /**
597 * Return the value of the specified indexed property of the specified
598 * bean, as a String. The index is specified as a method parameter and
599 * must *not* be included in the property name expression
600 *
601 * @param bean Bean whose property is to be extracted
602 * @param name Simple property name of the property value to be extracted
603 * @param index Index of the property value to be extracted
604 *
605 * @exception IllegalAccessException if the caller does not have
606 * access to the property accessor method
607 * @exception InvocationTargetException if the property accessor method
608 * throws an exception
609 * @exception NoSuchMethodException if an accessor method for this
610 * property cannot be found
611 */
612 public String getIndexedProperty(Object bean,
613 String name, int index)
614 throws IllegalAccessException, InvocationTargetException,
615 NoSuchMethodException {
616
617 Object value = getPropertyUtils().getIndexedProperty(bean, name, index);
618 return (getConvertUtils().convert(value));
619
620 }
621
622
623 /**
624 * Return the value of the specified indexed property of the specified
625 * bean, as a String. The String-valued key of the required value
626 * must be included (in parentheses) as a suffix to
627 * the property name, or <code>IllegalArgumentException</code> will be
628 * thrown.
629 *
630 * @param bean Bean whose property is to be extracted
631 * @param name <code>propertyname(index)</code> of the property value
632 * to be extracted
633 *
634 * @exception IllegalAccessException if the caller does not have
635 * access to the property accessor method
636 * @exception InvocationTargetException if the property accessor method
637 * throws an exception
638 * @exception NoSuchMethodException if an accessor method for this
639 * property cannot be found
640 */
641 public String getMappedProperty(Object bean, String name)
642 throws IllegalAccessException, InvocationTargetException,
643 NoSuchMethodException {
644
645 Object value = getPropertyUtils().getMappedProperty(bean, name);
646 return (getConvertUtils().convert(value));
647
648 }
649
650
651 /**
652 * Return the value of the specified mapped property of the specified
653 * bean, as a String. The key is specified as a method parameter and
654 * must *not* be included in the property name expression
655 *
656 * @param bean Bean whose property is to be extracted
657 * @param name Simple property name of the property value to be extracted
658 * @param key Lookup key of the property value to be extracted
659 *
660 * @exception IllegalAccessException if the caller does not have
661 * access to the property accessor method
662 * @exception InvocationTargetException if the property accessor method
663 * throws an exception
664 * @exception NoSuchMethodException if an accessor method for this
665 * property cannot be found
666 */
667 public String getMappedProperty(Object bean,
668 String name, String key)
669 throws IllegalAccessException, InvocationTargetException,
670 NoSuchMethodException {
671
672 Object value = getPropertyUtils().getMappedProperty(bean, name, key);
673 return (getConvertUtils().convert(value));
674
675 }
676
677
678 /**
679 * Return the value of the (possibly nested) property of the specified
680 * name, for the specified bean, as a String.
681 *
682 * @param bean Bean whose property is to be extracted
683 * @param name Possibly nested name of the property to be extracted
684 *
685 * @exception IllegalAccessException if the caller does not have
686 * access to the property accessor method
687 * @exception IllegalArgumentException if a nested reference to a
688 * property returns null
689 * @exception InvocationTargetException if the property accessor method
690 * throws an exception
691 * @exception NoSuchMethodException if an accessor method for this
692 * property cannot be found
693 */
694 public String getNestedProperty(Object bean, String name)
695 throws IllegalAccessException, InvocationTargetException,
696 NoSuchMethodException {
697
698 Object value = getPropertyUtils().getNestedProperty(bean, name);
699 return (getConvertUtils().convert(value));
700
701 }
702
703
704 /**
705 * Return the value of the specified property of the specified bean,
706 * no matter which property reference format is used, as a String.
707 *
708 * @param bean Bean whose property is to be extracted
709 * @param name Possibly indexed and/or nested name of the property
710 * to be extracted
711 *
712 * @exception IllegalAccessException if the caller does not have
713 * access to the property accessor method
714 * @exception InvocationTargetException if the property accessor method
715 * throws an exception
716 * @exception NoSuchMethodException if an accessor method for this
717 * property cannot be found
718 */
719 public String getProperty(Object bean, String name)
720 throws IllegalAccessException, InvocationTargetException,
721 NoSuchMethodException {
722
723 return (getNestedProperty(bean, name));
724
725 }
726
727
728 /**
729 * Return the value of the specified simple property of the specified
730 * bean, converted to a String.
731 *
732 * @param bean Bean whose property is to be extracted
733 * @param name Name of the property to be extracted
734 *
735 * @exception IllegalAccessException if the caller does not have
736 * access to the property accessor method
737 * @exception InvocationTargetException if the property accessor method
738 * throws an exception
739 * @exception NoSuchMethodException if an accessor method for this
740 * property cannot be found
741 */
742 public String getSimpleProperty(Object bean, String name)
743 throws IllegalAccessException, InvocationTargetException,
744 NoSuchMethodException {
745
746 Object value = getPropertyUtils().getSimpleProperty(bean, name);
747 return (getConvertUtils().convert(value));
748
749 }
750
751
752 /**
753 * <p>Populate the JavaBeans properties of the specified bean, based on
754 * the specified name/value pairs. This method uses Java reflection APIs
755 * to identify corresponding "property setter" method names, and deals
756 * with setter arguments of type <code>String</code>, <code>boolean</code>,
757 * <code>int</code>, <code>long</code>, <code>float</code>, and
758 * <code>double</code>. In addition, array setters for these types (or the
759 * corresponding primitive types) can also be identified.</p>
760 *
761 * <p>The particular setter method to be called for each property is
762 * determined using the usual JavaBeans introspection mechanisms. Thus,
763 * you may identify custom setter methods using a BeanInfo class that is
764 * associated with the class of the bean itself. If no such BeanInfo
765 * class is available, the standard method name conversion ("set" plus
766 * the capitalized name of the property in question) is used.</p>
767 *
768 * <p><strong>NOTE</strong>: It is contrary to the JavaBeans Specification
769 * to have more than one setter method (with different argument
770 * signatures) for the same property.</p>
771 *
772 * <p><strong>WARNING</strong> - The logic of this method is customized
773 * for extracting String-based request parameters from an HTTP request.
774 * It is probably not what you want for general property copying with
775 * type conversion. For that purpose, check out the
776 * <code>copyProperties()</code> method instead.</p>
777 *
778 * @param bean JavaBean whose properties are being populated
779 * @param properties Map keyed by property name, with the
780 * corresponding (String or String[]) value(s) to be set
781 *
782 * @exception IllegalAccessException if the caller does not have
783 * access to the property accessor method
784 * @exception InvocationTargetException if the property accessor method
785 * throws an exception
786 */
787 public void populate(Object bean, Map properties)
788 throws IllegalAccessException, InvocationTargetException {
789
790
791 if ((bean == null) || (properties == null)) {
792 return;
793 }
794 if (log.isDebugEnabled()) {
795 log.debug("BeanUtils.populate(" + bean + ", " +
796 properties + ")");
797 }
798
799
800 Iterator names = properties.keySet().iterator();
801 while (names.hasNext()) {
802
803
804 String name = (String) names.next();
805 if (name == null) {
806 continue;
807 }
808 Object value = properties.get(name);
809
810
811 setProperty(bean, name, value);
812
813 }
814
815 }
816
817
818 /**
819 * <p>Set the specified property value, performing type conversions as
820 * required to conform to the type of the destination property.</p>
821 *
822 * <p>If the property is read only then the method returns
823 * without throwing an exception.</p>
824 *
825 * <p>If <code>null</code> is passed into a property expecting a primitive value,
826 * then this will be converted as if it were a <code>null</code> string.</p>
827 *
828 * <p><strong>WARNING</strong> - The logic of this method is customized
829 * to meet the needs of <code>populate()</code>, and is probably not what
830 * you want for general property copying with type conversion. For that
831 * purpose, check out the <code>copyProperty()</code> method instead.</p>
832 *
833 * <p><strong>WARNING</strong> - PLEASE do not modify the behavior of this
834 * method without consulting with the Struts developer community. There
835 * are some subtleties to its functionality that are not documented in the
836 * Javadoc description above, yet are vital to the way that Struts utilizes
837 * this method.</p>
838 *
839 * @param bean Bean on which setting is to be performed
840 * @param name Property name (can be nested/indexed/mapped/combo)
841 * @param value Value to be set
842 *
843 * @exception IllegalAccessException if the caller does not have
844 * access to the property accessor method
845 * @exception InvocationTargetException if the property accessor method
846 * throws an exception
847 */
848 public void setProperty(Object bean, String name, Object value)
849 throws IllegalAccessException, InvocationTargetException {
850
851
852 if (log.isTraceEnabled()) {
853 StringBuffer sb = new StringBuffer(" setProperty(");
854 sb.append(bean);
855 sb.append(", ");
856 sb.append(name);
857 sb.append(", ");
858 if (value == null) {
859 sb.append("<NULL>");
860 } else if (value instanceof String) {
861 sb.append((String) value);
862 } else if (value instanceof String[]) {
863 String values[] = (String[]) value;
864 sb.append('[');
865 for (int i = 0; i < values.length; i++) {
866 if (i > 0) {
867 sb.append(',');
868 }
869 sb.append(values[i]);
870 }
871 sb.append(']');
872 } else {
873 sb.append(value.toString());
874 }
875 sb.append(')');
876 log.trace(sb.toString());
877 }
878
879
880 Object target = bean;
881 int delim = findLastNestedIndex(name);
882 if (delim >= 0) {
883 try {
884 target =
885 getPropertyUtils().getProperty(bean, name.substring(0, delim));
886 } catch (NoSuchMethodException e) {
887 return;
888 }
889 name = name.substring(delim + 1);
890 if (log.isTraceEnabled()) {
891 log.trace(" Target bean = " + target);
892 log.trace(" Target name = " + name);
893 }
894 }
895
896
897 String propName = null;
898 Class type = null;
899 int index = -1;
900 String key = null;
901
902
903 propName = name;
904 int i = propName.indexOf(PropertyUtils.INDEXED_DELIM);
905 if (i >= 0) {
906 int k = propName.indexOf(PropertyUtils.INDEXED_DELIM2);
907 try {
908 index =
909 Integer.parseInt(propName.substring(i + 1, k));
910 } catch (NumberFormatException e) {
911 ;
912 }
913 propName = propName.substring(0, i);
914 }
915 int j = propName.indexOf(PropertyUtils.MAPPED_DELIM);
916 if (j >= 0) {
917 int k = propName.indexOf(PropertyUtils.MAPPED_DELIM2);
918 try {
919 key = propName.substring(j + 1, k);
920 } catch (IndexOutOfBoundsException e) {
921 ;
922 }
923 propName = propName.substring(0, j);
924 }
925
926
927 if (target instanceof DynaBean) {
928 DynaClass dynaClass = ((DynaBean) target).getDynaClass();
929 DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
930 if (dynaProperty == null) {
931 return;
932 }
933 type = dynaProperty.getType();
934 } else {
935 PropertyDescriptor descriptor = null;
936 try {
937 descriptor =
938 getPropertyUtils().getPropertyDescriptor(target, name);
939 if (descriptor == null) {
940 return;
941 }
942 } catch (NoSuchMethodException e) {
943 return;
944 }
945 if (descriptor instanceof MappedPropertyDescriptor) {
946 if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) {
947 if (log.isDebugEnabled()) {
948 log.debug("Skipping read-only property");
949 }
950 return;
951 }
952 type = ((MappedPropertyDescriptor) descriptor).
953 getMappedPropertyType();
954 } else if (descriptor instanceof IndexedPropertyDescriptor) {
955 if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) {
956 if (log.isDebugEnabled()) {
957 log.debug("Skipping read-only property");
958 }
959 return;
960 }
961 type = ((IndexedPropertyDescriptor) descriptor).
962 getIndexedPropertyType();
963 } else {
964 if (descriptor.getWriteMethod() == null) {
965 if (log.isDebugEnabled()) {
966 log.debug("Skipping read-only property");
967 }
968 return;
969 }
970 type = descriptor.getPropertyType();
971 }
972 }
973
974
975 Object newValue = null;
976 if (type.isArray() && (index < 0)) {
977 if (value == null) {
978 String values[] = new String[1];
979 values[0] = (String) value;
980 newValue = getConvertUtils().convert((String[]) values, type);
981 } else if (value instanceof String) {
982 String values[] = new String[1];
983 values[0] = (String) value;
984 newValue = getConvertUtils().convert((String[]) values, type);
985 } else if (value instanceof String[]) {
986 newValue = getConvertUtils().convert((String[]) value, type);
987 } else {
988 newValue = value;
989 }
990 } else if (type.isArray()) {
991 if (value instanceof String) {
992 newValue = getConvertUtils().convert((String) value,
993 type.getComponentType());
994 } else if (value instanceof String[]) {
995 newValue = getConvertUtils().convert(((String[]) value)[0],
996 type.getComponentType());
997 } else {
998 newValue = value;
999 }
1000 } else {
1001 if ((value instanceof String) || (value == null)) {
1002 newValue = getConvertUtils().convert((String) value, type);
1003 } else if (value instanceof String[]) {
1004 newValue = getConvertUtils().convert(((String[]) value)[0],
1005 type);
1006 } else if (getConvertUtils().lookup(value.getClass()) != null) {
1007 newValue = getConvertUtils().convert(value.toString(), type);
1008 } else {
1009 newValue = value;
1010 }
1011 }
1012
1013
1014 try {
1015 if (index >= 0) {
1016 getPropertyUtils().setIndexedProperty(target, propName,
1017 index, newValue);
1018 } else if (key != null) {
1019 getPropertyUtils().setMappedProperty(target, propName,
1020 key, newValue);
1021 } else {
1022 getPropertyUtils().setProperty(target, propName, newValue);
1023 }
1024 } catch (NoSuchMethodException e) {
1025 throw new InvocationTargetException
1026 (e, "Cannot set " + propName);
1027 }
1028
1029 }
1030
1031 private int findLastNestedIndex(String expression)
1032 {
1033
1034
1035 int bracketCount = 0;
1036 for (int i = expression.length() - 1; i>=0 ; i--) {
1037 char at = expression.charAt(i);
1038 switch (at) {
1039 case PropertyUtils.NESTED_DELIM:
1040 if (bracketCount < 1) {
1041 return i;
1042 }
1043 break;
1044
1045 case PropertyUtils.MAPPED_DELIM:
1046 case PropertyUtils.INDEXED_DELIM:
1047
1048 --bracketCount;
1049 break;
1050
1051 case PropertyUtils.MAPPED_DELIM2:
1052 case PropertyUtils.INDEXED_DELIM2:
1053
1054 ++bracketCount;
1055 break;
1056 }
1057 }
1058
1059 return -1;
1060 }
1061
1062 /**
1063 * Gets the <code>ConvertUtilsBean</code> instance used to perform the conversions.
1064 */
1065 public ConvertUtilsBean getConvertUtils() {
1066 return convertUtilsBean;
1067 }
1068
1069 /**
1070 * Gets the <code>PropertyUtilsBean</code> instance used to access properties.
1071 */
1072 public PropertyUtilsBean getPropertyUtils() {
1073 return propertyUtilsBean;
1074 }
1075 }