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     */
017    package org.apache.commons.beanutils;
018    
019    import java.util.ArrayList;
020    import java.util.Map;
021    import java.util.Collection;
022    import java.util.Iterator;
023    import 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: 800638 $ $Date: 2009-08-04 01:23:41 +0100 (Tue, 04 Aug 2009) $
161     * @since 1.8.0
162     */
163    public 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    }