001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.beanutils2;
019
020import java.beans.IndexedPropertyDescriptor;
021import java.beans.IntrospectionException;
022import java.beans.Introspector;
023import java.beans.PropertyDescriptor;
024import java.lang.reflect.Array;
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.Method;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.CopyOnWriteArrayList;
033
034import org.apache.commons.beanutils2.expression.DefaultResolver;
035import org.apache.commons.beanutils2.expression.Resolver;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038
039/**
040 * Utility methods for using Java Reflection APIs to facilitate generic property getter and setter operations on Java objects. Much of this code was originally
041 * included in {@code BeanUtils}, but has been separated because of the volume of code involved.
042 * <p>
043 * In general, the objects that are examined and modified using these methods are expected to conform to the property getter and setter method naming
044 * conventions described in the JavaBeans Specification (Version 1.0.1). No data type conversions are performed, and there are no usage of any
045 * {@code PropertyEditor} classes that have been registered, although a convenient way to access the registered classes themselves is included.
046 * <p>
047 * For the purposes of this class, five formats for referencing a particular property value of a bean are defined, with the <em>default</em> layout of an
048 * identifying String in parentheses. However the notation for these formats and how they are resolved is now (since BeanUtils 1.8.0) controlled by the
049 * configured {@link Resolver} implementation:
050 * <ul>
051 * <li><strong>Simple ({@code name})</strong> - The specified {@code name} identifies an individual property of a particular JavaBean. The name of the actual
052 * getter or setter method to be used is determined using standard JavaBeans introspection, so that (unless overridden by a {@code BeanInfo} class, a property
053 * named "xyz" will have a getter method named {@code getXyz()} or (for boolean properties only) {@code isXyz()}, and a setter method named
054 * {@code setXyz()}.</li>
055 * <li><strong>Nested ({@code name1.name2.name3})</strong> The first name element is used to select a property getter, as for simple references above. The
056 * object returned for this property is then consulted, using the same approach, for a property getter for a property named {@code name2}, and so on. The
057 * property value that is ultimately retrieved or modified is the one identified by the last name element.</li>
058 * <li><strong>Indexed ({@code name[index]})</strong> - The underlying property value is assumed to be an array, or this JavaBean is assumed to have indexed
059 * property getter and setter methods. The appropriate (zero-relative) entry in the array is selected. {@code List} objects are now also supported for
060 * read/write. You simply need to define a getter that returns the {@code List}</li>
061 * <li><strong>Mapped ({@code name(key)})</strong> - The JavaBean is assumed to have an property getter and setter methods with an additional attribute of type
062 * {@link String}.</li>
063 * <li><strong>Combined ({@code name1.name2[index].name3(key)})</strong> - Combining mapped, nested, and indexed references is also supported.</li>
064 * </ul>
065 *
066 * @see Resolver
067 * @see PropertyUtils
068 * @since 1.7
069 */
070public class PropertyUtilsBean {
071
072    /** Log instance */
073    private static final Log LOG = LogFactory.getLog(PropertyUtilsBean.class);
074
075    /**
076     * Gets the PropertyUtils bean instance.
077     *
078     * @return The PropertyUtils bean instance
079     */
080    protected static PropertyUtilsBean getInstance() {
081        return BeanUtilsBean.getInstance().getPropertyUtils();
082    }
083
084    /**
085     * Converts an object to a list of objects. This method is used when dealing with indexed properties. It assumes that indexed properties are stored as lists
086     * of objects.
087     *
088     * @param obj the object to be converted
089     * @return the resulting list of objects
090     */
091    @SuppressWarnings("unchecked")
092    private static List<Object> toObjectList(final Object obj) {
093        // indexed properties are stored in lists of objects
094        return (List<Object>) obj;
095    }
096
097    /**
098     * Converts an object to a map with property values. This method is used when dealing with mapped properties. It assumes that mapped properties are stored
099     * in a Map&lt;String, Object&gt;.
100     *
101     * @param obj the object to be converted
102     * @return the resulting properties map
103     */
104    @SuppressWarnings("unchecked")
105    private static Map<String, Object> toPropertyMap(final Object obj) {
106        // mapped properties are stores in maps of type <String, Object>
107        return (Map<String, Object>) obj;
108    }
109
110    private Resolver resolver = new DefaultResolver();
111
112    /**
113     * The cache of PropertyDescriptor arrays for beans we have already introspected, keyed by the java.lang.Class of this object.
114     */
115    private final Map<Class<?>, BeanIntrospectionData> descriptorsCache;
116
117    private final Map<Class<?>, Map> mappedDescriptorsCache;
118
119    /** The list with BeanIntrospector objects. */
120    private final List<BeanIntrospector> introspectors;
121
122    /** Base constructor */
123    public PropertyUtilsBean() {
124        descriptorsCache = BeanUtils.createCache();
125        mappedDescriptorsCache = BeanUtils.createCache();
126        introspectors = new CopyOnWriteArrayList<>();
127        resetBeanIntrospectors();
128    }
129
130    /**
131     * Adds a {@code BeanIntrospector}. This object is invoked when the property descriptors of a class need to be obtained.
132     *
133     * @param introspector the {@code BeanIntrospector} to be added (must not be <strong>null</strong>
134     * @throws IllegalArgumentException if the argument is <strong>null</strong>
135     * @since 1.9
136     */
137    public void addBeanIntrospector(final BeanIntrospector introspector) {
138        introspectors.add(Objects.requireNonNull(introspector, "introspector"));
139    }
140
141    /**
142     * Clear any cached property descriptors information for all classes loaded by any class loaders. This is useful in cases where class loaders are thrown
143     * away to implement class reloading.
144     */
145    public void clearDescriptors() {
146        descriptorsCache.clear();
147        mappedDescriptorsCache.clear();
148        Introspector.flushCaches();
149    }
150
151    /**
152     * <p>
153     * Copy property values from the "origin" bean to the "destination" bean for all cases where the property names are the same (even though the actual getter
154     * and setter methods might have been customized via {@code BeanInfo} classes). No conversions are performed on the actual property values -- it is assumed
155     * that the values retrieved from the origin bean are assignment-compatible with the types expected by the destination bean.
156     * </p>
157     *
158     * <p>
159     * If the origin "bean" is actually a {@code Map}, it is assumed to contain String-valued <strong>simple</strong> property names as the keys, pointing at
160     * the corresponding property values that will be set in the destination bean.<strong>Note</strong> that this method is intended to perform a "shallow copy"
161     * of the properties and so complex properties (for example, nested ones) will not be copied.
162     * </p>
163     *
164     * <p>
165     * Note, that this method will not copy a List to a List, or an Object[] to an Object[]. It's specifically for copying JavaBean properties.
166     * </p>
167     *
168     * @param dest Destination bean whose properties are modified
169     * @param orig Origin bean whose properties are retrieved
170     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
171     * @throws IllegalArgumentException  if the {@code dest} or {@code orig} argument is null
172     * @throws InvocationTargetException if the property accessor method throws an exception
173     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
174     */
175    public void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException,
176            // TODO BEFORE 2.0
177            // MISMATCH between implementation and Javadoc.
178            NoSuchMethodException {
179        Objects.requireNonNull(dest, "dest");
180        Objects.requireNonNull(orig, "orig");
181        if (orig instanceof DynaBean) {
182            final DynaProperty[] origDescriptors = ((DynaBean) orig).getDynaClass().getDynaProperties();
183            for (final DynaProperty origDescriptor : origDescriptors) {
184                final String name = origDescriptor.getName();
185                if (isReadable(orig, name) && isWriteable(dest, name)) {
186                    try {
187                        final Object value = ((DynaBean) orig).get(name);
188                        if (dest instanceof DynaBean) {
189                            ((DynaBean) dest).set(name, value);
190                        } else {
191                            setSimpleProperty(dest, name, value);
192                        }
193                    } catch (final NoSuchMethodException e) {
194                        if (LOG.isDebugEnabled()) {
195                            LOG.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
196                        }
197                    }
198                }
199            }
200        } else if (orig instanceof Map) {
201            for (final Map.Entry<?, ?> entry : ((Map<?, ?>) orig).entrySet()) {
202                final String name = (String) entry.getKey();
203                if (isWriteable(dest, name)) {
204                    try {
205                        if (dest instanceof DynaBean) {
206                            ((DynaBean) dest).set(name, entry.getValue());
207                        } else {
208                            setSimpleProperty(dest, name, entry.getValue());
209                        }
210                    } catch (final NoSuchMethodException e) {
211                        if (LOG.isDebugEnabled()) {
212                            LOG.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
213                        }
214                    }
215                }
216            }
217        } else /* if (orig is a standard JavaBean) */ {
218            final PropertyDescriptor[] origDescriptors = getPropertyDescriptors(orig);
219            for (final PropertyDescriptor origDescriptor : origDescriptors) {
220                final String name = origDescriptor.getName();
221                if (isReadable(orig, name) && isWriteable(dest, name)) {
222                    try {
223                        final Object value = getSimpleProperty(orig, name);
224                        if (dest instanceof DynaBean) {
225                            ((DynaBean) dest).set(name, value);
226                        } else {
227                            setSimpleProperty(dest, name, value);
228                        }
229                    } catch (final NoSuchMethodException e) {
230                        if (LOG.isDebugEnabled()) {
231                            LOG.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
232                        }
233                    }
234                }
235            }
236        }
237
238    }
239
240    /**
241     * <p>
242     * Return the entire set of properties for which the specified bean provides a read method. This map contains the unconverted property values for all
243     * properties for which a read method is provided (i.e. where the {@code getReadMethod()} returns non-null).
244     * </p>
245     *
246     * <p>
247     * <strong>FIXME</strong> - Does not account for mapped properties.
248     * </p>
249     *
250     * @param bean Bean whose properties are to be extracted
251     * @return The set of properties for the bean
252     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
253     * @throws IllegalArgumentException  if {@code bean} is null
254     * @throws InvocationTargetException if the property accessor method throws an exception
255     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
256     */
257    public Map<String, Object> describe(final Object bean) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
258        Objects.requireNonNull(bean, "bean");
259        final Map<String, Object> description = new HashMap<>();
260        if (bean instanceof DynaBean) {
261            final DynaProperty[] descriptors = ((DynaBean) bean).getDynaClass().getDynaProperties();
262            for (final DynaProperty descriptor : descriptors) {
263                final String name = descriptor.getName();
264                description.put(name, getProperty(bean, name));
265            }
266        } else {
267            final PropertyDescriptor[] descriptors = getPropertyDescriptors(bean);
268            for (final PropertyDescriptor descriptor : descriptors) {
269                final String name = descriptor.getName();
270                if (descriptor.getReadMethod() != null) {
271                    description.put(name, getProperty(bean, name));
272                }
273            }
274        }
275        return description;
276    }
277
278    /**
279     * Performs introspection on the specified class. This method invokes all {@code BeanIntrospector} objects that were added to this instance.
280     *
281     * @param beanClass the class to be inspected
282     * @return a data object with the results of introspection
283     */
284    private BeanIntrospectionData fetchIntrospectionData(final Class<?> beanClass) {
285        final DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass);
286
287        for (final BeanIntrospector bi : introspectors) {
288            try {
289                bi.introspect(ictx);
290            } catch (final IntrospectionException iex) {
291                LOG.error("Exception during introspection", iex);
292            }
293        }
294
295        return new BeanIntrospectionData(ictx.getPropertyDescriptors());
296    }
297
298    /**
299     * Gets the value of the specified indexed property of the specified bean, with no type conversions. The zero-relative index of the required value must be
300     * included (in square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. In addition to supporting the
301     * JavaBeans specification, this method has been extended to support {@code List} objects as well.
302     *
303     * @param bean Bean whose property is to be extracted
304     * @param name {@code propertyname[index]} of the property value to be extracted
305     * @return the indexed property value
306     * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying array or List
307     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
308     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
309     * @throws InvocationTargetException if the property accessor method throws an exception
310     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
311     */
312    public Object getIndexedProperty(final Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
313        Objects.requireNonNull(bean, "bean");
314        Objects.requireNonNull(name, "name");
315        // Identify the index of the requested individual property
316        int index = -1;
317        try {
318            index = resolver.getIndex(name);
319        } catch (final IllegalArgumentException e) {
320            throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "' " + e.getMessage());
321        }
322        if (index < 0) {
323            throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'");
324        }
325
326        // Isolate the name
327        name = resolver.getProperty(name);
328
329        // Request the specified indexed property value
330        return getIndexedProperty(bean, name, index);
331    }
332
333    /**
334     * Gets the value of the specified indexed property of the specified bean, with no type conversions. In addition to supporting the JavaBeans specification,
335     * this method has been extended to support {@code List} objects as well.
336     *
337     * @param bean  Bean whose property is to be extracted
338     * @param name  Simple property name of the property value to be extracted
339     * @param index Index of the property value to be extracted
340     * @return the indexed property value
341     * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property
342     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
343     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
344     * @throws InvocationTargetException if the property accessor method throws an exception
345     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
346     */
347    public Object getIndexedProperty(final Object bean, final String name, final int index)
348            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
349        Objects.requireNonNull(bean, "bean");
350        if (name == null || name.isEmpty()) {
351            if (bean.getClass().isArray()) {
352                return Array.get(bean, index);
353            }
354            if (bean instanceof List) {
355                return ((List<?>) bean).get(index);
356            }
357        }
358        Objects.requireNonNull(name, "name");
359        // Handle DynaBean instances specially
360        if (bean instanceof DynaBean) {
361            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
362            if (descriptor == null) {
363                throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
364            }
365            return ((DynaBean) bean).get(name, index);
366        }
367
368        // Retrieve the property descriptor for the specified property
369        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
370        if (descriptor == null) {
371            throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
372        }
373
374        // Call the indexed getter method if there is one
375        if (descriptor instanceof IndexedPropertyDescriptor) {
376            Method readMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedReadMethod();
377            readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
378            if (readMethod != null) {
379                try {
380                    return invokeMethod(readMethod, bean, Integer.valueOf(index));
381                } catch (final InvocationTargetException e) {
382                    if (e.getTargetException() instanceof IndexOutOfBoundsException) {
383                        throw (IndexOutOfBoundsException) e.getTargetException();
384                    }
385                    throw e;
386                }
387            }
388        }
389
390        // Otherwise, the underlying property must be an array
391        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
392        if (readMethod == null) {
393            throw new NoSuchMethodException("Property '" + name + "' has no " + "getter method on bean class '" + bean.getClass() + "'");
394        }
395
396        // Call the property getter and return the value
397        final Object value = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
398        if (!value.getClass().isArray()) {
399            if (!(value instanceof List)) {
400                throw new IllegalArgumentException("Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'");
401            }
402            // get the List's value
403            return ((List<?>) value).get(index);
404        }
405        // get the array's value
406        try {
407            return Array.get(value, index);
408        } catch (final ArrayIndexOutOfBoundsException e) {
409            throw new ArrayIndexOutOfBoundsException("Index: " + index + ", Size: " + Array.getLength(value) + " for property '" + name + "'");
410        }
411    }
412
413    /**
414     * Obtains the {@code BeanIntrospectionData} object describing the specified bean class. This object is looked up in the internal cache. If necessary,
415     * introspection is performed now on the affected bean class, and the results object is created.
416     *
417     * @param beanClass the bean class in question
418     * @return the {@code BeanIntrospectionData} object for this class
419     * @throws IllegalArgumentException if the bean class is <strong>null</strong>
420     */
421    private BeanIntrospectionData getIntrospectionData(final Class<?> beanClass) {
422        Objects.requireNonNull(beanClass, "beanClass");
423        // Look up any cached information for this bean class
424        BeanIntrospectionData data = descriptorsCache.get(beanClass);
425        if (data == null) {
426            data = fetchIntrospectionData(beanClass);
427            descriptorsCache.put(beanClass, data);
428        }
429        return data;
430    }
431
432    /**
433     * Gets the value of the specified mapped property of the specified bean, with no type conversions. The key of the required value must be included (in
434     * brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown.
435     *
436     * @param bean Bean whose property is to be extracted
437     * @param name {@code propertyname(key)} of the property value to be extracted
438     * @return the mapped property value
439     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
440     * @throws InvocationTargetException if the property accessor method throws an exception
441     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
442     */
443    public Object getMappedProperty(final Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
444        Objects.requireNonNull(bean, "bean");
445        Objects.requireNonNull(name, "name");
446        // Identify the key of the requested individual property
447        String key = null;
448        try {
449            key = resolver.getKey(name);
450        } catch (final IllegalArgumentException e) {
451            throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "' " + e.getMessage());
452        }
453        if (key == null) {
454            throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'");
455        }
456
457        // Isolate the name
458        name = resolver.getProperty(name);
459
460        // Request the specified indexed property value
461        return getMappedProperty(bean, name, key);
462    }
463
464    /**
465     * Gets the value of the specified mapped property of the specified bean, with no type conversions.
466     *
467     * @param bean Bean whose property is to be extracted
468     * @param name Mapped property name of the property value to be extracted
469     * @param key  Key of the property value to be extracted
470     * @return the mapped property value
471     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
472     * @throws InvocationTargetException if the property accessor method throws an exception
473     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
474     */
475    public Object getMappedProperty(final Object bean, final String name, final String key)
476            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
477        Objects.requireNonNull(bean, "bean");
478        Objects.requireNonNull(name, "name");
479        Objects.requireNonNull(key, "key");
480        // Handle DynaBean instances specially
481        if (bean instanceof DynaBean) {
482            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
483            if (descriptor == null) {
484                throw new NoSuchMethodException("Unknown property '" + name + "'+ on bean class '" + bean.getClass() + "'");
485            }
486            return ((DynaBean) bean).get(name, key);
487        }
488
489        Object result = null;
490
491        // Retrieve the property descriptor for the specified property
492        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
493        if (descriptor == null) {
494            throw new NoSuchMethodException("Unknown property '" + name + "'+ on bean class '" + bean.getClass() + "'");
495        }
496
497        if (descriptor instanceof MappedPropertyDescriptor) {
498            // Call the keyed getter method if there is one
499            Method readMethod = ((MappedPropertyDescriptor) descriptor).getMappedReadMethod();
500            readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
501            if (readMethod == null) {
502                throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'");
503            }
504            result = invokeMethod(readMethod, bean, key);
505        } else {
506            /* means that the result has to be retrieved from a map */
507            final Method readMethod = getReadMethod(bean.getClass(), descriptor);
508            if (readMethod == null) {
509                throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'");
510            }
511            final Object invokeResult = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
512            /* test and fetch from the map */
513            if (invokeResult instanceof Map) {
514                result = ((Map<?, ?>) invokeResult).get(key);
515            }
516        }
517        return result;
518    }
519
520    /**
521     * <p>
522     * Return the mapped property descriptors for this bean class.
523     * </p>
524     *
525     * <p>
526     * <strong>FIXME</strong> - Does not work with DynaBeans.
527     * </p>
528     *
529     * @param beanClass Bean class to be introspected
530     * @return the mapped property descriptors
531     */
532    Map<Class<?>, Map> getMappedPropertyDescriptors(final Class<?> beanClass) {
533        if (beanClass == null) {
534            return null;
535        }
536        // Look up any cached descriptors for this bean class
537        return mappedDescriptorsCache.get(beanClass);
538    }
539
540    /**
541     * <p>
542     * Return the mapped property descriptors for this bean.
543     * </p>
544     *
545     * <p>
546     * <strong>FIXME</strong> - Does not work with DynaBeans.
547     * </p>
548     *
549     * @param bean Bean to be introspected
550     * @return the mapped property descriptors
551     */
552    Map getMappedPropertyDescriptors(final Object bean) {
553        if (bean == null) {
554            return null;
555        }
556        return getMappedPropertyDescriptors(bean.getClass());
557    }
558
559    /**
560     * Gets the value of the (possibly nested) property of the specified name, for the specified bean, with no type conversions.
561     *
562     * @param bean Bean whose property is to be extracted
563     * @param name Possibly nested name of the property to be extracted
564     * @return the nested property value
565     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
566     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
567     * @throws NestedNullException       if a nested reference to a property returns null
568     * @throws InvocationTargetException if the property accessor method throws an exception
569     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
570     */
571    public Object getNestedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
572        Objects.requireNonNull(bean, "bean");
573        Objects.requireNonNull(name, "name");
574        // Resolve nested references
575        while (resolver.hasNested(name)) {
576            final String next = resolver.next(name);
577            Object nestedBean = null;
578            if (bean instanceof Map) {
579                nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next);
580            } else if (resolver.isMapped(next)) {
581                nestedBean = getMappedProperty(bean, next);
582            } else if (resolver.isIndexed(next)) {
583                nestedBean = getIndexedProperty(bean, next);
584            } else {
585                nestedBean = getSimpleProperty(bean, next);
586            }
587            if (nestedBean == null) {
588                throw new NestedNullException("Null property value for '" + name + "' on bean class '" + bean.getClass() + "'");
589            }
590            bean = nestedBean;
591            name = resolver.remove(name);
592        }
593
594        if (bean instanceof Map) {
595            bean = getPropertyOfMapBean((Map<?, ?>) bean, name);
596        } else if (resolver.isMapped(name)) {
597            bean = getMappedProperty(bean, name);
598        } else if (resolver.isIndexed(name)) {
599            bean = getIndexedProperty(bean, name);
600        } else {
601            bean = getSimpleProperty(bean, name);
602        }
603        return bean;
604    }
605
606    /**
607     * Gets the value of the specified property of the specified bean, no matter which property reference format is used, with no type conversions.
608     *
609     * @param bean Bean whose property is to be extracted
610     * @param name Possibly indexed and/or nested name of the property to be extracted
611     * @return the property value
612     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
613     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
614     * @throws InvocationTargetException if the property accessor method throws an exception
615     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
616     */
617    public Object getProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
618        return getNestedProperty(bean, name);
619    }
620
621    /**
622     * <p>
623     * Retrieve the property descriptor for the specified property of the specified bean, or return {@code null} if there is no such descriptor. This method
624     * resolves indexed and nested property references in the same manner as other methods in this class, except that if the last (or only) name element is
625     * indexed, the descriptor for the last resolved property itself is returned.
626     * </p>
627     *
628     * <p>
629     * <strong>FIXME</strong> - Does not work with DynaBeans.
630     * </p>
631     *
632     * <p>
633     * Note that for Java 8 and above, this method no longer return IndexedPropertyDescriptor for {@link List}-typed properties, only for properties typed as
634     * native array. (BEANUTILS-492).
635     *
636     * @param bean Bean for which a property descriptor is requested
637     * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested
638     * @return the property descriptor
639     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
640     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
641     * @throws IllegalArgumentException  if a nested reference to a property returns null
642     * @throws InvocationTargetException if the property accessor method throws an exception
643     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
644     */
645    public PropertyDescriptor getPropertyDescriptor(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
646        Objects.requireNonNull(bean, "bean");
647        Objects.requireNonNull(name, "name");
648        // Resolve nested references
649        while (resolver.hasNested(name)) {
650            final String next = resolver.next(name);
651            final Object nestedBean = getProperty(bean, next);
652            if (nestedBean == null) {
653                throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'");
654            }
655            bean = nestedBean;
656            name = resolver.remove(name);
657        }
658
659        // Remove any subscript from the final name value
660        name = resolver.getProperty(name);
661
662        // Look up and return this property from our cache
663        // creating and adding it to the cache if not found.
664        if (name == null) {
665            return null;
666        }
667
668        final BeanIntrospectionData data = getIntrospectionData(bean.getClass());
669        PropertyDescriptor result = data.getDescriptor(name);
670        if (result != null) {
671            return result;
672        }
673
674        Map mappedDescriptors = getMappedPropertyDescriptors(bean);
675        if (mappedDescriptors == null) {
676            mappedDescriptors = new ConcurrentHashMap<Class<?>, Map>();
677            mappedDescriptorsCache.put(bean.getClass(), mappedDescriptors);
678        }
679        result = (PropertyDescriptor) mappedDescriptors.get(name);
680        if (result == null) {
681            // not found, try to create it
682            try {
683                result = new MappedPropertyDescriptor(name, bean.getClass());
684            } catch (final IntrospectionException ie) {
685                /*
686                 * Swallow IntrospectionException TODO: Why?
687                 */
688            }
689            if (result != null) {
690                mappedDescriptors.put(name, result);
691            }
692        }
693
694        return result;
695    }
696
697    /**
698     * <p>
699     * Retrieve the property descriptors for the specified class, introspecting and caching them the first time a particular bean class is encountered.
700     * </p>
701     *
702     * <p>
703     * <strong>FIXME</strong> - Does not work with DynaBeans.
704     * </p>
705     *
706     * @param beanClass Bean class for which property descriptors are requested
707     * @return the property descriptors
708     * @throws IllegalArgumentException if {@code beanClass} is null
709     */
710    public PropertyDescriptor[] getPropertyDescriptors(final Class<?> beanClass) {
711        return getIntrospectionData(beanClass).getDescriptors();
712    }
713
714    /**
715     * <p>
716     * Retrieve the property descriptors for the specified bean, introspecting and caching them the first time a particular bean class is encountered.
717     * </p>
718     *
719     * <p>
720     * <strong>FIXME</strong> - Does not work with DynaBeans.
721     * </p>
722     *
723     * @param bean Bean for which property descriptors are requested
724     * @return the property descriptors
725     * @throws IllegalArgumentException if {@code bean} is null
726     */
727    public PropertyDescriptor[] getPropertyDescriptors(final Object bean) {
728        Objects.requireNonNull(bean, "bean");
729        return getPropertyDescriptors(bean.getClass());
730    }
731
732    /**
733     * <p>
734     * Return the Java Class repesenting the property editor class that has been registered for this property (if any). This method follows the same name
735     * resolution rules used by {@code getPropertyDescriptor()}, so if the last element of a name reference is indexed, the property editor for the underlying
736     * property's class is returned.
737     * </p>
738     *
739     * <p>
740     * Note that {@code null} will be returned if there is no property, or if there is no registered property editor class. Because this return value is
741     * ambiguous, you should determine the existence of the property itself by other means.
742     * </p>
743     *
744     * <p>
745     * <strong>FIXME</strong> - Does not work with DynaBeans.
746     * </p>
747     *
748     * @param bean Bean for which a property descriptor is requested
749     * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested
750     * @return the property editor class
751     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
752     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
753     * @throws IllegalArgumentException  if a nested reference to a property returns null
754     * @throws InvocationTargetException if the property accessor method throws an exception
755     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
756     */
757    public Class<?> getPropertyEditorClass(final Object bean, final String name)
758            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
759        Objects.requireNonNull(bean, "bean");
760        Objects.requireNonNull(name, "name");
761        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
762        if (descriptor != null) {
763            return descriptor.getPropertyEditorClass();
764        }
765        return null;
766    }
767
768    /**
769     * This method is called by getNestedProperty and setNestedProperty to define what it means to get a property from an object which implements Map. See
770     * setPropertyOfMapBean for more information.
771     *
772     * @param bean         Map bean
773     * @param propertyName The property name
774     * @return the property value
775     * @throws IllegalArgumentException  when the propertyName is regarded as being invalid.
776     * @throws IllegalAccessException    just in case subclasses override this method to try to access real getter methods and find permission is denied.
777     * @throws InvocationTargetException just in case subclasses override this method to try to access real getter methods, and find it throws an exception when
778     *                                   invoked.
779     *
780     * @throws NoSuchMethodException     just in case subclasses override this method to try to access real getter methods, and want to fail if no simple method
781     *                                   is available.
782     * @since 1.8.0
783     */
784    protected Object getPropertyOfMapBean(final Map<?, ?> bean, String propertyName)
785            throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
786
787        if (resolver.isMapped(propertyName)) {
788            final String name = resolver.getProperty(propertyName);
789            if (name == null || name.isEmpty()) {
790                propertyName = resolver.getKey(propertyName);
791            }
792        }
793
794        if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) {
795            throw new IllegalArgumentException("Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName);
796        }
797
798        return bean.get(propertyName);
799    }
800
801    /**
802     * Gets the Java Class representing the property type of the specified property, or {@code null} if there is no such property for the specified bean. This
803     * method follows the same name resolution rules used by {@code getPropertyDescriptor()}, so if the last element of a name reference is indexed, the type of
804     * the property itself will be returned. If the last (or only) element has no property with the specified name, {@code null} is returned.
805     * <p>
806     * If the property is an indexed property (e.g. {@code String[]}), this method will return the type of the items within that array. Note that from Java 8
807     * and newer, this method do not support such index types from items within an Collection, and will instead return the collection type (e.g. java.util.List)
808     * from the getter method.
809     *
810     * @param bean Bean for which a property descriptor is requested
811     * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested
812     * @return The property type
813     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
814     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
815     * @throws IllegalArgumentException  if a nested reference to a property returns null
816     * @throws InvocationTargetException if the property accessor method throws an exception
817     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
818     */
819    public Class<?> getPropertyType(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
820        Objects.requireNonNull(bean, "bean");
821        Objects.requireNonNull(name, "name");
822        // Resolve nested references
823        while (resolver.hasNested(name)) {
824            final String next = resolver.next(name);
825            final Object nestedBean = getProperty(bean, next);
826            if (nestedBean == null) {
827                throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'");
828            }
829            bean = nestedBean;
830            name = resolver.remove(name);
831        }
832
833        // Remove any subscript from the final name value
834        name = resolver.getProperty(name);
835
836        // Special handling for DynaBeans
837        if (bean instanceof DynaBean) {
838            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
839            if (descriptor == null) {
840                return null;
841            }
842            final Class<?> type = descriptor.getType();
843            if (type == null) {
844                return null;
845            }
846            if (type.isArray()) {
847                return type.getComponentType();
848            }
849            return type;
850        }
851
852        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
853        if (descriptor == null) {
854            return null;
855        }
856        if (descriptor instanceof IndexedPropertyDescriptor) {
857            return ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType();
858        }
859        if (descriptor instanceof MappedPropertyDescriptor) {
860            return ((MappedPropertyDescriptor) descriptor).getMappedPropertyType();
861        }
862        return descriptor.getPropertyType();
863    }
864
865    /**
866     * <p>
867     * Return the property getter method for this property if accessible from given {@code clazz} (and if there is one at all); otherwise return {@code null}.
868     * </p>
869     *
870     * <p>
871     * <strong>FIXME</strong> - Does not work with DynaBeans.
872     * </p>
873     *
874     * <p>
875     * This fairly low-level method shouldn't be needed for most usecases. However, if you do have to implement something extra, you can improve consistency
876     * with the standard code (e.g. that of {@link #getProperty getProperty()}) by calling this method instead of using {@code descriptor.getReadMethod()}
877     * directly.
878     *
879     * @param clazz      The class of the read method will be invoked on
880     * @param descriptor Property descriptor to return a getter for
881     * @return The read method
882     * @since 2.0.0
883     */
884    public Method getReadMethod(final Class<?> clazz, final PropertyDescriptor descriptor) {
885        return MethodUtils.getAccessibleMethod(clazz, descriptor.getReadMethod());
886    }
887
888    /**
889     * <p>
890     * Return an accessible property getter method for this property, if there is one; otherwise return {@code null}.
891     * </p>
892     *
893     * <p>
894     * <strong>FIXME</strong> - Does not work with DynaBeans.
895     * </p>
896     *
897     * @param descriptor Property descriptor to return a getter for
898     * @return The read method
899     */
900    public Method getReadMethod(final PropertyDescriptor descriptor) {
901        return MethodUtils.getAccessibleMethod(descriptor.getReadMethod());
902    }
903
904    /**
905     * Gets the configured {@link Resolver} implementation used by BeanUtils.
906     * <p>
907     * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression
908     * language</em> that BeanUtils recognizes.
909     * <p>
910     * {@link DefaultResolver} is the default implementation used.
911     *
912     * @return resolver The property expression resolver.
913     * @since 1.8.0
914     */
915    public Resolver getResolver() {
916        return resolver;
917    }
918
919    /**
920     * Gets the value of the specified simple property of the specified bean, with no type conversions.
921     *
922     * @param bean Bean whose property is to be extracted
923     * @param name Name of the property to be extracted
924     * @return The property value
925     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
926     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
927     * @throws IllegalArgumentException  if the property name is nested or indexed
928     * @throws InvocationTargetException if the property accessor method throws an exception
929     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
930     */
931    public Object getSimpleProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
932        Objects.requireNonNull(bean, "bean");
933        Objects.requireNonNull(name, "name");
934        // Validate the syntax of the property name
935        if (resolver.hasNested(name)) {
936            throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
937        }
938        if (resolver.isIndexed(name)) {
939            throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
940        }
941        if (resolver.isMapped(name)) {
942            throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
943        }
944
945        // Handle DynaBean instances specially
946        if (bean instanceof DynaBean) {
947            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
948            if (descriptor == null) {
949                throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'");
950            }
951            return ((DynaBean) bean).get(name);
952        }
953
954        // Retrieve the property getter method for the specified property
955        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
956        if (descriptor == null) {
957            throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + bean.getClass() + "'");
958        }
959        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
960        if (readMethod == null) {
961            throw new NoSuchMethodException("Property '" + name + "' has no getter method in class '" + bean.getClass() + "'");
962        }
963
964        // Call the property getter and return the value
965        return invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
966    }
967
968    /**
969     * <p>
970     * Return the property setter method for this property if accessible from given {@code clazz} (and if there is one at all); otherwise return {@code null}.
971     * </p>
972     *
973     * <p>
974     * <strong>FIXME</strong> - Does not work with DynaBeans.
975     * </p>
976     *
977     * <p>
978     * This fairly low-level method shouldn't be needed for most usecases. However, if you do have to implement something extra, you can improve consistency
979     * with the standard code (e.g. that of {@link #setProperty setProperty()}) by calling this method instead of using {@code descriptor.getWriteMethod()}
980     * directly.
981     *
982     * @param clazz      The class of the read method will be invoked on
983     * @param descriptor Property descriptor to return a setter for
984     * @return The write method
985     * @since 1.9.1
986     */
987    public Method getWriteMethod(final Class<?> clazz, final PropertyDescriptor descriptor) {
988        final BeanIntrospectionData data = getIntrospectionData(clazz);
989        return MethodUtils.getAccessibleMethod(clazz, data.getWriteMethod(clazz, descriptor));
990    }
991
992    /**
993     * <p>
994     * Return an accessible property setter method for this property, if there is one; otherwise return {@code null}.
995     * </p>
996     *
997     * <p>
998     * <em>Note:</em> This method does not work correctly with custom bean introspection under certain circumstances. It may return {@code null} even if a write
999     * method is defined for the property in question. Use {@link #getWriteMethod(Class, PropertyDescriptor)} to be sure that the correct result is returned.
1000     * </p>
1001     * <p>
1002     * <strong>FIXME</strong> - Does not work with DynaBeans.
1003     * </p>
1004     *
1005     * @param descriptor Property descriptor to return a setter for
1006     * @return The write method
1007     */
1008    public Method getWriteMethod(final PropertyDescriptor descriptor) {
1009        return MethodUtils.getAccessibleMethod(descriptor.getWriteMethod());
1010    }
1011
1012    /**
1013     * Delegates to {@link Method#invoke(Object, Object...)} and handles some unchecked exceptions.
1014     *
1015     * @see Method#invoke(Object, Object...)
1016     */
1017    private Object invokeMethod(final Method method, final Object bean, final Object... values) throws IllegalAccessException, InvocationTargetException {
1018        Objects.requireNonNull(bean, "bean");
1019        try {
1020            return method.invoke(bean, values);
1021        } catch (final NullPointerException | IllegalArgumentException cause) {
1022            // JDK 1.3 and JDK 1.4 throw NullPointerException if an argument is
1023            // null for a primitive value (JDK 1.5+ throw IllegalArgumentException)
1024            final StringBuilder valueString = new StringBuilder();
1025            if (values != null) {
1026                for (int i = 0; i < values.length; i++) {
1027                    if (i > 0) {
1028                        valueString.append(", ");
1029                    }
1030                    if (values[i] == null) {
1031                        valueString.append("<null>");
1032                    } else {
1033                        valueString.append(values[i].getClass().getName());
1034                    }
1035                }
1036            }
1037            final StringBuilder expectedString = new StringBuilder();
1038            final Class<?>[] parTypes = method.getParameterTypes();
1039            if (parTypes != null) {
1040                for (int i = 0; i < parTypes.length; i++) {
1041                    if (i > 0) {
1042                        expectedString.append(", ");
1043                    }
1044                    expectedString.append(parTypes[i].getName());
1045                }
1046            }
1047            throw new IllegalArgumentException("Cannot invoke " + method.getDeclaringClass().getName() + "." + method.getName() + " on bean class '"
1048                    + bean.getClass() + "' - " + cause.getMessage()
1049                    // as per https://issues.apache.org/jira/browse/BEANUTILS-224
1050                    + " - had objects of type \"" + valueString + "\" but expected signature \"" + expectedString + "\"", cause);
1051        }
1052    }
1053
1054    /**
1055     * Return {@code true} if the specified property name identifies a readable property on the specified bean; otherwise, return {@code false}.
1056     *
1057     * @param bean Bean to be examined (may be a {@link DynaBean}
1058     * @param name Property name to be evaluated
1059     * @return {@code true} if the property is readable, otherwise {@code false}
1060     * @throws IllegalArgumentException if {@code bean} or {@code name</code> is <code>null}
1061     * @since 1.6
1062     */
1063    public boolean isReadable(Object bean, String name) {
1064        // Validate method parameters
1065        Objects.requireNonNull(bean, "bean");
1066        Objects.requireNonNull(name, "name");
1067        // Resolve nested references
1068        while (resolver.hasNested(name)) {
1069            final String next = resolver.next(name);
1070            Object nestedBean = null;
1071            try {
1072                nestedBean = getProperty(bean, next);
1073            } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
1074                return false;
1075            }
1076            if (nestedBean == null) {
1077                throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'");
1078            }
1079            bean = nestedBean;
1080            name = resolver.remove(name);
1081        }
1082
1083        // Remove any subscript from the final name value
1084        name = resolver.getProperty(name);
1085
1086        // Treat WrapDynaBean as special case - may be a write-only property
1087        // (see Jira issue# BEANUTILS-61)
1088        if (bean instanceof WrapDynaBean) {
1089            bean = ((WrapDynaBean) bean).getInstance();
1090        }
1091
1092        // Return the requested result
1093        if (bean instanceof DynaBean) {
1094            // All DynaBean properties are readable
1095            return ((DynaBean) bean).getDynaClass().getDynaProperty(name) != null;
1096        }
1097        try {
1098            final PropertyDescriptor desc = getPropertyDescriptor(bean, name);
1099            if (desc != null) {
1100                Method readMethod = getReadMethod(bean.getClass(), desc);
1101                if (readMethod == null) {
1102                    if (desc instanceof IndexedPropertyDescriptor) {
1103                        readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod();
1104                    } else if (desc instanceof MappedPropertyDescriptor) {
1105                        readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod();
1106                    }
1107                    readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
1108                }
1109                return readMethod != null;
1110            }
1111            return false;
1112        } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
1113            return false;
1114        }
1115    }
1116
1117    /**
1118     * Return {@code true} if the specified property name identifies a writable property on the specified bean; otherwise, return {@code false}.
1119     *
1120     * @param bean Bean to be examined (may be a {@link DynaBean}
1121     * @param name Property name to be evaluated
1122     * @return {@code true} if the property is writable, otherwise {@code false}
1123     * @throws IllegalArgumentException if {@code bean} or {@code name</code> is <code>null}
1124     * @since 1.6
1125     */
1126    public boolean isWriteable(Object bean, String name) {
1127        // Validate method parameters
1128        Objects.requireNonNull(bean, "bean");
1129        Objects.requireNonNull(name, "name");
1130        // Resolve nested references
1131        while (resolver.hasNested(name)) {
1132            final String next = resolver.next(name);
1133            Object nestedBean = null;
1134            try {
1135                nestedBean = getProperty(bean, next);
1136            } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
1137                return false;
1138            }
1139            if (nestedBean == null) {
1140                throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'");
1141            }
1142            bean = nestedBean;
1143            name = resolver.remove(name);
1144        }
1145
1146        // Remove any subscript from the final name value
1147        name = resolver.getProperty(name);
1148
1149        // Treat WrapDynaBean as special case - may be a read-only property
1150        // (see Jira issue# BEANUTILS-61)
1151        if (bean instanceof WrapDynaBean) {
1152            bean = ((WrapDynaBean) bean).getInstance();
1153        }
1154
1155        // Return the requested result
1156        if (bean instanceof DynaBean) {
1157            // All DynaBean properties are writable
1158            return ((DynaBean) bean).getDynaClass().getDynaProperty(name) != null;
1159        }
1160        try {
1161            final PropertyDescriptor desc = getPropertyDescriptor(bean, name);
1162            if (desc != null) {
1163                Method writeMethod = getWriteMethod(bean.getClass(), desc);
1164                if (writeMethod == null) {
1165                    if (desc instanceof IndexedPropertyDescriptor) {
1166                        writeMethod = ((IndexedPropertyDescriptor) desc).getIndexedWriteMethod();
1167                    } else if (desc instanceof MappedPropertyDescriptor) {
1168                        writeMethod = ((MappedPropertyDescriptor) desc).getMappedWriteMethod();
1169                    }
1170                    writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
1171                }
1172                return writeMethod != null;
1173            }
1174            return false;
1175        } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
1176            return false;
1177        }
1178    }
1179
1180    /**
1181     * Removes the specified {@code BeanIntrospector}.
1182     *
1183     * @param introspector the {@code BeanIntrospector} to be removed
1184     * @return <strong>true</strong> if the {@code BeanIntrospector} existed and could be removed, <strong>false</strong> otherwise
1185     * @since 1.9
1186     */
1187    public boolean removeBeanIntrospector(final BeanIntrospector introspector) {
1188        return introspectors.remove(introspector);
1189    }
1190
1191    /**
1192     * Resets the {@link BeanIntrospector} objects registered at this instance. After this method was called, only the default {@code BeanIntrospector} is
1193     * registered.
1194     *
1195     * @since 1.9
1196     */
1197    public final void resetBeanIntrospectors() {
1198        introspectors.clear();
1199        introspectors.add(DefaultBeanIntrospector.INSTANCE);
1200        introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS);
1201    }
1202
1203    /**
1204     * Sets the value of the specified indexed property of the specified bean, with no type conversions. In addition to supporting the JavaBeans specification,
1205     * this method has been extended to support {@code List} objects as well.
1206     *
1207     * @param bean  Bean whose property is to be set
1208     * @param name  Simple property name of the property value to be set
1209     * @param index Index of the property value to be set
1210     * @param value Value to which the indexed property element is to be set
1211     * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property
1212     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1213     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1214     * @throws InvocationTargetException if the property accessor method throws an exception
1215     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1216     */
1217    public void setIndexedProperty(final Object bean, final String name, final int index, final Object value)
1218            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1219        Objects.requireNonNull(bean, "bean");
1220        if (name == null || name.isEmpty()) {
1221            if (bean.getClass().isArray()) {
1222                Array.set(bean, index, value);
1223                return;
1224            }
1225            if (bean instanceof List) {
1226                final List<Object> list = toObjectList(bean);
1227                list.set(index, value);
1228                return;
1229            }
1230        }
1231        Objects.requireNonNull(name, "name");
1232        // Handle DynaBean instances specially
1233        if (bean instanceof DynaBean) {
1234            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1235            if (descriptor == null) {
1236                throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1237            }
1238            ((DynaBean) bean).set(name, index, value);
1239            return;
1240        }
1241
1242        // Retrieve the property descriptor for the specified property
1243        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1244        if (descriptor == null) {
1245            throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1246        }
1247
1248        // Call the indexed setter method if there is one
1249        if (descriptor instanceof IndexedPropertyDescriptor) {
1250            Method writeMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod();
1251            writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
1252            if (writeMethod != null) {
1253                try {
1254                    if (LOG.isTraceEnabled()) {
1255                        final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1256                        LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with index=" + index + ", value=" + value + " (class "
1257                                + valueClassName + ")");
1258                    }
1259                    invokeMethod(writeMethod, bean, Integer.valueOf(index), value);
1260                } catch (final InvocationTargetException e) {
1261                    if (e.getTargetException() instanceof IndexOutOfBoundsException) {
1262                        throw (IndexOutOfBoundsException) e.getTargetException();
1263                    }
1264                    throw e;
1265                }
1266                return;
1267            }
1268        }
1269
1270        // Otherwise, the underlying property must be an array or a list
1271        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
1272        if (readMethod == null) {
1273            throw new NoSuchMethodException("Property '" + name + "' has no getter method on bean class '" + bean.getClass() + "'");
1274        }
1275
1276        // Call the property getter to get the array or list
1277        final Object array = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
1278        if (!array.getClass().isArray()) {
1279            if (!(array instanceof List)) {
1280                throw new IllegalArgumentException("Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'");
1281            }
1282            // Modify the specified value in the List
1283            final List<Object> list = toObjectList(array);
1284            list.set(index, value);
1285        } else {
1286            // Modify the specified value in the array
1287            Array.set(array, index, value);
1288        }
1289    }
1290
1291    /**
1292     * Sets the value of the specified indexed property of the specified bean, with no type conversions. The zero-relative index of the required value must be
1293     * included (in square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. In addition to supporting the
1294     * JavaBeans specification, this method has been extended to support {@code List} objects as well.
1295     *
1296     * @param bean  Bean whose property is to be modified
1297     * @param name  {@code propertyname[index]} of the property value to be modified
1298     * @param value Value to which the specified property element should be set
1299     * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property
1300     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1301     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1302     * @throws InvocationTargetException if the property accessor method throws an exception
1303     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1304     */
1305    public void setIndexedProperty(final Object bean, String name, final Object value)
1306            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1307        Objects.requireNonNull(bean, "bean");
1308        Objects.requireNonNull(name, "name");
1309        // Identify the index of the requested individual property
1310        int index = -1;
1311        try {
1312            index = resolver.getIndex(name);
1313        } catch (final IllegalArgumentException e) {
1314            throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'");
1315        }
1316        if (index < 0) {
1317            throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'");
1318        }
1319
1320        // Isolate the name
1321        name = resolver.getProperty(name);
1322
1323        // Set the specified indexed property value
1324        setIndexedProperty(bean, name, index, value);
1325    }
1326
1327    /**
1328     * Sets the value of the specified mapped property of the specified bean, with no type conversions. The key of the value to set must be included (in
1329     * brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown.
1330     *
1331     * @param bean  Bean whose property is to be set
1332     * @param name  {@code propertyname(key)} of the property value to be set
1333     * @param value The property value to be set
1334     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1335     * @throws InvocationTargetException if the property accessor method throws an exception
1336     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1337     */
1338    public void setMappedProperty(final Object bean, String name, final Object value)
1339            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1340        Objects.requireNonNull(bean, "bean");
1341        Objects.requireNonNull(name, "name");
1342
1343        // Identify the key of the requested individual property
1344        String key = null;
1345        try {
1346            key = resolver.getKey(name);
1347        } catch (final IllegalArgumentException e) {
1348            throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'");
1349        }
1350        if (key == null) {
1351            throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'");
1352        }
1353
1354        // Isolate the name
1355        name = resolver.getProperty(name);
1356
1357        // Request the specified indexed property value
1358        setMappedProperty(bean, name, key, value);
1359    }
1360
1361    /**
1362     * Sets the value of the specified mapped property of the specified bean, with no type conversions.
1363     *
1364     * @param bean  Bean whose property is to be set
1365     * @param name  Mapped property name of the property value to be set
1366     * @param key   Key of the property value to be set
1367     * @param value The property value to be set
1368     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1369     * @throws InvocationTargetException if the property accessor method throws an exception
1370     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1371     */
1372    public void setMappedProperty(final Object bean, final String name, final String key, final Object value)
1373            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1374        Objects.requireNonNull(bean, "bean");
1375        Objects.requireNonNull(name, "name");
1376        Objects.requireNonNull(key, "key");
1377        // Handle DynaBean instances specially
1378        if (bean instanceof DynaBean) {
1379            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1380            if (descriptor == null) {
1381                throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1382            }
1383            ((DynaBean) bean).set(name, key, value);
1384            return;
1385        }
1386
1387        // Retrieve the property descriptor for the specified property
1388        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1389        if (descriptor == null) {
1390            throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1391        }
1392
1393        if (descriptor instanceof MappedPropertyDescriptor) {
1394            // Call the keyed setter method if there is one
1395            Method mappedWriteMethod = ((MappedPropertyDescriptor) descriptor).getMappedWriteMethod();
1396            mappedWriteMethod = MethodUtils.getAccessibleMethod(bean.getClass(), mappedWriteMethod);
1397            if (mappedWriteMethod == null) {
1398                throw new NoSuchMethodException("Property '" + name + "' has no mapped setter method" + "on bean class '" + bean.getClass() + "'");
1399            }
1400            if (LOG.isTraceEnabled()) {
1401                final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1402                LOG.trace("setSimpleProperty: Invoking method " + mappedWriteMethod + " with key=" + key + ", value=" + value + " (class " + valueClassName
1403                        + ")");
1404            }
1405            invokeMethod(mappedWriteMethod, bean, key, value);
1406        } else {
1407            /* means that the result has to be retrieved from a map */
1408            final Method readMethod = getReadMethod(bean.getClass(), descriptor);
1409            if (readMethod == null) {
1410                throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'");
1411            }
1412            final Object invokeResult = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
1413            /* test and fetch from the map */
1414            if (invokeResult instanceof Map) {
1415                toPropertyMap(invokeResult).put(key, value);
1416            }
1417        }
1418    }
1419
1420    /**
1421     * Sets the value of the (possibly nested) property of the specified name, for the specified bean, with no type conversions.
1422     * <p>
1423     * Example values for parameter "name" are:
1424     * <ul>
1425     * <li>"a" -- sets the value of property a of the specified bean</li>
1426     * <li>"a.b" -- gets the value of property a of the specified bean, then on that object sets the value of property b.</li>
1427     * <li>"a(key)" -- sets a value of mapped-property a on the specified bean. This effectively means bean.setA("key").</li>
1428     * <li>"a[3]" -- sets a value of indexed-property a on the specified bean. This effectively means bean.setA(3).</li>
1429     * </ul>
1430     *
1431     * @param bean  Bean whose property is to be modified
1432     * @param name  Possibly nested name of the property to be modified
1433     * @param value Value to which the property is to be set
1434     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1435     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1436     * @throws IllegalArgumentException  if a nested reference to a property returns null
1437     * @throws InvocationTargetException if the property accessor method throws an exception
1438     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1439     */
1440    public void setNestedProperty(Object bean, String name, final Object value)
1441            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1442        Objects.requireNonNull(bean, "bean");
1443        Objects.requireNonNull(name, "name");
1444        // Resolve nested references
1445        while (resolver.hasNested(name)) {
1446            final String next = resolver.next(name);
1447            Object nestedBean = null;
1448            if (bean instanceof Map) {
1449                nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next);
1450            } else if (resolver.isMapped(next)) {
1451                nestedBean = getMappedProperty(bean, next);
1452            } else if (resolver.isIndexed(next)) {
1453                nestedBean = getIndexedProperty(bean, next);
1454            } else {
1455                nestedBean = getSimpleProperty(bean, next);
1456            }
1457            if (nestedBean == null) {
1458                throw new NestedNullException("Null property value for '" + name + "' on bean class '" + bean.getClass() + "'");
1459            }
1460            bean = nestedBean;
1461            name = resolver.remove(name);
1462        }
1463
1464        if (bean instanceof Map) {
1465            setPropertyOfMapBean(toPropertyMap(bean), name, value);
1466        } else if (resolver.isMapped(name)) {
1467            setMappedProperty(bean, name, value);
1468        } else if (resolver.isIndexed(name)) {
1469            setIndexedProperty(bean, name, value);
1470        } else {
1471            setSimpleProperty(bean, name, value);
1472        }
1473    }
1474
1475    /**
1476     * Sets the value of the specified property of the specified bean, no matter which property reference format is used, with no type conversions.
1477     *
1478     * @param bean  Bean whose property is to be modified
1479     * @param name  Possibly indexed and/or nested name of the property to be modified
1480     * @param value Value to which this property is to be set
1481     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1482     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1483     * @throws InvocationTargetException if the property accessor method throws an exception
1484     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1485     */
1486    public void setProperty(final Object bean, final String name, final Object value)
1487            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1488        setNestedProperty(bean, name, value);
1489    }
1490
1491    /**
1492     * This method is called by method setNestedProperty when the current bean is found to be a Map object, and defines how to deal with setting a property on a
1493     * Map.
1494     * <p>
1495     * The standard implementation here is to:
1496     * <ul>
1497     * <li>call bean.set(propertyName) for all propertyName values.</li>
1498     * <li>throw an IllegalArgumentException if the property specifier contains MAPPED_DELIM or INDEXED_DELIM, as Map entries are essentially simple properties;
1499     * mapping and indexing operations do not make sense when accessing a map (even thought the returned object may be a Map or an Array).</li>
1500     * </ul>
1501     * <p>
1502     * The default behavior of BeanUtils 1.7.1 or later is for assigning to "a.b" to mean a.put(b, obj) always. However the behavior of BeanUtils version 1.6.0,
1503     * 1.6.1, 1.7.0 was for "a.b" to mean a.setB(obj) if such a method existed, and a.put(b, obj) otherwise. In version 1.5 it meant a.put(b, obj) always (ie
1504     * the same as the behavior in the current version). In versions prior to 1.5 it meant a.setB(obj) always. [yes, this is all <em>very</em> unfortunate]
1505     * <p>
1506     * Users who would like to customize the meaning of "a.b" in method setNestedProperty when a is a Map can create a custom subclass of this class and
1507     * override this method to implement the behavior of their choice, such as restoring the pre-1.4 behavior of this class if they wish. When overriding this
1508     * method, do not forget to deal with MAPPED_DELIM and INDEXED_DELIM characters in the propertyName.
1509     * <p>
1510     * Note, however, that the recommended solution for objects that implement Map but want their simple properties to come first is for <em>those</em> objects
1511     * to override their get/put methods to implement that behavior, and <em>not</em> to solve the problem by modifying the default behavior of the
1512     * PropertyUtilsBean class by overriding this method.
1513     *
1514     * @param bean         Map bean
1515     * @param propertyName The property name
1516     * @param value        the property value
1517     * @throws IllegalArgumentException  when the propertyName is regarded as being invalid.
1518     * @throws IllegalAccessException    just in case subclasses override this method to try to access real setter methods and find permission is denied.
1519     * @throws InvocationTargetException just in case subclasses override this method to try to access real setter methods, and find it throws an exception when
1520     *                                   invoked.
1521     *
1522     * @throws NoSuchMethodException     just in case subclasses override this method to try to access real setter methods, and want to fail if no simple method
1523     *                                   is available.
1524     * @since 1.8.0
1525     */
1526    protected void setPropertyOfMapBean(final Map<String, Object> bean, String propertyName, final Object value)
1527            throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1528        if (resolver.isMapped(propertyName)) {
1529            final String name = resolver.getProperty(propertyName);
1530            if (name == null || name.isEmpty()) {
1531                propertyName = resolver.getKey(propertyName);
1532            }
1533        }
1534
1535        if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) {
1536            throw new IllegalArgumentException("Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName);
1537        }
1538
1539        bean.put(propertyName, value);
1540    }
1541
1542    /**
1543     * Configure the {@link Resolver} implementation used by BeanUtils.
1544     * <p>
1545     * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression
1546     * language</em> that BeanUtils recognizes.
1547     * <p>
1548     * {@link DefaultResolver} is the default implementation used.
1549     *
1550     * @param resolver The property expression resolver.
1551     * @since 1.8.0
1552     */
1553    public void setResolver(final Resolver resolver) {
1554        if (resolver == null) {
1555            this.resolver = new DefaultResolver();
1556        } else {
1557            this.resolver = resolver;
1558        }
1559    }
1560
1561    /**
1562     * Sets the value of the specified simple property of the specified bean, with no type conversions.
1563     *
1564     * @param bean  Bean whose property is to be modified
1565     * @param name  Name of the property to be modified
1566     * @param value Value to which the property should be set
1567     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1568     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1569     * @throws IllegalArgumentException  if the property name is nested or indexed
1570     * @throws InvocationTargetException if the property accessor method throws an exception
1571     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1572     */
1573    public void setSimpleProperty(final Object bean, final String name, final Object value)
1574            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1575        Objects.requireNonNull(bean, "bean");
1576        Objects.requireNonNull(name, "name");
1577        final Class<?> beanClass = bean.getClass();
1578        // Validate the syntax of the property name
1579        if (resolver.hasNested(name)) {
1580            throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1581        }
1582        if (resolver.isIndexed(name)) {
1583            throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1584        }
1585        if (resolver.isMapped(name)) {
1586            throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1587        }
1588
1589        // Handle DynaBean instances specially
1590        if (bean instanceof DynaBean) {
1591            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1592            if (descriptor == null) {
1593                throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'");
1594            }
1595            ((DynaBean) bean).set(name, value);
1596            return;
1597        }
1598
1599        // Retrieve the property setter method for the specified property
1600        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1601        if (descriptor == null) {
1602            throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + beanClass + "'");
1603        }
1604        final Method writeMethod = getWriteMethod(beanClass, descriptor);
1605        if (writeMethod == null) {
1606            throw new NoSuchMethodException("Property '" + name + "' has no setter method in class '" + beanClass + "'");
1607        }
1608
1609        // Call the property setter method
1610        if (LOG.isTraceEnabled()) {
1611            final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1612            LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with value " + value + " (class " + valueClassName + ")");
1613        }
1614        invokeMethod(writeMethod, bean, value);
1615    }
1616}