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