LazyDynaBean.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.beanutils2;

  18. import java.lang.reflect.Array;
  19. import java.math.BigDecimal;
  20. import java.math.BigInteger;
  21. import java.util.ArrayList;
  22. import java.util.Date;
  23. import java.util.HashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Objects;

  27. import org.apache.commons.logging.Log;
  28. import org.apache.commons.logging.LogFactory;

  29. /**
  30.  * <p>
  31.  * DynaBean which automatically adds properties to the {@code DynaClass} and provides <em>Lazy List</em> and <em>Lazy Map</em> features.
  32.  * </p>
  33.  *
  34.  * <p>
  35.  * DynaBeans deal with three types of properties - <em>simple</em>, <em>indexed</em> and <em>mapped</em> and have the following
  36.  * {@code get()</code> and <code>set()} methods for
  37.  *    each of these types:</p>
  38.  *    <ul>
  39.  *        <li><em>Simple</em> property methods - {@code get(name)} and
  40.  *                          {@code set(name, value)}</li>
  41.  *        <li><em>Indexed</em> property methods - {@code get(name, index)} and
  42.  *                          {@code set(name, index, value)}</li>
  43.  *        <li><em>Mapped</em> property methods - {@code get(name, key)} and
  44.  *                          {@code set(name, key, value)}</li>
  45.  *    </ul>
  46.  *
  47.  * <p><strong><u>Getting Property Values</u></strong></p>
  48.  * <p>Calling any of the {@code get()} methods, for a property which
  49.  *    doesn't exist, returns {@code null} in this implementation.</p>
  50.  *
  51.  * <p><strong><u>Setting Simple Properties</u></strong></p>
  52.  *    <p>The {@code LazyDynaBean</code> will automatically add a property to the <code>DynaClass}
  53.  *       if it doesn't exist when the {@code set(name, value)} method is called.</p>
  54.  *
  55.  *     <pre>{@code
  56.  *         DynaBean myBean = new LazyDynaBean();
  57.  *         myBean.set("myProperty", "myValue");
  58.  *     }</pre>
  59.  *
  60.  * <p><strong><u>Setting Indexed Properties</u></strong></p>
  61.  *    <p>If the property <strong>doesn't</strong> exist, the {@code LazyDynaBean} will automatically add
  62.  *       a property with an {@code ArrayList</code> type to the <code>DynaClass} when
  63.  *       the {@code set(name, index, value)} method is called.
  64.  *       It will also instantiate a new {@code ArrayList} and automatically <em>grow</em>
  65.  *       the {@code List} so that it is big enough to accommodate the index being set.
  66.  *       {@code ArrayList} is the default indexed property that LazyDynaBean uses but
  67.  *       this can be easily changed by overriding the {@code defaultIndexedProperty(name)}
  68.  *       method.</p>
  69.  *
  70.  *     <pre>{@code
  71.  *         DynaBean myBean = new LazyDynaBean();
  72.  *         myBean.set("myIndexedProperty", 0, "myValue1");
  73.  *         myBean.set("myIndexedProperty", 1, "myValue2");
  74.  *     }</pre>
  75.  *
  76.  *    <p>If the indexed property <strong>does</strong> exist in the {@code DynaClass} but is set to
  77.  *      {@code null</code> in the <code>LazyDynaBean}, then it will instantiate a
  78.  *      new {@code List</code> or <code>Array} as specified by the property's type
  79.  *      in the {@code DynaClass</code> and automatically <em>grow</em> the <code>List}
  80.  *      or {@code Array} so that it is big enough to accommodate the index being set.</p>
  81.  *
  82.  *     <pre>{@code
  83.  *         DynaBean myBean = new LazyDynaBean();
  84.  *         MutableDynaClass myClass = (MutableDynaClass)myBean.getDynaClass();
  85.  *         myClass.add("myIndexedProperty", int[].class);
  86.  *         myBean.set("myIndexedProperty", 0, Integer.valueOf(10));
  87.  *         myBean.set("myIndexedProperty", 1, Integer.valueOf(20));
  88.  *     }</pre>
  89.  *
  90.  * <p><strong><u>Setting Mapped Properties</u></strong></p>
  91.  *    <p>If the property <strong>doesn't</strong> exist, the {@code LazyDynaBean} will automatically add
  92.  *       a property with a {@code HashMap</code> type to the <code>DynaClass} and
  93.  *       instantiate a new {@code HashMap} in the DynaBean when the
  94.  *       {@code set(name, key, value)</code> method is called. <code>HashMap} is the default
  95.  *       mapped property that LazyDynaBean uses but this can be easily changed by overriding
  96.  *       the {@code defaultMappedProperty(name)} method.</p>
  97.  *
  98.  *     <pre>{@code
  99.  *         DynaBean myBean = new LazyDynaBean();
  100.  *         myBean.set("myMappedProperty", "myKey", "myValue");
  101.  *     }</pre>
  102.  *
  103.  *    <p>If the mapped property <strong>does</strong> exist in the {@code DynaClass} but is set to
  104.  *      {@code null</code> in the <code>LazyDynaBean}, then it will instantiate a
  105.  *      new {@code Map</code> as specified by the property's type in the <code>DynaClass}.</p>
  106.  *
  107.  *     <pre>{@code
  108.  *         DynaBean myBean = new LazyDynaBean();
  109.  *         MutableDynaClass myClass = (MutableDynaClass)myBean.getDynaClass();
  110.  *         myClass.add("myMappedProperty", TreeMap.class);
  111.  *         myBean.set("myMappedProperty", "myKey", "myValue");
  112.  *     }</pre>
  113.  *
  114.  * <p><strong><u><em>Restricted</em> DynaClass</u></strong></p>
  115.  *    <p>{@code MutableDynaClass</code> have a facility to <em>restrict</em> the <code>DynaClass} so that its properties cannot be modified. If the
  116.  * {@code MutableDynaClass} is restricted then calling any of the {@code set()} methods for a property which doesn't exist will result in a
  117.  * {@code IllegalArgumentException} being thrown.
  118.  * </p>
  119.  *
  120.  * @see LazyDynaClass
  121.  */
  122. public class LazyDynaBean implements DynaBean {

  123.     private static final long serialVersionUID = 1L;

  124.     /**
  125.      * Commons Logging
  126.      */
  127.     private static transient Log LOG = LogFactory.getLog(LazyDynaBean.class);

  128.     /** BigInteger Zero */
  129.     protected static final BigInteger BigInteger_ZERO = new BigInteger("0");
  130.     /** BigDecimal Zero */
  131.     protected static final BigDecimal BigDecimal_ZERO = new BigDecimal("0");
  132.     /** Character Space */
  133.     protected static final Character Character_SPACE = Character.valueOf(' ');
  134.     /** Byte Zero */
  135.     protected static final Byte Byte_ZERO = Byte.valueOf((byte) 0);
  136.     /** Short Zero */
  137.     protected static final Short Short_ZERO = Short.valueOf((short) 0);
  138.     /** Integer Zero */
  139.     protected static final Integer Integer_ZERO = Integer.valueOf(0);
  140.     /** Long Zero */
  141.     protected static final Long Long_ZERO = Long.valueOf(0);
  142.     /** Float Zero */
  143.     protected static final Float Float_ZERO = Float.valueOf((byte) 0);
  144.     /** Double Zero */
  145.     protected static final Double Double_ZERO = Double.valueOf((byte) 0);

  146.     static final LazyDynaBean[] EMPTY_ARRAY = {};

  147.     /**
  148.      * The {@code MutableDynaClass} "base class" that this DynaBean is associated with.
  149.      */
  150.     protected Map<String, Object> values;

  151.     /** Map decorator for this DynaBean */
  152.     private transient Map<String, Object> mapDecorator;

  153.     /**
  154.      * The {@code MutableDynaClass} "base class" that this DynaBean is associated with.
  155.      */
  156.     protected MutableDynaClass dynaClass;

  157.     /**
  158.      * Constructs a new {@code LazyDynaBean</code> with a <code>LazyDynaClass} instance.
  159.      */
  160.     public LazyDynaBean() {
  161.         this(new LazyDynaClass());
  162.     }

  163.     /**
  164.      * Constructs a new {@code DynaBean} associated with the specified {@code DynaClass</code> instance - if its not a <code>MutableDynaClass} then a new
  165.      * {@code LazyDynaClass} is created and the properties copied.
  166.      *
  167.      * @param dynaClass The DynaClass we are associated with
  168.      */
  169.     public LazyDynaBean(final DynaClass dynaClass) {
  170.         values = newMap();

  171.         if (dynaClass instanceof MutableDynaClass) {
  172.             this.dynaClass = (MutableDynaClass) dynaClass;
  173.         } else {
  174.             this.dynaClass = new LazyDynaClass(dynaClass.getName(), dynaClass.getDynaProperties());
  175.         }
  176.     }

  177.     /**
  178.      * Constructs a new {@code LazyDynaBean</code> with a <code>LazyDynaClass} instance.
  179.      *
  180.      * @param name Name of this DynaBean class
  181.      */
  182.     public LazyDynaBean(final String name) {
  183.         this(new LazyDynaClass(name));
  184.     }

  185.     /**
  186.      * Does the specified mapped property contain a value for the specified key value?
  187.      *
  188.      * @param name Name of the property to check
  189.      * @param key  Name of the key to check
  190.      * @return {@code true} if the mapped property contains a value for the specified key, otherwise {@code false}
  191.      * @throws IllegalArgumentException if no property name is specified
  192.      */
  193.     @Override
  194.     public boolean contains(final String name, final String key) {
  195.         Objects.requireNonNull(name, "name");
  196.         final Object value = values.get(name);
  197.         if (value == null) {
  198.             return false;
  199.         }

  200.         if (value instanceof Map) {
  201.             return ((Map<?, ?>) value).containsKey(key);
  202.         }

  203.         return false;
  204.     }

  205.     /**
  206.      * Create a new Instance of a 'DynaBean' Property.
  207.      *
  208.      * @param name The name of the property
  209.      * @param type The class of the property
  210.      * @return The new value
  211.      */
  212.     protected Object createDynaBeanProperty(final String name, final Class<?> type) {
  213.         try {
  214.             return type.newInstance();
  215.         } catch (final Exception ex) {
  216.             if (logger().isWarnEnabled()) {
  217.                 logger().warn("Error instantiating DynaBean property of type '" + type.getName() + "' for '" + name + "' ", ex);
  218.             }
  219.             return null;
  220.         }
  221.     }

  222.     /**
  223.      * Create a new Instance of an 'Indexed' Property
  224.      *
  225.      * @param name The name of the property
  226.      * @param type The class of the property
  227.      * @return The new value
  228.      */
  229.     protected Object createIndexedProperty(final String name, final Class<?> type) {
  230.         // Create the indexed object
  231.         Object indexedProperty = null;

  232.         if (type == null) {

  233.             indexedProperty = defaultIndexedProperty(name);

  234.         } else if (type.isArray()) {

  235.             indexedProperty = Array.newInstance(type.getComponentType(), 0);

  236.         } else if (List.class.isAssignableFrom(type)) {
  237.             if (type.isInterface()) {
  238.                 indexedProperty = defaultIndexedProperty(name);
  239.             } else {
  240.                 try {
  241.                     indexedProperty = type.newInstance();
  242.                 } catch (final Exception ex) {
  243.                     throw new IllegalArgumentException("Error instantiating indexed property of type '" + type.getName() + "' for '" + name + "' " + ex);
  244.                 }
  245.             }
  246.         } else {

  247.             throw new IllegalArgumentException("Non-indexed property of type '" + type.getName() + "' for '" + name + "'");
  248.         }

  249.         return indexedProperty;
  250.     }

  251.     /**
  252.      * Create a new Instance of a 'Mapped' Property
  253.      *
  254.      * @param name The name of the property
  255.      * @param type The class of the property
  256.      * @return The new value
  257.      */
  258.     protected Object createMappedProperty(final String name, final Class<?> type) {
  259.         // Create the mapped object
  260.         Object mappedProperty = null;

  261.         if (type == null || type.isInterface()) {

  262.             mappedProperty = defaultMappedProperty(name);

  263.         } else if (Map.class.isAssignableFrom(type)) {
  264.             try {
  265.                 mappedProperty = type.newInstance();
  266.             } catch (final Exception ex) {
  267.                 throw new IllegalArgumentException("Error instantiating mapped property of type '" + type.getName() + "' for '" + name + "' " + ex);
  268.             }
  269.         } else {

  270.             throw new IllegalArgumentException("Non-mapped property of type '" + type.getName() + "' for '" + name + "'");
  271.         }

  272.         return mappedProperty;
  273.     }

  274.     /**
  275.      * Create a new Instance of a {@link Number} Property.
  276.      *
  277.      * @param name The name of the property
  278.      * @param type The class of the property
  279.      * @return The new value
  280.      */
  281.     protected Object createNumberProperty(final String name, final Class<?> type) {
  282.         return null;
  283.     }

  284.     /**
  285.      * Create a new Instance of other Property types
  286.      *
  287.      * @param name The name of the property
  288.      * @param type The class of the property
  289.      * @return The new value
  290.      */
  291.     protected Object createOtherProperty(final String name, final Class<?> type) {
  292.         if (type == Object.class || type == String.class || type == Boolean.class || type == Character.class || Date.class.isAssignableFrom(type)) {

  293.             return null;

  294.         }

  295.         try {
  296.             return type.newInstance();
  297.         } catch (final Exception ex) {
  298.             if (logger().isWarnEnabled()) {
  299.                 logger().warn("Error instantiating property of type '" + type.getName() + "' for '" + name + "' ", ex);
  300.             }
  301.             return null;
  302.         }
  303.     }

  304.     /**
  305.      * Create a new Instance of a 'Primitive' Property.
  306.      *
  307.      * @param name The name of the property
  308.      * @param type The class of the property
  309.      * @return The new value
  310.      */
  311.     protected Object createPrimitiveProperty(final String name, final Class<?> type) {
  312.         if (type == Boolean.TYPE) {
  313.             return Boolean.FALSE;
  314.         }
  315.         if (type == Integer.TYPE) {
  316.             return Integer_ZERO;
  317.         }
  318.         if (type == Long.TYPE) {
  319.             return Long_ZERO;
  320.         }
  321.         if (type == Double.TYPE) {
  322.             return Double_ZERO;
  323.         }
  324.         if (type == Float.TYPE) {
  325.             return Float_ZERO;
  326.         }
  327.         if (type == Byte.TYPE) {
  328.             return Byte_ZERO;
  329.         }
  330.         if (type == Short.TYPE) {
  331.             return Short_ZERO;
  332.         }
  333.         if (type == Character.TYPE) {
  334.             return Character_SPACE;
  335.         }
  336.         return null;
  337.     }

  338.     /**
  339.      * Create a new Instance of a Property
  340.      *
  341.      * @param name The name of the property
  342.      * @param type The class of the property
  343.      * @return The new value
  344.      */
  345.     protected Object createProperty(final String name, final Class<?> type) {
  346.         if (type == null) {
  347.             return null;
  348.         }

  349.         // Create Lists, arrays or DynaBeans
  350.         if (type.isArray() || List.class.isAssignableFrom(type)) {
  351.             return createIndexedProperty(name, type);
  352.         }

  353.         if (Map.class.isAssignableFrom(type)) {
  354.             return createMappedProperty(name, type);
  355.         }

  356.         if (DynaBean.class.isAssignableFrom(type)) {
  357.             return createDynaBeanProperty(name, type);
  358.         }

  359.         if (type.isPrimitive()) {
  360.             return createPrimitiveProperty(name, type);
  361.         }

  362.         if (Number.class.isAssignableFrom(type)) {
  363.             return createNumberProperty(name, type);
  364.         }

  365.         return createOtherProperty(name, type);
  366.     }

  367.     /**
  368.      * <p>
  369.      * Creates a new {@code ArrayList} for an 'indexed' property which doesn't exist.
  370.      * </p>
  371.      *
  372.      * <p>
  373.      * This method should be overridden if an alternative {@code List} or {@code Array} implementation is required for 'indexed' properties.
  374.      * </p>
  375.      *
  376.      * @param name Name of the 'indexed property.
  377.      * @return The default value for an indexed property (java.util.ArrayList)
  378.      */
  379.     protected Object defaultIndexedProperty(final String name) {
  380.         return new ArrayList<>();
  381.     }

  382.     /**
  383.      * <p>
  384.      * Creates a new {@code HashMap} for a 'mapped' property which doesn't exist.
  385.      * </p>
  386.      *
  387.      * <p>
  388.      * This method can be overridden if an alternative {@code Map} implementation is required for 'mapped' properties.
  389.      * </p>
  390.      *
  391.      * @param name Name of the 'mapped property.
  392.      * @return The default value for a mapped property (java.util.HashMap)
  393.      */
  394.     protected Map<String, Object> defaultMappedProperty(final String name) {
  395.         return new HashMap<>();
  396.     }

  397.     /**
  398.      * <p>
  399.      * Return the value of a simple property with the specified name.
  400.      * </p>
  401.      *
  402.      * <p>
  403.      * <strong>N.B.</strong> Returns {@code null} if there is no property of the specified name.
  404.      * </p>
  405.      *
  406.      * @param name Name of the property whose value is to be retrieved.
  407.      * @return The property's value
  408.      * @throws IllegalArgumentException if no property name is specified
  409.      */
  410.     @Override
  411.     public Object get(final String name) {
  412.         Objects.requireNonNull(name, "name");
  413.         // Value found
  414.         Object value = values.get(name);
  415.         if (value != null) {
  416.             return value;
  417.         }

  418.         // Property doesn't exist
  419.         if (!isDynaProperty(name)) {
  420.             return null;
  421.         }

  422.         // Property doesn't exist
  423.         value = createProperty(name, dynaClass.getDynaProperty(name).getType());

  424.         if (value != null) {
  425.             set(name, value);
  426.         }

  427.         return value;
  428.     }

  429.     /**
  430.      * <p>
  431.      * Return the value of an indexed property with the specified name.
  432.      * </p>
  433.      *
  434.      * <p>
  435.      * <strong>N.B.</strong> Returns {@code null} if there is no 'indexed' property of the specified name.
  436.      * </p>
  437.      *
  438.      * @param name  Name of the property whose value is to be retrieved
  439.      * @param index Index of the value to be retrieved
  440.      * @return The indexed property's value
  441.      * @throws IllegalArgumentException  if the specified property exists, but is not indexed
  442.      * @throws IndexOutOfBoundsException if the specified index is outside the range of the underlying property
  443.      */
  444.     @Override
  445.     public Object get(final String name, final int index) {
  446.         // If its not a property, then create default indexed property
  447.         if (!isDynaProperty(name)) {
  448.             set(name, defaultIndexedProperty(name));
  449.         }

  450.         // Get the indexed property
  451.         Object indexedProperty = get(name);

  452.         // Check that the property is indexed
  453.         if (!dynaClass.getDynaProperty(name).isIndexed()) {
  454.             throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]' " + dynaClass.getDynaProperty(name).getName());
  455.         }

  456.         // Grow indexed property to appropriate size
  457.         indexedProperty = growIndexedProperty(name, indexedProperty, index);

  458.         // Return the indexed value
  459.         if (indexedProperty.getClass().isArray()) {
  460.             return Array.get(indexedProperty, index);
  461.         }
  462.         if (indexedProperty instanceof List) {
  463.             return ((List<?>) indexedProperty).get(index);
  464.         }
  465.         throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]' " + indexedProperty.getClass().getName());
  466.     }

  467.     /**
  468.      * <p>
  469.      * Return the value of a mapped property with the specified name.
  470.      * </p>
  471.      *
  472.      * <p>
  473.      * <strong>N.B.</strong> Returns {@code null} if there is no 'mapped' property of the specified name.
  474.      * </p>
  475.      *
  476.      * @param name Name of the property whose value is to be retrieved
  477.      * @param key  Key of the value to be retrieved
  478.      * @return The mapped property's value
  479.      * @throws IllegalArgumentException if the specified property exists, but is not mapped
  480.      */
  481.     @Override
  482.     public Object get(final String name, final String key) {
  483.         // If its not a property, then create default mapped property
  484.         if (!isDynaProperty(name)) {
  485.             set(name, defaultMappedProperty(name));
  486.         }

  487.         // Get the mapped property
  488.         final Object mappedProperty = get(name);

  489.         // Check that the property is mapped
  490.         if (!dynaClass.getDynaProperty(name).isMapped()) {
  491.             throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")' " + dynaClass.getDynaProperty(name).getType().getName());
  492.         }

  493.         // Get the value from the Map
  494.         if (mappedProperty instanceof Map) {
  495.             return ((Map<?, ?>) mappedProperty).get(key);
  496.         }
  497.         throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'" + mappedProperty.getClass().getName());
  498.     }

  499.     /**
  500.      * Gets the {@code DynaClass} instance that describes the set of properties available for this DynaBean.
  501.      *
  502.      * @return The associated DynaClass
  503.      */
  504.     @Override
  505.     public DynaClass getDynaClass() {
  506.         return dynaClass;
  507.     }

  508.     /**
  509.      * <p>
  510.      * Gets a Map representation of this DynaBean.
  511.      * </p>
  512.      * This, for example, could be used in JSTL in the following way to access a DynaBean's {@code fooProperty}:
  513.      * <ul>
  514.      * <li>{@code ${myDynaBean.<strong>map</strong>.fooProperty}}</li>
  515.      * </ul>
  516.      *
  517.      * @return a Map representation of this DynaBean
  518.      */
  519.     public Map<String, Object> getMap() {
  520.         // cache the Map
  521.         if (mapDecorator == null) {
  522.             mapDecorator = new DynaBeanPropertyMapDecorator(this);
  523.         }
  524.         return mapDecorator;
  525.     }

  526.     /**
  527.      * Grow the size of an indexed property
  528.      *
  529.      * @param name            The name of the property
  530.      * @param indexedProperty The current property value
  531.      * @param index           The indexed value to grow the property to (i.e. one less than the required size)
  532.      * @return The new property value (grown to the appropriate size)
  533.      */
  534.     protected Object growIndexedProperty(final String name, Object indexedProperty, final int index) {
  535.         // Grow a List to the appropriate size
  536.         if (indexedProperty instanceof List) {

  537.             @SuppressWarnings("unchecked")
  538.             final
  539.             // Indexed properties are stored as List<Object>
  540.             List<Object> list = (List<Object>) indexedProperty;
  541.             while (index >= list.size()) {
  542.                 final Class<?> contentType = getDynaClass().getDynaProperty(name).getContentType();
  543.                 Object value = null;
  544.                 if (contentType != null) {
  545.                     value = createProperty(name + "[" + list.size() + "]", contentType);
  546.                 }
  547.                 list.add(value);
  548.             }

  549.         }

  550.         // Grow an Array to the appropriate size
  551.         if (indexedProperty.getClass().isArray()) {

  552.             final int length = Array.getLength(indexedProperty);
  553.             if (index >= length) {
  554.                 final Class<?> componentType = indexedProperty.getClass().getComponentType();
  555.                 final Object newArray = Array.newInstance(componentType, index + 1);
  556.                 System.arraycopy(indexedProperty, 0, newArray, 0, length);
  557.                 indexedProperty = newArray;
  558.                 set(name, indexedProperty);
  559.                 final int newLength = Array.getLength(indexedProperty);
  560.                 for (int i = length; i < newLength; i++) {
  561.                     Array.set(indexedProperty, i, createProperty(name + "[" + i + "]", componentType));
  562.                 }
  563.             }
  564.         }

  565.         return indexedProperty;
  566.     }

  567.     /**
  568.      * Is an object of the source class assignable to the destination class?
  569.      *
  570.      * @param dest   Destination class
  571.      * @param source Source class
  572.      * @return {@code true} if the source class is assignable to the destination class, otherwise {@code false}
  573.      */
  574.     protected boolean isAssignable(final Class<?> dest, final Class<?> source) {
  575.         if (dest.isAssignableFrom(source) || dest == Boolean.TYPE && source == Boolean.class || dest == Byte.TYPE && source == Byte.class
  576.                 || dest == Character.TYPE && source == Character.class || dest == Double.TYPE && source == Double.class
  577.                 || dest == Float.TYPE && source == Float.class || dest == Integer.TYPE && source == Integer.class || dest == Long.TYPE && source == Long.class
  578.                 || dest == Short.TYPE && source == Short.class) {
  579.             return true;
  580.         }
  581.         return false;

  582.     }

  583.     /**
  584.      * Indicates if there is a property with the specified name.
  585.      *
  586.      * @param name The name of the property to check
  587.      * @return {@code true} if there is a property of the specified name, otherwise {@code false}
  588.      */
  589.     protected boolean isDynaProperty(final String name) {
  590.         Objects.requireNonNull(name, "name");
  591.         // Handle LazyDynaClasses
  592.         if (dynaClass instanceof LazyDynaClass) {
  593.             return ((LazyDynaClass) dynaClass).isDynaProperty(name);
  594.         }
  595.         // Handle other MutableDynaClass
  596.         return dynaClass.getDynaProperty(name) != null;
  597.     }

  598.     /**
  599.      * <p>
  600.      * Returns the {@code Log}.
  601.      */
  602.     private Log logger() {
  603.         if (LOG == null) {
  604.             LOG = LogFactory.getLog(LazyDynaBean.class);
  605.         }
  606.         return LOG;
  607.     }

  608.     /**
  609.      * <p>
  610.      * Creates a new instance of the {@code Map}.
  611.      * </p>
  612.      *
  613.      * @return a new Map instance
  614.      */
  615.     protected Map<String, Object> newMap() {
  616.         return new HashMap<>();
  617.     }

  618.     /**
  619.      * Remove any existing value for the specified key on the specified mapped property.
  620.      *
  621.      * @param name Name of the property for which a value is to be removed
  622.      * @param key  Key of the value to be removed
  623.      * @throws IllegalArgumentException if there is no property of the specified name
  624.      */
  625.     @Override
  626.     public void remove(final String name, final String key) {
  627.         if (name == null) {
  628.             throw new IllegalArgumentException("No property name specified");
  629.         }

  630.         final Object value = values.get(name);
  631.         if (value == null) {
  632.             return;
  633.         }

  634.         if (!(value instanceof Map)) {
  635.             throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'" + value.getClass().getName());
  636.         }
  637.         ((Map<?, ?>) value).remove(key);
  638.     }

  639.     /**
  640.      * Sets the value of an indexed property with the specified name.
  641.      *
  642.      * @param name  Name of the property whose value is to be set
  643.      * @param index Index of the property to be set
  644.      * @param value Value to which this property is to be set
  645.      * @throws ConversionException       if the specified value cannot be converted to the type required for this property
  646.      * @throws IllegalArgumentException  if there is no property of the specified name
  647.      * @throws IllegalArgumentException  if the specified property exists, but is not indexed
  648.      * @throws IndexOutOfBoundsException if the specified index is outside the range of the underlying property
  649.      */
  650.     @Override
  651.     public void set(final String name, final int index, final Object value) {
  652.         // If its not a property, then create default indexed property
  653.         if (!isDynaProperty(name)) {
  654.             set(name, defaultIndexedProperty(name));
  655.         }

  656.         // Get the indexed property
  657.         Object indexedProperty = get(name);

  658.         // Check that the property is indexed
  659.         if (!dynaClass.getDynaProperty(name).isIndexed()) {
  660.             throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]'" + dynaClass.getDynaProperty(name).getType().getName());
  661.         }

  662.         // Grow indexed property to appropriate size
  663.         indexedProperty = growIndexedProperty(name, indexedProperty, index);

  664.         // Set the value in an array
  665.         if (indexedProperty.getClass().isArray()) {
  666.             Array.set(indexedProperty, index, value);
  667.         } else if (indexedProperty instanceof List) {
  668.             @SuppressWarnings("unchecked")
  669.             final
  670.             // Indexed properties are stored in a List<Object>
  671.             List<Object> values = (List<Object>) indexedProperty;
  672.             values.set(index, value);
  673.         } else {
  674.             throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]' " + indexedProperty.getClass().getName());
  675.         }
  676.     }

  677.     /**
  678.      * Sets the value of a simple property with the specified name.
  679.      *
  680.      * @param name  Name of the property whose value is to be set
  681.      * @param value Value to which this property is to be set
  682.      * @throws IllegalArgumentException if this is not an existing property name for our DynaClass and the MutableDynaClass is restricted
  683.      * @throws ConversionException      if the specified value cannot be converted to the type required for this property
  684.      * @throws NullPointerException     if an attempt is made to set a primitive property to null
  685.      */
  686.     @Override
  687.     public void set(final String name, final Object value) {
  688.         // If the property doesn't exist, then add it
  689.         if (!isDynaProperty(name)) {

  690.             if (dynaClass.isRestricted()) {
  691.                 throw new IllegalArgumentException("Invalid property name '" + name + "' (DynaClass is restricted)");
  692.             }
  693.             if (value == null) {
  694.                 dynaClass.add(name);
  695.             } else {
  696.                 dynaClass.add(name, value.getClass());
  697.             }

  698.         }

  699.         final DynaProperty descriptor = dynaClass.getDynaProperty(name);

  700.         if (value == null) {
  701.             if (descriptor.getType().isPrimitive()) {
  702.                 throw new NullPointerException("Primitive value for '" + name + "'");
  703.             }
  704.         } else if (!isAssignable(descriptor.getType(), value.getClass())) {
  705.             throw ConversionException.format("Cannot assign value of type '%s' to property '%s' of type '%s'", value.getClass().getName(), name,
  706.                     descriptor.getType().getName());
  707.         }

  708.         // Set the property's value
  709.         values.put(name, value);
  710.     }

  711.     /**
  712.      * Sets the value of a mapped property with the specified name.
  713.      *
  714.      * @param name  Name of the property whose value is to be set
  715.      * @param key   Key of the property to be set
  716.      * @param value Value to which this property is to be set
  717.      * @throws ConversionException      if the specified value cannot be converted to the type required for this property
  718.      * @throws IllegalArgumentException if there is no property of the specified name
  719.      * @throws IllegalArgumentException if the specified property exists, but is not mapped
  720.      */
  721.     @SuppressWarnings("unchecked")
  722.     @Override
  723.     public void set(final String name, final String key, final Object value) {
  724.         // If the 'mapped' property doesn't exist, then add it
  725.         if (!isDynaProperty(name)) {
  726.             set(name, defaultMappedProperty(name));
  727.         }
  728.         // Get the mapped property
  729.         final Object mappedProperty = get(name);

  730.         // Check that the property is mapped
  731.         if (!dynaClass.getDynaProperty(name).isMapped()) {
  732.             throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'" + dynaClass.getDynaProperty(name).getType().getName());
  733.         }
  734.         // Set the value in the Map
  735.         // mapped properties are stored in a Map<String, Object>
  736.         ((Map<String, Object>) mappedProperty).put(key, value);
  737.     }

  738.     /**
  739.      * <p>
  740.      * Return the size of an indexed or mapped property.
  741.      * </p>
  742.      *
  743.      * @param name Name of the property
  744.      * @return The indexed or mapped property size
  745.      * @throws IllegalArgumentException if no property name is specified
  746.      */
  747.     public int size(final String name) {
  748.         Objects.requireNonNull(name, "name");
  749.         final Object value = values.get(name);
  750.         if (value == null) {
  751.             return 0;
  752.         }

  753.         if (value instanceof Map) {
  754.             return ((Map<?, ?>) value).size();
  755.         }

  756.         if (value instanceof List) {
  757.             return ((List<?>) value).size();
  758.         }

  759.         if (value.getClass().isArray()) {
  760.             return Array.getLength(value);
  761.         }

  762.         return 0;
  763.     }

  764. }