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}