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