ArrayConverter.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.converters;

  18. import java.io.IOException;
  19. import java.io.StreamTokenizer;
  20. import java.io.StringReader;
  21. import java.lang.reflect.Array;
  22. import java.util.ArrayList;
  23. import java.util.Collection;
  24. import java.util.Collections;
  25. import java.util.Date;
  26. import java.util.Iterator;
  27. import java.util.List;
  28. import java.util.Objects;

  29. import org.apache.commons.beanutils2.ConversionException;
  30. import org.apache.commons.beanutils2.Converter;

  31. /**
  32.  * Generic {@link Converter} implementation that handles conversion to and from <strong>array</strong> objects.
  33.  * <p>
  34.  * Can be configured to either return a <em>default value</em> or throw a {@code ConversionException} if a conversion error occurs.
  35.  * <p>
  36.  * The main features of this implementation are:
  37.  * <ul>
  38.  * <li><strong>Element Conversion</strong> - delegates to a {@link Converter}, appropriate for the type, to convert individual elements of the array. This
  39.  * leverages the power of existing converters without having to replicate their functionality for converting to the element type and removes the need to create
  40.  * a specific array type converters.</li>
  41.  * <li><strong>Arrays or Collections</strong> - can convert from either arrays or Collections to an array, limited only by the capability of the delegate
  42.  * {@link Converter}.</li>
  43.  * <li><strong>Delimited Lists</strong> - can Convert <strong>to</strong> and <strong>from</strong> a delimited list in String format.</li>
  44.  * <li><strong>Conversion to String</strong> - converts an array to a {@code String} in one of two ways: as a <em>delimited list</em> or by converting the first
  45.  * element in the array to a String - this is controlled by the {@link ArrayConverter#setOnlyFirstToString(boolean)} parameter.</li>
  46.  * <li><strong>Multi Dimensional Arrays</strong> - it is possible to convert a {@code String} to a multi-dimensional arrays, by embedding {@link ArrayConverter}
  47.  * within each other - see example below.</li>
  48.  * <li><strong>Default Value</strong>
  49.  * <ul>
  50.  * <li><strong><em>No Default</em></strong> - use the {@link ArrayConverter#ArrayConverter(Class, Converter)} constructor to create a converter which throws a
  51.  * {@link ConversionException} if the value is missing or invalid.</li>
  52.  * <li><strong><em>Default values</em></strong> - use the {@link ArrayConverter#ArrayConverter(Class, Converter, int)} constructor to create a converter which
  53.  * returns a <em>default value</em>. The <em>defaultSize</em> parameter controls the <em>default value</em> in the following way:
  54.  * <ul>
  55.  * <li><em>defaultSize &lt; 0</em> - default is {@code null}</li>
  56.  * <li><em>defaultSize = 0</em> - default is an array of length zero</li>
  57.  * <li><em>defaultSize &gt; 0</em> - default is an array with a length specified by {@code defaultSize} (N.B. elements in the array will be {@code null})</li>
  58.  * </ul>
  59.  * </li>
  60.  * </ul>
  61.  * </li>
  62.  * </ul>
  63.  *
  64.  * <h2>Parsing Delimited Lists</h2> This implementation can convert a delimited list in {@code String} format into an array of the appropriate type. By default,
  65.  * it uses a comma as the delimiter but the following methods can be used to configure parsing:
  66.  * <ul>
  67.  * <li>{@code setDelimiter(char)} - allows the character used as the delimiter to be configured [default is a comma].</li>
  68.  * <li>{@code setAllowedChars(char[])} - adds additional characters (to the default alphabetic/numeric) to those considered to be valid token characters.
  69.  * </ul>
  70.  *
  71.  * <h2>Multi Dimensional Arrays</h2> It is possible to convert a {@code String} to multi-dimensional arrays by using {@link ArrayConverter} as the element
  72.  * {@link Converter} within another {@link ArrayConverter}.
  73.  * <p>
  74.  * For example, the following code demonstrates how to construct a {@link Converter} to convert a delimited {@code String} into a two dimensional integer array:
  75.  * </p>
  76.  *
  77.  * <pre>
  78.  * // Construct an Integer Converter
  79.  * IntegerConverter integerConverter = new IntegerConverter();
  80.  *
  81.  * // Construct an array Converter for an integer array (i.e. int[]) using
  82.  * // an IntegerConverter as the element converter.
  83.  * // N.B. Uses the default comma (i.e. ",") as the delimiter between individual numbers
  84.  * ArrayConverter arrayConverter = new ArrayConverter(int[].class, integerConverter);
  85.  *
  86.  * // Construct a "Matrix" Converter which converts arrays of integer arrays using
  87.  * // the preceding ArrayConverter as the element Converter.
  88.  * // N.B. Uses a semicolon (i.e. ";") as the delimiter to separate the different sets of numbers.
  89.  * // Also the delimiter used by the first ArrayConverter needs to be added to the
  90.  * // "allowed characters" for this one.
  91.  * ArrayConverter matrixConverter = new ArrayConverter(int[][].class, arrayConverter);
  92.  * matrixConverter.setDelimiter(';');
  93.  * matrixConverter.setAllowedChars(new char[] { ',' });
  94.  *
  95.  * // Do the Conversion
  96.  * String matrixString = "11,12,13 ; 21,22,23 ; 31,32,33 ; 41,42,43";
  97.  * int[][] result = (int[][]) matrixConverter.convert(int[][].class, matrixString);
  98.  * </pre>
  99.  *
  100.  * @param <C> The converter type.
  101.  * @since 1.8.0
  102.  */
  103. public class ArrayConverter<C> extends AbstractConverter<C> {

  104.     private final Class<C> defaultType;
  105.     private final Converter elementConverter;
  106.     private int defaultSize;
  107.     private char delimiter = ',';
  108.     private char[] allowedChars = { '.', '-' };
  109.     private boolean onlyFirstToString = true;

  110.     /**
  111.      * Constructs an <strong>array</strong> {@code Converter} with the specified <strong>component</strong> {@code Converter} that throws a
  112.      * {@code ConversionException} if an error occurs.
  113.      *
  114.      * @param defaultType      The default array type this {@code Converter} handles
  115.      * @param elementConverter Converter used to convert individual array elements.
  116.      */
  117.     public ArrayConverter(final Class<C> defaultType, final Converter elementConverter) {
  118.         Objects.requireNonNull(defaultType, "defaultType");
  119.         if (!defaultType.isArray()) {
  120.             throw new IllegalArgumentException("Default type must be an array.");
  121.         }
  122.         this.elementConverter = Objects.requireNonNull(elementConverter, "elementConverter");
  123.         this.defaultType = defaultType;
  124.     }

  125.     /**
  126.      * Constructs an <strong>array</strong> {@code Converter} with the specified <strong>component</strong> {@code Converter} that returns a default array of
  127.      * the specified size (or {@code null}) if an error occurs.
  128.      *
  129.      * @param defaultType      The default array type this {@code Converter} handles
  130.      * @param elementConverter Converter used to convert individual array elements.
  131.      * @param defaultSize      Specifies the size of the default array value or if less than zero indicates that a {@code null} default value should be used.
  132.      */
  133.     public ArrayConverter(final Class<C> defaultType, final Converter elementConverter, final int defaultSize) {
  134.         this(defaultType, elementConverter);
  135.         this.defaultSize = defaultSize;
  136.         C defaultValue = null;
  137.         if (defaultSize >= 0) {
  138.             defaultValue = (C) Array.newInstance(defaultType.getComponentType(), defaultSize);
  139.         }
  140.         setDefaultValue(defaultValue);
  141.     }

  142.     /**
  143.      * Returns the value unchanged.
  144.      *
  145.      * @param value The value to convert
  146.      * @return The value unchanged
  147.      */
  148.     @Override
  149.     protected Object convertArray(final Object value) {
  150.         return value;
  151.     }

  152.     /**
  153.      * Converts non-array values to a Collection prior to being converted either to an array or a String.
  154.      * <ul>
  155.      * <li>{@link Collection} values are returned unchanged</li>
  156.      * <li>{@link Number}, {@link Boolean} and {@link java.util.Date} values returned as a the only element in a List.</li>
  157.      * <li>All other types are converted to a String and parsed as a delimited list.</li>
  158.      * </ul>
  159.      *
  160.      * <strong>N.B.</strong> The method is called by both the {@link ArrayConverter#convertToType(Class, Object)} and
  161.      * {@link ArrayConverter#convertToString(Object)} methods for <em>non-array</em> types.
  162.      *
  163.      * @param value value to be converted
  164.      * @return Collection elements.
  165.      */
  166.     protected Collection<?> convertToCollection(final Object value) {
  167.         if (value instanceof Collection) {
  168.             return (Collection<?>) value;
  169.         }
  170.         if (value instanceof Number || value instanceof Boolean || value instanceof Date) {
  171.             final List<Object> list = new ArrayList<>(1);
  172.             list.add(value);
  173.             return list;
  174.         }

  175.         return parseElements(value.toString());
  176.     }

  177.     /**
  178.      * Handles conversion to a String.
  179.      *
  180.      * @param value The value to be converted.
  181.      * @return the converted String value.
  182.      * @throws IllegalArgumentException if an error occurs converting to a String
  183.      */
  184.     @Override
  185.     protected String convertToString(final Object value) {
  186.         int size = 0;
  187.         Iterator<?> iterator = null;
  188.         final Class<?> type = value.getClass();
  189.         if (type.isArray()) {
  190.             size = Array.getLength(value);
  191.         } else {
  192.             final Collection<?> collection = convertToCollection(value);
  193.             size = collection.size();
  194.             iterator = collection.iterator();
  195.         }

  196.         if (size == 0) {
  197.             return (String) getDefault(String.class);
  198.         }

  199.         if (onlyFirstToString) {
  200.             size = 1;
  201.         }

  202.         // Create a StringBuilder containing a delimited list of the values
  203.         final StringBuilder buffer = new StringBuilder();
  204.         for (int i = 0; i < size; i++) {
  205.             if (i > 0) {
  206.                 buffer.append(delimiter);
  207.             }
  208.             Object element = iterator == null ? Array.get(value, i) : iterator.next();
  209.             element = elementConverter.convert(String.class, element);
  210.             if (element != null) {
  211.                 buffer.append(element);
  212.             }
  213.         }

  214.         return buffer.toString();
  215.     }

  216.     /**
  217.      * Handles conversion to an array of the specified type.
  218.      *
  219.      * @param <T>   Target type of the conversion.
  220.      * @param type  The type to which this value should be converted.
  221.      * @param value The input value to be converted.
  222.      * @return The converted value.
  223.      * @throws Throwable if an error occurs converting to the specified type
  224.      */
  225.     @Override
  226.     protected <T> T convertToType(final Class<T> type, final Object value) throws Throwable {
  227.         if (!type.isArray()) {
  228.             throw ConversionException.format("%s cannot handle conversion to '%s' (not an array).", toString(getClass()), toString(type));
  229.         }

  230.         // Handle the source
  231.         int size = 0;
  232.         Iterator<?> iterator = null;
  233.         if (value.getClass().isArray()) {
  234.             size = Array.getLength(value);
  235.         } else {
  236.             final Collection<?> collection = convertToCollection(value);
  237.             size = collection.size();
  238.             iterator = collection.iterator();
  239.         }

  240.         // Allocate a new Array
  241.         final Class<?> componentType = type.getComponentType();
  242.         final Object newArray = Array.newInstance(componentType, size);

  243.         // Convert and set each element in the new Array
  244.         for (int i = 0; i < size; i++) {
  245.             Object element = iterator == null ? Array.get(value, i) : iterator.next();
  246.             // TODO - probably should catch conversion errors and throw
  247.             // new exception providing better info back to the user
  248.             element = elementConverter.convert(componentType, element);
  249.             Array.set(newArray, i, element);
  250.         }
  251.         // This is safe because T is an array type and newArray is an array of
  252.         // T's component type
  253.         return (T) newArray;
  254.     }

  255.     /**
  256.      * Gets the default value for conversions to the specified type.
  257.      *
  258.      * @param type Data type to which this value should be converted.
  259.      * @return The default value for the specified type.
  260.      */
  261.     @Override
  262.     protected Object getDefault(final Class<?> type) {
  263.         if (type.equals(String.class)) {
  264.             return null;
  265.         }

  266.         final Object defaultValue = super.getDefault(type);
  267.         if (defaultValue == null) {
  268.             return null;
  269.         }

  270.         if (defaultValue.getClass().equals(type)) {
  271.             return defaultValue;
  272.         }
  273.         return Array.newInstance(type.getComponentType(), defaultSize);
  274.     }

  275.     /**
  276.      * Gets the default type this {@code Converter} handles.
  277.      *
  278.      * @return The default type this {@code Converter} handles.
  279.      */
  280.     @Override
  281.     protected Class<C> getDefaultType() {
  282.         return defaultType;
  283.     }

  284.     /**
  285.      * <p>
  286.      * Parse an incoming String of the form similar to an array initializer in the Java language into a {@code List} individual Strings for each element,
  287.      * according to the following rules.
  288.      * </p>
  289.      * <ul>
  290.      * <li>The string is expected to be a comma-separated list of values.</li>
  291.      * <li>The string may optionally have matching '{' and '}' delimiters around the list.</li>
  292.      * <li>Whitespace before and after each element is stripped.</li>
  293.      * <li>Elements in the list may be delimited by single or double quotes. Within a quoted elements, the normal Java escape sequences are valid.</li>
  294.      * </ul>
  295.      *
  296.      * @param value String value to be parsed
  297.      * @return List of parsed elements.
  298.      * @throws ConversionException  if the syntax of {@code value} is not syntactically valid
  299.      * @throws NullPointerException if {@code value} is {@code null}
  300.      */
  301.     private List<String> parseElements(String value) {
  302.         if (log().isDebugEnabled()) {
  303.             log().debug("Parsing elements, delimiter=[" + delimiter + "], value=[" + value + "]");
  304.         }

  305.         // Trim any matching '{' and '}' delimiters
  306.         value = toTrim(value);
  307.         if (value.startsWith("{") && value.endsWith("}")) {
  308.             value = value.substring(1, value.length() - 1);
  309.         }

  310.         final String typeName = toString(String.class);
  311.         try {

  312.             // Set up a StreamTokenizer on the characters in this String
  313.             final StreamTokenizer st = new StreamTokenizer(new StringReader(value));
  314.             st.whitespaceChars(delimiter, delimiter); // Set the delimiters
  315.             st.ordinaryChars('0', '9'); // Needed to turn off numeric flag
  316.             st.wordChars('0', '9'); // Needed to make part of tokens
  317.             for (final char allowedChar : allowedChars) {
  318.                 st.ordinaryChars(allowedChar, allowedChar);
  319.                 st.wordChars(allowedChar, allowedChar);
  320.             }

  321.             // Split comma-delimited tokens into a List
  322.             List<String> list = null;
  323.             while (true) {
  324.                 final int ttype = st.nextToken();
  325.                 if (ttype == StreamTokenizer.TT_WORD || ttype > 0) {
  326.                     if (st.sval != null) {
  327.                         if (list == null) {
  328.                             list = new ArrayList<>();
  329.                         }
  330.                         list.add(st.sval);
  331.                     }
  332.                 } else if (ttype == StreamTokenizer.TT_EOF) {
  333.                     break;
  334.                 } else {
  335.                     throw ConversionException.format("Encountered token of type %s parsing elements to '%s'.", ttype, typeName);
  336.                 }
  337.             }

  338.             if (list == null) {
  339.                 list = Collections.emptyList();
  340.             }
  341.             if (log().isDebugEnabled()) {
  342.                 log().debug(list.size() + " elements parsed");
  343.             }

  344.             // Return the completed list
  345.             return list;

  346.         } catch (final IOException e) {
  347.             throw new ConversionException("Error converting from String to '" + typeName + "': " + e.getMessage(), e);
  348.         }
  349.     }

  350.     /**
  351.      * Sets the allowed characters to be used for parsing a delimited String.
  352.      *
  353.      * @param allowedChars Characters which are to be considered as part of the tokens when parsing a delimited String [default is '.' and '-']
  354.      */
  355.     public void setAllowedChars(final char[] allowedChars) {
  356.         this.allowedChars = Objects.requireNonNull(allowedChars, "allowedChars").clone();
  357.     }

  358.     /**
  359.      * Sets the delimiter to be used for parsing a delimited String.
  360.      *
  361.      * @param delimiter The delimiter [default ',']
  362.      */
  363.     public void setDelimiter(final char delimiter) {
  364.         this.delimiter = delimiter;
  365.     }

  366.     /**
  367.      * Indicates whether converting to a String should create a delimited list or just convert the first value.
  368.      *
  369.      * @param onlyFirstToString {@code true} converts only the first value in the array to a String, {@code false} converts all values in the array into a
  370.      *                          delimited list (default is {@code true}
  371.      */
  372.     public void setOnlyFirstToString(final boolean onlyFirstToString) {
  373.         this.onlyFirstToString = onlyFirstToString;
  374.     }

  375.     /**
  376.      * Provide a String representation of this array converter.
  377.      *
  378.      * @return A String representation of this array converter
  379.      */
  380.     @Override
  381.     public String toString() {
  382.         final StringBuilder buffer = new StringBuilder();
  383.         buffer.append(toString(getClass()));
  384.         buffer.append("[UseDefault=");
  385.         buffer.append(isUseDefault());
  386.         buffer.append(", ");
  387.         buffer.append(elementConverter.toString());
  388.         buffer.append(']');
  389.         return buffer.toString();
  390.     }

  391. }