    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.converters;
    018
    019import java.io.IOException;
    020import java.io.StreamTokenizer;
    021import java.io.StringReader;
    022import java.lang.reflect.Array;
    023import java.util.ArrayList;
    024import java.util.Collection;
    025import java.util.Collections;
    026import java.util.Date;
    027import java.util.Iterator;
    028import java.util.List;
    029
    030import org.apache.commons.beanutils.ConversionException;
    031import org.apache.commons.beanutils.Converter;
    032
    033/**
    034 * Generic {@link Converter} implementation that handles conversion
    035 * to and from <strong>array</strong> objects.
    036 * <p>
    037 * Can be configured to either return a <em>default value</em> or throw a
    038 * <code>ConversionException</code> if a conversion error occurs.
    039 * </p>
    040 * <p>
    041 * The main features of this implementation are:
    042 * </p>
    043 * <ul>
    044 *     <li><strong>Element Conversion</strong> - delegates to a {@link Converter},
    045 *         appropriate for the type, to convert individual elements
    046 *         of the array. This leverages the power of existing converters
    047 *         without having to replicate their functionality for converting
    048 *         to the element type and removes the need to create a specifc
    049 *         array type converters.</li>
    050 *     <li><strong>Arrays or Collections</strong> - can convert from either arrays or
    051 *         Collections to an array, limited only by the capability
    052 *         of the delegate {@link Converter}.</li>
    053 *     <li><strong>Delimited Lists</strong> - can Convert <strong>to</strong> and <strong>from</strong> a
    054 *         delimited list in String format.</li>
    055 *     <li><strong>Conversion to String</strong> - converts an array to a
    056 *         <code>String</code> in one of two ways: as a <em>delimited list</em>
    057 *         or by converting the first element in the array to a String - this
    058 *         is controlled by the {@link ArrayConverter#setOnlyFirstToString(boolean)}
    059 *         parameter.</li>
    060 *     <li><strong>Multi Dimensional Arrays</strong> - it is possible to convert a <code>String</code>
    061 *         to a multi-dimensional arrays, by embedding {@link ArrayConverter}
    062 *         within each other - see example below.</li>
    063 *     <li><strong>Default Value</strong>
    064 *         <ul>
    065 *             <li><strong><em>No Default</em></strong> - use the
    066 *                 {@link ArrayConverter#ArrayConverter(Class, Converter)}
    067 *                 constructor to create a converter which throws a
    068 *                 {@link ConversionException} if the value is missing or
    069 *                 invalid.</li>
    070 *             <li><strong><em>Default values</em></strong> - use the
    071 *                 {@link ArrayConverter#ArrayConverter(Class, Converter, int)}
    072 *                 constructor to create a converter which returns a <i>default
    073 *                 value</i>. The <em>defaultSize</em> parameter controls the
    074 *                 <em>default value</em> in the following way:
    075 *                 <ul>
    076 *                    <li><em>defaultSize &lt; 0</em> - default is <code>null</code></li>
    077 *                    <li><em>defaultSize = 0</em> - default is an array of length zero</li>
    078 *                    <li><em>defaultSize &gt; 0</em> - default is an array with a
    079 *                        length specified by <code>defaultSize</code> (N.B. elements
    080 *                        in the array will be <code>null</code>)</li>
    081 *                 </ul>
    082 *             </li>
    083 *         </ul>
    084 *     </li>
    085 * </ul>
    086 *
    087 * <h2>Parsing Delimited Lists</h2>
    088 * This implementation can convert a delimited list in <code>String</code> format
    089 * into an array of the appropriate type. By default, it uses a comma as the delimiter
    090 * but the following methods can be used to configure parsing:
    091 * <ul>
    092 *     <li><code>setDelimiter(char)</code> - allows the character used as
    093 *         the delimiter to be configured [default is a comma].</li>
    094 *     <li><code>setAllowedChars(char[])</code> - adds additional characters
    095 *         (to the default alphabetic/numeric) to those considered to be
    096 *         valid token characters.
    097 * </ul>
    098 *
    099 * <h2>Multi Dimensional Arrays</h2>
    100 * It is possible to convert a <code>String</code> to mulit-dimensional arrays by using
    101 * {@link ArrayConverter} as the element {@link Converter}
    102 * within another {@link ArrayConverter}.
    103 * <p>
    104 * For example, the following code demonstrates how to construct a {@link Converter}
    105 * to convert a delimited <code>String</code> into a two dimensional integer array:
    106 * </p>
    107 * <pre>
    108 *    // Construct an Integer Converter
    109 *    IntegerConverter integerConverter = new IntegerConverter();
    110 *
    111 *    // Construct an array Converter for an integer array (i.e. int[]) using
    112 *    // an IntegerConverter as the element converter.
    113 *    // N.B. Uses the default comma (i.e. ",") as the delimiter between individual numbers
    114 *    ArrayConverter arrayConverter = new ArrayConverter(int[].class, integerConverter);
    115 *
    116 *    // Construct a "Matrix" Converter which converts arrays of integer arrays using
    117 *    // the pre-ceeding ArrayConverter as the element Converter.
    118 *    // N.B. Uses a semi-colon (i.e. ";") as the delimiter to separate the different sets of numbers.
    119 *    //      Also the delimiter used by the first ArrayConverter needs to be added to the
    120 *    //      "allowed characters" for this one.
    121 *    ArrayConverter matrixConverter = new ArrayConverter(int[][].class, arrayConverter);
    122 *    matrixConverter.setDelimiter(';');
    123 *    matrixConverter.setAllowedChars(new char[] {','});
    124 *
    125 *    // Do the Conversion
    126 *    String matrixString = "11,12,13 ; 21,22,23 ; 31,32,33 ; 41,42,43";
    127 *    int[][] result = (int[][])matrixConverter.convert(int[][].class, matrixString);
    128 * </pre>
    129 *
    130 * @since 1.8.0
    131 */
    132public class ArrayConverter extends AbstractConverter {
    133
    134    private final Class<?> defaultType;
    135    private final Converter elementConverter;
    136    private int defaultSize;
    137    private char delimiter    = ',';
    138    private char[] allowedChars = {'.', '-'};
    139    private boolean onlyFirstToString = true;
    140
    141    /**
    142     * Construct an <strong>array</strong> <code>Converter</code> with the specified
    143     * <strong>component</strong> <code>Converter</code> that throws a
    144     * <code>ConversionException</code> if an error occurs.
    145     *
    146     * @param defaultType The default array type this
    147     *  <code>Converter</code> handles
    148     * @param elementConverter Converter used to convert
    149     *  individual array elements.
    150     */
    151    public ArrayConverter(final Class<?> defaultType, final Converter elementConverter) {
    152        if (defaultType == null) {
    153            throw new IllegalArgumentException("Default type is missing");
    154        }
    155        if (!defaultType.isArray()) {
    156            throw new IllegalArgumentException("Default type must be an array.");
    157        }
    158        if (elementConverter == null) {
    159            throw new IllegalArgumentException("Component Converter is missing.");
    160        }
    161        this.defaultType = defaultType;
    162        this.elementConverter = elementConverter;
    163    }
    164
    165    /**
    166     * Construct an <strong>array</strong> <code>Converter</code> with the specified
    167     * <strong>component</strong> <code>Converter</code> that returns a default
    168     * array of the specified size (or <code>null</code>) if an error occurs.
    169     *
    170     * @param defaultType The default array type this
    171     *  <code>Converter</code> handles
    172     * @param elementConverter Converter used to convert
    173     *  individual array elements.
    174     * @param defaultSize Specifies the size of the default array value or if less
    175     *  than zero indicates that a <code>null</code> default value should be used.
    176     */
    177    public ArrayConverter(final Class<?> defaultType, final Converter elementConverter, final int defaultSize) {
    178        this(defaultType, elementConverter);
    179        this.defaultSize = defaultSize;
    180        Object defaultValue = null;
    181        if (defaultSize >= 0) {
    182            defaultValue = Array.newInstance(defaultType.getComponentType(), defaultSize);
    183        }
    184        setDefaultValue(defaultValue);
    185    }
    186
    187    /**
    188     * Returns the value unchanged.
    189     *
    190     * @param value The value to convert
    191     * @return The value unchanged
    192     */
    193    @Override
    194    protected Object convertArray(final Object value) {
    195        return value;
    196    }
    197
    198    /**
    199     * Converts non-array values to a Collection prior
    200     * to being converted either to an array or a String.
    201     * <ul>
    202     *   <li>{@link Collection} values are returned unchanged</li>
    203     *   <li>{@link Number}, {@link Boolean}  and {@link java.util.Date}
    204     *       values returned as a the only element in a List.</li>
    205     *   <li>All other types are converted to a String and parsed
    206     *       as a delimited list.</li>
    207     * </ul>
    208     * <p>
    209     * <strong>N.B.</strong> The method is called by both the
    210     * {@link ArrayConverter#convertToType(Class, Object)} and
    211     * {@link ArrayConverter#convertToString(Object)} methods for
    212     * <em>non-array</em> types.
    213     * </p>
    214     *
    215     * @param type The type to convert the value to
    216     * @param value value to be converted
    217     * @return Collection elements.
    218     */
    219    protected Collection<?> convertToCollection(final Class<?> type, final Object value) {
    220        if (value instanceof Collection) {
    221            return (Collection<?>)value;
    222        }
    223        if (value instanceof Number ||
    224            value instanceof Boolean ||
    225            value instanceof Date) {
    226            final List<Object> list = new ArrayList<>(1);
    227            list.add(value);
    228            return list;
    229        }
    230
    231        return parseElements(type, value.toString());
    232    }
    233
    234    /**
    235     * Handles conversion to a String.
    236     *
    237     * @param value The value to be converted.
    238     * @return the converted String value.
    239     * @throws Throwable if an error occurs converting to a String
    240     */
    241    @Override
    242    protected String convertToString(final Object value) throws Throwable {
    243
    244        int size = 0;
    245        Iterator<?> iterator = null;
    246        final Class<?> type = value.getClass();
    247        if (type.isArray()) {
    248            size = Array.getLength(value);
    249        } else {
    250            final Collection<?> collection = convertToCollection(type, value);
    251            size = collection.size();
    252            iterator = collection.iterator();
    253        }
    254
    255        if (size == 0) {
    256            return (String)getDefault(String.class);
    257        }
    258
    259        if (onlyFirstToString) {
    260            size = 1;
    261        }
    262
    263        // Create a StringBuffer containing a delimited list of the values
    264        final StringBuilder buffer = new StringBuilder();
    265        for (int i = 0; i < size; i++) {
    266            if (i > 0) {
    267                buffer.append(delimiter);
    268            }
    269            Object element = iterator == null ? Array.get(value, i) : iterator.next();
    270            element = elementConverter.convert(String.class, element);
    271            if (element != null) {
    272                buffer.append(element);
    273            }
    274        }
    275
    276        return buffer.toString();
    277
    278    }
    279
    280    /**
    281     * Handles conversion to an array of the specified type.
    282     *
    283     * @param <T> Target type of the conversion.
    284     * @param type The type to which this value should be converted.
    285     * @param value The input value to be converted.
    286     * @return The converted value.
    287     * @throws Throwable if an error occurs converting to the specified type
    288     */
    289    @Override
    290    protected <T> T convertToType(final Class<T> type, final Object value) throws Throwable {
    291
    292        if (!type.isArray()) {
    293            throw new ConversionException(toString(getClass())
    294                    + " cannot handle conversion to '"
    295                    + toString(type) + "' (not an array).");
    296        }
    297
    298        // Handle the source
    299        int size = 0;
    300        Iterator<?> iterator = null;
    301        if (value.getClass().isArray()) {
    302            size = Array.getLength(value);
    303        } else {
    304            final Collection<?> collection = convertToCollection(type, value);
    305            size = collection.size();
    306            iterator = collection.iterator();
    307        }
    308
    309        // Allocate a new Array
    310        final Class<?> componentType = type.getComponentType();
    311        final Object newArray = Array.newInstance(componentType, size);
    312
    313        // Convert and set each element in the new Array
    314        for (int i = 0; i < size; i++) {
    315            Object element = iterator == null ? Array.get(value, i) : iterator.next();
    316            // TODO - probably should catch conversion errors and throw
    317            //        new exception providing better info back to the user
    318            element = elementConverter.convert(componentType, element);
    319            Array.set(newArray, i, element);
    320        }
    321
    322        @SuppressWarnings("unchecked")
    323        final
    324        // This is safe because T is an array type and newArray is an array of
    325        // T's component type
    326        T result = (T) newArray;
    327        return result;
    328    }
    329
    330    /**
    331     * Return the default value for conversions to the specified
    332     * type.
    333     * @param type Data type to which this value should be converted.
    334     * @return The default value for the specified type.
    335     */
    336    @Override
    337    protected Object getDefault(final Class<?> type) {
    338        if (type.equals(String.class)) {
    339            return null;
    340        }
    341
    342        final Object defaultValue = super.getDefault(type);
    343        if (defaultValue == null) {
    344            return null;
    345        }
    346
    347        if (defaultValue.getClass().equals(type)) {
    348            return defaultValue;
    349        }
    350        return Array.newInstance(type.getComponentType(), defaultSize);
    351
    352    }
    353
    354    /**
    355     * Return the default type this <code>Converter</code> handles.
    356     *
    357     * @return The default type this <code>Converter</code> handles.
    358     */
    359    @Override
    360    protected Class<?> getDefaultType() {
    361        return defaultType;
    362    }
    363
    364    /**
    365     * <p>Parse an incoming String of the form similar to an array initializer
    366     * in the Java language into a <code>List</code> individual Strings
    367     * for each element, according to the following rules.</p>
    368     * <ul>
    369     * <li>The string is expected to be a comma-separated list of values.</li>
    370     * <li>The string may optionally have matching '{' and '}' delimiters
    371     *   around the list.</li>
    372     * <li>Whitespace before and after each element is stripped.</li>
    373     * <li>Elements in the list may be delimited by single or double quotes.
    374     *  Within a quoted elements, the normal Java escape sequences are valid.</li>
    375     * </ul>
    376     *
    377     * @param type The type to convert the value to
    378     * @param value String value to be parsed
    379     * @return List of parsed elements.
    380     * @throws ConversionException if the syntax of <code>svalue</code>
    381     *  is not syntactically valid
    382     * @throws NullPointerException if <code>svalue</code>
    383     *  is <code>null</code>
    384     */
    385    private List<String> parseElements(final Class<?> type, String value) {
    386
    387        if (log().isDebugEnabled()) {
    388            log().debug("Parsing elements, delimiter=[" + delimiter + "], value=[" + value + "]");
    389        }
    390
    391        // Trim any matching '{' and '}' delimiters
    392        value = value.trim();
    393        if (value.startsWith("{") && value.endsWith("}")) {
    394            value = value.substring(1, value.length() - 1);
    395        }
    396
    397        try {
    398
    399            // Set up a StreamTokenizer on the characters in this String
    400            final StreamTokenizer st = new StreamTokenizer(new StringReader(value));
    401            st.whitespaceChars(delimiter , delimiter); // Set the delimiters
    402            st.ordinaryChars('0', '9');  // Needed to turn off numeric flag
    403            st.wordChars('0', '9');      // Needed to make part of tokens
    404            for (final char allowedChar : allowedChars) {
    405                st.ordinaryChars(allowedChar, allowedChar);
    406                st.wordChars(allowedChar, allowedChar);
    407            }
    408
    409            // Split comma-delimited tokens into a List
    410            List<String> list = null;
    411            while (true) {
    412                final int ttype = st.nextToken();
    413                if (ttype == StreamTokenizer.TT_WORD || ttype > 0) {
    414                    if (st.sval != null) {
    415                        if (list == null) {
    416                            list = new ArrayList<>();
    417                        }
    418                        list.add(st.sval);
    419                    }
    420                } else if (ttype == StreamTokenizer.TT_EOF) {
    421                    break;
    422                } else {
    423                    throw new ConversionException("Encountered token of type "
    424                        + ttype + " parsing elements to '" + toString(type) + ".");
    425                }
    426            }
    427
    428            if (list == null) {
    429                list = Collections.emptyList();
    430            }
    431            if (log().isDebugEnabled()) {
    432                log().debug(list.size() + " elements parsed");
    433            }
    434
    435            // Return the completed list
    436            return list;
    437
    438        } catch (final IOException e) {
    439
    440            throw new ConversionException("Error converting from String to '"
    441                    + toString(type) + "': " + e.getMessage(), e);
    442
    443        }
    444
    445    }
    446
    447    /**
    448     * Set the allowed characters to be used for parsing a delimited String.
    449     *
    450     * @param allowedChars Characters which are to be considered as part of
    451     * the tokens when parsing a delimited String [default is '.' and '-']
    452     */
    453    public void setAllowedChars(final char[] allowedChars) {
    454        this.allowedChars = allowedChars;
    455    }
    456
    457    /**
    458     * Set the delimiter to be used for parsing a delimited String.
    459     *
    460     * @param delimiter The delimiter [default ',']
    461     */
    462    public void setDelimiter(final char delimiter) {
    463        this.delimiter = delimiter;
    464    }
    465
    466    /**
    467     * Indicates whether converting to a String should create
    468     * a delimited list or just convert the first value.
    469     *
    470     * @param onlyFirstToString <code>true</code> converts only
    471     * the first value in the array to a String, <code>false</code>
    472     * converts all values in the array into a delimited list (default
    473     * is <code>true</code>
    474     */
    475    public void setOnlyFirstToString(final boolean onlyFirstToString) {
    476        this.onlyFirstToString = onlyFirstToString;
    477    }
    478
    479    /**
    480     * Provide a String representation of this array converter.
    481     *
    482     * @return A String representation of this array converter
    483     */
    484    @Override
    485    public String toString() {
    486        final StringBuilder buffer = new StringBuilder();
    487        buffer.append(toString(getClass()));
    488        buffer.append("[UseDefault=");
    489        buffer.append(isUseDefault());
    490        buffer.append(", ");
    491        buffer.append(elementConverter.toString());
    492        buffer.append(']');
    493        return buffer.toString();
    494    }
    495
    496}