    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.lang.reflect.Array;
    020import java.math.BigDecimal;
    021import java.math.BigInteger;
    022import java.util.ArrayList;
    023import java.util.Date;
    024import java.util.HashMap;
    025import java.util.List;
    026import java.util.Map;
    027import java.util.Objects;
    028
    029import org.apache.commons.logging.Log;
    030import org.apache.commons.logging.LogFactory;
    031
    032/**
    033 * <p>
    034 * DynaBean which automatically adds properties to the {@code DynaClass} and provides <em>Lazy List</em> and <em>Lazy Map</em> features.
    035 * </p>
    036 *
    037 * <p>
    038 * DynaBeans deal with three types of properties - <em>simple</em>, <em>indexed</em> and <em>mapped</em> and have the following
    039 * {@code get()</code> and <code>set()} methods for
    040 *    each of these types:</p>
    041 *    <ul>
    042 *        <li><em>Simple</em> property methods - {@code get(name)} and
    043 *                          {@code set(name, value)}</li>
    044 *        <li><em>Indexed</em> property methods - {@code get(name, index)} and
    045 *                          {@code set(name, index, value)}</li>
    046 *        <li><em>Mapped</em> property methods - {@code get(name, key)} and
    047 *                          {@code set(name, key, value)}</li>
    048 *    </ul>
    049 *
    050 * <p><strong><u>Getting Property Values</u></strong></p>
    051 * <p>Calling any of the {@code get()} methods, for a property which
    052 *    doesn't exist, returns {@code null} in this implementation.</p>
    053 *
    054 * <p><strong><u>Setting Simple Properties</u></strong></p>
    055 *    <p>The {@code LazyDynaBean</code> will automatically add a property to the <code>DynaClass}
    056 *       if it doesn't exist when the {@code set(name, value)} method is called.</p>
    057 *
    058 *     <pre>{@code
    059 *         DynaBean myBean = new LazyDynaBean();
    060 *         myBean.set("myProperty", "myValue");
    061 *     }</pre>
    062 *
    063 * <p><strong><u>Setting Indexed Properties</u></strong></p>
    064 *    <p>If the property <strong>doesn't</strong> exist, the {@code LazyDynaBean} will automatically add
    065 *       a property with an {@code ArrayList</code> type to the <code>DynaClass} when
    066 *       the {@code set(name, index, value)} method is called.
    067 *       It will also instantiate a new {@code ArrayList} and automatically <em>grow</em>
    068 *       the {@code List} so that it is big enough to accommodate the index being set.
    069 *       {@code ArrayList} is the default indexed property that LazyDynaBean uses but
    070 *       this can be easily changed by overriding the {@code defaultIndexedProperty(name)}
    071 *       method.</p>
    072 *
    073 *     <pre>{@code
    074 *         DynaBean myBean = new LazyDynaBean();
    075 *         myBean.set("myIndexedProperty", 0, "myValue1");
    076 *         myBean.set("myIndexedProperty", 1, "myValue2");
    077 *     }</pre>
    078 *
    079 *    <p>If the indexed property <strong>does</strong> exist in the {@code DynaClass} but is set to
    080 *      {@code null</code> in the <code>LazyDynaBean}, then it will instantiate a
    081 *      new {@code List</code> or <code>Array} as specified by the property's type
    082 *      in the {@code DynaClass</code> and automatically <em>grow</em> the <code>List}
    083 *      or {@code Array} so that it is big enough to accommodate the index being set.</p>
    084 *
    085 *     <pre>{@code
    086 *         DynaBean myBean = new LazyDynaBean();
    087 *         MutableDynaClass myClass = (MutableDynaClass)myBean.getDynaClass();
    088 *         myClass.add("myIndexedProperty", int[].class);
    089 *         myBean.set("myIndexedProperty", 0, Integer.valueOf(10));
    090 *         myBean.set("myIndexedProperty", 1, Integer.valueOf(20));
    091 *     }</pre>
    092 *
    093 * <p><strong><u>Setting Mapped Properties</u></strong></p>
    094 *    <p>If the property <strong>doesn't</strong> exist, the {@code LazyDynaBean} will automatically add
    095 *       a property with a {@code HashMap</code> type to the <code>DynaClass} and
    096 *       instantiate a new {@code HashMap} in the DynaBean when the
    097 *       {@code set(name, key, value)</code> method is called. <code>HashMap} is the default
    098 *       mapped property that LazyDynaBean uses but this can be easily changed by overriding
    099 *       the {@code defaultMappedProperty(name)} method.</p>
    100 *
    101 *     <pre>{@code
    102 *         DynaBean myBean = new LazyDynaBean();
    103 *         myBean.set("myMappedProperty", "myKey", "myValue");
    104 *     }</pre>
    105 *
    106 *    <p>If the mapped property <strong>does</strong> exist in the {@code DynaClass} but is set to
    107 *      {@code null</code> in the <code>LazyDynaBean}, then it will instantiate a
    108 *      new {@code Map</code> as specified by the property's type in the <code>DynaClass}.</p>
    109 *
    110 *     <pre>{@code
    111 *         DynaBean myBean = new LazyDynaBean();
    112 *         MutableDynaClass myClass = (MutableDynaClass)myBean.getDynaClass();
    113 *         myClass.add("myMappedProperty", TreeMap.class);
    114 *         myBean.set("myMappedProperty", "myKey", "myValue");
    115 *     }</pre>
    116 *
    117 * <p><strong><u><em>Restricted</em> DynaClass</u></strong></p>
    118 *    <p>{@code MutableDynaClass</code> have a facility to <em>restrict</em> the <code>DynaClass} so that its properties cannot be modified. If the
    119 * {@code MutableDynaClass} is restricted then calling any of the {@code set()} methods for a property which doesn't exist will result in a
    120 * {@code IllegalArgumentException} being thrown.
    121 * </p>
    122 *
    123 * @see LazyDynaClass
    124 */
    125public class LazyDynaBean implements DynaBean {
    126
    127    private static final long serialVersionUID = 1L;
    128
    129    /**
    130     * Commons Logging
    131     */
    132    private static transient Log LOG = LogFactory.getLog(LazyDynaBean.class);
    133
    134    /** BigInteger Zero */
    135    protected static final BigInteger BigInteger_ZERO = new BigInteger("0");
    136    /** BigDecimal Zero */
    137    protected static final BigDecimal BigDecimal_ZERO = new BigDecimal("0");
    138    /** Character Space */
    139    protected static final Character Character_SPACE = Character.valueOf(' ');
    140    /** Byte Zero */
    141    protected static final Byte Byte_ZERO = Byte.valueOf((byte) 0);
    142    /** Short Zero */
    143    protected static final Short Short_ZERO = Short.valueOf((short) 0);
    144    /** Integer Zero */
    145    protected static final Integer Integer_ZERO = Integer.valueOf(0);
    146    /** Long Zero */
    147    protected static final Long Long_ZERO = Long.valueOf(0);
    148    /** Float Zero */
    149    protected static final Float Float_ZERO = Float.valueOf((byte) 0);
    150    /** Double Zero */
    151    protected static final Double Double_ZERO = Double.valueOf((byte) 0);
    152
    153    static final LazyDynaBean[] EMPTY_ARRAY = {};
    154
    155    /**
    156     * The {@code MutableDynaClass} "base class" that this DynaBean is associated with.
    157     */
    158    protected Map<String, Object> values;
    159
    160    /** Map decorator for this DynaBean */
    161    private transient Map<String, Object> mapDecorator;
    162
    163    /**
    164     * The {@code MutableDynaClass} "base class" that this DynaBean is associated with.
    165     */
    166    protected MutableDynaClass dynaClass;
    167
    168    /**
    169     * Constructs a new {@code LazyDynaBean</code> with a <code>LazyDynaClass} instance.
    170     */
    171    public LazyDynaBean() {
    172        this(new LazyDynaClass());
    173    }
    174
    175    /**
    176     * Constructs a new {@code DynaBean} associated with the specified {@code DynaClass</code> instance - if its not a <code>MutableDynaClass} then a new
    177     * {@code LazyDynaClass} is created and the properties copied.
    178     *
    179     * @param dynaClass The DynaClass we are associated with
    180     */
    181    public LazyDynaBean(final DynaClass dynaClass) {
    182        values = newMap();
    183
    184        if (dynaClass instanceof MutableDynaClass) {
    185            this.dynaClass = (MutableDynaClass) dynaClass;
    186        } else {
    187            this.dynaClass = new LazyDynaClass(dynaClass.getName(), dynaClass.getDynaProperties());
    188        }
    189    }
    190
    191    /**
    192     * Constructs a new {@code LazyDynaBean</code> with a <code>LazyDynaClass} instance.
    193     *
    194     * @param name Name of this DynaBean class
    195     */
    196    public LazyDynaBean(final String name) {
    197        this(new LazyDynaClass(name));
    198    }
    199
    200    /**
    201     * Does the specified mapped property contain a value for the specified key value?
    202     *
    203     * @param name Name of the property to check
    204     * @param key  Name of the key to check
    205     * @return {@code true} if the mapped property contains a value for the specified key, otherwise {@code false}
    206     * @throws IllegalArgumentException if no property name is specified
    207     */
    208    @Override
    209    public boolean contains(final String name, final String key) {
    210        Objects.requireNonNull(name, "name");
    211        final Object value = values.get(name);
    212        if (value == null) {
    213            return false;
    214        }
    215
    216        if (value instanceof Map) {
    217            return ((Map<?, ?>) value).containsKey(key);
    218        }
    219
    220        return false;
    221    }
    222
    223    /**
    224     * Create a new Instance of a 'DynaBean' Property.
    225     *
    226     * @param name The name of the property
    227     * @param type The class of the property
    228     * @return The new value
    229     */
    230    protected Object createDynaBeanProperty(final String name, final Class<?> type) {
    231        try {
    232            return type.newInstance();
    233        } catch (final Exception ex) {
    234            if (logger().isWarnEnabled()) {
    235                logger().warn("Error instantiating DynaBean property of type '" + type.getName() + "' for '" + name + "' ", ex);
    236            }
    237            return null;
    238        }
    239    }
    240
    241    /**
    242     * Create a new Instance of an 'Indexed' Property
    243     *
    244     * @param name The name of the property
    245     * @param type The class of the property
    246     * @return The new value
    247     */
    248    protected Object createIndexedProperty(final String name, final Class<?> type) {
    249        // Create the indexed object
    250        Object indexedProperty = null;
    251
    252        if (type == null) {
    253
    254            indexedProperty = defaultIndexedProperty(name);
    255
    256        } else if (type.isArray()) {
    257
    258            indexedProperty = Array.newInstance(type.getComponentType(), 0);
    259
    260        } else if (List.class.isAssignableFrom(type)) {
    261            if (type.isInterface()) {
    262                indexedProperty = defaultIndexedProperty(name);
    263            } else {
    264                try {
    265                    indexedProperty = type.newInstance();
    266                } catch (final Exception ex) {
    267                    throw new IllegalArgumentException("Error instantiating indexed property of type '" + type.getName() + "' for '" + name + "' " + ex);
    268                }
    269            }
    270        } else {
    271
    272            throw new IllegalArgumentException("Non-indexed property of type '" + type.getName() + "' for '" + name + "'");
    273        }
    274
    275        return indexedProperty;
    276    }
    277
    278    /**
    279     * Create a new Instance of a 'Mapped' Property
    280     *
    281     * @param name The name of the property
    282     * @param type The class of the property
    283     * @return The new value
    284     */
    285    protected Object createMappedProperty(final String name, final Class<?> type) {
    286        // Create the mapped object
    287        Object mappedProperty = null;
    288
    289        if (type == null || type.isInterface()) {
    290
    291            mappedProperty = defaultMappedProperty(name);
    292
    293        } else if (Map.class.isAssignableFrom(type)) {
    294            try {
    295                mappedProperty = type.newInstance();
    296            } catch (final Exception ex) {
    297                throw new IllegalArgumentException("Error instantiating mapped property of type '" + type.getName() + "' for '" + name + "' " + ex);
    298            }
    299        } else {
    300
    301            throw new IllegalArgumentException("Non-mapped property of type '" + type.getName() + "' for '" + name + "'");
    302        }
    303
    304        return mappedProperty;
    305    }
    306
    307    /**
    308     * Create a new Instance of a {@link Number} Property.
    309     *
    310     * @param name The name of the property
    311     * @param type The class of the property
    312     * @return The new value
    313     */
    314    protected Object createNumberProperty(final String name, final Class<?> type) {
    315        return null;
    316    }
    317
    318    /**
    319     * Create a new Instance of other Property types
    320     *
    321     * @param name The name of the property
    322     * @param type The class of the property
    323     * @return The new value
    324     */
    325    protected Object createOtherProperty(final String name, final Class<?> type) {
    326        if (type == Object.class || type == String.class || type == Boolean.class || type == Character.class || Date.class.isAssignableFrom(type)) {
    327
    328            return null;
    329
    330        }
    331
    332        try {
    333            return type.newInstance();
    334        } catch (final Exception ex) {
    335            if (logger().isWarnEnabled()) {
    336                logger().warn("Error instantiating property of type '" + type.getName() + "' for '" + name + "' ", ex);
    337            }
    338            return null;
    339        }
    340    }
    341
    342    /**
    343     * Create a new Instance of a 'Primitive' Property.
    344     *
    345     * @param name The name of the property
    346     * @param type The class of the property
    347     * @return The new value
    348     */
    349    protected Object createPrimitiveProperty(final String name, final Class<?> type) {
    350        if (type == Boolean.TYPE) {
    351            return Boolean.FALSE;
    352        }
    353        if (type == Integer.TYPE) {
    354            return Integer_ZERO;
    355        }
    356        if (type == Long.TYPE) {
    357            return Long_ZERO;
    358        }
    359        if (type == Double.TYPE) {
    360            return Double_ZERO;
    361        }
    362        if (type == Float.TYPE) {
    363            return Float_ZERO;
    364        }
    365        if (type == Byte.TYPE) {
    366            return Byte_ZERO;
    367        }
    368        if (type == Short.TYPE) {
    369            return Short_ZERO;
    370        }
    371        if (type == Character.TYPE) {
    372            return Character_SPACE;
    373        }
    374        return null;
    375    }
    376
    377    /**
    378     * Create a new Instance of a Property
    379     *
    380     * @param name The name of the property
    381     * @param type The class of the property
    382     * @return The new value
    383     */
    384    protected Object createProperty(final String name, final Class<?> type) {
    385        if (type == null) {
    386            return null;
    387        }
    388
    389        // Create Lists, arrays or DynaBeans
    390        if (type.isArray() || List.class.isAssignableFrom(type)) {
    391            return createIndexedProperty(name, type);
    392        }
    393
    394        if (Map.class.isAssignableFrom(type)) {
    395            return createMappedProperty(name, type);
    396        }
    397
    398        if (DynaBean.class.isAssignableFrom(type)) {
    399            return createDynaBeanProperty(name, type);
    400        }
    401
    402        if (type.isPrimitive()) {
    403            return createPrimitiveProperty(name, type);
    404        }
    405
    406        if (Number.class.isAssignableFrom(type)) {
    407            return createNumberProperty(name, type);
    408        }
    409
    410        return createOtherProperty(name, type);
    411    }
    412
    413    /**
    414     * <p>
    415     * Creates a new {@code ArrayList} for an 'indexed' property which doesn't exist.
    416     * </p>
    417     *
    418     * <p>
    419     * This method should be overridden if an alternative {@code List} or {@code Array} implementation is required for 'indexed' properties.
    420     * </p>
    421     *
    422     * @param name Name of the 'indexed property.
    423     * @return The default value for an indexed property (java.util.ArrayList)
    424     */
    425    protected Object defaultIndexedProperty(final String name) {
    426        return new ArrayList<>();
    427    }
    428
    429    /**
    430     * <p>
    431     * Creates a new {@code HashMap} for a 'mapped' property which doesn't exist.
    432     * </p>
    433     *
    434     * <p>
    435     * This method can be overridden if an alternative {@code Map} implementation is required for 'mapped' properties.
    436     * </p>
    437     *
    438     * @param name Name of the 'mapped property.
    439     * @return The default value for a mapped property (java.util.HashMap)
    440     */
    441    protected Map<String, Object> defaultMappedProperty(final String name) {
    442        return new HashMap<>();
    443    }
    444
    445    /**
    446     * <p>
    447     * Return the value of a simple property with the specified name.
    448     * </p>
    449     *
    450     * <p>
    451     * <strong>N.B.</strong> Returns {@code null} if there is no property of the specified name.
    452     * </p>
    453     *
    454     * @param name Name of the property whose value is to be retrieved.
    455     * @return The property's value
    456     * @throws IllegalArgumentException if no property name is specified
    457     */
    458    @Override
    459    public Object get(final String name) {
    460        Objects.requireNonNull(name, "name");
    461        // Value found
    462        Object value = values.get(name);
    463        if (value != null) {
    464            return value;
    465        }
    466
    467        // Property doesn't exist
    468        if (!isDynaProperty(name)) {
    469            return null;
    470        }
    471
    472        // Property doesn't exist
    473        value = createProperty(name, dynaClass.getDynaProperty(name).getType());
    474
    475        if (value != null) {
    476            set(name, value);
    477        }
    478
    479        return value;
    480    }
    481
    482    /**
    483     * <p>
    484     * Return the value of an indexed property with the specified name.
    485     * </p>
    486     *
    487     * <p>
    488     * <strong>N.B.</strong> Returns {@code null} if there is no 'indexed' property of the specified name.
    489     * </p>
    490     *
    491     * @param name  Name of the property whose value is to be retrieved
    492     * @param index Index of the value to be retrieved
    493     * @return The indexed property's value
    494     * @throws IllegalArgumentException  if the specified property exists, but is not indexed
    495     * @throws IndexOutOfBoundsException if the specified index is outside the range of the underlying property
    496     */
    497    @Override
    498    public Object get(final String name, final int index) {
    499        // If its not a property, then create default indexed property
    500        if (!isDynaProperty(name)) {
    501            set(name, defaultIndexedProperty(name));
    502        }
    503
    504        // Get the indexed property
    505        Object indexedProperty = get(name);
    506
    507        // Check that the property is indexed
    508        if (!dynaClass.getDynaProperty(name).isIndexed()) {
    509            throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]' " + dynaClass.getDynaProperty(name).getName());
    510        }
    511
    512        // Grow indexed property to appropriate size
    513        indexedProperty = growIndexedProperty(name, indexedProperty, index);
    514
    515        // Return the indexed value
    516        if (indexedProperty.getClass().isArray()) {
    517            return Array.get(indexedProperty, index);
    518        }
    519        if (indexedProperty instanceof List) {
    520            return ((List<?>) indexedProperty).get(index);
    521        }
    522        throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]' " + indexedProperty.getClass().getName());
    523    }
    524
    525    /**
    526     * <p>
    527     * Return the value of a mapped property with the specified name.
    528     * </p>
    529     *
    530     * <p>
    531     * <strong>N.B.</strong> Returns {@code null} if there is no 'mapped' property of the specified name.
    532     * </p>
    533     *
    534     * @param name Name of the property whose value is to be retrieved
    535     * @param key  Key of the value to be retrieved
    536     * @return The mapped property's value
    537     * @throws IllegalArgumentException if the specified property exists, but is not mapped
    538     */
    539    @Override
    540    public Object get(final String name, final String key) {
    541        // If its not a property, then create default mapped property
    542        if (!isDynaProperty(name)) {
    543            set(name, defaultMappedProperty(name));
    544        }
    545
    546        // Get the mapped property
    547        final Object mappedProperty = get(name);
    548
    549        // Check that the property is mapped
    550        if (!dynaClass.getDynaProperty(name).isMapped()) {
    551            throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")' " + dynaClass.getDynaProperty(name).getType().getName());
    552        }
    553
    554        // Get the value from the Map
    555        if (mappedProperty instanceof Map) {
    556            return ((Map<?, ?>) mappedProperty).get(key);
    557        }
    558        throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'" + mappedProperty.getClass().getName());
    559    }
    560
    561    /**
    562     * Gets the {@code DynaClass} instance that describes the set of properties available for this DynaBean.
    563     *
    564     * @return The associated DynaClass
    565     */
    566    @Override
    567    public DynaClass getDynaClass() {
    568        return dynaClass;
    569    }
    570
    571    /**
    572     * <p>
    573     * Gets a Map representation of this DynaBean.
    574     * </p>
    575     * This, for example, could be used in JSTL in the following way to access a DynaBean's {@code fooProperty}:
    576     * <ul>
    577     * <li>{@code ${myDynaBean.<strong>map</strong>.fooProperty}}</li>
    578     * </ul>
    579     *
    580     * @return a Map representation of this DynaBean
    581     */
    582    public Map<String, Object> getMap() {
    583        // cache the Map
    584        if (mapDecorator == null) {
    585            mapDecorator = new DynaBeanPropertyMapDecorator(this);
    586        }
    587        return mapDecorator;
    588    }
    589
    590    /**
    591     * Grow the size of an indexed property
    592     *
    593     * @param name            The name of the property
    594     * @param indexedProperty The current property value
    595     * @param index           The indexed value to grow the property to (i.e. one less than the required size)
    596     * @return The new property value (grown to the appropriate size)
    597     */
    598    protected Object growIndexedProperty(final String name, Object indexedProperty, final int index) {
    599        // Grow a List to the appropriate size
    600        if (indexedProperty instanceof List) {
    601
    602            @SuppressWarnings("unchecked")
    603            final
    604            // Indexed properties are stored as List<Object>
    605            List<Object> list = (List<Object>) indexedProperty;
    606            while (index >= list.size()) {
    607                final Class<?> contentType = getDynaClass().getDynaProperty(name).getContentType();
    608                Object value = null;
    609                if (contentType != null) {
    610                    value = createProperty(name + "[" + list.size() + "]", contentType);
    611                }
    612                list.add(value);
    613            }
    614
    615        }
    616
    617        // Grow an Array to the appropriate size
    618        if (indexedProperty.getClass().isArray()) {
    619
    620            final int length = Array.getLength(indexedProperty);
    621            if (index >= length) {
    622                final Class<?> componentType = indexedProperty.getClass().getComponentType();
    623                final Object newArray = Array.newInstance(componentType, index + 1);
    624                System.arraycopy(indexedProperty, 0, newArray, 0, length);
    625                indexedProperty = newArray;
    626                set(name, indexedProperty);
    627                final int newLength = Array.getLength(indexedProperty);
    628                for (int i = length; i < newLength; i++) {
    629                    Array.set(indexedProperty, i, createProperty(name + "[" + i + "]", componentType));
    630                }
    631            }
    632        }
    633
    634        return indexedProperty;
    635    }
    636
    637    /**
    638     * Is an object of the source class assignable to the destination class?
    639     *
    640     * @param dest   Destination class
    641     * @param source Source class
    642     * @return {@code true} if the source class is assignable to the destination class, otherwise {@code false}
    643     */
    644    protected boolean isAssignable(final Class<?> dest, final Class<?> source) {
    645        if (dest.isAssignableFrom(source) || dest == Boolean.TYPE && source == Boolean.class || dest == Byte.TYPE && source == Byte.class
    646                || dest == Character.TYPE && source == Character.class || dest == Double.TYPE && source == Double.class
    647                || dest == Float.TYPE && source == Float.class || dest == Integer.TYPE && source == Integer.class || dest == Long.TYPE && source == Long.class
    648                || dest == Short.TYPE && source == Short.class) {
    649            return true;
    650        }
    651        return false;
    652
    653    }
    654
    655    /**
    656     * Indicates if there is a property with the specified name.
    657     *
    658     * @param name The name of the property to check
    659     * @return {@code true} if there is a property of the specified name, otherwise {@code false}
    660     */
    661    protected boolean isDynaProperty(final String name) {
    662        Objects.requireNonNull(name, "name");
    663        // Handle LazyDynaClasses
    664        if (dynaClass instanceof LazyDynaClass) {
    665            return ((LazyDynaClass) dynaClass).isDynaProperty(name);
    666        }
    667        // Handle other MutableDynaClass
    668        return dynaClass.getDynaProperty(name) != null;
    669    }
    670
    671    /**
    672     * <p>
    673     * Returns the {@code Log}.
    674     */
    675    private Log logger() {
    676        if (LOG == null) {
    677            LOG = LogFactory.getLog(LazyDynaBean.class);
    678        }
    679        return LOG;
    680    }
    681
    682    /**
    683     * <p>
    684     * Creates a new instance of the {@code Map}.
    685     * </p>
    686     *
    687     * @return a new Map instance
    688     */
    689    protected Map<String, Object> newMap() {
    690        return new HashMap<>();
    691    }
    692
    693    /**
    694     * Remove any existing value for the specified key on the specified mapped property.
    695     *
    696     * @param name Name of the property for which a value is to be removed
    697     * @param key  Key of the value to be removed
    698     * @throws IllegalArgumentException if there is no property of the specified name
    699     */
    700    @Override
    701    public void remove(final String name, final String key) {
    702        if (name == null) {
    703            throw new IllegalArgumentException("No property name specified");
    704        }
    705
    706        final Object value = values.get(name);
    707        if (value == null) {
    708            return;
    709        }
    710
    711        if (!(value instanceof Map)) {
    712            throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'" + value.getClass().getName());
    713        }
    714        ((Map<?, ?>) value).remove(key);
    715    }
    716
    717    /**
    718     * Sets the value of an indexed property with the specified name.
    719     *
    720     * @param name  Name of the property whose value is to be set
    721     * @param index Index of the property to be set
    722     * @param value Value to which this property is to be set
    723     * @throws ConversionException       if the specified value cannot be converted to the type required for this property
    724     * @throws IllegalArgumentException  if there is no property of the specified name
    725     * @throws IllegalArgumentException  if the specified property exists, but is not indexed
    726     * @throws IndexOutOfBoundsException if the specified index is outside the range of the underlying property
    727     */
    728    @Override
    729    public void set(final String name, final int index, final Object value) {
    730        // If its not a property, then create default indexed property
    731        if (!isDynaProperty(name)) {
    732            set(name, defaultIndexedProperty(name));
    733        }
    734
    735        // Get the indexed property
    736        Object indexedProperty = get(name);
    737
    738        // Check that the property is indexed
    739        if (!dynaClass.getDynaProperty(name).isIndexed()) {
    740            throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]'" + dynaClass.getDynaProperty(name).getType().getName());
    741        }
    742
    743        // Grow indexed property to appropriate size
    744        indexedProperty = growIndexedProperty(name, indexedProperty, index);
    745
    746        // Set the value in an array
    747        if (indexedProperty.getClass().isArray()) {
    748            Array.set(indexedProperty, index, value);
    749        } else if (indexedProperty instanceof List) {
    750            @SuppressWarnings("unchecked")
    751            final
    752            // Indexed properties are stored in a List<Object>
    753            List<Object> values = (List<Object>) indexedProperty;
    754            values.set(index, value);
    755        } else {
    756            throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]' " + indexedProperty.getClass().getName());
    757        }
    758    }
    759
    760    /**
    761     * Sets the value of a simple property with the specified name.
    762     *
    763     * @param name  Name of the property whose value is to be set
    764     * @param value Value to which this property is to be set
    765     * @throws IllegalArgumentException if this is not an existing property name for our DynaClass and the MutableDynaClass is restricted
    766     * @throws ConversionException      if the specified value cannot be converted to the type required for this property
    767     * @throws NullPointerException     if an attempt is made to set a primitive property to null
    768     */
    769    @Override
    770    public void set(final String name, final Object value) {
    771        // If the property doesn't exist, then add it
    772        if (!isDynaProperty(name)) {
    773
    774            if (dynaClass.isRestricted()) {
    775                throw new IllegalArgumentException("Invalid property name '" + name + "' (DynaClass is restricted)");
    776            }
    777            if (value == null) {
    778                dynaClass.add(name);
    779            } else {
    780                dynaClass.add(name, value.getClass());
    781            }
    782
    783        }
    784
    785        final DynaProperty descriptor = dynaClass.getDynaProperty(name);
    786
    787        if (value == null) {
    788            if (descriptor.getType().isPrimitive()) {
    789                throw new NullPointerException("Primitive value for '" + name + "'");
    790            }
    791        } else if (!isAssignable(descriptor.getType(), value.getClass())) {
    792            throw ConversionException.format("Cannot assign value of type '%s' to property '%s' of type '%s'", value.getClass().getName(), name,
    793                    descriptor.getType().getName());
    794        }
    795
    796        // Set the property's value
    797        values.put(name, value);
    798    }
    799
    800    /**
    801     * Sets the value of a mapped property with the specified name.
    802     *
    803     * @param name  Name of the property whose value is to be set
    804     * @param key   Key of the property to be set
    805     * @param value Value to which this property is to be set
    806     * @throws ConversionException      if the specified value cannot be converted to the type required for this property
    807     * @throws IllegalArgumentException if there is no property of the specified name
    808     * @throws IllegalArgumentException if the specified property exists, but is not mapped
    809     */
    810    @SuppressWarnings("unchecked")
    811    @Override
    812    public void set(final String name, final String key, final Object value) {
    813        // If the 'mapped' property doesn't exist, then add it
    814        if (!isDynaProperty(name)) {
    815            set(name, defaultMappedProperty(name));
    816        }
    817        // Get the mapped property
    818        final Object mappedProperty = get(name);
    819
    820        // Check that the property is mapped
    821        if (!dynaClass.getDynaProperty(name).isMapped()) {
    822            throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'" + dynaClass.getDynaProperty(name).getType().getName());
    823        }
    824        // Set the value in the Map
    825        // mapped properties are stored in a Map<String, Object>
    826        ((Map<String, Object>) mappedProperty).put(key, value);
    827    }
    828
    829    /**
    830     * <p>
    831     * Return the size of an indexed or mapped property.
    832     * </p>
    833     *
    834     * @param name Name of the property
    835     * @return The indexed or mapped property size
    836     * @throws IllegalArgumentException if no property name is specified
    837     */
    838    public int size(final String name) {
    839        Objects.requireNonNull(name, "name");
    840        final Object value = values.get(name);
    841        if (value == null) {
    842            return 0;
    843        }
    844
    845        if (value instanceof Map) {
    846            return ((Map<?, ?>) value).size();
    847        }
    848
    849        if (value instanceof List) {
    850            return ((List<?>) value).size();
    851        }
    852
    853        if (value.getClass().isArray()) {
    854            return Array.getLength(value);
    855        }
    856
    857        return 0;
    858    }
    859
    860}