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 *      https://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 * <h2><em>Lazy</em> DynaBean List.</h2>
026 *
027 * <p>There are two main purposes for this class:</p>
028 *    <ul>
029 *        <li>To provide <em>Lazy List</em> behavior - automatically
030 *            <em>growing</em> and <em>populating</em> 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 <em>and</em> 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 * <h3><code>toArray()</code></h3>
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 * <h3><code>toDynaBeanArray()</code></h3>
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 * <h3>Example 1</h3>
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 * <h3>Example 2</h3>
095 * <p>Alternatively you can create an <em>empty</em> 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 *    // for example For Maps
101 *    List lazyList = new LazyDynaList(TreeMap.class);
102 *
103 *    // for example For POJO Beans
104 *    List lazyList = new LazyDynaList(MyPojo.class);
105 *
106 *    // for example 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 <em>empty</em> 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 *    // for example For Maps
117 *    DynaClass dynaClass = new LazyDynaMap(new HashMap());
118 *    List lazyList = new LazyDynaList(dynaClass);
119 *
120 *    // for example For POJO Beans
121 *    DynaClass dynaClass = (new WrapDynaBean(myPojo)).getDynaClass();
122 *    List lazyList = new LazyDynaList(dynaClass);
123 *
124 *    // for example 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 <em>default</em> 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 * @since 1.8.0
159 */
160public class LazyDynaList extends ArrayList<Object> {
161
162    private static final long serialVersionUID = 1L;
163
164    /**
165     * The DynaClass of the List's elements.
166     */
167    private DynaClass elementDynaClass;
168
169    /**
170     * The WrapDynaClass if the List's contains
171     * POJO Bean elements.
172     *
173     * WrapDynaClass isn't serlializable, which
174     *      is why its stored separately in a
175     *      transient instance variable.
176     */
177    private transient WrapDynaClass wrapDynaClass;
178
179    /**
180     * The type of the List's elements.
181     */
182    private Class<?> elementType;
183
184    /**
185     * The DynaBean type of the List's elements.
186     */
187    private Class<?> elementDynaBeanType;
188
189    /**
190     * Constructs a new instance.
191     */
192    public LazyDynaList() {
193    }
194
195    /**
196     * Construct a  LazyDynaList with a
197     * specified type for its elements.
198     *
199     * @param elementType The Type of the List's elements.
200     */
201    public LazyDynaList(final Class<?> elementType) {
202        setElementType(elementType);
203    }
204
205    /**
206     * Construct a  LazyDynaList populated with the
207     * elements of a Collection.
208     *
209     * @param collection The Collection to populate the List from.
210     */
211    public LazyDynaList(final Collection<?> collection) {
212        super(collection.size());
213        addAll(collection);
214    }
215
216    /**
217     * Construct a  LazyDynaList with a
218     * specified DynaClass for its elements.
219     *
220     * @param elementDynaClass The DynaClass of the List's elements.
221     */
222    public LazyDynaList(final DynaClass elementDynaClass) {
223        setElementDynaClass(elementDynaClass);
224    }
225
226    /**
227     * Construct a LazyDynaList with the
228     * specified capacity.
229     *
230     * @param capacity The initial capacity of the list.
231     */
232    public LazyDynaList(final int capacity) {
233        super(capacity);
234
235    }
236
237    /**
238     * Construct a  LazyDynaList populated with the
239     * elements of an Array.
240     *
241     * @param array The Array to populate the List from.
242     */
243    public LazyDynaList(final Object[] array) {
244        super(array.length);
245        for (final Object element : array) {
246            add(element);
247        }
248    }
249
250    /**
251     * <p>Insert an element at the specified index position.</p>
252     *
253     * <p>If the index position is greater than the current
254     *    size of the List, then the List is automatically
255     *    <em>grown</em> to the appropriate size.</p>
256     *
257     * @param index The index position to insert the new element.
258     * @param element The new element to add.
259     */
260    @Override
261    public void add(final int index, final Object element) {
262
263        final DynaBean dynaBean = transform(element);
264
265        growList(index);
266
267        super.add(index, dynaBean);
268
269    }
270
271    /**
272     * <p>Add an element to the List.</p>
273     *
274     * @param element The new element to add.
275     * @return true.
276     */
277    @Override
278    public boolean add(final Object element) {
279
280        final DynaBean dynaBean = transform(element);
281
282        return super.add(dynaBean);
283
284    }
285
286    /**
287     * <p>Add all the elements from a Collection to the list.
288     *
289     * @param collection The Collection of new elements.
290     * @return true if elements were added.
291     */
292    @Override
293    public boolean addAll(final Collection<?> collection) {
294
295        if (collection == null || collection.size() == 0) {
296            return false;
297        }
298
299        ensureCapacity(size() + collection.size());
300
301        for (final Object e : collection) {
302            add(e);
303        }
304
305        return true;
306
307    }
308
309    /**
310     * <p>Insert all the elements from a Collection into the
311     *    list at a specified position.
312     *
313     * <p>If the index position is greater than the current
314     *    size of the List, then the List is automatically
315     *    <em>grown</em> to the appropriate size.</p>
316     *
317     * @param collection The Collection of new elements.
318     * @param index The index position to insert the new elements at.
319     * @return true if elements were added.
320     */
321    @Override
322    public boolean addAll(final int index, final Collection<?> collection) {
323
324        if (collection == null || collection.size() == 0) {
325            return false;
326        }
327
328        ensureCapacity((index > size() ? index : size()) + collection.size());
329
330        // Call "transform" with first element, before
331        // List is "grown" to ensure the correct DynaClass
332        // is set.
333        if (size() == 0) {
334            transform(collection.iterator().next());
335        }
336
337        growList(index);
338
339        int currentIndex = index;
340        for (final Object e : collection) {
341            add(currentIndex++, e);
342        }
343
344        return true;
345
346    }
347
348    /**
349     * Creates a new {@code LazyDynaMap} object for the given property value.
350     *
351     * @param value the property value
352     * @return the newly created {@code LazyDynaMap}
353     */
354    private LazyDynaMap createDynaBeanForMapProperty(final Object value) {
355        @SuppressWarnings("unchecked")
356        final
357        // map properties are always stored as Map<String, Object>
358        Map<String, Object> valueMap = (Map<String, Object>) value;
359        return new LazyDynaMap(valueMap);
360    }
361
362    /**
363     * <p>Return the element at the specified position.</p>
364     *
365     * <p>If the position requested is greater than the current
366     *    size of the List, then the List is automatically
367     *    <em>grown</em> (and populated) to the appropriate size.</p>
368     *
369     * @param index The index position to insert the new elements at.
370     * @return The element at the specified position.
371     */
372    @Override
373    public Object get(final int index) {
374
375        growList(index + 1);
376
377        return super.get(index);
378
379    }
380
381    /**
382     * Return the DynaClass.
383     */
384    private DynaClass getDynaClass() {
385        return elementDynaClass == null ? wrapDynaClass : elementDynaClass;
386    }
387
388    /**
389     * <p>Automatically <em>grown</em> the List
390     *    to the appropriate size, populating with
391     *    DynaBeans.</p>
392     *
393     * @param requiredSize the required size of the List.
394     */
395    private void growList(final int requiredSize) {
396
397        if (requiredSize < size()) {
398            return;
399        }
400
401        ensureCapacity(requiredSize + 1);
402
403        for (int i = size(); i < requiredSize; i++) {
404            final DynaBean dynaBean = transform(null);
405            super.add(dynaBean);
406        }
407
408    }
409
410    /**
411     * <p>Set the element at the specified position.</p>
412     *
413     * <p>If the position requested is greater than the current
414     *    size of the List, then the List is automatically
415     *    <em>grown</em> (and populated) to the appropriate size.</p>
416     *
417     * @param index The index position to insert the new element at.
418     * @param element The new element.
419     * @return The new element.
420     */
421    @Override
422    public Object set(final int index, final Object element) {
423
424        final DynaBean dynaBean = transform(element);
425
426        growList(index + 1);
427
428        return super.set(index, dynaBean);
429
430    }
431
432    /**
433     * <p>Set the element Type and DynaClass.</p>
434     *
435     * @param elementDynaClass The DynaClass of the elements.
436     * @throws IllegalArgumentException if the List already
437     *            contains elements or the DynaClass is null.
438     */
439    public void setElementDynaClass(final DynaClass elementDynaClass) {
440
441        if (elementDynaClass == null) {
442            throw new IllegalArgumentException("Element DynaClass is missing");
443        }
444
445        if (size() > 0) {
446            throw new IllegalStateException("Element DynaClass cannot be reset");
447        }
448
449        // Try to create a new instance of the DynaBean
450        try {
451            final DynaBean dynaBean  = elementDynaClass.newInstance();
452            this.elementDynaBeanType = dynaBean.getClass();
453            if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType)) {
454                this.elementType = ((WrapDynaBean)dynaBean).getInstance().getClass();
455                this.wrapDynaClass = (WrapDynaClass)elementDynaClass;
456            } else {
457                if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType)) {
458                    this.elementType = ((LazyDynaMap)dynaBean).getMap().getClass();
459                } else {
460                    this.elementType = dynaBean.getClass();
461                }
462                this.elementDynaClass = elementDynaClass;
463            }
464        } catch (final Exception e) {
465            throw new IllegalArgumentException(
466                        "Error creating DynaBean from " +
467                        elementDynaClass.getClass().getName() + " - " + e);
468        }
469
470    }
471
472    /**
473     * <p>Set the element Type and DynaClass.</p>
474     *
475     * @param elementType The type of the elements.
476     * @throws IllegalArgumentException if the List already
477     *            contains elements or the DynaClass is null.
478     */
479    public void setElementType(final Class<?> elementType) {
480
481        if (elementType == null) {
482            throw new IllegalArgumentException("Element Type is missing");
483        }
484
485        final boolean changeType = this.elementType != null && !this.elementType.equals(elementType);
486        if (changeType && size() > 0) {
487            throw new IllegalStateException("Element Type cannot be reset");
488        }
489
490        this.elementType = elementType;
491
492        // Create a new object of the specified type
493        Object object = null;
494        try {
495            object = elementType.getConstructor().newInstance();
496        } catch (final Exception e) {
497            throw new IllegalArgumentException("Error creating type: "
498                           + elementType.getName() + " - " + e);
499        }
500
501        // Create a DynaBean
502        DynaBean dynaBean = null;
503        if (Map.class.isAssignableFrom(elementType)) {
504            dynaBean = createDynaBeanForMapProperty(object);
505            this.elementDynaClass = dynaBean.getDynaClass();
506        } else if (DynaBean.class.isAssignableFrom(elementType)) {
507            dynaBean = (DynaBean)object;
508            this.elementDynaClass = dynaBean.getDynaClass();
509        } else {
510            dynaBean = new WrapDynaBean(object);
511            this.wrapDynaClass = (WrapDynaClass)dynaBean.getDynaClass();
512        }
513
514        this.elementDynaBeanType = dynaBean.getClass();
515
516        // Re-calculate the type
517        if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType)) {
518            this.elementType = ((WrapDynaBean) dynaBean).getInstance().getClass();
519        } else if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType)) {
520            this.elementType = ((LazyDynaMap) dynaBean).getMap().getClass();
521        }
522
523    }
524
525    /**
526     * <p>Converts the List to an Array.</p>
527     *
528     * <p>The type of Array created depends on the contents
529     *    of the List:</p>
530     * <ul>
531     *    <li>If the List contains only LazyDynaMap type elements
532     *        then a java.util.Map[] array will be created.</li>
533     *    <li>If the List contains only elements which are
534     *        "wrapped" DynaBeans then an Object[] of the most
535     *        suitable type will be created.</li>
536     *    <li>...otherwise a DynaBean[] will be created.</li>
537     * </ul>
538     *
539     * @return An Array of the elements in this List.
540     */
541    @Override
542    public Object[] toArray() {
543
544        if (size() == 0 && elementType == null) {
545            return new LazyDynaBean[0];
546        }
547
548        final Object[] array = (Object[])Array.newInstance(elementType, size());
549        for (int i = 0; i < size(); i++) {
550            if (Map.class.isAssignableFrom(elementType)) {
551                array[i] = ((LazyDynaMap)get(i)).getMap();
552            } else if (DynaBean.class.isAssignableFrom(elementType)) {
553                array[i] = get(i);
554            } else {
555                array[i] = ((WrapDynaBean)get(i)).getInstance();
556            }
557        }
558        return array;
559
560    }
561
562    /**
563     * <p>Converts the List to an Array of the specified type.</p>
564     *
565     * @param <T> The type of the array elements
566     * @param model The model for the type of array to return
567     * @return An Array of the elements in this List.
568     */
569    @Override
570    public <T> T[] toArray(final T[] model) {
571
572        final Class<?> arrayType = model.getClass().getComponentType();
573        if (DynaBean.class.isAssignableFrom(arrayType)
574                || size() == 0 && elementType == null) {
575            return super.toArray(model);
576        }
577
578        if (arrayType.isAssignableFrom(elementType)) {
579            T[] array;
580            if (model.length >= size()) {
581                array = model;
582            } else {
583                @SuppressWarnings("unchecked")
584                final
585                // This is safe because we know the element type
586                T[] tempArray = (T[]) Array.newInstance(arrayType, size());
587                array = tempArray;
588            }
589
590            for (int i = 0; i < size(); i++) {
591                Object elem;
592                if (Map.class.isAssignableFrom(elementType)) {
593                    elem = ((LazyDynaMap) get(i)).getMap();
594                } else if (DynaBean.class.isAssignableFrom(elementType)) {
595                    elem = get(i);
596                } else {
597                    elem = ((WrapDynaBean) get(i)).getInstance();
598                }
599                Array.set(array, i, elem);
600            }
601            return array;
602        }
603
604        throw new IllegalArgumentException("Invalid array type: "
605                  + arrayType.getName() + " - not compatible with '"
606                  + elementType.getName());
607
608    }
609
610    /**
611     * <p>Converts the List to an DynaBean Array.</p>
612     *
613     * @return A DynaBean[] of the elements in this List.
614     */
615    public DynaBean[] toDynaBeanArray() {
616
617        if (size() == 0 && elementDynaBeanType == null) {
618            return new LazyDynaBean[0];
619        }
620
621        final DynaBean[] array = (DynaBean[])Array.newInstance(elementDynaBeanType, size());
622        for (int i = 0; i < size(); i++) {
623            array[i] = (DynaBean)get(i);
624        }
625        return array;
626
627    }
628
629    /**
630     * <p>Transform the element into a DynaBean:</p>
631     *
632     * <ul>
633     *    <li>Map elements are turned into LazyDynaMap's.</li>
634     *    <li>POJO Beans are "wrapped" in a WrapDynaBean.</li>
635     *    <li>DynaBeans are unchanged.</li>
636     * </li>
637     *
638     * @param element The element to transformed.
639     * @param The DynaBean to store in the List.
640     */
641    private DynaBean transform(final Object element) {
642
643        DynaBean dynaBean     = null;
644        Class<?> newDynaBeanType = null;
645        Class<?> newElementType  = null;
646
647        // Create a new element
648        if (element == null) {
649
650            // Default Types to LazyDynaBean
651            // if not specified
652            if (elementType == null) {
653                setElementDynaClass(new LazyDynaClass());
654            }
655
656            // Get DynaClass (restore WrapDynaClass lost in serialization)
657            if (getDynaClass() == null) {
658                setElementType(elementType);
659            }
660
661            // Create a new DynaBean
662            try {
663                dynaBean = getDynaClass().newInstance();
664                newDynaBeanType = dynaBean.getClass();
665            } catch (final Exception e) {
666                throw new IllegalArgumentException("Error creating DynaBean: "
667                              + getDynaClass().getClass().getName()
668                              + " - " + e);
669            }
670
671        } else {
672
673            // Transform Object to a DynaBean
674            newElementType = element.getClass();
675            if (Map.class.isAssignableFrom(element.getClass())) {
676                dynaBean = createDynaBeanForMapProperty(element);
677            } else if (DynaBean.class.isAssignableFrom(element.getClass())) {
678                dynaBean = (DynaBean)element;
679            } else {
680                dynaBean = new WrapDynaBean(element);
681            }
682
683            newDynaBeanType = dynaBean.getClass();
684
685        }
686
687        // Re-calculate the element type
688        newElementType = dynaBean.getClass();
689        if (WrapDynaBean.class.isAssignableFrom(newDynaBeanType)) {
690            newElementType = ((WrapDynaBean)dynaBean).getInstance().getClass();
691        } else if (LazyDynaMap.class.isAssignableFrom(newDynaBeanType)) {
692            newElementType = ((LazyDynaMap)dynaBean).getMap().getClass();
693        }
694
695        // Check the new element type, matches all the
696        // other elements in the List
697        if (elementType != null && !newElementType.equals(elementType)) {
698            throw new IllegalArgumentException("Element Type "  + newElementType
699                       + " doesn't match other elements " + elementType);
700        }
701
702        return dynaBean;
703
704    }
705}