View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.beanutils.converters;
18  
19  import java.io.IOException;
20  import java.io.StreamTokenizer;
21  import java.io.StringReader;
22  import java.lang.reflect.Array;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.Iterator;
27  import java.util.List;
28  
29  import org.apache.commons.beanutils.ConversionException;
30  import org.apache.commons.beanutils.Converter;
31  
32  /**
33   * Generic {@link Converter} implementation that handles conversion
34   * to and from <b>array</b> objects.
35   * <p>
36   * Can be configured to either return a <i>default value</i> or throw a
37   * <code>ConversionException</code> if a conversion error occurs.
38   * <p>
39   * The main features of this implementation are:
40   * <ul>
41   *     <li><b>Element Conversion</b> - delegates to a {@link Converter},
42   *         appropriate for the type, to convert individual elements
43   *         of the array. This leverages the power of existing converters
44   *         without having to replicate their functionality for converting
45   *         to the element type and removes the need to create a specifc
46   *         array type converters.</li>
47   *     <li><b>Arrays or Collections</b> - can convert from either arrays or
48   *         Collections to an array, limited only by the capability
49   *         of the delegate {@link Converter}.</li>
50   *     <li><b>Delimited Lists</b> - can Convert <b>to</b> and <b>from</b> a
51   *         delimited list in String format.</li>
52   *     <li><b>Conversion to String</b> - converts an array to a
53   *         <code>String</code> in one of two ways: as a <i>delimited list</i>
54   *         or by converting the first element in the array to a String - this
55   *         is controlled by the {@link ArrayConverter#setOnlyFirstToString(boolean)}
56   *         parameter.</li>
57   *     <li><b>Multi Dimensional Arrays</b> - it is possible to convert a <code>String</code>
58   *         to a multi-dimensional arrays, by embedding {@link ArrayConverter}
59   *         within each other - see example below.</li>
60   *     <li><b>Default Value</b></li>
61   *         <ul>
62   *             <li><b><i>No Default</b></i> - use the
63   *                 {@link ArrayConverter#ArrayConverter(Class, Converter)}
64   *                 constructor to create a converter which throws a
65   *                 {@link ConversionException} if the value is missing or
66   *                 invalid.</li>
67   *             <li><b><i>Default values</b></i> - use the
68   *                 {@link ArrayConverter#ArrayConverter(Class, Converter, int)}
69   *                 constructor to create a converter which returns a <i>default
70   *                 value</i>. The <i>defaultSize</i> parameter controls the
71   *                 <i>default value</i> in the following way:</li>
72   *                 <ul>
73   *                    <li><i>defaultSize &lt; 0</i> - default is <code>null</code></li>
74   *                    <li><i>defaultSize = 0</i> - default is an array of length zero</li>
75   *                    <li><i>defaultSize &gt; 0</i> - default is an array with a
76   *                        length specified by <code>defaultSize</code> (N.B. elements
77   *                        in the array will be <code>null</code>)</li>
78   *                 </ul>
79   *         </ul>
80   * </ul>
81   *
82   * <h3>Parsing Delimited Lists</h3>
83   * This implementation can convert a delimited list in <code>String</code> format
84   * into an array of the appropriate type. By default, it uses a comma as the delimiter
85   * but the following methods can be used to configure parsing:
86   * <ul>
87   *     <li><code>setDelimiter(char)</code> - allows the character used as
88   *         the delimiter to be configured [default is a comma].</li>
89   *     <li><code>setAllowedChars(char[])</code> - adds additional characters
90   *         (to the default alphabetic/numeric) to those considered to be
91   *         valid token characters.
92   * </ul>
93   *
94   * <h3>Multi Dimensional Arrays</h3>
95   * It is possible to convert a <code>String</code> to mulit-dimensional arrays by using
96   * {@link ArrayConverter} as the element {@link Converter}
97   * within another {@link ArrayConverter}.
98   * <p>
99   * For example, the following code demonstrates how to construct a {@link Converter}
100  * to convert a delimited <code>String</code> into a two dimensional integer array:
101  * <p>
102  * <pre>
103  *    // Construct an Integer Converter
104  *    IntegerConverter integerConverter = new IntegerConverter();
105  *
106  *    // Construct an array Converter for an integer array (i.e. int[]) using
107  *    // an IntegerConverter as the element converter.
108  *    // N.B. Uses the default comma (i.e. ",") as the delimiter between individual numbers
109  *    ArrayConverter arrayConverter = new ArrayConverter(int[].class, integerConverter);
110  *
111  *    // Construct a "Matrix" Converter which converts arrays of integer arrays using
112  *    // the pre-ceeding ArrayConverter as the element Converter.
113  *    // N.B. Uses a semi-colon (i.e. ";") as the delimiter to separate the different sets of numbers.
114  *    //      Also the delimiter used by the first ArrayConverter needs to be added to the
115  *    //      "allowed characters" for this one.
116  *    ArrayConverter matrixConverter = new ArrayConverter(int[][].class, arrayConverter);
117  *    matrixConverter.setDelimiter(';');
118  *    matrixConverter.setAllowedChars(new char[] {','});
119  *
120  *    // Do the Conversion
121  *    String matrixString = "11,12,13 ; 21,22,23 ; 31,32,33 ; 41,42,43";
122  *    int[][] result = (int[][])matrixConverter.convert(int[][].class, matrixString);
123  * </pre>
124  *
125  * @version $Id$
126  * @since 1.8.0
127  */
128 public class ArrayConverter extends AbstractConverter {
129 
130     private final Class<?> defaultType;
131     private final Converter elementConverter;
132     private int defaultSize;
133     private char delimiter    = ',';
134     private char[] allowedChars = new char[] {'.', '-'};
135     private boolean onlyFirstToString = true;
136 
137     // ----------------------------------------------------------- Constructors
138 
139     /**
140      * Construct an <b>array</b> <code>Converter</code> with the specified
141      * <b>component</b> <code>Converter</code> that throws a
142      * <code>ConversionException</code> if an error occurs.
143      *
144      * @param defaultType The default array type this
145      *  <code>Converter</code> handles
146      * @param elementConverter Converter used to convert
147      *  individual array elements.
148      */
149     public ArrayConverter(final Class<?> defaultType, final Converter elementConverter) {
150         super();
151         if (defaultType == null) {
152             throw new IllegalArgumentException("Default type is missing");
153         }
154         if (!defaultType.isArray()) {
155             throw new IllegalArgumentException("Default type must be an array.");
156         }
157         if (elementConverter == null) {
158             throw new IllegalArgumentException("Component Converter is missing.");
159         }
160         this.defaultType = defaultType;
161         this.elementConverter = elementConverter;
162     }
163 
164     /**
165      * Construct an <b>array</b> <code>Converter</code> with the specified
166      * <b>component</b> <code>Converter</code> that returns a default
167      * array of the specified size (or <code>null</code>) if an error occurs.
168      *
169      * @param defaultType The default array type this
170      *  <code>Converter</code> handles
171      * @param elementConverter Converter used to convert
172      *  individual array elements.
173      * @param defaultSize Specifies the size of the default array value or if less
174      *  than zero indicates that a <code>null</code> default value should be used.
175      */
176     public ArrayConverter(final Class<?> defaultType, final Converter elementConverter, final int defaultSize) {
177         this(defaultType, elementConverter);
178         this.defaultSize = defaultSize;
179         Object defaultValue = null;
180         if (defaultSize >= 0) {
181             defaultValue = Array.newInstance(defaultType.getComponentType(), defaultSize);
182         }
183         setDefaultValue(defaultValue);
184     }
185 
186     /**
187      * Set the delimiter to be used for parsing a delimited String.
188      *
189      * @param delimiter The delimiter [default ',']
190      */
191     public void setDelimiter(final char delimiter) {
192         this.delimiter = delimiter;
193     }
194 
195     /**
196      * Set the allowed characters to be used for parsing a delimited String.
197      *
198      * @param allowedChars Characters which are to be considered as part of
199      * the tokens when parsing a delimited String [default is '.' and '-']
200      */
201     public void setAllowedChars(final char[] allowedChars) {
202         this.allowedChars = allowedChars;
203     }
204 
205     /**
206      * Indicates whether converting to a String should create
207      * a delimited list or just convert the first value.
208      *
209      * @param onlyFirstToString <code>true</code> converts only
210      * the first value in the array to a String, <code>false</code>
211      * converts all values in the array into a delimited list (default
212      * is <code>true</code>
213      */
214     public void setOnlyFirstToString(final boolean onlyFirstToString) {
215         this.onlyFirstToString = onlyFirstToString;
216     }
217 
218     /**
219      * Return the default type this <code>Converter</code> handles.
220      *
221      * @return The default type this <code>Converter</code> handles.
222      */
223     @Override
224     protected Class<?> getDefaultType() {
225         return defaultType;
226     }
227 
228     /**
229      * Handles conversion to a String.
230      *
231      * @param value The value to be converted.
232      * @return the converted String value.
233      * @throws Throwable if an error occurs converting to a String
234      */
235     @Override
236     protected String convertToString(final Object value) throws Throwable {
237 
238         int size = 0;
239         Iterator<?> iterator = null;
240         final Class<?> type = value.getClass();
241         if (type.isArray()) {
242             size = Array.getLength(value);
243         } else {
244             final Collection<?> collection = convertToCollection(type, value);
245             size = collection.size();
246             iterator = collection.iterator();
247         }
248 
249         if (size == 0) {
250             return (String)getDefault(String.class);
251         }
252 
253         if (onlyFirstToString) {
254             size = 1;
255         }
256 
257         // Create a StringBuffer containing a delimited list of the values
258         final StringBuilder buffer = new StringBuilder();
259         for (int i = 0; i < size; i++) {
260             if (i > 0) {
261                 buffer.append(delimiter);
262             }
263             Object element = iterator == null ? Array.get(value, i) : iterator.next();
264             element = elementConverter.convert(String.class, element);
265             if (element != null) {
266                 buffer.append(element);
267             }
268         }
269 
270         return buffer.toString();
271 
272     }
273 
274     /**
275      * Handles conversion to an array of the specified type.
276      *
277      * @param <T> Target type of the conversion.
278      * @param type The type to which this value should be converted.
279      * @param value The input value to be converted.
280      * @return The converted value.
281      * @throws Throwable if an error occurs converting to the specified type
282      */
283     @Override
284     protected <T> T convertToType(final Class<T> type, final Object value) throws Throwable {
285 
286         if (!type.isArray()) {
287             throw new ConversionException(toString(getClass())
288                     + " cannot handle conversion to '"
289                     + toString(type) + "' (not an array).");
290         }
291 
292         // Handle the source
293         int size = 0;
294         Iterator<?> iterator = null;
295         if (value.getClass().isArray()) {
296             size = Array.getLength(value);
297         } else {
298             final Collection<?> collection = convertToCollection(type, value);
299             size = collection.size();
300             iterator = collection.iterator();
301         }
302 
303         // Allocate a new Array
304         final Class<?> componentType = type.getComponentType();
305         final Object newArray = Array.newInstance(componentType, size);
306 
307         // Convert and set each element in the new Array
308         for (int i = 0; i < size; i++) {
309             Object element = iterator == null ? Array.get(value, i) : iterator.next();
310             // TODO - probably should catch conversion errors and throw
311             //        new exception providing better info back to the user
312             element = elementConverter.convert(componentType, element);
313             Array.set(newArray, i, element);
314         }
315 
316         @SuppressWarnings("unchecked")
317         final
318         // This is safe because T is an array type and newArray is an array of
319         // T's component type
320         T result = (T) newArray;
321         return result;
322     }
323 
324     /**
325      * Returns the value unchanged.
326      *
327      * @param value The value to convert
328      * @return The value unchanged
329      */
330     @Override
331     protected Object convertArray(final Object value) {
332         return value;
333     }
334 
335     /**
336      * Converts non-array values to a Collection prior
337      * to being converted either to an array or a String.
338      * </p>
339      * <ul>
340      *   <li>{@link Collection} values are returned unchanged</li>
341      *   <li>{@link Number}, {@link Boolean}  and {@link java.util.Date}
342      *       values returned as a the only element in a List.</li>
343      *   <li>All other types are converted to a String and parsed
344      *       as a delimited list.</li>
345      * </ul>
346      *
347      * <strong>N.B.</strong> The method is called by both the
348      * {@link ArrayConverter#convertToType(Class, Object)} and
349      * {@link ArrayConverter#convertToString(Object)} methods for
350      * <i>non-array</i> types.
351      *
352      * @param type The type to convert the value to
353      * @param value value to be converted
354      * @return Collection elements.
355      */
356     protected Collection<?> convertToCollection(final Class<?> type, final Object value) {
357         if (value instanceof Collection) {
358             return (Collection<?>)value;
359         }
360         if (value instanceof Number ||
361             value instanceof Boolean ||
362             value instanceof java.util.Date) {
363             final List<Object> list = new ArrayList<Object>(1);
364             list.add(value);
365             return list;
366         }
367 
368         return parseElements(type, value.toString());
369     }
370 
371     /**
372      * Return the default value for conversions to the specified
373      * type.
374      * @param type Data type to which this value should be converted.
375      * @return The default value for the specified type.
376      */
377     @Override
378     protected Object getDefault(final Class<?> type) {
379         if (type.equals(String.class)) {
380             return null;
381         }
382 
383         final Object defaultValue = super.getDefault(type);
384         if (defaultValue == null) {
385             return null;
386         }
387 
388         if (defaultValue.getClass().equals(type)) {
389             return defaultValue;
390         } else {
391             return Array.newInstance(type.getComponentType(), defaultSize);
392         }
393 
394     }
395 
396     /**
397      * Provide a String representation of this array converter.
398      *
399      * @return A String representation of this array converter
400      */
401     @Override
402     public String toString() {
403         final StringBuilder buffer = new StringBuilder();
404         buffer.append(toString(getClass()));
405         buffer.append("[UseDefault=");
406         buffer.append(isUseDefault());
407         buffer.append(", ");
408         buffer.append(elementConverter.toString());
409         buffer.append(']');
410         return buffer.toString();
411     }
412 
413     /**
414      * <p>Parse an incoming String of the form similar to an array initializer
415      * in the Java language into a <code>List</code> individual Strings
416      * for each element, according to the following rules.</p>
417      * <ul>
418      * <li>The string is expected to be a comma-separated list of values.</li>
419      * <li>The string may optionally have matching '{' and '}' delimiters
420      *   around the list.</li>
421      * <li>Whitespace before and after each element is stripped.</li>
422      * <li>Elements in the list may be delimited by single or double quotes.
423      *  Within a quoted elements, the normal Java escape sequences are valid.</li>
424      * </ul>
425      *
426      * @param type The type to convert the value to
427      * @param value String value to be parsed
428      * @return List of parsed elements.
429      *
430      * @throws ConversionException if the syntax of <code>svalue</code>
431      *  is not syntactically valid
432      * @throws NullPointerException if <code>svalue</code>
433      *  is <code>null</code>
434      */
435     private List<String> parseElements(final Class<?> type, String value) {
436 
437         if (log().isDebugEnabled()) {
438             log().debug("Parsing elements, delimiter=[" + delimiter + "], value=[" + value + "]");
439         }
440 
441         // Trim any matching '{' and '}' delimiters
442         value = value.trim();
443         if (value.startsWith("{") && value.endsWith("}")) {
444             value = value.substring(1, value.length() - 1);
445         }
446 
447         try {
448 
449             // Set up a StreamTokenizer on the characters in this String
450             final StreamTokenizer st = new StreamTokenizer(new StringReader(value));
451             st.whitespaceChars(delimiter , delimiter); // Set the delimiters
452             st.ordinaryChars('0', '9');  // Needed to turn off numeric flag
453             st.wordChars('0', '9');      // Needed to make part of tokens
454             for (char allowedChar : allowedChars) {
455                 st.ordinaryChars(allowedChar, allowedChar);
456                 st.wordChars(allowedChar, allowedChar);
457             }
458 
459             // Split comma-delimited tokens into a List
460             List<String> list = null;
461             while (true) {
462                 final int ttype = st.nextToken();
463                 if ((ttype == StreamTokenizer.TT_WORD) || (ttype > 0)) {
464                     if (st.sval != null) {
465                         if (list == null) {
466                             list = new ArrayList<String>();
467                         }
468                         list.add(st.sval);
469                     }
470                 } else if (ttype == StreamTokenizer.TT_EOF) {
471                     break;
472                 } else {
473                     throw new ConversionException("Encountered token of type "
474                         + ttype + " parsing elements to '" + toString(type) + ".");
475                 }
476             }
477 
478             if (list == null) {
479                 list = Collections.emptyList();
480             }
481             if (log().isDebugEnabled()) {
482                 log().debug(list.size() + " elements parsed");
483             }
484 
485             // Return the completed list
486             return (list);
487 
488         } catch (final IOException e) {
489 
490             throw new ConversionException("Error converting from String to '"
491                     + toString(type) + "': " + e.getMessage(), e);
492 
493         }
494 
495     }
496 
497 }