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