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