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