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 * https://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 }