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.lang.reflect.Array;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Map;
023
024/**
025 * <h1><i>Lazy</i> DynaBean List.</h1>
026 *
027 * <p>There are two main purposes for this class:</p>
028 *    <ul>
029 *        <li>To provide <i>Lazy List</i> behaviour - automatically
030 *            <i>growing</i> and <i>populating</i> the <code>List</code>
031 *            with either <code>DynaBean</code>, <code>java.util.Map</code>
032 *            or POJO Beans.</li>
033 *        <li>To provide a straight forward way of putting a Collection
034 *            or Array into the lazy list <i>and</i> a straight forward
035 *            way to get it out again at the end.</li>
036 *    </ul>
037 *
038 * <p>All elements added to the List are stored as <code>DynaBean</code>'s:</p>
039 * <ul>
040 *    <li><code>java.util.Map</code> elements are "wrapped" in a <code>LazyDynaMap</code>.</li>
041 *    <li>POJO Bean elements are "wrapped" in a <code>WrapDynaBean</code>.</li>
042 *    <li><code>DynaBean</code>'s are stored un-changed.</li>
043 * </ul>
044 *
045 * <h2><code>toArray()</code></h2>
046 * <p>The <code>toArray()</code> method returns an array of the
047 *    elements of the appropriate type. If the <code>LazyDynaList</code>
048 *    is populated with <code>java.util.Map</code> objects a
049 *    <code>Map[]</code> array is returned.
050 *    If the list is populated with POJO Beans an appropriate
051 *    array of the POJO Beans is returned. Otherwise a <code>DynaBean[]</code>
052 *    array is returned.
053 * </p>
054 *
055 * <h2><code>toDynaBeanArray()</code></h2>
056 * <p>The <code>toDynaBeanArray()</code> method returns a
057 *    <code>DynaBean[]</code> array of the elements in the List.
058 * </p>
059 *
060 * <p><strong>N.B.</strong>All the elements in the List must be the
061 *    same type. If the <code>DynaClass</code> or <code>Class</code>
062 *    of the <code>LazyDynaList</code>'s elements is
063 *    not specified, then it will be automatically set to the type
064 *    of the first element populated.
065 * </p>
066 *
067 * <h2>Example 1</h2>
068 * <p>If you have an array of <code>java.util.Map[]</code> - you can put that into
069 *    a <code>LazyDynaList</code>.</p>
070 *
071 * <pre><code>
072 *    TreeMap[] myArray = .... // your Map[]
073 *    List lazyList = new LazyDynaList(myArray);
074 * </code></pre>
075 *
076 * <p>New elements of the appropriate Map type are
077 *    automatically populated:</p>
078 *
079 * <pre><code>
080 *    // get(index) automatically grows the list
081 *    DynaBean newElement = (DynaBean)lazyList.get(lazyList.size());
082 *    newElement.put("someProperty", "someValue");
083 * </code></pre>
084 *
085 * <p>Once you've finished you can get back an Array of the
086 *    elements of the appropriate type:</p>
087 *
088 * <pre><code>
089 *    // Retrieve the array from the list
090 *    TreeMap[] myArray = (TreeMap[])lazyList.toArray());
091 * </code></pre>
092 *
093 *
094 * <h2>Example 2</h2>
095 * <p>Alternatively you can create an <i>empty</i> List and
096 *    specify the Class for List's elements. The LazyDynaList
097 *    uses the Class to automatically populate elements:</p>
098 *
099 * <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 * <h2>Example 3</h2>
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 * <h2>Example 4</h2>
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 */
161public 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     * </ul>
410     *
411     * @return An Array of the elements in this List.
412     */
413    @Override
414    public Object[] toArray() {
415
416        if (size() == 0 && elementType == null) {
417            return new LazyDynaBean[0];
418        }
419
420        final Object[] array = (Object[])Array.newInstance(elementType, size());
421        for (int i = 0; i < size(); i++) {
422            if (Map.class.isAssignableFrom(elementType)) {
423                array[i] = ((LazyDynaMap)get(i)).getMap();
424            } else if (DynaBean.class.isAssignableFrom(elementType)) {
425                array[i] = get(i);
426            } else {
427                array[i] = ((WrapDynaBean)get(i)).getInstance();
428            }
429        }
430        return array;
431
432    }
433
434    /**
435     * <p>Converts the List to an Array of the specified type.</p>
436     *
437     * @param <T> The type of the array elements
438     * @param model The model for the type of array to return
439     * @return An Array of the elements in this List.
440     */
441    @Override
442    public <T> T[] toArray(final T[] model) {
443
444        final Class<?> arrayType = model.getClass().getComponentType();
445        if ((DynaBean.class.isAssignableFrom(arrayType))
446                || (size() == 0 && elementType == null)) {
447            return super.toArray(model);
448        }
449
450        if ((arrayType.isAssignableFrom(elementType))) {
451            T[] array;
452            if (model.length >= size()) {
453                array = model;
454            } else {
455                @SuppressWarnings("unchecked")
456                final
457                // This is safe because we know the element type
458                T[] tempArray = (T[]) Array.newInstance(arrayType, size());
459                array = tempArray;
460            }
461
462            for (int i = 0; i < size(); i++) {
463                Object elem;
464                if (Map.class.isAssignableFrom(elementType)) {
465                    elem = ((LazyDynaMap) get(i)).getMap();
466                } else if (DynaBean.class.isAssignableFrom(elementType)) {
467                    elem = get(i);
468                } else {
469                    elem = ((WrapDynaBean) get(i)).getInstance();
470                }
471                Array.set(array, i, elem);
472            }
473            return array;
474        }
475
476        throw new IllegalArgumentException("Invalid array type: "
477                  + arrayType.getName() + " - not compatible with '"
478                  + elementType.getName());
479
480    }
481
482
483    // ------------------- Public Methods ----------------------------
484
485    /**
486     * <p>Converts the List to an DynaBean Array.</p>
487     *
488     * @return A DynaBean[] of the elements in this List.
489     */
490    public DynaBean[] toDynaBeanArray() {
491
492        if (size() == 0 && elementDynaBeanType == null) {
493            return new LazyDynaBean[0];
494        }
495
496        final DynaBean[] array = (DynaBean[])Array.newInstance(elementDynaBeanType, size());
497        for (int i = 0; i < size(); i++) {
498            array[i] = (DynaBean)get(i);
499        }
500        return array;
501
502    }
503
504    /**
505     * <p>Set the element Type and DynaClass.</p>
506     *
507     * @param elementType The type of the elements.
508     * @throws IllegalArgumentException if the List already
509     *            contains elements or the DynaClass is null.
510     */
511    public void setElementType(final Class<?> elementType) {
512
513        if (elementType == null) {
514            throw new IllegalArgumentException("Element Type is missing");
515        }
516
517        final boolean changeType = (this.elementType != null && !this.elementType.equals(elementType));
518        if (changeType && size() > 0) {
519            throw new IllegalStateException("Element Type cannot be reset");
520        }
521
522        this.elementType = elementType;
523
524        // Create a new object of the specified type
525        Object object = null;
526        try {
527            object = elementType.newInstance();
528        } catch (final Exception e) {
529            throw new IllegalArgumentException("Error creating type: "
530                           + elementType.getName() + " - " + e);
531        }
532
533        // Create a DynaBean
534        DynaBean dynaBean = null;
535        if (Map.class.isAssignableFrom(elementType)) {
536            dynaBean = createDynaBeanForMapProperty(object);
537            this.elementDynaClass = dynaBean.getDynaClass();
538        } else if (DynaBean.class.isAssignableFrom(elementType)) {
539            dynaBean = (DynaBean)object;
540            this.elementDynaClass = dynaBean.getDynaClass();
541        } else {
542            dynaBean = new WrapDynaBean(object);
543            this.wrapDynaClass = (WrapDynaClass)dynaBean.getDynaClass();
544        }
545
546        this.elementDynaBeanType = dynaBean.getClass();
547
548        // Re-calculate the type
549        if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType )) {
550            this.elementType = ((WrapDynaBean)dynaBean).getInstance().getClass();
551        } else if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType )) {
552            this.elementType = ((LazyDynaMap)dynaBean).getMap().getClass();
553        }
554
555    }
556
557    /**
558     * <p>Set the element Type and DynaClass.</p>
559     *
560     * @param elementDynaClass The DynaClass of the elements.
561     * @throws IllegalArgumentException if the List already
562     *            contains elements or the DynaClass is null.
563     */
564    public void setElementDynaClass(final DynaClass elementDynaClass) {
565
566        if (elementDynaClass == null) {
567            throw new IllegalArgumentException("Element DynaClass is missing");
568        }
569
570        if (size() > 0) {
571            throw new IllegalStateException("Element DynaClass cannot be reset");
572        }
573
574        // Try to create a new instance of the DynaBean
575        try {
576            final DynaBean dynaBean  = elementDynaClass.newInstance();
577            this.elementDynaBeanType = dynaBean.getClass();
578            if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType)) {
579                this.elementType = ((WrapDynaBean)dynaBean).getInstance().getClass();
580                this.wrapDynaClass = (WrapDynaClass)elementDynaClass;
581            } else if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType)) {
582                this.elementType = ((LazyDynaMap)dynaBean).getMap().getClass();
583                this.elementDynaClass = elementDynaClass;
584            } else {
585                this.elementType = dynaBean.getClass();
586                this.elementDynaClass = elementDynaClass;
587            }
588        } catch (final Exception e) {
589            throw new IllegalArgumentException(
590                        "Error creating DynaBean from " +
591                        elementDynaClass.getClass().getName() + " - " + e);
592        }
593
594    }
595
596
597    // ------------------- Private Methods ---------------------------
598
599    /**
600     * <p>Automatically <i>grown</i> the List
601     *    to the appropriate size, populating with
602     *    DynaBeans.</p>
603     *
604     * @param requiredSize the required size of the List.
605     */
606    private void growList(final int requiredSize) {
607
608        if (requiredSize < size()) {
609            return;
610        }
611
612        ensureCapacity(requiredSize + 1);
613
614        for (int i = size(); i < requiredSize; i++) {
615            final DynaBean dynaBean = transform(null);
616            super.add(dynaBean);
617        }
618
619    }
620
621    /**
622     * <p>Transform the element into a DynaBean:</p>
623     *
624     * <ul>
625     *    <li>Map elements are turned into LazyDynaMap's.</li>
626     *    <li>POJO Beans are "wrapped" in a WrapDynaBean.</li>
627     *    <li>DynaBeans are unchanged.</li>
628     * </li>
629     *
630     * @param element The element to transformed.
631     * @param The DynaBean to store in the List.
632     */
633    private DynaBean transform(final Object element) {
634
635        DynaBean dynaBean     = null;
636        Class<?> newDynaBeanType = null;
637        Class<?> newElementType  = null;
638
639        // Create a new element
640        if (element == null) {
641
642            // Default Types to LazyDynaBean
643            // if not specified
644            if (elementType == null) {
645                setElementDynaClass(new LazyDynaClass());
646            }
647
648            // Get DynaClass (restore WrapDynaClass lost in serialization)
649            if (getDynaClass() == null) {
650                setElementType(elementType);
651            }
652
653            // Create a new DynaBean
654            try {
655                dynaBean = getDynaClass().newInstance();
656                newDynaBeanType = dynaBean.getClass();
657            } catch (final Exception e) {
658                throw new IllegalArgumentException("Error creating DynaBean: "
659                              + getDynaClass().getClass().getName()
660                              + " - " + e);
661            }
662
663        } else {
664
665            // Transform Object to a DynaBean
666            newElementType = element.getClass();
667            if (Map.class.isAssignableFrom(element.getClass())) {
668                dynaBean = createDynaBeanForMapProperty(element);
669            } else if (DynaBean.class.isAssignableFrom(element.getClass())) {
670                dynaBean = (DynaBean)element;
671            } else {
672                dynaBean = new WrapDynaBean(element);
673            }
674
675            newDynaBeanType = dynaBean.getClass();
676
677        }
678
679        // Re-calculate the element type
680        newElementType = dynaBean.getClass();
681        if (WrapDynaBean.class.isAssignableFrom(newDynaBeanType)) {
682            newElementType = ((WrapDynaBean)dynaBean).getInstance().getClass();
683        } else if (LazyDynaMap.class.isAssignableFrom(newDynaBeanType)) {
684            newElementType = ((LazyDynaMap)dynaBean).getMap().getClass();
685        }
686
687        // Check the new element type, matches all the
688        // other elements in the List
689        if (elementType != null && !newElementType.equals(elementType)) {
690            throw new IllegalArgumentException("Element Type "  + newElementType
691                       + " doesn't match other elements " + elementType);
692        }
693
694        return dynaBean;
695
696    }
697
698    /**
699     * Creates a new {@code LazyDynaMap} object for the given property value.
700     *
701     * @param value the property value
702     * @return the newly created {@code LazyDynaMap}
703     */
704    private LazyDynaMap createDynaBeanForMapProperty(final Object value) {
705        @SuppressWarnings("unchecked")
706        final
707        // map properties are always stored as Map<String, Object>
708        Map<String, Object> valueMap = (Map<String, Object>) value;
709        return new LazyDynaMap(valueMap);
710    }
711
712    /**
713     * Return the DynaClass.
714     */
715    private DynaClass getDynaClass() {
716        return (elementDynaClass == null ? wrapDynaClass : elementDynaClass);
717    }
718}