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.beanutils2.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.Date;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Objects;
30  
31  import org.apache.commons.beanutils2.ConversionException;
32  import org.apache.commons.beanutils2.Converter;
33  
34  /**
35   * Generic {@link Converter} implementation that handles conversion to and from <strong>array</strong> objects.
36   * <p>
37   * Can be configured to either return a <em>default value</em> or throw a {@code ConversionException} if a conversion error occurs.
38   * <p>
39   * The main features of this implementation are:
40   * <ul>
41   * <li><strong>Element Conversion</strong> - delegates to a {@link Converter}, appropriate for the type, to convert individual elements of the array. This
42   * leverages the power of existing converters without having to replicate their functionality for converting to the element type and removes the need to create
43   * a specific array type converters.</li>
44   * <li><strong>Arrays or Collections</strong> - can convert from either arrays or Collections to an array, limited only by the capability of the delegate
45   * {@link Converter}.</li>
46   * <li><strong>Delimited Lists</strong> - can Convert <strong>to</strong> and <strong>from</strong> a delimited list in String format.</li>
47   * <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
48   * element in the array to a String - this is controlled by the {@link ArrayConverter#setOnlyFirstToString(boolean)} parameter.</li>
49   * <li><strong>Multi Dimensional Arrays</strong> - it is possible to convert a {@code String} to a multi-dimensional arrays, by embedding {@link ArrayConverter}
50   * within each other - see example below.</li>
51   * <li><strong>Default Value</strong>
52   * <ul>
53   * <li><strong><em>No Default</em></strong> - use the {@link ArrayConverter#ArrayConverter(Class, Converter)} constructor to create a converter which throws a
54   * {@link ConversionException} if the value is missing or invalid.</li>
55   * <li><strong><em>Default values</em></strong> - use the {@link ArrayConverter#ArrayConverter(Class, Converter, int)} constructor to create a converter which
56   * returns a <em>default value</em>. The <em>defaultSize</em> parameter controls the <em>default value</em> in the following way:
57   * <ul>
58   * <li><em>defaultSize &lt; 0</em> - default is {@code null}</li>
59   * <li><em>defaultSize = 0</em> - default is an array of length zero</li>
60   * <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>
61   * </ul>
62   * </li>
63   * </ul>
64   * </li>
65   * </ul>
66   *
67   * <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,
68   * it uses a comma as the delimiter but the following methods can be used to configure parsing:
69   * <ul>
70   * <li>{@code setDelimiter(char)} - allows the character used as the delimiter to be configured [default is a comma].</li>
71   * <li>{@code setAllowedChars(char[])} - adds additional characters (to the default alphabetic/numeric) to those considered to be valid token characters.
72   * </ul>
73   *
74   * <h2>Multi Dimensional Arrays</h2> It is possible to convert a {@code String} to multi-dimensional arrays by using {@link ArrayConverter} as the element
75   * {@link Converter} within another {@link ArrayConverter}.
76   * <p>
77   * For example, the following code demonstrates how to construct a {@link Converter} to convert a delimited {@code String} into a two dimensional integer array:
78   * </p>
79   *
80   * <pre>
81   * // Construct an Integer Converter
82   * IntegerConverter integerConverter = new IntegerConverter();
83   *
84   * // Construct an array Converter for an integer array (i.e. int[]) using
85   * // an IntegerConverter as the element converter.
86   * // N.B. Uses the default comma (i.e. ",") as the delimiter between individual numbers
87   * ArrayConverter arrayConverter = new ArrayConverter(int[].class, integerConverter);
88   *
89   * // Construct a "Matrix" Converter which converts arrays of integer arrays using
90   * // the preceding ArrayConverter as the element Converter.
91   * // N.B. Uses a semicolon (i.e. ";") as the delimiter to separate the different sets of numbers.
92   * // Also the delimiter used by the first ArrayConverter needs to be added to the
93   * // "allowed characters" for this one.
94   * ArrayConverter matrixConverter = new ArrayConverter(int[][].class, arrayConverter);
95   * matrixConverter.setDelimiter(';');
96   * matrixConverter.setAllowedChars(new char[] { ',' });
97   *
98   * // Do the Conversion
99   * 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  */
106 public 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 }