View Javadoc
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    *      http://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      * 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 }