    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 */
    017package org.apache.commons.beanutils;
    018
    019import java.beans.BeanInfo;
    020import java.beans.IntrospectionException;
    021import java.beans.Introspector;
    022import java.beans.PropertyDescriptor;
    023import java.lang.reflect.Constructor;
    024import java.lang.reflect.InvocationTargetException;
    025import java.lang.reflect.Method;
    026import java.util.AbstractMap;
    027import java.util.AbstractSet;
    028import java.util.ArrayList;
    029import java.util.Collection;
    030import java.util.Collections;
    031import java.util.HashMap;
    032import java.util.Iterator;
    033import java.util.Map;
    034import java.util.Set;
    035
    036import org.apache.commons.collections.Transformer;
    037import org.apache.commons.collections.keyvalue.AbstractMapEntry;
    038
    039/**
    040 * An implementation of Map for JavaBeans which uses introspection to get and put properties in the bean.
    041 * <p>
    042 * If an exception occurs during attempts to get or set a property then the property is considered non existent in the Map
    043 *
    044 */
    045public class BeanMap extends AbstractMap<Object, Object> implements Cloneable {
    046
    047    /**
    048     * Map entry used by {@link BeanMap}.
    049     */
    050    protected static class Entry extends AbstractMapEntry {
    051        private final BeanMap owner;
    052
    053        /**
    054         * Constructs a new <code>Entry</code>.
    055         *
    056         * @param owner the BeanMap this entry belongs to
    057         * @param key   the key for this entry
    058         * @param value the value for this entry
    059         */
    060        protected Entry(final BeanMap owner, final Object key, final Object value) {
    061            super(key, value);
    062            this.owner = owner;
    063        }
    064
    065        /**
    066         * Sets the value.
    067         *
    068         * @param value the new value for the entry
    069         * @return the old value for the entry
    070         */
    071        @Override
    072        public Object setValue(final Object value) {
    073            final Object key = getKey();
    074            final Object oldValue = owner.get(key);
    075
    076            owner.put(key, value);
    077            final Object newValue = owner.get(key);
    078            super.setValue(newValue);
    079            return oldValue;
    080        }
    081    }
    082
    083    /**
    084     * An empty array. Used to invoke accessors via reflection.
    085     */
    086    public static final Object[] NULL_ARGUMENTS = {};
    087    /**
    088     * Maps primitive Class types to transformers. The transformer transform strings into the appropriate primitive wrapper.
    089     *
    090     * N.B. private & unmodifiable replacement for the (public & static) defaultTransformers instance.
    091     */
    092    private static final Map<Class<? extends Object>, Transformer> typeTransformers = Collections.unmodifiableMap(createTypeTransformers());
    093    /**
    094     * This HashMap has been made unmodifiable to prevent issues when loaded in a shared ClassLoader enviroment.
    095     *
    096     * @see "http://issues.apache.org/jira/browse/BEANUTILS-112"
    097     * @deprecated Use {@link BeanMap#getTypeTransformer(Class)} method
    098     */
    099    @Deprecated
    100    public static HashMap defaultTransformers = new HashMap() {
    101        private static final long serialVersionUID = 1L;
    102
    103        @Override
    104        public void clear() {
    105            throw new UnsupportedOperationException();
    106        }
    107
    108        @Override
    109        public boolean containsKey(final Object key) {
    110            return typeTransformers.containsKey(key);
    111        }
    112
    113        @Override
    114        public boolean containsValue(final Object value) {
    115            return typeTransformers.containsValue(value);
    116        }
    117
    118        @Override
    119        public Set entrySet() {
    120            return typeTransformers.entrySet();
    121        }
    122
    123        @Override
    124        public Object get(final Object key) {
    125            return typeTransformers.get(key);
    126        }
    127
    128        @Override
    129        public boolean isEmpty() {
    130            return false;
    131        }
    132
    133        @Override
    134        public Set keySet() {
    135            return typeTransformers.keySet();
    136        }
    137
    138        @Override
    139        public Object put(final Object key, final Object value) {
    140            throw new UnsupportedOperationException();
    141        }
    142
    143        @Override
    144        public void putAll(final Map m) {
    145            throw new UnsupportedOperationException();
    146        }
    147
    148        @Override
    149        public Object remove(final Object key) {
    150            throw new UnsupportedOperationException();
    151        }
    152
    153        @Override
    154        public int size() {
    155            return typeTransformers.size();
    156        }
    157
    158        @Override
    159        public Collection values() {
    160            return typeTransformers.values();
    161        }
    162    };
    163
    164    private static Map<Class<? extends Object>, Transformer> createTypeTransformers() {
    165        final Map<Class<? extends Object>, Transformer> defaultTransformers = new HashMap<>();
    166        defaultTransformers.put(Boolean.TYPE, input -> Boolean.valueOf(input.toString()));
    167        defaultTransformers.put(Character.TYPE, input -> Character.valueOf(input.toString().charAt(0)));
    168        defaultTransformers.put(Byte.TYPE, input -> Byte.valueOf(input.toString()));
    169        defaultTransformers.put(Short.TYPE, input -> Short.valueOf(input.toString()));
    170        defaultTransformers.put(Integer.TYPE, input -> Integer.valueOf(input.toString()));
    171        defaultTransformers.put(Long.TYPE, input -> Long.valueOf(input.toString()));
    172        defaultTransformers.put(Float.TYPE, input -> Float.valueOf(input.toString()));
    173        defaultTransformers.put(Double.TYPE, input -> Double.valueOf(input.toString()));
    174        return defaultTransformers;
    175    }
    176
    177    private transient Object bean;
    178
    179    private transient HashMap<String, Method> readMethods = new HashMap<>();
    180
    181    private transient HashMap<String, Method> writeMethods = new HashMap<>();
    182
    183    // Constructors
    184
    185    private transient HashMap<String, Class<? extends Object>> types = new HashMap<>();
    186
    187    /**
    188     * Constructs a new empty <code>BeanMap</code>.
    189     */
    190    public BeanMap() {
    191    }
    192
    193    // Map interface
    194
    195    /**
    196     * Constructs a new <code>BeanMap</code> that operates on the specified bean. If the given bean is <code>null</code>, then this map will be empty.
    197     *
    198     * @param bean the bean for this map to operate on
    199     */
    200    public BeanMap(final Object bean) {
    201        this.bean = bean;
    202        initialise();
    203    }
    204
    205    /**
    206     * This method reinitializes the bean map to have default values for the bean's properties. This is accomplished by constructing a new instance of the bean
    207     * which the map uses as its underlying data source. This behavior for <code>clear()</code> differs from the Map contract in that the mappings are not
    208     * actually removed from the map (the mappings for a BeanMap are fixed).
    209     */
    210    @Override
    211    public void clear() {
    212        if (bean == null) {
    213            return;
    214        }
    215        Class<? extends Object> beanClass = null;
    216        try {
    217            beanClass = bean.getClass();
    218            bean = beanClass.getConstructor().newInstance();
    219        } catch (final Exception e) {
    220            throw new UnsupportedOperationException("Could not create new instance of class: " + beanClass, e);
    221        }
    222    }
    223
    224    /**
    225     * Clone this bean map using the following process:
    226     *
    227     * <ul>
    228     * <li>If there is no underlying bean, return a cloned BeanMap without a bean.</li>
    229     * <li>Since there is an underlying bean, try to instantiate a new bean of the same type using Class.newInstance().</li>
    230     * <li>If the instantiation fails, throw a CloneNotSupportedException</li>
    231     * <li>Clone the bean map and set the newly instantiated bean as the underlying bean for the bean map.</li>
    232     * <li>Copy each property that is both readable and writable from the existing object to a cloned bean map.</li>
    233     * <li>If anything fails along the way, throw a CloneNotSupportedException.</li>
    234     * </ul>
    235     *
    236     * @return a cloned instance of this bean map
    237     * @throws CloneNotSupportedException if the underlying bean cannot be cloned
    238     */
    239    @Override
    240    public Object clone() throws CloneNotSupportedException {
    241        final BeanMap newMap = (BeanMap) super.clone();
    242        if (bean == null) {
    243            // no bean, just an empty bean map at the moment. return a newly
    244            // cloned and empty bean map.
    245            return newMap;
    246        }
    247        Object newBean = null;
    248        final Class<? extends Object> beanClass = bean.getClass(); // Cannot throw Exception
    249        try {
    250            newBean = beanClass.getConstructor().newInstance();
    251        } catch (final Exception e) {
    252            // unable to instantiate
    253            final CloneNotSupportedException cnse = new CloneNotSupportedException(
    254                    "Unable to instantiate the underlying bean \"" + beanClass.getName() + "\": " + e);
    255            cnse.initCause(e);
    256            throw cnse;
    257        }
    258        try {
    259            newMap.setBean(newBean);
    260        } catch (final Exception exception) {
    261            final CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to set bean in the cloned bean map: " + exception);
    262            cnse.initCause(exception);
    263            throw cnse;
    264        }
    265        try {
    266            for (final Object key : readMethods.keySet()) {
    267                if (getWriteMethod(key) != null) {
    268                    newMap.put(key, get(key));
    269                }
    270            }
    271        } catch (final Exception exception) {
    272            final CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to copy bean values to cloned bean map: " + exception);
    273            cnse.initCause(exception);
    274            throw cnse;
    275        }
    276
    277        return newMap;
    278    }
    279
    280    /**
    281     * Returns true if the bean defines a property with the given name.
    282     * <p>
    283     * The given name must be a <code>String</code>; if not, this method returns false. This method will also return false if the bean does not define a
    284     * property with that name.
    285     * <p>
    286     * Write-only properties will not be matched as the test operates against property read methods.
    287     *
    288     * @param name the name of the property to check
    289     * @return false if the given name is null or is not a <code>String</code>; false if the bean does not define a property with that name; or true if the bean
    290     *         does define a property with that name
    291     */
    292    @Override
    293    public boolean containsKey(final Object name) {
    294        final Method method = getReadMethod(name);
    295        return method != null;
    296    }
    297
    298    /**
    299     * Returns true if the bean defines a property whose current value is the given object.
    300     *
    301     * @param value the value to check
    302     * @return false true if the bean has at least one property whose current value is that object, false otherwise
    303     */
    304    @Override
    305    public boolean containsValue(final Object value) {
    306        // use default implementation
    307        return super.containsValue(value);
    308    }
    309
    310    /**
    311     * Converts the given value to the given type. First, reflection is is used to find a public constructor declared by the given class that takes one
    312     * argument, which must be the precise type of the given value. If such a constructor is found, a new object is created by passing the given value to that
    313     * constructor, and the newly constructed object is returned.
    314     * <P>
    315     *
    316     * If no such constructor exists, and the given type is a primitive type, then the given value is converted to a string using its {@link Object#toString()
    317     * toString()} method, and that string is parsed into the correct primitive type using, for instance, {@link Integer#valueOf(String)} to convert the string
    318     * into an <code>int</code>.
    319     * <P>
    320     *
    321     * If no special constructor exists and the given type is not a primitive type, this method returns the original value.
    322     *
    323     * @param newType the type to convert the value to
    324     * @param value   the value to convert
    325     * @return the converted value
    326     * @throws NumberFormatException     if newType is a primitive type, and the string representation of the given value cannot be converted to that type
    327     * @throws InstantiationException    if the constructor found with reflection raises it
    328     * @throws InvocationTargetException if the constructor found with reflection raises it
    329     * @throws IllegalAccessException    never
    330     * @throws IllegalArgumentException  never
    331     */
    332    protected Object convertType(final Class<?> newType, final Object value)
    333            throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    334
    335        // try call constructor
    336        final Class<?>[] types = { value.getClass() };
    337        try {
    338            final Constructor<?> constructor = newType.getConstructor(types);
    339            final Object[] arguments = { value };
    340            return constructor.newInstance(arguments);
    341        } catch (final NoSuchMethodException e) {
    342            // try using the transformers
    343            final Transformer transformer = getTypeTransformer(newType);
    344            if (transformer != null) {
    345                return transformer.transform(value);
    346            }
    347            return value;
    348        }
    349    }
    350
    351    /**
    352     * Creates an array of parameters to pass to the given mutator method. If the given object is not the right type to pass to the method directly, it will be
    353     * converted using {@link #convertType(Class,Object)}.
    354     *
    355     * @param method the mutator method
    356     * @param value  the value to pass to the mutator method
    357     * @return an array containing one object that is either the given value or a transformed value
    358     * @throws IllegalAccessException   if {@link #convertType(Class,Object)} raises it
    359     * @throws IllegalArgumentException if any other exception is raised by {@link #convertType(Class,Object)}
    360     * @throws ClassCastException       if an error occurs creating the method args
    361     */
    362    protected Object[] createWriteMethodArguments(final Method method, Object value) throws IllegalAccessException, ClassCastException {
    363        try {
    364            if (value != null) {
    365                final Class<? extends Object>[] types = method.getParameterTypes();
    366                if (types != null && types.length > 0) {
    367                    final Class<? extends Object> paramType = types[0];
    368                    if (!paramType.isAssignableFrom(value.getClass())) {
    369                        value = convertType(paramType, value);
    370                    }
    371                }
    372            }
    373            return new Object[] { value };
    374        } catch (final InvocationTargetException | InstantiationException e) {
    375            throw new IllegalArgumentException(e.getMessage(), e);
    376        }
    377    }
    378
    379    /**
    380     * Convenience method for getting an iterator over the entries.
    381     *
    382     * @return an iterator over the entries
    383     */
    384    public Iterator<Map.Entry<Object, Object>> entryIterator() {
    385        final Iterator<String> iter = keyIterator();
    386        return new Iterator<Map.Entry<Object, Object>>() {
    387            @Override
    388            public boolean hasNext() {
    389                return iter.hasNext();
    390            }
    391
    392            @Override
    393            public Map.Entry<Object, Object> next() {
    394                final Object key = iter.next();
    395                final Object value = get(key);
    396                @SuppressWarnings("unchecked")
    397                final
    398                // This should not cause any problems; the key is actually a
    399                // string, but it does no harm to expose it as Object
    400                Map.Entry<Object, Object> tmpEntry = new Entry(BeanMap.this, key, value);
    401                return tmpEntry;
    402            }
    403
    404            @Override
    405            public void remove() {
    406                throw new UnsupportedOperationException("remove() not supported for BeanMap");
    407            }
    408        };
    409    }
    410
    411    /**
    412     * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
    413     * <p>
    414     * Each MapEntry can be set but not removed.
    415     *
    416     * @return the unmodifiable set of mappings
    417     */
    418    @Override
    419    public Set<Map.Entry<Object, Object>> entrySet() {
    420        return Collections.unmodifiableSet(new AbstractSet<Map.Entry<Object, Object>>() {
    421            @Override
    422            public Iterator<Map.Entry<Object, Object>> iterator() {
    423                return entryIterator();
    424            }
    425
    426            @Override
    427            public int size() {
    428                return BeanMap.this.readMethods.size();
    429            }
    430        });
    431    }
    432
    433    /**
    434     * Called during a successful {@link #put(Object,Object)} operation. Default implementation does nothing. Override to be notified of property changes in the
    435     * bean caused by this map.
    436     *
    437     * @param key      the name of the property that changed
    438     * @param oldValue the old value for that property
    439     * @param newValue the new value for that property
    440     */
    441    protected void firePropertyChange(final Object key, final Object oldValue, final Object newValue) {
    442    }
    443
    444    /**
    445     * Returns the value of the bean's property with the given name.
    446     * <p>
    447     * The given name must be a {@link String} and must not be null; otherwise, this method returns <code>null</code>. If the bean defines a property with the
    448     * given name, the value of that property is returned. Otherwise, <code>null</code> is returned.
    449     * <p>
    450     * Write-only properties will not be matched as the test operates against property read methods.
    451     *
    452     * @param name the name of the property whose value to return
    453     * @return the value of the property with that name
    454     */
    455    @Override
    456    public Object get(final Object name) {
    457        if (bean != null) {
    458            final Method method = getReadMethod(name);
    459            if (method != null) {
    460                try {
    461                    return method.invoke(bean, NULL_ARGUMENTS);
    462                } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException e) {
    463                    logWarn(e);
    464                }
    465            }
    466        }
    467        return null;
    468    }
    469
    470    /**
    471     * Returns the bean currently being operated on. The return value may be null if this map is empty.
    472     *
    473     * @return the bean being operated on by this map
    474     */
    475    public Object getBean() {
    476        return bean;
    477    }
    478
    479    // Helper methods
    480
    481    /**
    482     * Returns the accessor for the property with the given name.
    483     *
    484     * @param name the name of the property
    485     * @return null if the name is null; null if the name is not a {@link String}; null if no such property exists; or the accessor method for that property
    486     */
    487    protected Method getReadMethod(final Object name) {
    488        return readMethods.get(name);
    489    }
    490
    491    /**
    492     * Returns the accessor for the property with the given name.
    493     *
    494     * @param name the name of the property
    495     * @return the accessor method for the property, or null
    496     */
    497    public Method getReadMethod(final String name) {
    498        return readMethods.get(name);
    499    }
    500
    501    /**
    502     * Returns the type of the property with the given name.
    503     *
    504     * @param name the name of the property
    505     * @return the type of the property, or <code>null</code> if no such property exists
    506     */
    507    public Class<?> getType(final String name) {
    508        return types.get(name);
    509    }
    510
    511    /**
    512     * Returns a transformer for the given primitive type.
    513     *
    514     * @param aType the primitive type whose transformer to return
    515     * @return a transformer that will convert strings into that type, or null if the given type is not a primitive type
    516     */
    517    protected Transformer getTypeTransformer(final Class<?> aType) {
    518        return typeTransformers.get(aType);
    519    }
    520
    521    // Properties
    522
    523    /**
    524     * Returns the mutator for the property with the given name.
    525     *
    526     * @param name the name of the
    527     * @return null if the name is null; null if the name is not a {@link String}; null if no such property exists; null if the property is read-only; or the
    528     *         mutator method for that property
    529     */
    530    protected Method getWriteMethod(final Object name) {
    531        return writeMethods.get(name);
    532    }
    533
    534    /**
    535     * Returns the mutator for the property with the given name.
    536     *
    537     * @param name the name of the property
    538     * @return the mutator method for the property, or null
    539     */
    540    public Method getWriteMethod(final String name) {
    541        return writeMethods.get(name);
    542    }
    543
    544    private void initialise() {
    545        if (getBean() == null) {
    546            return;
    547        }
    548
    549        final Class<? extends Object> beanClass = getBean().getClass();
    550        try {
    551            // BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
    552            final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
    553            final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
    554            if (propertyDescriptors != null) {
    555                for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {
    556                    if (propertyDescriptor != null) {
    557                        final String name = propertyDescriptor.getName();
    558                        final Method readMethod = propertyDescriptor.getReadMethod();
    559                        final Method writeMethod = propertyDescriptor.getWriteMethod();
    560                        final Class<? extends Object> aType = propertyDescriptor.getPropertyType();
    561
    562                        if (readMethod != null) {
    563                            readMethods.put(name, readMethod);
    564                        }
    565                        if (writeMethod != null) {
    566                            writeMethods.put(name, writeMethod);
    567                        }
    568                        types.put(name, aType);
    569                    }
    570                }
    571            }
    572        } catch (final IntrospectionException e) {
    573            logWarn(e);
    574        }
    575    }
    576
    577    /**
    578     * Convenience method for getting an iterator over the keys.
    579     * <p>
    580     * Write-only properties will not be returned in the iterator.
    581     *
    582     * @return an iterator over the keys
    583     */
    584    public Iterator<String> keyIterator() {
    585        return readMethods.keySet().iterator();
    586    }
    587
    588    // Implementation methods
    589
    590    /**
    591     * Get the keys for this BeanMap.
    592     * <p>
    593     * Write-only properties are <strong>not</strong> included in the returned set of property names, although it is possible to set their value and to get
    594     * their type.
    595     *
    596     * @return BeanMap keys. The Set returned by this method is not modifiable.
    597     */
    598    @SuppressWarnings({ "unchecked", "rawtypes" })
    599    // The set actually contains strings; however, because it cannot be
    600    // modified there is no danger in selling it as Set<Object>
    601    @Override
    602    public Set<Object> keySet() {
    603        return Collections.unmodifiableSet((Set) readMethods.keySet());
    604    }
    605
    606    /**
    607     * Logs the given exception to <code>System.out</code>. Used to display warnings while accessing/mutating the bean.
    608     *
    609     * @param ex the exception to log
    610     */
    611    protected void logInfo(final Exception ex) {
    612        // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
    613        System.out.println("INFO: Exception: " + ex);
    614    }
    615
    616    /**
    617     * Logs the given exception to <code>System.err</code>. Used to display errors while accessing/mutating the bean.
    618     *
    619     * @param ex the exception to log
    620     */
    621    protected void logWarn(final Exception ex) {
    622        // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
    623        System.out.println("WARN: Exception: " + ex);
    624        ex.printStackTrace();
    625    }
    626
    627    /**
    628     * Sets the bean property with the given name to the given value.
    629     *
    630     * @param name  the name of the property to set
    631     * @param value the value to set that property to
    632     * @return the previous value of that property
    633     * @throws IllegalArgumentException if the given name is null; if the given name is not a {@link String}; if the bean doesn't define a property with that
    634     *                                  name; or if the bean property with that name is read-only
    635     * @throws ClassCastException       if an error occurs creating the method args
    636     */
    637    @Override
    638    public Object put(final Object name, final Object value) throws IllegalArgumentException, ClassCastException {
    639        if (bean != null) {
    640            final Object oldValue = get(name);
    641            final Method method = getWriteMethod(name);
    642            if (method == null) {
    643                throw new IllegalArgumentException("The bean of type: " + bean.getClass().getName() + " has no property called: " + name);
    644            }
    645            try {
    646                final Object[] arguments = createWriteMethodArguments(method, value);
    647                method.invoke(bean, arguments);
    648
    649                final Object newValue = get(name);
    650                firePropertyChange(name, oldValue, newValue);
    651            } catch (final InvocationTargetException | IllegalAccessException e) {
    652                throw new IllegalArgumentException(e.getMessage(), e);
    653            }
    654            return oldValue;
    655        }
    656        return null;
    657    }
    658
    659    /**
    660     * Puts all of the writable properties from the given BeanMap into this BeanMap. Read-only and Write-only properties will be ignored.
    661     *
    662     * @param map the BeanMap whose properties to put
    663     */
    664    public void putAllWriteable(final BeanMap map) {
    665        for (final Object key : map.readMethods.keySet()) {
    666            if (getWriteMethod(key) != null) {
    667                put(key, map.get(key));
    668            }
    669        }
    670    }
    671
    672    // Implementation classes
    673
    674    /**
    675     * Reinitializes this bean. Called during {@link #setBean(Object)}. Does introspection to find properties.
    676     */
    677    protected void reinitialise() {
    678        readMethods.clear();
    679        writeMethods.clear();
    680        types.clear();
    681        initialise();
    682    }
    683
    684    /**
    685     * Sets the bean to be operated on by this map. The given value may be null, in which case this map will be empty.
    686     *
    687     * @param newBean the new bean to operate on
    688     */
    689    public void setBean(final Object newBean) {
    690        bean = newBean;
    691        reinitialise();
    692    }
    693
    694    /**
    695     * Returns the number of properties defined by the bean.
    696     *
    697     * @return the number of properties defined by the bean
    698     */
    699    @Override
    700    public int size() {
    701        return readMethods.size();
    702    }
    703
    704    /**
    705     * Renders a string representation of this object.
    706     *
    707     * @return a <code>String</code> representation of this object
    708     */
    709    @Override
    710    public String toString() {
    711        return "BeanMap<" + String.valueOf(bean) + ">";
    712    }
    713
    714    /**
    715     * Convenience method for getting an iterator over the values.
    716     *
    717     * @return an iterator over the values
    718     */
    719    public Iterator<Object> valueIterator() {
    720        final Iterator<?> iter = keyIterator();
    721        return new Iterator<Object>() {
    722            @Override
    723            public boolean hasNext() {
    724                return iter.hasNext();
    725            }
    726
    727            @Override
    728            public Object next() {
    729                final Object key = iter.next();
    730                return get(key);
    731            }
    732
    733            @Override
    734            public void remove() {
    735                throw new UnsupportedOperationException("remove() not supported for BeanMap");
    736            }
    737        };
    738    }
    739
    740    /**
    741     * Returns the values for the BeanMap.
    742     *
    743     * @return values for the BeanMap. The returned collection is not modifiable.
    744     */
    745    @Override
    746    public Collection<Object> values() {
    747        final ArrayList<Object> answer = new ArrayList<>(readMethods.size());
    748        for (final Iterator<Object> iter = valueIterator(); iter.hasNext();) {
    749            answer.add(iter.next());
    750        }
    751        return Collections.unmodifiableList(answer);
    752    }
    753}