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 */
017 package org.apache.commons.beanutils.converters;
018
019 import java.util.Collections;
020 import java.util.List;
021 import java.util.ArrayList;
022 import java.util.Iterator;
023 import java.util.Collection;
024 import java.io.StreamTokenizer;
025 import java.io.StringReader;
026 import java.io.IOException;
027 import java.lang.reflect.Array;
028 import org.apache.commons.beanutils.ConversionException;
029 import 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 < 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 > 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 */
127 public 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 }