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