View Javadoc

1   /*
2    * Copyright 2001-2004 The Apache Software Foundation.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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      // ------------------------------------------------------ Private Class Variables
60  
61      /** 
62       * Contains <code>BeanUtilsBean</code> instances indexed by context classloader.
63       */
64      private static final ContextClassLoaderLocal 
65              beansByClassLoader = new ContextClassLoaderLocal() {
66                          // Creates the default instance used when the context classloader is unavailable
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      // --------------------------------------------------------- Attributes
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     // --------------------------------------------------------- Constuctors
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     // --------------------------------------------------------- Public Methods
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         // Validate existence of the specified beans
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         // Copy the properties, converting as necessary
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 /* if (orig is a standard JavaBean) */ {
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; // No point in trying to set an object's class
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                         ; // Should not happen
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         // Trace logging (if enabled)
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         // Resolve any nested expression to get the actual target bean
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; // Skip this property setter
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         // Declare local variables we will require
352         String propName = null;          // Simple name of target property
353         Class type = null;               // Java type of target property
354         int index = -1;                  // Indexed subscript value (if any)
355         String key = null;               // Mapped key value (if any)
356 
357         // Calculate the target property name, index, and key values
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         // 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             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) {            // Destination must be mapped
430             // Maps do not know what the preferred data type is,
431             // so perform no conversions at all
432             // FIXME - should we create or support a TypedMap?
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 {                             // Destination must be simple
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         //            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         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                     // convert to string using convert utils
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                     // convert to string using convert utils
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         // Do nothing unless both arguments have been specified
791         if ((bean == null) || (properties == null)) {
792             return;
793         }
794         if (log.isDebugEnabled()) {
795             log.debug("BeanUtils.populate(" + bean + ", " +
796                     properties + ")");
797         }
798 
799         // Loop through the property name/value pairs to be set
800         Iterator names = properties.keySet().iterator();
801         while (names.hasNext()) {
802 
803             // Identify the property name and value(s) to be assigned
804             String name = (String) names.next();
805             if (name == null) {
806                 continue;
807             }
808             Object value = properties.get(name);
809 
810             // Perform the assignment for this property
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         // Trace logging (if enabled)
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         // Resolve any nested expression to get the actual target bean
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; // Skip this property setter
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         // Declare local variables we will require
897         String propName = null;          // Simple name of target property
898         Class type = null;               // Java type of target property
899         int index = -1;                  // Indexed subscript value (if any)
900         String key = null;               // Mapped key value (if any)
901 
902         // Calculate the property name, index, and key values
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         // Calculate the property type
927         if (target instanceof DynaBean) {
928             DynaClass dynaClass = ((DynaBean) target).getDynaClass();
929             DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
930             if (dynaProperty == null) {
931                 return; // Skip this property setter
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; // Skip this property setter
941                 }
942             } catch (NoSuchMethodException e) {
943                 return; // Skip this property setter
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; // Read-only, skip this property setter
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; // Read-only, skip this property setter
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; // Read-only, skip this property setter
969                 }
970                 type = descriptor.getPropertyType();
971             }
972         }
973 
974         // Convert the specified value to the required type
975         Object newValue = null;
976         if (type.isArray() && (index < 0)) { // Scalar value into array
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()) {         // Indexed value into array
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 {                             // Value into scalar
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         // Invoke the setter method
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         // walk back from the end to the start 
1034         // and find the first index that 
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                     // not bothered which
1048                     --bracketCount;
1049                     break;
1050                 
1051                 case PropertyUtils.MAPPED_DELIM2:
1052                 case PropertyUtils.INDEXED_DELIM2:
1053                     // not bothered which
1054                     ++bracketCount;
1055                     break;            
1056             }
1057         }
1058         // can't find any
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 }