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