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