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