    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.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     * 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}