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.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Map; 024import java.util.Objects; 025 026/** 027 * <h2><em>Lazy</em> DynaBean List.</h2> 028 * 029 * <p> 030 * There are two main purposes for this class: 031 * </p> 032 * <ul> 033 * <li>To provide <em>Lazy List</em> behavior - automatically <em>growing</em> and <em>populating</em> the {@code List} with either 034 * {@code DynaBean</code>, <code>java.util.Map} 035 * or POJO Beans.</li> 036 * <li>To provide a straight forward way of putting a Collection 037 * or Array into the lazy list <em>and</em> a straight forward 038 * way to get it out again at the end.</li> 039 * </ul> 040 * 041 * <p>All elements added to the List are stored as {@code DynaBean}'s:</p> 042 * <ul> 043 * <li>{@code java.util.Map</code> elements are "wrapped" in a <code>LazyDynaMap}.</li> 044 * <li>POJO Bean elements are "wrapped" in a {@code WrapDynaBean}.</li> 045 * <li>{@code DynaBean}'s are stored un-changed.</li> 046 * </ul> 047 * 048 * <h2>{@code toArray()}</h2> 049 * <p>The {@code toArray()} method returns an array of the 050 * elements of the appropriate type. If the {@code LazyDynaList} 051 * is populated with {@link java.util.Map} objects a 052 * {@code Map[]} array is returned. 053 * If the list is populated with POJO Beans an appropriate 054 * array of the POJO Beans is returned. Otherwise a {@code DynaBean[]} 055 * array is returned. 056 * </p> 057 * 058 * <h2>{@code toDynaBeanArray()}</h2> 059 * <p>The {@code toDynaBeanArray()} method returns a 060 * {@code DynaBean[]} array of the elements in the List. 061 * </p> 062 * 063 * <p><strong>N.B.</strong>All the elements in the List must be the 064 * same type. If the {@code DynaClass</code> or <code>Class} 065 * of the {@code LazyDynaList}'s elements is 066 * not specified, then it will be automatically set to the type 067 * of the first element populated. 068 * </p> 069 * 070 * <h2>Example 1</h2> 071 * <p>If you have an array of {@code java.util.Map[]} - you can put that into 072 * a {@code LazyDynaList}.</p> 073 * 074 * <pre>{@code 075 * TreeMap[] myArray = .... // your Map[] 076 * List lazyList = new LazyDynaList(myArray); 077 * }</pre> 078 * 079 * <p>New elements of the appropriate Map type are 080 * automatically populated:</p> 081 * 082 * <pre>{@code 083 * // get(index) automatically grows the list 084 * DynaBean newElement = (DynaBean)lazyList.get(lazyList.size()); 085 * newElement.put("someProperty", "someValue"); 086 * }</pre> 087 * 088 * <p>Once you've finished you can get back an Array of the 089 * elements of the appropriate type:</p> 090 * 091 * <pre>{@code 092 * // Retrieve the array from the list 093 * TreeMap[] myArray = (TreeMap[])lazyList.toArray()); 094 * }</pre> 095 * 096 * 097 * <h2>Example 2</h2> 098 * <p>Alternatively you can create an <em>empty</em> List and 099 * specify the Class for List's elements. The LazyDynaList 100 * uses the Class to automatically populate elements:</p> 101 * 102 * <pre>{@code 103 * // for example For Maps 104 * List lazyList = new LazyDynaList(TreeMap.class); 105 * 106 * // for example For POJO Beans 107 * List lazyList = new LazyDynaList(MyPojo.class); 108 * 109 * // for example For DynaBeans 110 * List lazyList = new LazyDynaList(MyDynaBean.class); 111 * }</pre> 112 * 113 * <h2>Example 3</h2> 114 * <p>Alternatively you can create an <em>empty</em> List and specify the 115 * DynaClass for List's elements. The LazyDynaList uses 116 * the DynaClass to automatically populate elements:</p> 117 * 118 * <pre>{@code 119 * // for example For Maps 120 * DynaClass dynaClass = new LazyDynaMap(new HashMap()); 121 * List lazyList = new LazyDynaList(dynaClass); 122 * 123 * // for example For POJO Beans 124 * DynaClass dynaClass = (new WrapDynaBean(myPojo)).getDynaClass(); 125 * List lazyList = new LazyDynaList(dynaClass); 126 * 127 * // for example For DynaBeans 128 * DynaClass dynaClass = new BasicDynaClass(properties); 129 * List lazyList = new LazyDynaList(dynaClass); 130 * }</pre> 131 * 132 * <p><strong>N.B.</strong> You may wonder why control the type 133 * using a {@code DynaClass</code> rather than the <code>Class} as in the previous example - the reason is that some {@code DynaBean} implementations don't 134 * have a <em>default</em> empty constructor and therefore need to be instantiated using the {@code DynaClass.newInstance()} method. 135 * </p> 136 * 137 * <h2>Example 4</h2> 138 * <p> 139 * A slight variation - set the element type using either the {@code setElementType(Class)} method or the {@code setElementDynaClass(DynaClass)} method - then 140 * populate with the normal {@link java.util.List} methods (i.e. {@code add()}, {@code addAll()} or {@code set()}). 141 * </p> 142 * 143 * <pre>{@code 144 * // Create a new LazyDynaList (100 element capacity) 145 * LazyDynaList lazyList = new LazyDynaList(100); 146 * 147 * // Either Set the element type... 148 * lazyList.setElementType(TreeMap.class); 149 * 150 * // ...or the element DynaClass... 151 * lazyList.setElementDynaClass(new MyCustomDynaClass()); 152 * 153 * // Populate from a collection 154 * lazyList.addAll(myCollection); 155 * 156 * }</pre> 157 * 158 * @since 1.8.0 159 */ 160public class LazyDynaList extends ArrayList<Object> { 161 162 private static final long serialVersionUID = 1L; 163 164 /** 165 * The DynaClass of the List's elements. 166 */ 167 private DynaClass elementDynaClass; 168 169 /** 170 * The WrapDynaClass if the List's contains POJO Bean elements. 171 * 172 * N.B. WrapDynaClass isn't serializable, which is why its stored separately in a transient instance variable. 173 */ 174 private transient WrapDynaClass wrapDynaClass; 175 176 /** 177 * The type of the List's elements. 178 */ 179 private Class<?> elementType; 180 181 /** 182 * The DynaBean type of the List's elements. 183 */ 184 private Class<?> elementDynaBeanType; 185 186 /** 187 * Constructs a new instance. 188 */ 189 public LazyDynaList() { 190 } 191 192 /** 193 * Constructs a LazyDynaList with a specified type for its elements. 194 * 195 * @param elementType The Type of the List's elements. 196 */ 197 public LazyDynaList(final Class<?> elementType) { 198 setElementType(elementType); 199 } 200 201 /** 202 * Constructs a LazyDynaList populated with the elements of a Collection. 203 * 204 * @param collection The Collection to populate the List from. 205 */ 206 public LazyDynaList(final Collection<?> collection) { 207 super(collection.size()); 208 addAll(collection); 209 } 210 211 /** 212 * Constructs a LazyDynaList with a specified DynaClass for its elements. 213 * 214 * @param elementDynaClass The DynaClass of the List's elements. 215 */ 216 public LazyDynaList(final DynaClass elementDynaClass) { 217 setElementDynaClass(elementDynaClass); 218 } 219 220 /** 221 * Constructs a LazyDynaList with the specified capacity. 222 * 223 * @param capacity The initial capacity of the list. 224 */ 225 public LazyDynaList(final int capacity) { 226 super(capacity); 227 228 } 229 230 /** 231 * Constructs a LazyDynaList populated with the elements of an Array. 232 * 233 * @param array The Array to populate the List from. 234 */ 235 public LazyDynaList(final Object[] array) { 236 super(array.length); 237 this.addAll(Arrays.asList(array)); 238 } 239 240 /** 241 * <p> 242 * Insert an element at the specified index position. 243 * </p> 244 * 245 * <p> 246 * If the index position is greater than the current size of the List, then the List is automatically <em>grown</em> to the appropriate size. 247 * </p> 248 * 249 * @param index The index position to insert the new element. 250 * @param element The new element to add. 251 */ 252 @Override 253 public void add(final int index, final Object element) { 254 final DynaBean dynaBean = transform(element); 255 256 growList(index); 257 258 super.add(index, dynaBean); 259 } 260 261 /** 262 * <p> 263 * Add an element to the List. 264 * </p> 265 * 266 * @param element The new element to add. 267 * @return true. 268 */ 269 @Override 270 public boolean add(final Object element) { 271 final DynaBean dynaBean = transform(element); 272 273 return super.add(dynaBean); 274 } 275 276 /** 277 * <p> 278 * Add all the elements from a Collection to the list. 279 * 280 * @param collection The Collection of new elements. 281 * @return true if elements were added. 282 */ 283 @Override 284 public boolean addAll(final Collection<?> collection) { 285 if (collection == null || collection.isEmpty()) { 286 return false; 287 } 288 289 ensureCapacity(size() + collection.size()); 290 291 collection.forEach(this::add); 292 293 return true; 294 } 295 296 /** 297 * <p> 298 * Insert all the elements from a Collection into the list at a specified position. 299 * 300 * <p> 301 * If the index position is greater than the current size of the List, then the List is automatically <em>grown</em> to the appropriate size. 302 * </p> 303 * 304 * @param collection The Collection of new elements. 305 * @param index The index position to insert the new elements at. 306 * @return true if elements were added. 307 */ 308 @Override 309 public boolean addAll(final int index, final Collection<?> collection) { 310 if (collection == null || collection.isEmpty()) { 311 return false; 312 } 313 314 ensureCapacity(Math.max(index, size()) + collection.size()); 315 316 // Call "transform" with first element, before 317 // List is "grown" to ensure the correct DynaClass 318 // is set. 319 if (isEmpty()) { 320 transform(collection.iterator().next()); 321 } 322 323 growList(index); 324 325 int currentIndex = index; 326 for (final Object e : collection) { 327 add(currentIndex++, e); 328 } 329 330 return true; 331 } 332 333 /** 334 * Creates a new {@code LazyDynaMap} object for the given property value. 335 * 336 * @param value the property value 337 * @return the newly created {@code LazyDynaMap} 338 */ 339 private LazyDynaMap createDynaBeanForMapProperty(final Object value) { 340 @SuppressWarnings("unchecked") 341 final 342 // map properties are always stored as Map<String, Object> 343 Map<String, Object> valueMap = (Map<String, Object>) value; 344 return new LazyDynaMap(valueMap); 345 } 346 347 /** 348 * <p> 349 * Return the element at the specified position. 350 * </p> 351 * 352 * <p> 353 * If the position requested is greater than the current size of the List, then the List is automatically <em>grown</em> (and populated) to the appropriate 354 * size. 355 * </p> 356 * 357 * @param index The index position to insert the new elements at. 358 * @return The element at the specified position. 359 */ 360 @Override 361 public Object get(final int index) { 362 growList(index + 1); 363 364 return super.get(index); 365 } 366 367 /** 368 * Gets the DynaClass. 369 */ 370 private DynaClass getDynaClass() { 371 return elementDynaClass == null ? wrapDynaClass : elementDynaClass; 372 } 373 374 /** 375 * <p> 376 * Automatically <em>grown</em> the List to the appropriate size, populating with DynaBeans. 377 * </p> 378 * 379 * @param requiredSize the required size of the List. 380 */ 381 private void growList(final int requiredSize) { 382 if (requiredSize < size()) { 383 return; 384 } 385 386 ensureCapacity(requiredSize + 1); 387 388 for (int i = size(); i < requiredSize; i++) { 389 final DynaBean dynaBean = transform(null); 390 super.add(dynaBean); 391 } 392 } 393 394 /** 395 * <p> 396 * Set the element at the specified position. 397 * </p> 398 * 399 * <p> 400 * If the position requested is greater than the current size of the List, then the List is automatically <em>grown</em> (and populated) to the appropriate 401 * size. 402 * </p> 403 * 404 * @param index The index position to insert the new element at. 405 * @param element The new element. 406 * @return The new element. 407 */ 408 @Override 409 public Object set(final int index, final Object element) { 410 final DynaBean dynaBean = transform(element); 411 412 growList(index + 1); 413 414 return super.set(index, dynaBean); 415 } 416 417 /** 418 * <p> 419 * Set the element Type and DynaClass. 420 * </p> 421 * 422 * @param elementDynaClass The DynaClass of the elements. 423 * @throws IllegalArgumentException if the List already contains elements or the DynaClass is null. 424 */ 425 public void setElementDynaClass(final DynaClass elementDynaClass) { 426 Objects.requireNonNull(elementDynaClass, "elementDynaClass"); 427 if (!isEmpty()) { 428 throw new IllegalStateException("Element DynaClass cannot be reset"); 429 } 430 431 // Try to create a new instance of the DynaBean 432 try { 433 final DynaBean dynaBean = elementDynaClass.newInstance(); 434 this.elementDynaBeanType = dynaBean.getClass(); 435 if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType)) { 436 this.elementType = ((WrapDynaBean) dynaBean).getInstance().getClass(); 437 this.wrapDynaClass = (WrapDynaClass) elementDynaClass; 438 } else { 439 if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType)) { 440 this.elementType = ((LazyDynaMap) dynaBean).getMap().getClass(); 441 } else { 442 this.elementType = dynaBean.getClass(); 443 } 444 this.elementDynaClass = elementDynaClass; 445 } 446 } catch (final Exception e) { 447 throw new IllegalArgumentException("Error creating DynaBean from " + elementDynaClass.getClass().getName() + " - " + e); 448 } 449 } 450 451 /** 452 * <p> 453 * Set the element Type and DynaClass. 454 * </p> 455 * 456 * @param elementType The type of the elements. 457 * @throws IllegalArgumentException if the List already contains elements or the DynaClass is null. 458 */ 459 public void setElementType(final Class<?> elementType) { 460 Objects.requireNonNull(elementType, "elementType"); 461 final boolean changeType = this.elementType != null && !this.elementType.equals(elementType); 462 if (changeType && !isEmpty()) { 463 throw new IllegalStateException("Element Type cannot be reset"); 464 } 465 466 this.elementType = elementType; 467 468 // Create a new object of the specified type 469 Object object = null; 470 try { 471 object = elementType.newInstance(); 472 } catch (final Exception e) { 473 throw new IllegalArgumentException("Error creating type: " + elementType.getName() + " - " + e); 474 } 475 476 // Create a DynaBean 477 DynaBean dynaBean = null; 478 if (Map.class.isAssignableFrom(elementType)) { 479 dynaBean = createDynaBeanForMapProperty(object); 480 this.elementDynaClass = dynaBean.getDynaClass(); 481 } else if (DynaBean.class.isAssignableFrom(elementType)) { 482 dynaBean = (DynaBean) object; 483 this.elementDynaClass = dynaBean.getDynaClass(); 484 } else { 485 dynaBean = new WrapDynaBean(object); 486 this.wrapDynaClass = (WrapDynaClass) dynaBean.getDynaClass(); 487 } 488 489 this.elementDynaBeanType = dynaBean.getClass(); 490 491 // Re-calculate the type 492 if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType)) { 493 this.elementType = ((WrapDynaBean) dynaBean).getInstance().getClass(); 494 } else if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType)) { 495 this.elementType = ((LazyDynaMap) dynaBean).getMap().getClass(); 496 } 497 } 498 499 /** 500 * <p> 501 * Converts the List to an Array. 502 * </p> 503 * 504 * <p> 505 * The type of Array created depends on the contents of the List: 506 * </p> 507 * <ul> 508 * <li>If the List contains only LazyDynaMap type elements then a java.util.Map[] array will be created.</li> 509 * <li>If the List contains only elements which are "wrapped" DynaBeans then an Object[] of the most suitable type will be created.</li> 510 * <li>...otherwise a DynaBean[] will be created.</li> 511 * </ul> 512 * 513 * @return An Array of the elements in this List. 514 */ 515 @Override 516 public Object[] toArray() { 517 if (isEmpty() && elementType == null) { 518 return LazyDynaBean.EMPTY_ARRAY; 519 } 520 521 final Object[] array = (Object[]) Array.newInstance(elementType, size()); 522 for (int i = 0; i < size(); i++) { 523 if (Map.class.isAssignableFrom(elementType)) { 524 array[i] = ((LazyDynaMap) get(i)).getMap(); 525 } else if (DynaBean.class.isAssignableFrom(elementType)) { 526 array[i] = get(i); 527 } else { 528 array[i] = ((WrapDynaBean) get(i)).getInstance(); 529 } 530 } 531 return array; 532 } 533 534 /** 535 * <p> 536 * Converts the List to an Array of the specified type. 537 * </p> 538 * 539 * @param <T> The type of the array elements 540 * @param model The model for the type of array to return 541 * @return An Array of the elements in this List. 542 */ 543 @Override 544 public <T> T[] toArray(final T[] model) { 545 final Class<?> arrayType = model.getClass().getComponentType(); 546 if (DynaBean.class.isAssignableFrom(arrayType) || isEmpty() && elementType == null) { 547 return super.toArray(model); 548 } 549 550 if (arrayType.isAssignableFrom(elementType)) { 551 T[] array; 552 if (model.length >= size()) { 553 array = model; 554 } else { 555 @SuppressWarnings("unchecked") 556 final 557 // This is safe because we know the element type 558 T[] tempArray = (T[]) Array.newInstance(arrayType, size()); 559 array = tempArray; 560 } 561 562 for (int i = 0; i < size(); i++) { 563 Object elem; 564 if (Map.class.isAssignableFrom(elementType)) { 565 elem = ((LazyDynaMap) get(i)).getMap(); 566 } else if (DynaBean.class.isAssignableFrom(elementType)) { 567 elem = get(i); 568 } else { 569 elem = ((WrapDynaBean) get(i)).getInstance(); 570 } 571 Array.set(array, i, elem); 572 } 573 return array; 574 } 575 576 throw new IllegalArgumentException("Invalid array type: " + arrayType.getName() + " - not compatible with '" + elementType.getName()); 577 } 578 579 /** 580 * <p> 581 * Converts the List to an DynaBean Array. 582 * </p> 583 * 584 * @return A DynaBean[] of the elements in this List. 585 */ 586 public DynaBean[] toDynaBeanArray() { 587 if (isEmpty() && elementDynaBeanType == null) { 588 return LazyDynaBean.EMPTY_ARRAY; 589 } 590 591 final DynaBean[] array = (DynaBean[]) Array.newInstance(elementDynaBeanType, size()); 592 for (int i = 0; i < size(); i++) { 593 array[i] = (DynaBean) get(i); 594 } 595 return array; 596 } 597 598 /** 599 * <p> 600 * Transform the element into a DynaBean: 601 * </p> 602 * 603 * <ul> 604 * <li>Map elements are turned into LazyDynaMap's.</li> 605 * <li>POJO Beans are "wrapped" in a WrapDynaBean.</li> 606 * <li>DynaBeans are unchanged.</li></li> 607 * 608 * @param element The element to transformed. 609 * @return The DynaBean to store in the List. 610 */ 611 private DynaBean transform(final Object element) { 612 DynaBean dynaBean = null; 613 Class<?> newDynaBeanType = null; 614 Class<?> newElementType; 615 616 // Create a new element 617 if (element == null) { 618 619 // Default Types to LazyDynaBean 620 // if not specified 621 if (elementType == null) { 622 setElementDynaClass(new LazyDynaClass()); 623 } 624 625 // Get DynaClass (restore WrapDynaClass lost in serialization) 626 if (getDynaClass() == null) { 627 setElementType(elementType); 628 } 629 630 // Create a new DynaBean 631 try { 632 dynaBean = getDynaClass().newInstance(); 633 newDynaBeanType = dynaBean.getClass(); 634 } catch (final Exception e) { 635 throw new IllegalArgumentException("Error creating DynaBean: " + getDynaClass().getClass().getName() + " - " + e); 636 } 637 638 } else { 639 640 // Transform Object to a DynaBean 641 newElementType = element.getClass(); 642 if (Map.class.isAssignableFrom(element.getClass())) { 643 dynaBean = createDynaBeanForMapProperty(element); 644 } else if (DynaBean.class.isAssignableFrom(element.getClass())) { 645 dynaBean = (DynaBean) element; 646 } else { 647 dynaBean = new WrapDynaBean(element); 648 } 649 650 newDynaBeanType = dynaBean.getClass(); 651 652 } 653 654 // Re-calculate the element type 655 newElementType = dynaBean.getClass(); 656 if (WrapDynaBean.class.isAssignableFrom(newDynaBeanType)) { 657 newElementType = ((WrapDynaBean) dynaBean).getInstance().getClass(); 658 } else if (LazyDynaMap.class.isAssignableFrom(newDynaBeanType)) { 659 newElementType = ((LazyDynaMap) dynaBean).getMap().getClass(); 660 } 661 662 // Check the new element type, matches all the 663 // other elements in the List 664 if (elementType != null && !newElementType.equals(elementType)) { 665 throw new IllegalArgumentException("Element Type " + newElementType + " doesn't match other elements " + elementType); 666 } 667 668 return dynaBean; 669 } 670}