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