View Javadoc
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  
19  import java.lang.reflect.Array;
20  import java.math.BigDecimal;
21  import java.math.BigInteger;
22  import java.util.ArrayList;
23  import java.util.Date;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  
32  /**
33   * <p>
34   * DynaBean which automatically adds properties to the {@code DynaClass} and provides <em>Lazy List</em> and <em>Lazy Map</em> features.
35   * </p>
36   *
37   * <p>
38   * DynaBeans deal with three types of properties - <em>simple</em>, <em>indexed</em> and <em>mapped</em> and have the following
39   * {@code get()</code> and <code>set()} methods for
40   *    each of these types:</p>
41   *    <ul>
42   *        <li><em>Simple</em> property methods - {@code get(name)} and
43   *                          {@code set(name, value)}</li>
44   *        <li><em>Indexed</em> property methods - {@code get(name, index)} and
45   *                          {@code set(name, index, value)}</li>
46   *        <li><em>Mapped</em> property methods - {@code get(name, key)} and
47   *                          {@code set(name, key, value)}</li>
48   *    </ul>
49   *
50   * <p><strong><u>Getting Property Values</u></strong></p>
51   * <p>Calling any of the {@code get()} methods, for a property which
52   *    doesn't exist, returns {@code null} in this implementation.</p>
53   *
54   * <p><strong><u>Setting Simple Properties</u></strong></p>
55   *    <p>The {@code LazyDynaBean</code> will automatically add a property to the <code>DynaClass}
56   *       if it doesn't exist when the {@code set(name, value)} method is called.</p>
57   *
58   *     <pre>{@code
59   *         DynaBean myBean = new LazyDynaBean();
60   *         myBean.set("myProperty", "myValue");
61   *     }</pre>
62   *
63   * <p><strong><u>Setting Indexed Properties</u></strong></p>
64   *    <p>If the property <strong>doesn't</strong> exist, the {@code LazyDynaBean} will automatically add
65   *       a property with an {@code ArrayList</code> type to the <code>DynaClass} when
66   *       the {@code set(name, index, value)} method is called.
67   *       It will also instantiate a new {@code ArrayList} and automatically <em>grow</em>
68   *       the {@code List} so that it is big enough to accommodate the index being set.
69   *       {@code ArrayList} is the default indexed property that LazyDynaBean uses but
70   *       this can be easily changed by overriding the {@code defaultIndexedProperty(name)}
71   *       method.</p>
72   *
73   *     <pre>{@code
74   *         DynaBean myBean = new LazyDynaBean();
75   *         myBean.set("myIndexedProperty", 0, "myValue1");
76   *         myBean.set("myIndexedProperty", 1, "myValue2");
77   *     }</pre>
78   *
79   *    <p>If the indexed property <strong>does</strong> exist in the {@code DynaClass} but is set to
80   *      {@code null</code> in the <code>LazyDynaBean}, then it will instantiate a
81   *      new {@code List</code> or <code>Array} as specified by the property's type
82   *      in the {@code DynaClass</code> and automatically <em>grow</em> the <code>List}
83   *      or {@code Array} so that it is big enough to accommodate the index being set.</p>
84   *
85   *     <pre>{@code
86   *         DynaBean myBean = new LazyDynaBean();
87   *         MutableDynaClass myClass = (MutableDynaClass)myBean.getDynaClass();
88   *         myClass.add("myIndexedProperty", int[].class);
89   *         myBean.set("myIndexedProperty", 0, Integer.valueOf(10));
90   *         myBean.set("myIndexedProperty", 1, Integer.valueOf(20));
91   *     }</pre>
92   *
93   * <p><strong><u>Setting Mapped Properties</u></strong></p>
94   *    <p>If the property <strong>doesn't</strong> exist, the {@code LazyDynaBean} will automatically add
95   *       a property with a {@code HashMap</code> type to the <code>DynaClass} and
96   *       instantiate a new {@code HashMap} in the DynaBean when the
97   *       {@code set(name, key, value)</code> method is called. <code>HashMap} is the default
98   *       mapped property that LazyDynaBean uses but this can be easily changed by overriding
99   *       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  */
125 public 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 }