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