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}