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