    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.beanutils;
    019
    020import java.io.Serializable;
    021import java.lang.reflect.Array;
    022import java.util.HashMap;
    023import java.util.List;
    024import java.util.Map;
    025import java.util.Objects;
    026
    027/**
    028 * <p>Minimal implementation of the <code>DynaBean</code> interface.  Can be
    029 * used as a convenience base class for more sophisticated implementations.</p>
    030 *
    031 * <p><strong>IMPLEMENTATION NOTE</strong> - Instances of this class that are
    032 * accessed from multiple threads simultaneously need to be synchronized.</p>
    033 *
    034 * <p><strong>IMPLEMENTATION NOTE</strong> - Instances of this class can be
    035 * successfully serialized and deserialized <strong>ONLY</strong> if all
    036 * property values are <code>Serializable</code>.</p>
    037 *
    038 */
    039
    040public class BasicDynaBean implements DynaBean, Serializable {
    041
    042    private static final long serialVersionUID = 1L;
    043
    044    /**
    045     * The <code>DynaClass</code> "base class" that this DynaBean
    046     * is associated with.
    047     */
    048    protected DynaClass dynaClass;
    049
    050    /**
    051     * The set of property values for this DynaBean, keyed by property name.
    052     */
    053    protected HashMap<String, Object> values = new HashMap<>();
    054
    055    /** Map decorator for this DynaBean */
    056    private transient Map<String, Object> mapDecorator;
    057
    058    /**
    059     * Construct a new <code>DynaBean</code> associated with the specified
    060     * <code>DynaClass</code> instance.
    061     *
    062     * @param dynaClass The DynaClass we are associated with
    063     */
    064    public BasicDynaBean(final DynaClass dynaClass) {
    065
    066        this.dynaClass = dynaClass;
    067
    068    }
    069
    070    /**
    071     * Does the specified mapped property contain a value for the specified
    072     * key value?
    073     *
    074     * @param name Name of the property to check
    075     * @param key Name of the key to check
    076     * @return <code>true</code> if the mapped property contains a value for
    077     * the specified key, otherwise <code>false</code>
    078     *
    079     * @throws IllegalArgumentException if there is no property
    080     *  of the specified name
    081     */
    082    @Override
    083    public boolean contains(final String name, final String key) {
    084        final Object value = values.get(name);
    085        Objects.requireNonNull(value, () -> "No mapped value for '" + name + "(" + key + ")'");
    086        if (value instanceof Map) {
    087            return ((Map<?, ?>) value).containsKey(key);
    088        }
    089        throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'");
    090    }
    091
    092    /**
    093     * Return the value of a simple property with the specified name.
    094     *
    095     * @param name Name of the property whose value is to be retrieved
    096     * @return The property's value
    097     * @throws IllegalArgumentException if there is no property
    098     *  of the specified name
    099     */
    100    @Override
    101    public Object get(final String name) {
    102
    103        // Return any non-null value for the specified property
    104        final Object value = values.get(name);
    105        if (value != null) {
    106            return value;
    107        }
    108
    109        // Return a null value for a non-primitive property
    110        final Class<?> type = getDynaProperty(name).getType();
    111        if (!type.isPrimitive()) {
    112            return value;
    113        }
    114
    115        // Manufacture default values for primitive properties
    116        if (type == Boolean.TYPE) {
    117            return Boolean.FALSE;
    118        }
    119        if (type == Byte.TYPE) {
    120            return Byte.valueOf((byte) 0);
    121        }
    122        if (type == Character.TYPE) {
    123            return Character.valueOf((char) 0);
    124        }
    125        if (type == Double.TYPE) {
    126            return Double.valueOf(0.0);
    127        }
    128        if (type == Float.TYPE) {
    129            return Float.valueOf((float) 0.0);
    130        }
    131        if (type == Integer.TYPE) {
    132            return Integer.valueOf(0);
    133        }
    134        if (type == Long.TYPE) {
    135            return Long.valueOf(0);
    136        }
    137        if (type == Short.TYPE) {
    138            return Short.valueOf((short) 0);
    139        }
    140        return null;
    141    }
    142
    143    /**
    144     * Return the value of an indexed property with the specified name.
    145     *
    146     * @param name Name of the property whose value is to be retrieved
    147     * @param index Index of the value to be retrieved
    148     * @return The indexed property's value
    149     * @throws IllegalArgumentException if there is no property
    150     *  of the specified name
    151     * @throws IllegalArgumentException if the specified property
    152     *  exists, but is not indexed
    153     * @throws IndexOutOfBoundsException if the specified index
    154     *  is outside the range of the underlying property
    155     * @throws NullPointerException if no array or List has been
    156     *  initialized for this property
    157     */
    158    @Override
    159    public Object get(final String name, final int index) {
    160        final Object value = values.get(name);
    161        Objects.requireNonNull(value, () -> "No indexed value for '" + name + "[" + index + "]'");
    162        if (value.getClass().isArray()) {
    163            return Array.get(value, index);
    164        }
    165        if (value instanceof List) {
    166            return ((List<?>) value).get(index);
    167        }
    168        throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]'");
    169    }
    170
    171    /**
    172     * Return the value of a mapped property with the specified name,
    173     * or <code>null</code> if there is no value for the specified key.
    174     *
    175     * @param name Name of the property whose value is to be retrieved
    176     * @param key Key of the value to be retrieved
    177     * @return The mapped property's value
    178     * @throws IllegalArgumentException if there is no property
    179     *  of the specified name
    180     * @throws IllegalArgumentException if the specified property
    181     *  exists, but is not mapped
    182     */
    183    @Override
    184    public Object get(final String name, final String key) {
    185        final Object value = values.get(name);
    186        Objects.requireNonNull(value, () -> "No mapped value for '" + name + "(" + key + ")'");
    187        if (value instanceof Map) {
    188            return ((Map<?, ?>) value).get(key);
    189        }
    190        throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'");
    191    }
    192
    193    /**
    194     * Return the <code>DynaClass</code> instance that describes the set of
    195     * properties available for this DynaBean.
    196     *
    197     * @return The associated DynaClass
    198     */
    199    @Override
    200    public DynaClass getDynaClass() {
    201
    202        return this.dynaClass;
    203
    204    }
    205
    206    /**
    207     * Return the property descriptor for the specified property name.
    208     *
    209     * @param name Name of the property for which to retrieve the descriptor
    210     * @return The property descriptor
    211     * @throws IllegalArgumentException if this is not a valid property
    212     *  name for our DynaClass
    213     */
    214    protected DynaProperty getDynaProperty(final String name) {
    215
    216        final DynaProperty descriptor = getDynaClass().getDynaProperty(name);
    217        if (descriptor == null) {
    218            throw new IllegalArgumentException
    219                    ("Invalid property name '" + name + "'");
    220        }
    221        return descriptor;
    222
    223    }
    224
    225    /**
    226     * Return a Map representation of this DynaBean.
    227     * <p>
    228     * This, for example, could be used in JSTL in the following way to access
    229     * a DynaBean's <code>fooProperty</code>:
    230     * </p>
    231     * <ul><li><code>${myDynaBean.<strong>map</strong>.fooProperty}</code></li></ul>
    232     *
    233     * @return a Map representation of this DynaBean
    234     * @since 1.8.0
    235     */
    236    public Map<String, Object> getMap() {
    237
    238        // cache the Map
    239        if (mapDecorator == null) {
    240            mapDecorator = new DynaBeanPropertyMapDecorator(this);
    241        }
    242        return mapDecorator;
    243
    244    }
    245
    246    /**
    247     * Is an object of the source class assignable to the destination class?
    248     *
    249     * @param dest Destination class
    250     * @param source Source class
    251     * @return <code>true</code> if the source class is assignable to the
    252     * destination class, otherwise <code>false</code>
    253     */
    254    protected boolean isAssignable(final Class<?> dest, final Class<?> source) {
    255
    256        if (dest.isAssignableFrom(source) ||
    257                dest == Boolean.TYPE && source == Boolean.class ||
    258                dest == Byte.TYPE && source == Byte.class ||
    259                dest == Character.TYPE && source == Character.class ||
    260                dest == Double.TYPE && source == Double.class ||
    261                dest == Float.TYPE && source == Float.class ||
    262                dest == Integer.TYPE && source == Integer.class ||
    263                dest == Long.TYPE && source == Long.class ||
    264                dest == Short.TYPE && source == Short.class) {
    265            return true;
    266        }
    267        return false;
    268
    269    }
    270
    271    /**
    272     * Remove any existing value for the specified key on the
    273     * specified mapped property.
    274     *
    275     * @param name Name of the property for which a value is to
    276     *  be removed
    277     * @param key Key of the value to be removed
    278     * @throws IllegalArgumentException if there is no property
    279     *  of the specified name
    280     */
    281    @Override
    282    public void remove(final String name, final String key) {
    283        final Object value = values.get(name);
    284        Objects.requireNonNull(value, () -> "No mapped value for '" + name + "(" + key + ")'");
    285        if (!(value instanceof Map)) {
    286            throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'");
    287        }
    288        ((Map<?, ?>) value).remove(key);
    289    }
    290
    291    /**
    292     * Set the value of an indexed property with the specified name.
    293     *
    294     * @param name Name of the property whose value is to be set
    295     * @param index Index of the property to be set
    296     * @param value Value to which this property is to be set
    297     * @throws ConversionException if the specified value cannot be
    298     *  converted to the type required for this property
    299     * @throws IllegalArgumentException if there is no property
    300     *  of the specified name
    301     * @throws IllegalArgumentException if the specified property
    302     *  exists, but is not indexed
    303     * @throws IndexOutOfBoundsException if the specified index
    304     *  is outside the range of the underlying property
    305     */
    306    @Override
    307    public void set(final String name, final int index, final Object value) {
    308
    309        final Object prop = values.get(name);
    310        Objects.requireNonNull(prop, () -> "No indexed value for '" + name + "[" + index + "]'");
    311        if (prop.getClass().isArray()) {
    312            Array.set(prop, index, value);
    313        } else if (prop instanceof List) {
    314            try {
    315                @SuppressWarnings("unchecked")
    316                final
    317                // This is safe to cast because list properties are always
    318                // of type Object
    319                List<Object> list = (List<Object>) prop;
    320                list.set(index, value);
    321            } catch (final ClassCastException e) {
    322                throw new ConversionException(e.getMessage());
    323            }
    324        } else {
    325            throw new IllegalArgumentException
    326                    ("Non-indexed property for '" + name + "[" + index + "]'");
    327        }
    328
    329    }
    330
    331    /**
    332     * Set the value of a simple property with the specified name.
    333     *
    334     * @param name Name of the property whose value is to be set
    335     * @param value Value to which this property is to be set
    336     * @throws ConversionException if the specified value cannot be
    337     *  converted to the type required for this property
    338     * @throws IllegalArgumentException if there is no property
    339     *  of the specified name
    340     * @throws NullPointerException if an attempt is made to set a
    341     *  primitive property to null
    342     */
    343    @Override
    344    public void set(final String name, final Object value) {
    345
    346        final DynaProperty descriptor = getDynaProperty(name);
    347        if (value == null) {
    348            if (descriptor.getType().isPrimitive()) {
    349                throw new NullPointerException
    350                        ("Primitive value for '" + name + "'");
    351            }
    352        } else if (!isAssignable(descriptor.getType(), value.getClass())) {
    353            throw new ConversionException
    354                    ("Cannot assign value of type '" +
    355                    value.getClass().getName() +
    356                    "' to property '" + name + "' of type '" +
    357                    descriptor.getType().getName() + "'");
    358        }
    359        values.put(name, value);
    360
    361    }
    362
    363    /**
    364     * Set the value of a mapped property with the specified name.
    365     *
    366     * @param name Name of the property whose value is to be set
    367     * @param key Key of the property to be set
    368     * @param value Value to which this property is to be set
    369     * @throws ConversionException if the specified value cannot be
    370     *  converted to the type required for this property
    371     * @throws IllegalArgumentException if there is no property
    372     *  of the specified name
    373     * @throws IllegalArgumentException if the specified property
    374     *  exists, but is not mapped
    375     */
    376    @Override
    377    public void set(final String name, final String key, final Object value) {
    378        final Object prop = values.get(name);
    379        Objects.requireNonNull(prop, () -> "No mapped value for '" + name + "(" + key + ")'");
    380        if (!(prop instanceof Map)) {
    381            throw new IllegalArgumentException
    382                    ("Non-mapped property for '" + name + "(" + key + ")'");
    383        }
    384        // This is safe to cast because mapped properties are always
    385        // maps of types String -> Object
    386        @SuppressWarnings("unchecked")
    387        final Map<String, Object> map = (Map<String, Object>) prop;
    388        map.put(key, value);
    389    }
    390
    391}
    392