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.beanutils;
18  
19  import java.lang.reflect.Array;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.Map;
23  
24  /**
25   * <h2><i>Lazy</i> DynaBean List.</h2>
26   *
27   * <p>There are two main purposes for this class:</p>
28   *    <ul>
29   *        <li>To provide <i>Lazy List</i> behaviour - automatically
30   *            <i>growing</i> and <i>populating</i> the <code>List</code>
31   *            with either <code>DynaBean</code>, <code>java.util.Map</code>
32   *            or POJO Beans.</li>
33   *        <li>To provide a straight forward way of putting a Collection
34   *            or Array into the lazy list <i>and</i> a straight forward
35   *            way to get it out again at the end.</li>
36   *    </ul>
37   *
38   * <p>All elements added to the List are stored as <code>DynaBean</code>'s:</p>
39   * <ul>
40   *    <li><code>java.util.Map</code> elements are "wrapped" in a <code>LazyDynaMap</code>.</i>
41   *    <li>POJO Bean elements are "wrapped" in a <code>WrapDynaBean.</code></i>
42   *    <li><code>DynaBean</code>'s are stored un-changed.</i>
43   * </ul>
44   *
45   * <h4><code>toArray()</code></h4>
46   * <p>The <code>toArray()</code> method returns an array of the
47   *    elements of the appropriate type. If the <code>LazyDynaList</code>
48   *    is populated with <code>java.util.Map</code> objects a
49   *    <code>Map[]</code> array is returned.
50   *    If the list is populated with POJO Beans an appropriate
51   *    array of the POJO Beans is returned. Otherwise a <code>DynaBean[]</code>
52   *    array is returned.
53   * </p>
54   *
55   * <h4><code>toDynaBeanArray()</code></h4>
56   * <p>The <code>toDynaBeanArray()</code> method returns a
57   *    <code>DynaBean[]</code> array of the elements in the List.
58   * </p>
59   *
60   * <p><strong>N.B.</strong>All the elements in the List must be the
61   *    same type. If the <code>DynaClass</code> or <code>Class</code>
62   *    of the <code>LazyDynaList</code>'s elements is
63   *    not specified, then it will be automatically set to the type
64   *    of the first element populated.
65   * </p>
66   *
67   * <h3>Example 1</h3>
68   * <p>If you have an array of <code>java.util.Map[]</code> - you can put that into
69   *    a <code>LazyDynaList</code>.</p>
70   *
71   * <pre><code>
72   *    TreeMap[] myArray = .... // your Map[]
73   *    List lazyList = new LazyDynaList(myArray);
74   * </code></pre>
75   *
76   * <p>New elements of the appropriate Map type are
77   *    automatically populated:</p>
78   *
79   * <pre><code>
80   *    // get(index) automatically grows the list
81   *    DynaBean newElement = (DynaBean)lazyList.get(lazyList.size());
82   *    newElement.put("someProperty", "someValue");
83   * </code></pre>
84   *
85   * <p>Once you've finished you can get back an Array of the
86   *    elements of the appropriate type:</p>
87   *
88   * <pre><code>
89   *    // Retrieve the array from the list
90   *    TreeMap[] myArray = (TreeMap[])lazyList.toArray());
91   * </code></pre>
92   *
93   *
94   * <h3>Example 2</h3>
95   * <p>Alternatively you can create an <i>empty</i> List and
96   *    specify the Class for List's elements. The LazyDynaList
97   *    uses the Class to automatically populate elements:</p>
98   *
99   * <pre><code>
100  *    // e.g. For Maps
101  *    List lazyList = new LazyDynaList(TreeMap.class);
102  *
103  *    // e.g. For POJO Beans
104  *    List lazyList = new LazyDynaList(MyPojo.class);
105  *
106  *    // e.g. For DynaBeans
107  *    List lazyList = new LazyDynaList(MyDynaBean.class);
108  * </code></pre>
109  *
110  * <h3>Example 3</h3>
111  * <p>Alternatively you can create an <i>empty</i> List and specify the
112  *    DynaClass for List's elements. The LazyDynaList uses
113  *    the DynaClass to automatically populate elements:</p>
114  *
115  * <pre><code>
116  *    // e.g. For Maps
117  *    DynaClass dynaClass = new LazyDynaMap(new HashMap());
118  *    List lazyList = new LazyDynaList(dynaClass);
119  *
120  *    // e.g. For POJO Beans
121  *    DynaClass dynaClass = (new WrapDynaBean(myPojo)).getDynaClass();
122  *    List lazyList = new LazyDynaList(dynaClass);
123  *
124  *    // e.g. For DynaBeans
125  *    DynaClass dynaClass = new BasicDynaClass(properties);
126  *    List lazyList = new LazyDynaList(dynaClass);
127  * </code></pre>
128  *
129  * <p><strong>N.B.</strong> You may wonder why control the type
130  *    using a <code>DynaClass</code> rather than the <code>Class</code>
131  *    as in the previous example - the reason is that some <code>DynaBean</code>
132  *    implementations don't have a <i>default</i> empty constructor and
133  *    therefore need to be instantiated using the <code>DynaClass.newInstance()</code>
134  *    method.</p>
135  *
136  * <h3>Example 4</h3>
137  * <p>A slight variation - set the element type using either
138  *    the <code>setElementType(Class)</code> method or the
139  *    <code>setElementDynaClass(DynaClass)</code> method - then populate
140  *    with the normal <code>java.util.List</code> methods(i.e.
141  *    <code>add()</code>, <code>addAll()</code> or <code>set()</code>).</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  * </code></pre>
157  *
158  * @version $Id$
159  * @since 1.8.0
160  */
161 public class LazyDynaList extends ArrayList<Object> {
162 
163     /**
164      * The DynaClass of the List's elements.
165      */
166     private DynaClass elementDynaClass;
167 
168     /**
169      * The WrapDynaClass if the List's contains
170      * POJO Bean elements.
171      *
172      * N.B. WrapDynaClass isn't serlializable, which
173      *      is why its stored separately in a
174      *      transient instance variable.
175      */
176     private transient WrapDynaClass wrapDynaClass;
177 
178     /**
179      * The type of the List's elements.
180      */
181     private Class<?> elementType;
182 
183     /**
184      * The DynaBean type of the List's elements.
185      */
186     private Class<?> elementDynaBeanType;
187 
188 
189     // ------------------- Constructors ------------------------------
190 
191     /**
192      * Default Constructor.
193      */
194     public LazyDynaList() {
195         super();
196     }
197 
198     /**
199      * Construct a LazyDynaList with the
200      * specified capacity.
201      *
202      * @param capacity The initial capacity of the list.
203      */
204     public LazyDynaList(final int capacity) {
205         super(capacity);
206 
207     }
208 
209     /**
210      * Construct a  LazyDynaList with a
211      * specified DynaClass for its elements.
212      *
213      * @param elementDynaClass The DynaClass of the List's elements.
214      */
215     public LazyDynaList(final DynaClass elementDynaClass) {
216         super();
217         setElementDynaClass(elementDynaClass);
218     }
219 
220     /**
221      * Construct a  LazyDynaList with a
222      * specified type for its elements.
223      *
224      * @param elementType The Type of the List's elements.
225      */
226     public LazyDynaList(final Class<?> elementType) {
227         super();
228         setElementType(elementType);
229     }
230 
231     /**
232      * Construct a  LazyDynaList populated with the
233      * elements of a Collection.
234      *
235      * @param collection The Collection to populate the List from.
236      */
237     public LazyDynaList(final Collection<?> collection) {
238         super(collection.size());
239         addAll(collection);
240     }
241 
242     /**
243      * Construct a  LazyDynaList populated with the
244      * elements of an Array.
245      *
246      * @param array The Array to populate the List from.
247      */
248     public LazyDynaList(final Object[] array) {
249         super(array.length);
250         for (Object element : array) {
251             add(element);
252         }
253     }
254 
255 
256     // ------------------- java.util.List Methods --------------------
257 
258     /**
259      * <p>Insert an element at the specified index position.</p>
260      *
261      * <p>If the index position is greater than the current
262      *    size of the List, then the List is automatically
263      *    <i>grown</i> to the appropriate size.</p>
264      *
265      * @param index The index position to insert the new element.
266      * @param element The new element to add.
267      */
268     @Override
269     public void add(final int index, final Object element) {
270 
271         final DynaBean dynaBean = transform(element);
272 
273         growList(index);
274 
275         super.add(index, dynaBean);
276 
277     }
278 
279     /**
280      * <p>Add an element to the List.</p>
281      *
282      * @param element The new element to add.
283      * @return true.
284      */
285     @Override
286     public boolean add(final Object element) {
287 
288         final DynaBean dynaBean = transform(element);
289 
290         return super.add(dynaBean);
291 
292     }
293 
294     /**
295      * <p>Add all the elements from a Collection to the list.
296      *
297      * @param collection The Collection of new elements.
298      * @return true if elements were added.
299      */
300     @Override
301     public boolean addAll(final Collection<?> collection) {
302 
303         if (collection == null || collection.size() == 0) {
304             return false;
305         }
306 
307         ensureCapacity(size() + collection.size());
308 
309         for (final Object e : collection) {
310             add(e);
311         }
312 
313         return true;
314 
315     }
316 
317     /**
318      * <p>Insert all the elements from a Collection into the
319      *    list at a specified position.
320      *
321      * <p>If the index position is greater than the current
322      *    size of the List, then the List is automatically
323      *    <i>grown</i> to the appropriate size.</p>
324      *
325      * @param collection The Collection of new elements.
326      * @param index The index position to insert the new elements at.
327      * @return true if elements were added.
328      */
329     @Override
330     public boolean addAll(final int index, final Collection<?> collection) {
331 
332         if (collection == null || collection.size() == 0) {
333             return false;
334         }
335 
336         ensureCapacity((index > size() ? index : size()) + collection.size());
337 
338         // Call "transform" with first element, before
339         // List is "grown" to ensure the correct DynaClass
340         // is set.
341         if (size() == 0) {
342             transform(collection.iterator().next());
343         }
344 
345         growList(index);
346 
347         int currentIndex = index;
348         for (final Object e : collection) {
349             add(currentIndex++, e);
350         }
351 
352         return true;
353 
354     }
355 
356     /**
357      * <p>Return the element at the specified position.</p>
358      *
359      * <p>If the position requested is greater than the current
360      *    size of the List, then the List is automatically
361      *    <i>grown</i> (and populated) to the appropriate size.</p>
362      *
363      * @param index The index position to insert the new elements at.
364      * @return The element at the specified position.
365      */
366     @Override
367     public Object get(final int index) {
368 
369         growList(index + 1);
370 
371         return super.get(index);
372 
373     }
374 
375     /**
376      * <p>Set the element at the specified position.</p>
377      *
378      * <p>If the position requested is greater than the current
379      *    size of the List, then the List is automatically
380      *    <i>grown</i> (and populated) to the appropriate size.</p>
381      *
382      * @param index The index position to insert the new element at.
383      * @param element The new element.
384      * @return The new element.
385      */
386     @Override
387     public Object set(final int index, final Object element) {
388 
389         final DynaBean dynaBean = transform(element);
390 
391         growList(index + 1);
392 
393         return super.set(index, dynaBean);
394 
395     }
396 
397     /**
398      * <p>Converts the List to an Array.</p>
399      *
400      * <p>The type of Array created depends on the contents
401      *    of the List:</p>
402      * <ul>
403      *    <li>If the List contains only LazyDynaMap type elements
404      *        then a java.util.Map[] array will be created.</li>
405      *    <li>If the List contains only elements which are
406      *        "wrapped" DynaBeans then an Object[] of the most
407      *        suitable type will be created.</li>
408      *    <li>...otherwise a DynaBean[] will be created.</li>
409      *
410      * @return An Array of the elements in this List.
411      */
412     @Override
413     public Object[] toArray() {
414 
415         if (size() == 0 && elementType == null) {
416             return new LazyDynaBean[0];
417         }
418 
419         final Object[] array = (Object[])Array.newInstance(elementType, size());
420         for (int i = 0; i < size(); i++) {
421             if (Map.class.isAssignableFrom(elementType)) {
422                 array[i] = ((LazyDynaMap)get(i)).getMap();
423             } else if (DynaBean.class.isAssignableFrom(elementType)) {
424                 array[i] = get(i);
425             } else {
426                 array[i] = ((WrapDynaBean)get(i)).getInstance();
427             }
428         }
429         return array;
430 
431     }
432 
433     /**
434      * <p>Converts the List to an Array of the specified type.</p>
435      *
436      * @param <T> The type of the array elements
437      * @param model The model for the type of array to return
438      * @return An Array of the elements in this List.
439      */
440     @Override
441     public <T> T[] toArray(final T[] model) {
442 
443         final Class<?> arrayType = model.getClass().getComponentType();
444         if ((DynaBean.class.isAssignableFrom(arrayType))
445                 || (size() == 0 && elementType == null)) {
446             return super.toArray(model);
447         }
448 
449         if ((arrayType.isAssignableFrom(elementType))) {
450             T[] array;
451             if (model.length >= size()) {
452                 array = model;
453             } else {
454                 @SuppressWarnings("unchecked")
455                 final
456                 // This is safe because we know the element type
457                 T[] tempArray = (T[]) Array.newInstance(arrayType, size());
458                 array = tempArray;
459             }
460 
461             for (int i = 0; i < size(); i++) {
462                 Object elem;
463                 if (Map.class.isAssignableFrom(elementType)) {
464                     elem = ((LazyDynaMap) get(i)).getMap();
465                 } else if (DynaBean.class.isAssignableFrom(elementType)) {
466                     elem = get(i);
467                 } else {
468                     elem = ((WrapDynaBean) get(i)).getInstance();
469                 }
470                 Array.set(array, i, elem);
471             }
472             return array;
473         }
474 
475         throw new IllegalArgumentException("Invalid array type: "
476                   + arrayType.getName() + " - not compatible with '"
477                   + elementType.getName());
478 
479     }
480 
481 
482     // ------------------- Public Methods ----------------------------
483 
484     /**
485      * <p>Converts the List to an DynaBean Array.</p>
486      *
487      * @return A DynaBean[] of the elements in this List.
488      */
489     public DynaBean[] toDynaBeanArray() {
490 
491         if (size() == 0 && elementDynaBeanType == null) {
492             return new LazyDynaBean[0];
493         }
494 
495         final DynaBean[] array = (DynaBean[])Array.newInstance(elementDynaBeanType, size());
496         for (int i = 0; i < size(); i++) {
497             array[i] = (DynaBean)get(i);
498         }
499         return array;
500 
501     }
502 
503     /**
504      * <p>Set the element Type and DynaClass.</p>
505      *
506      * @param elementType The type of the elements.
507      * @throws IllegalArgumentException if the List already
508      *            contains elements or the DynaClass is null.
509      */
510     public void setElementType(final Class<?> elementType) {
511 
512         if (elementType == null) {
513             throw new IllegalArgumentException("Element Type is missing");
514         }
515 
516         final boolean changeType = (this.elementType != null && !this.elementType.equals(elementType));
517         if (changeType && size() > 0) {
518             throw new IllegalStateException("Element Type cannot be reset");
519         }
520 
521         this.elementType = elementType;
522 
523         // Create a new object of the specified type
524         Object object = null;
525         try {
526             object = elementType.newInstance();
527         } catch (final Exception e) {
528             throw new IllegalArgumentException("Error creating type: "
529                            + elementType.getName() + " - " + e);
530         }
531 
532         // Create a DynaBean
533         DynaBean dynaBean = null;
534         if (Map.class.isAssignableFrom(elementType)) {
535             dynaBean = createDynaBeanForMapProperty(object);
536             this.elementDynaClass = dynaBean.getDynaClass();
537         } else if (DynaBean.class.isAssignableFrom(elementType)) {
538             dynaBean = (DynaBean)object;
539             this.elementDynaClass = dynaBean.getDynaClass();
540         } else {
541             dynaBean = new WrapDynaBean(object);
542             this.wrapDynaClass = (WrapDynaClass)dynaBean.getDynaClass();
543         }
544 
545         this.elementDynaBeanType = dynaBean.getClass();
546 
547         // Re-calculate the type
548         if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType )) {
549             this.elementType = ((WrapDynaBean)dynaBean).getInstance().getClass();
550         } else if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType )) {
551             this.elementType = ((LazyDynaMap)dynaBean).getMap().getClass();
552         }
553 
554     }
555 
556     /**
557      * <p>Set the element Type and DynaClass.</p>
558      *
559      * @param elementDynaClass The DynaClass of the elements.
560      * @throws IllegalArgumentException if the List already
561      *            contains elements or the DynaClass is null.
562      */
563     public void setElementDynaClass(final DynaClass elementDynaClass) {
564 
565         if (elementDynaClass == null) {
566             throw new IllegalArgumentException("Element DynaClass is missing");
567         }
568 
569         if (size() > 0) {
570             throw new IllegalStateException("Element DynaClass cannot be reset");
571         }
572 
573         // Try to create a new instance of the DynaBean
574         try {
575             final DynaBean dynaBean  = elementDynaClass.newInstance();
576             this.elementDynaBeanType = dynaBean.getClass();
577             if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType)) {
578                 this.elementType = ((WrapDynaBean)dynaBean).getInstance().getClass();
579                 this.wrapDynaClass = (WrapDynaClass)elementDynaClass;
580             } else if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType)) {
581                 this.elementType = ((LazyDynaMap)dynaBean).getMap().getClass();
582                 this.elementDynaClass = elementDynaClass;
583             } else {
584                 this.elementType = dynaBean.getClass();
585                 this.elementDynaClass = elementDynaClass;
586             }
587         } catch (final Exception e) {
588             throw new IllegalArgumentException(
589                         "Error creating DynaBean from " +
590                         elementDynaClass.getClass().getName() + " - " + e);
591         }
592 
593     }
594 
595 
596     // ------------------- Private Methods ---------------------------
597 
598     /**
599      * <p>Automatically <i>grown</i> the List
600      *    to the appropriate size, populating with
601      *    DynaBeans.</p>
602      *
603      * @param requiredSize the required size of the List.
604      */
605     private void growList(final int requiredSize) {
606 
607         if (requiredSize < size()) {
608             return;
609         }
610 
611         ensureCapacity(requiredSize + 1);
612 
613         for (int i = size(); i < requiredSize; i++) {
614             final DynaBean dynaBean = transform(null);
615             super.add(dynaBean);
616         }
617 
618     }
619 
620     /**
621      * <p>Transform the element into a DynaBean:</p>
622      *
623      * <ul>
624      *    <li>Map elements are turned into LazyDynaMap's.</li>
625      *    <li>POJO Beans are "wrapped" in a WrapDynaBean.</li>
626      *    <li>DynaBeans are unchanged.</li>
627      * </li>
628      *
629      * @param element The element to transformed.
630      * @param The DynaBean to store in the List.
631      */
632     private DynaBean transform(final Object element) {
633 
634         DynaBean dynaBean     = null;
635         Class<?> newDynaBeanType = null;
636         Class<?> newElementType  = null;
637 
638         // Create a new element
639         if (element == null) {
640 
641             // Default Types to LazyDynaBean
642             // if not specified
643             if (elementType == null) {
644                 setElementDynaClass(new LazyDynaClass());
645             }
646 
647             // Get DynaClass (restore WrapDynaClass lost in serialization)
648             if (getDynaClass() == null) {
649                 setElementType(elementType);
650             }
651 
652             // Create a new DynaBean
653             try {
654                 dynaBean = getDynaClass().newInstance();
655                 newDynaBeanType = dynaBean.getClass();
656             } catch (final Exception e) {
657                 throw new IllegalArgumentException("Error creating DynaBean: "
658                               + getDynaClass().getClass().getName()
659                               + " - " + e);
660             }
661 
662         } else {
663 
664             // Transform Object to a DynaBean
665             newElementType = element.getClass();
666             if (Map.class.isAssignableFrom(element.getClass())) {
667                 dynaBean = createDynaBeanForMapProperty(element);
668             } else if (DynaBean.class.isAssignableFrom(element.getClass())) {
669                 dynaBean = (DynaBean)element;
670             } else {
671                 dynaBean = new WrapDynaBean(element);
672             }
673 
674             newDynaBeanType = dynaBean.getClass();
675 
676         }
677 
678         // Re-calculate the element type
679         newElementType = dynaBean.getClass();
680         if (WrapDynaBean.class.isAssignableFrom(newDynaBeanType)) {
681             newElementType = ((WrapDynaBean)dynaBean).getInstance().getClass();
682         } else if (LazyDynaMap.class.isAssignableFrom(newDynaBeanType)) {
683             newElementType = ((LazyDynaMap)dynaBean).getMap().getClass();
684         }
685 
686         // Check the new element type, matches all the
687         // other elements in the List
688         if (elementType != null && !newElementType.equals(elementType)) {
689             throw new IllegalArgumentException("Element Type "  + newElementType
690                        + " doesn't match other elements " + elementType);
691         }
692 
693         return dynaBean;
694 
695     }
696 
697     /**
698      * Creates a new {@code LazyDynaMap} object for the given property value.
699      *
700      * @param value the property value
701      * @return the newly created {@code LazyDynaMap}
702      */
703     private LazyDynaMap createDynaBeanForMapProperty(final Object value) {
704         @SuppressWarnings("unchecked")
705         final
706         // map properties are always stored as Map<String, Object>
707         Map<String, Object> valueMap = (Map<String, Object>) value;
708         return new LazyDynaMap(valueMap);
709     }
710 
711     /**
712      * Return the DynaClass.
713      */
714     private DynaClass getDynaClass() {
715         return (elementDynaClass == null ? wrapDynaClass : elementDynaClass);
716     }
717 }