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.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collection;
23 import java.util.Map;
24 import java.util.Objects;
25
26 /**
27 * <h2><em>Lazy</em> DynaBean List.</h2>
28 *
29 * <p>
30 * There are two main purposes for this class:
31 * </p>
32 * <ul>
33 * <li>To provide <em>Lazy List</em> behavior - automatically <em>growing</em> and <em>populating</em> the {@code List} with either
34 * {@code DynaBean</code>, <code>java.util.Map}
35 * or POJO Beans.</li>
36 * <li>To provide a straight forward way of putting a Collection
37 * or Array into the lazy list <em>and</em> a straight forward
38 * way to get it out again at the end.</li>
39 * </ul>
40 *
41 * <p>All elements added to the List are stored as {@code DynaBean}'s:</p>
42 * <ul>
43 * <li>{@code java.util.Map</code> elements are "wrapped" in a <code>LazyDynaMap}.</li>
44 * <li>POJO Bean elements are "wrapped" in a {@code WrapDynaBean}.</li>
45 * <li>{@code DynaBean}'s are stored un-changed.</li>
46 * </ul>
47 *
48 * <h2>{@code toArray()}</h2>
49 * <p>The {@code toArray()} method returns an array of the
50 * elements of the appropriate type. If the {@code LazyDynaList}
51 * is populated with {@link java.util.Map} objects a
52 * {@code Map[]} array is returned.
53 * If the list is populated with POJO Beans an appropriate
54 * array of the POJO Beans is returned. Otherwise a {@code DynaBean[]}
55 * array is returned.
56 * </p>
57 *
58 * <h2>{@code toDynaBeanArray()}</h2>
59 * <p>The {@code toDynaBeanArray()} method returns a
60 * {@code DynaBean[]} array of the elements in the List.
61 * </p>
62 *
63 * <p><strong>N.B.</strong>All the elements in the List must be the
64 * same type. If the {@code DynaClass</code> or <code>Class}
65 * of the {@code LazyDynaList}'s elements is
66 * not specified, then it will be automatically set to the type
67 * of the first element populated.
68 * </p>
69 *
70 * <h2>Example 1</h2>
71 * <p>If you have an array of {@code java.util.Map[]} - you can put that into
72 * a {@code LazyDynaList}.</p>
73 *
74 * <pre>{@code
75 * TreeMap[] myArray = .... // your Map[]
76 * List lazyList = new LazyDynaList(myArray);
77 * }</pre>
78 *
79 * <p>New elements of the appropriate Map type are
80 * automatically populated:</p>
81 *
82 * <pre>{@code
83 * // get(index) automatically grows the list
84 * DynaBean newElement = (DynaBean)lazyList.get(lazyList.size());
85 * newElement.put("someProperty", "someValue");
86 * }</pre>
87 *
88 * <p>Once you've finished you can get back an Array of the
89 * elements of the appropriate type:</p>
90 *
91 * <pre>{@code
92 * // Retrieve the array from the list
93 * TreeMap[] myArray = (TreeMap[])lazyList.toArray());
94 * }</pre>
95 *
96 *
97 * <h2>Example 2</h2>
98 * <p>Alternatively you can create an <em>empty</em> List and
99 * 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 */
160 public 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 * 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 }