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}