LazyDynaList.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.beanutils2;

  18. import java.lang.reflect.Array;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collection;
  22. import java.util.Map;
  23. import java.util.Objects;

  24. /**
  25.  * <h2><em>Lazy</em> DynaBean List.</h2>
  26.  *
  27.  * <p>
  28.  * There are two main purposes for this class:
  29.  * </p>
  30.  * <ul>
  31.  * <li>To provide <em>Lazy List</em> behavior - automatically <em>growing</em> and <em>populating</em> the {@code List} with either
  32.  * {@code DynaBean</code>, <code>java.util.Map}
  33.  *            or POJO Beans.</li>
  34.  *        <li>To provide a straight forward way of putting a Collection
  35.  *            or Array into the lazy list <em>and</em> a straight forward
  36.  *            way to get it out again at the end.</li>
  37.  *    </ul>
  38.  *
  39.  * <p>All elements added to the List are stored as {@code DynaBean}'s:</p>
  40.  * <ul>
  41.  *    <li>{@code java.util.Map</code> elements are "wrapped" in a <code>LazyDynaMap}.</li>
  42.  *    <li>POJO Bean elements are "wrapped" in a {@code WrapDynaBean}.</li>
  43.  *    <li>{@code DynaBean}'s are stored un-changed.</li>
  44.  * </ul>
  45.  *
  46.  * <h2>{@code toArray()}</h2>
  47.  * <p>The {@code toArray()} method returns an array of the
  48.  *    elements of the appropriate type. If the {@code LazyDynaList}
  49.  *    is populated with {@link java.util.Map} objects a
  50.  *    {@code Map[]} array is returned.
  51.  *    If the list is populated with POJO Beans an appropriate
  52.  *    array of the POJO Beans is returned. Otherwise a {@code DynaBean[]}
  53.  *    array is returned.
  54.  * </p>
  55.  *
  56.  * <h2>{@code toDynaBeanArray()}</h2>
  57.  * <p>The {@code toDynaBeanArray()} method returns a
  58.  *    {@code DynaBean[]} array of the elements in the List.
  59.  * </p>
  60.  *
  61.  * <p><strong>N.B.</strong>All the elements in the List must be the
  62.  *    same type. If the {@code DynaClass</code> or <code>Class}
  63.  *    of the {@code LazyDynaList}'s elements is
  64.  *    not specified, then it will be automatically set to the type
  65.  *    of the first element populated.
  66.  * </p>
  67.  *
  68.  * <h2>Example 1</h2>
  69.  * <p>If you have an array of {@code java.util.Map[]} - you can put that into
  70.  *    a {@code LazyDynaList}.</p>
  71.  *
  72.  * <pre>{@code
  73.  *    TreeMap[] myArray = .... // your Map[]
  74.  *    List lazyList = new LazyDynaList(myArray);
  75.  * }</pre>
  76.  *
  77.  * <p>New elements of the appropriate Map type are
  78.  *    automatically populated:</p>
  79.  *
  80.  * <pre>{@code
  81.  *    // get(index) automatically grows the list
  82.  *    DynaBean newElement = (DynaBean)lazyList.get(lazyList.size());
  83.  *    newElement.put("someProperty", "someValue");
  84.  * }</pre>
  85.  *
  86.  * <p>Once you've finished you can get back an Array of the
  87.  *    elements of the appropriate type:</p>
  88.  *
  89.  * <pre>{@code
  90.  *    // Retrieve the array from the list
  91.  *    TreeMap[] myArray = (TreeMap[])lazyList.toArray());
  92.  * }</pre>
  93.  *
  94.  *
  95.  * <h2>Example 2</h2>
  96.  * <p>Alternatively you can create an <em>empty</em> List and
  97.  *    specify the Class for List's elements. The LazyDynaList
  98.  *    uses the Class to automatically populate elements:</p>
  99.  *
  100.  * <pre>{@code
  101.  *    // for example For Maps
  102.  *    List lazyList = new LazyDynaList(TreeMap.class);
  103.  *
  104.  *    // for example For POJO Beans
  105.  *    List lazyList = new LazyDynaList(MyPojo.class);
  106.  *
  107.  *    // for example For DynaBeans
  108.  *    List lazyList = new LazyDynaList(MyDynaBean.class);
  109.  * }</pre>
  110.  *
  111.  * <h2>Example 3</h2>
  112.  * <p>Alternatively you can create an <em>empty</em> List and specify the
  113.  *    DynaClass for List's elements. The LazyDynaList uses
  114.  *    the DynaClass to automatically populate elements:</p>
  115.  *
  116.  * <pre>{@code
  117.  *    // for example For Maps
  118.  *    DynaClass dynaClass = new LazyDynaMap(new HashMap());
  119.  *    List lazyList = new LazyDynaList(dynaClass);
  120.  *
  121.  *    // for example For POJO Beans
  122.  *    DynaClass dynaClass = (new WrapDynaBean(myPojo)).getDynaClass();
  123.  *    List lazyList = new LazyDynaList(dynaClass);
  124.  *
  125.  *    // for example For DynaBeans
  126.  *    DynaClass dynaClass = new BasicDynaClass(properties);
  127.  *    List lazyList = new LazyDynaList(dynaClass);
  128.  * }</pre>
  129.  *
  130.  * <p><strong>N.B.</strong> You may wonder why control the type
  131.  *    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
  132.  * have a <em>default</em> empty constructor and therefore need to be instantiated using the {@code DynaClass.newInstance()} method.
  133.  * </p>
  134.  *
  135.  * <h2>Example 4</h2>
  136.  * <p>
  137.  * A slight variation - set the element type using either the {@code setElementType(Class)} method or the {@code setElementDynaClass(DynaClass)} method - then
  138.  * populate with the normal {@link java.util.List} methods (i.e. {@code add()}, {@code addAll()} or {@code set()}).
  139.  * </p>
  140.  *
  141.  * <pre>{@code
  142.  * // Create a new LazyDynaList (100 element capacity)
  143.  * LazyDynaList lazyList = new LazyDynaList(100);
  144.  *
  145.  * // Either Set the element type...
  146.  * lazyList.setElementType(TreeMap.class);
  147.  *
  148.  * // ...or the element DynaClass...
  149.  * lazyList.setElementDynaClass(new MyCustomDynaClass());
  150.  *
  151.  * // Populate from a collection
  152.  * lazyList.addAll(myCollection);
  153.  *
  154.  * }</pre>
  155.  *
  156.  * @since 1.8.0
  157.  */
  158. public class LazyDynaList extends ArrayList<Object> {

  159.     private static final long serialVersionUID = 1L;

  160.     /**
  161.      * The DynaClass of the List's elements.
  162.      */
  163.     private DynaClass elementDynaClass;

  164.     /**
  165.      * The WrapDynaClass if the List's contains POJO Bean elements.
  166.      *
  167.      * N.B. WrapDynaClass isn't serializable, which is why its stored separately in a transient instance variable.
  168.      */
  169.     private transient WrapDynaClass wrapDynaClass;

  170.     /**
  171.      * The type of the List's elements.
  172.      */
  173.     private Class<?> elementType;

  174.     /**
  175.      * The DynaBean type of the List's elements.
  176.      */
  177.     private Class<?> elementDynaBeanType;

  178.     /**
  179.      * Constructs a new instance.
  180.      */
  181.     public LazyDynaList() {
  182.     }

  183.     /**
  184.      * Constructs a LazyDynaList with a specified type for its elements.
  185.      *
  186.      * @param elementType The Type of the List's elements.
  187.      */
  188.     public LazyDynaList(final Class<?> elementType) {
  189.         setElementType(elementType);
  190.     }

  191.     /**
  192.      * Constructs a LazyDynaList populated with the elements of a Collection.
  193.      *
  194.      * @param collection The Collection to populate the List from.
  195.      */
  196.     public LazyDynaList(final Collection<?> collection) {
  197.         super(collection.size());
  198.         addAll(collection);
  199.     }

  200.     /**
  201.      * Constructs a LazyDynaList with a specified DynaClass for its elements.
  202.      *
  203.      * @param elementDynaClass The DynaClass of the List's elements.
  204.      */
  205.     public LazyDynaList(final DynaClass elementDynaClass) {
  206.         setElementDynaClass(elementDynaClass);
  207.     }

  208.     /**
  209.      * Constructs a LazyDynaList with the specified capacity.
  210.      *
  211.      * @param capacity The initial capacity of the list.
  212.      */
  213.     public LazyDynaList(final int capacity) {
  214.         super(capacity);

  215.     }

  216.     /**
  217.      * Constructs a LazyDynaList populated with the elements of an Array.
  218.      *
  219.      * @param array The Array to populate the List from.
  220.      */
  221.     public LazyDynaList(final Object[] array) {
  222.         super(array.length);
  223.         this.addAll(Arrays.asList(array));
  224.     }

  225.     /**
  226.      * <p>
  227.      * Insert an element at the specified index position.
  228.      * </p>
  229.      *
  230.      * <p>
  231.      * 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.
  232.      * </p>
  233.      *
  234.      * @param index   The index position to insert the new element.
  235.      * @param element The new element to add.
  236.      */
  237.     @Override
  238.     public void add(final int index, final Object element) {
  239.         final DynaBean dynaBean = transform(element);

  240.         growList(index);

  241.         super.add(index, dynaBean);
  242.     }

  243.     /**
  244.      * <p>
  245.      * Add an element to the List.
  246.      * </p>
  247.      *
  248.      * @param element The new element to add.
  249.      * @return true.
  250.      */
  251.     @Override
  252.     public boolean add(final Object element) {
  253.         final DynaBean dynaBean = transform(element);

  254.         return super.add(dynaBean);
  255.     }

  256.     /**
  257.      * <p>
  258.      * Add all the elements from a Collection to the list.
  259.      *
  260.      * @param collection The Collection of new elements.
  261.      * @return true if elements were added.
  262.      */
  263.     @Override
  264.     public boolean addAll(final Collection<?> collection) {
  265.         if (collection == null || collection.isEmpty()) {
  266.             return false;
  267.         }

  268.         ensureCapacity(size() + collection.size());

  269.         collection.forEach(this::add);

  270.         return true;
  271.     }

  272.     /**
  273.      * <p>
  274.      * Insert all the elements from a Collection into the list at a specified position.
  275.      *
  276.      * <p>
  277.      * 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.
  278.      * </p>
  279.      *
  280.      * @param collection The Collection of new elements.
  281.      * @param index      The index position to insert the new elements at.
  282.      * @return true if elements were added.
  283.      */
  284.     @Override
  285.     public boolean addAll(final int index, final Collection<?> collection) {
  286.         if (collection == null || collection.isEmpty()) {
  287.             return false;
  288.         }

  289.         ensureCapacity(Math.max(index, size()) + collection.size());

  290.         // Call "transform" with first element, before
  291.         // List is "grown" to ensure the correct DynaClass
  292.         // is set.
  293.         if (isEmpty()) {
  294.             transform(collection.iterator().next());
  295.         }

  296.         growList(index);

  297.         int currentIndex = index;
  298.         for (final Object e : collection) {
  299.             add(currentIndex++, e);
  300.         }

  301.         return true;
  302.     }

  303.     /**
  304.      * Creates a new {@code LazyDynaMap} object for the given property value.
  305.      *
  306.      * @param value the property value
  307.      * @return the newly created {@code LazyDynaMap}
  308.      */
  309.     private LazyDynaMap createDynaBeanForMapProperty(final Object value) {
  310.         @SuppressWarnings("unchecked")
  311.         final
  312.         // map properties are always stored as Map<String, Object>
  313.         Map<String, Object> valueMap = (Map<String, Object>) value;
  314.         return new LazyDynaMap(valueMap);
  315.     }

  316.     /**
  317.      * <p>
  318.      * Return the element at the specified position.
  319.      * </p>
  320.      *
  321.      * <p>
  322.      * 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
  323.      * size.
  324.      * </p>
  325.      *
  326.      * @param index The index position to insert the new elements at.
  327.      * @return The element at the specified position.
  328.      */
  329.     @Override
  330.     public Object get(final int index) {
  331.         growList(index + 1);

  332.         return super.get(index);
  333.     }

  334.     /**
  335.      * Gets the DynaClass.
  336.      */
  337.     private DynaClass getDynaClass() {
  338.         return elementDynaClass == null ? wrapDynaClass : elementDynaClass;
  339.     }

  340.     /**
  341.      * <p>
  342.      * Automatically <em>grown</em> the List to the appropriate size, populating with DynaBeans.
  343.      * </p>
  344.      *
  345.      * @param requiredSize the required size of the List.
  346.      */
  347.     private void growList(final int requiredSize) {
  348.         if (requiredSize < size()) {
  349.             return;
  350.         }

  351.         ensureCapacity(requiredSize + 1);

  352.         for (int i = size(); i < requiredSize; i++) {
  353.             final DynaBean dynaBean = transform(null);
  354.             super.add(dynaBean);
  355.         }
  356.     }

  357.     /**
  358.      * <p>
  359.      * Set the element at the specified position.
  360.      * </p>
  361.      *
  362.      * <p>
  363.      * 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
  364.      * size.
  365.      * </p>
  366.      *
  367.      * @param index   The index position to insert the new element at.
  368.      * @param element The new element.
  369.      * @return The new element.
  370.      */
  371.     @Override
  372.     public Object set(final int index, final Object element) {
  373.         final DynaBean dynaBean = transform(element);

  374.         growList(index + 1);

  375.         return super.set(index, dynaBean);
  376.     }

  377.     /**
  378.      * <p>
  379.      * Set the element Type and DynaClass.
  380.      * </p>
  381.      *
  382.      * @param elementDynaClass The DynaClass of the elements.
  383.      * @throws IllegalArgumentException if the List already contains elements or the DynaClass is null.
  384.      */
  385.     public void setElementDynaClass(final DynaClass elementDynaClass) {
  386.         Objects.requireNonNull(elementDynaClass, "elementDynaClass");
  387.         if (!isEmpty()) {
  388.             throw new IllegalStateException("Element DynaClass cannot be reset");
  389.         }

  390.         // Try to create a new instance of the DynaBean
  391.         try {
  392.             final DynaBean dynaBean = elementDynaClass.newInstance();
  393.             this.elementDynaBeanType = dynaBean.getClass();
  394.             if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType)) {
  395.                 this.elementType = ((WrapDynaBean) dynaBean).getInstance().getClass();
  396.                 this.wrapDynaClass = (WrapDynaClass) elementDynaClass;
  397.             } else {
  398.                 if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType)) {
  399.                     this.elementType = ((LazyDynaMap) dynaBean).getMap().getClass();
  400.                 } else {
  401.                     this.elementType = dynaBean.getClass();
  402.                 }
  403.                 this.elementDynaClass = elementDynaClass;
  404.             }
  405.         } catch (final Exception e) {
  406.             throw new IllegalArgumentException("Error creating DynaBean from " + elementDynaClass.getClass().getName() + " - " + e);
  407.         }
  408.     }

  409.     /**
  410.      * <p>
  411.      * Set the element Type and DynaClass.
  412.      * </p>
  413.      *
  414.      * @param elementType The type of the elements.
  415.      * @throws IllegalArgumentException if the List already contains elements or the DynaClass is null.
  416.      */
  417.     public void setElementType(final Class<?> elementType) {
  418.         Objects.requireNonNull(elementType, "elementType");
  419.         final boolean changeType = this.elementType != null && !this.elementType.equals(elementType);
  420.         if (changeType && !isEmpty()) {
  421.             throw new IllegalStateException("Element Type cannot be reset");
  422.         }

  423.         this.elementType = elementType;

  424.         // Create a new object of the specified type
  425.         Object object = null;
  426.         try {
  427.             object = elementType.newInstance();
  428.         } catch (final Exception e) {
  429.             throw new IllegalArgumentException("Error creating type: " + elementType.getName() + " - " + e);
  430.         }

  431.         // Create a DynaBean
  432.         DynaBean dynaBean = null;
  433.         if (Map.class.isAssignableFrom(elementType)) {
  434.             dynaBean = createDynaBeanForMapProperty(object);
  435.             this.elementDynaClass = dynaBean.getDynaClass();
  436.         } else if (DynaBean.class.isAssignableFrom(elementType)) {
  437.             dynaBean = (DynaBean) object;
  438.             this.elementDynaClass = dynaBean.getDynaClass();
  439.         } else {
  440.             dynaBean = new WrapDynaBean(object);
  441.             this.wrapDynaClass = (WrapDynaClass) dynaBean.getDynaClass();
  442.         }

  443.         this.elementDynaBeanType = dynaBean.getClass();

  444.         // Re-calculate the type
  445.         if (WrapDynaBean.class.isAssignableFrom(elementDynaBeanType)) {
  446.             this.elementType = ((WrapDynaBean) dynaBean).getInstance().getClass();
  447.         } else if (LazyDynaMap.class.isAssignableFrom(elementDynaBeanType)) {
  448.             this.elementType = ((LazyDynaMap) dynaBean).getMap().getClass();
  449.         }
  450.     }

  451.     /**
  452.      * <p>
  453.      * Converts the List to an Array.
  454.      * </p>
  455.      *
  456.      * <p>
  457.      * The type of Array created depends on the contents of the List:
  458.      * </p>
  459.      * <ul>
  460.      * <li>If the List contains only LazyDynaMap type elements then a java.util.Map[] array will be created.</li>
  461.      * <li>If the List contains only elements which are "wrapped" DynaBeans then an Object[] of the most suitable type will be created.</li>
  462.      * <li>...otherwise a DynaBean[] will be created.</li>
  463.      * </ul>
  464.      *
  465.      * @return An Array of the elements in this List.
  466.      */
  467.     @Override
  468.     public Object[] toArray() {
  469.         if (isEmpty() && elementType == null) {
  470.             return LazyDynaBean.EMPTY_ARRAY;
  471.         }

  472.         final Object[] array = (Object[]) Array.newInstance(elementType, size());
  473.         for (int i = 0; i < size(); i++) {
  474.             if (Map.class.isAssignableFrom(elementType)) {
  475.                 array[i] = ((LazyDynaMap) get(i)).getMap();
  476.             } else if (DynaBean.class.isAssignableFrom(elementType)) {
  477.                 array[i] = get(i);
  478.             } else {
  479.                 array[i] = ((WrapDynaBean) get(i)).getInstance();
  480.             }
  481.         }
  482.         return array;
  483.     }

  484.     /**
  485.      * <p>
  486.      * Converts the List to an Array of the specified type.
  487.      * </p>
  488.      *
  489.      * @param <T>   The type of the array elements
  490.      * @param model The model for the type of array to return
  491.      * @return An Array of the elements in this List.
  492.      */
  493.     @Override
  494.     public <T> T[] toArray(final T[] model) {
  495.         final Class<?> arrayType = model.getClass().getComponentType();
  496.         if (DynaBean.class.isAssignableFrom(arrayType) || isEmpty() && elementType == null) {
  497.             return super.toArray(model);
  498.         }

  499.         if (arrayType.isAssignableFrom(elementType)) {
  500.             T[] array;
  501.             if (model.length >= size()) {
  502.                 array = model;
  503.             } else {
  504.                 @SuppressWarnings("unchecked")
  505.                 final
  506.                 // This is safe because we know the element type
  507.                 T[] tempArray = (T[]) Array.newInstance(arrayType, size());
  508.                 array = tempArray;
  509.             }

  510.             for (int i = 0; i < size(); i++) {
  511.                 Object elem;
  512.                 if (Map.class.isAssignableFrom(elementType)) {
  513.                     elem = ((LazyDynaMap) get(i)).getMap();
  514.                 } else if (DynaBean.class.isAssignableFrom(elementType)) {
  515.                     elem = get(i);
  516.                 } else {
  517.                     elem = ((WrapDynaBean) get(i)).getInstance();
  518.                 }
  519.                 Array.set(array, i, elem);
  520.             }
  521.             return array;
  522.         }

  523.         throw new IllegalArgumentException("Invalid array type: " + arrayType.getName() + " - not compatible with '" + elementType.getName());
  524.     }

  525.     /**
  526.      * <p>
  527.      * Converts the List to an DynaBean Array.
  528.      * </p>
  529.      *
  530.      * @return A DynaBean[] of the elements in this List.
  531.      */
  532.     public DynaBean[] toDynaBeanArray() {
  533.         if (isEmpty() && elementDynaBeanType == null) {
  534.             return LazyDynaBean.EMPTY_ARRAY;
  535.         }

  536.         final DynaBean[] array = (DynaBean[]) Array.newInstance(elementDynaBeanType, size());
  537.         for (int i = 0; i < size(); i++) {
  538.             array[i] = (DynaBean) get(i);
  539.         }
  540.         return array;
  541.     }

  542.     /**
  543.      * <p>
  544.      * Transform the element into a DynaBean:
  545.      * </p>
  546.      *
  547.      * <ul>
  548.      * <li>Map elements are turned into LazyDynaMap's.</li>
  549.      * <li>POJO Beans are "wrapped" in a WrapDynaBean.</li>
  550.      * <li>DynaBeans are unchanged.</li></li>
  551.      *
  552.      * @param element The element to transformed.
  553.      * @return The DynaBean to store in the List.
  554.      */
  555.     private DynaBean transform(final Object element) {
  556.         DynaBean dynaBean = null;
  557.         Class<?> newDynaBeanType = null;
  558.         Class<?> newElementType;

  559.         // Create a new element
  560.         if (element == null) {

  561.             // Default Types to LazyDynaBean
  562.             // if not specified
  563.             if (elementType == null) {
  564.                 setElementDynaClass(new LazyDynaClass());
  565.             }

  566.             // Get DynaClass (restore WrapDynaClass lost in serialization)
  567.             if (getDynaClass() == null) {
  568.                 setElementType(elementType);
  569.             }

  570.             // Create a new DynaBean
  571.             try {
  572.                 dynaBean = getDynaClass().newInstance();
  573.                 newDynaBeanType = dynaBean.getClass();
  574.             } catch (final Exception e) {
  575.                 throw new IllegalArgumentException("Error creating DynaBean: " + getDynaClass().getClass().getName() + " - " + e);
  576.             }

  577.         } else {

  578.             // Transform Object to a DynaBean
  579.             newElementType = element.getClass();
  580.             if (Map.class.isAssignableFrom(element.getClass())) {
  581.                 dynaBean = createDynaBeanForMapProperty(element);
  582.             } else if (DynaBean.class.isAssignableFrom(element.getClass())) {
  583.                 dynaBean = (DynaBean) element;
  584.             } else {
  585.                 dynaBean = new WrapDynaBean(element);
  586.             }

  587.             newDynaBeanType = dynaBean.getClass();

  588.         }

  589.         // Re-calculate the element type
  590.         newElementType = dynaBean.getClass();
  591.         if (WrapDynaBean.class.isAssignableFrom(newDynaBeanType)) {
  592.             newElementType = ((WrapDynaBean) dynaBean).getInstance().getClass();
  593.         } else if (LazyDynaMap.class.isAssignableFrom(newDynaBeanType)) {
  594.             newElementType = ((LazyDynaMap) dynaBean).getMap().getClass();
  595.         }

  596.         // Check the new element type, matches all the
  597.         // other elements in the List
  598.         if (elementType != null && !newElementType.equals(elementType)) {
  599.             throw new IllegalArgumentException("Element Type " + newElementType + " doesn't match other elements " + elementType);
  600.         }

  601.         return dynaBean;
  602.     }
  603. }