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