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: 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: // ----------------------------------------------------------- Constructors
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(
151: "Default type must be an array.");
152: }
153: if (elementConverter == null) {
154: throw new IllegalArgumentException(
155: "Component Converter is missing.");
156: }
157: this .elementConverter = elementConverter;
158: }
159:
160: /**
161: * Construct an <b>array</b> <code>Converter</code> with the specified
162: * <b>component</b> <code>Converter</code> that returns a default
163: * array of the specified size (or <code>null</code>) if an error occurs.
164: *
165: * @param defaultType The default array type this
166: * <code>Converter</code> handles
167: * @param elementConverter Converter used to convert
168: * individual array elements.
169: * @param defaultSize Specifies the size of the default array value or if less
170: * than zero indicates that a <code>null</code> default value should be used.
171: */
172: public ArrayConverter(Class defaultType,
173: Converter elementConverter, int defaultSize) {
174: this (defaultType, elementConverter);
175: this .defaultSize = defaultSize;
176: Object defaultValue = null;
177: if (defaultSize >= 0) {
178: defaultValue = Array.newInstance(defaultType
179: .getComponentType(), defaultSize);
180: }
181: setDefaultValue(defaultValue);
182: }
183:
184: /**
185: * Set the delimiter to be used for parsing a delimited String.
186: *
187: * @param delimiter The delimiter [default ',']
188: */
189: public void setDelimiter(char delimiter) {
190: this .delimiter = delimiter;
191: }
192:
193: /**
194: * Set the allowed characters to be used for parsing a delimited String.
195: *
196: * @param allowedChars Characters which are to be considered as part of
197: * the tokens when parsing a delimited String [default is '.' and '-']
198: */
199: public void setAllowedChars(char[] allowedChars) {
200: this .allowedChars = allowedChars;
201: }
202:
203: /**
204: * Indicates whether converting to a String should create
205: * a delimited list or just convert the first value.
206: *
207: * @param onlyFirstToString <code>true</code> converts only
208: * the first value in the array to a String, <code>false</code>
209: * converts all values in the array into a delimited list (default
210: * is <code>true</code>
211: */
212: public void setOnlyFirstToString(boolean onlyFirstToString) {
213: this .onlyFirstToString = onlyFirstToString;
214: }
215:
216: /**
217: * Handles conversion to a String.
218: *
219: * @param value The value to be converted.
220: * @return the converted String value.
221: * @throws Throwable if an error occurs converting to a String
222: */
223: protected String convertToString(Object value) throws Throwable {
224:
225: int size = 0;
226: Iterator iterator = null;
227: Class type = value.getClass();
228: if (type.isArray()) {
229: size = Array.getLength(value);
230: } else {
231: Collection collection = convertToCollection(type, value);
232: size = collection.size();
233: iterator = collection.iterator();
234: }
235:
236: if (size == 0) {
237: return (String) getDefault(String.class);
238: }
239:
240: if (onlyFirstToString) {
241: size = 1;
242: }
243:
244: // Create a StringBuffer containing a delimited list of the values
245: StringBuffer buffer = new StringBuffer();
246: for (int i = 0; i < size; i++) {
247: if (i > 0) {
248: buffer.append(delimiter);
249: }
250: Object element = iterator == null ? Array.get(value, i)
251: : iterator.next();
252: element = elementConverter.convert(String.class, element);
253: if (element != null) {
254: buffer.append(element);
255: }
256: }
257:
258: return buffer.toString();
259:
260: }
261:
262: /**
263: * Handles conversion to an array of the specified type.
264: *
265: * @param type The type to which this value should be converted.
266: * @param value The input value to be converted.
267: * @return The converted value.
268: * @throws Throwable if an error occurs converting to the specified type
269: */
270: protected Object convertToType(Class type, Object value)
271: throws Throwable {
272:
273: if (!type.isArray()) {
274: throw new ConversionException(toString(getClass())
275: + " cannot handle conversion to '" + toString(type)
276: + "' (not an array).");
277: }
278:
279: // Handle the source
280: int size = 0;
281: Iterator iterator = null;
282: if (value.getClass().isArray()) {
283: size = Array.getLength(value);
284: } else {
285: Collection collection = convertToCollection(type, value);
286: size = collection.size();
287: iterator = collection.iterator();
288: }
289:
290: // Allocate a new Array
291: Class componentType = type.getComponentType();
292: Object newArray = Array.newInstance(componentType, size);
293:
294: // Convert and set each element in the new Array
295: for (int i = 0; i < size; i++) {
296: Object element = iterator == null ? Array.get(value, i)
297: : iterator.next();
298: // TODO - probably should catch conversion errors and throw
299: // new exception providing better info back to the user
300: element = elementConverter.convert(componentType, element);
301: Array.set(newArray, i, element);
302: }
303:
304: return newArray;
305: }
306:
307: /**
308: * Returns the value unchanged.
309: *
310: * @param value The value to convert
311: * @return The value unchanged
312: */
313: protected Object convertArray(Object value) {
314: return value;
315: }
316:
317: /**
318: * Converts non-array values to a Collection prior
319: * to being converted either to an array or a String.
320: * </p>
321: * <ul>
322: * <li>{@link Collection} values are returned unchanged</li>
323: * <li>{@link Number}, {@link Boolean} and {@link java.util.Date}
324: * values returned as a the only element in a List.</li>
325: * <li>All other types are converted to a String and parsed
326: * as a delimited list.</li>
327: * </ul>
328: *
329: * <strong>N.B.</strong> The method is called by both the
330: * {@link ArrayConverter#convertToType(Class, Object)} and
331: * {@link ArrayConverter#convertToString(Object)} methods for
332: * <i>non-array</i> types.
333: *
334: * @param type The type to convert the value to
335: * @param value value to be converted
336: * @return Collection elements.
337: */
338: protected Collection convertToCollection(Class type, Object value) {
339: if (value instanceof Collection) {
340: return (Collection) value;
341: }
342: if (value instanceof Number || value instanceof Boolean
343: || value instanceof java.util.Date) {
344: List list = new ArrayList(1);
345: list.add(value);
346: return list;
347: }
348:
349: return parseElements(type, value.toString());
350: }
351:
352: /**
353: * Return the default value for conversions to the specified
354: * type.
355: * @param type Data type to which this value should be converted.
356: * @return The default value for the specified type.
357: */
358: protected Object getDefault(Class type) {
359: if (type.equals(String.class)) {
360: return null;
361: }
362:
363: Object defaultValue = super .getDefault(type);
364: if (defaultValue == null) {
365: return null;
366: }
367:
368: if (defaultValue.getClass().equals(type)) {
369: return defaultValue;
370: } else {
371: return Array.newInstance(type.getComponentType(),
372: defaultSize);
373: }
374:
375: }
376:
377: /**
378: * Provide a String representation of this array converter.
379: *
380: * @return A String representation of this array converter
381: */
382: public String toString() {
383: StringBuffer buffer = new StringBuffer();
384: buffer.append(toString(getClass()));
385: buffer.append("[UseDefault=");
386: buffer.append(isUseDefault());
387: buffer.append(", ");
388: buffer.append(elementConverter.toString());
389: buffer.append(']');
390: return buffer.toString();
391: }
392:
393: /**
394: * <p>Parse an incoming String of the form similar to an array initializer
395: * in the Java language into a <code>List</code> individual Strings
396: * for each element, according to the following rules.</p>
397: * <ul>
398: * <li>The string is expected to be a comma-separated list of values.</li>
399: * <li>The string may optionally have matching '{' and '}' delimiters
400: * around the list.</li>
401: * <li>Whitespace before and after each element is stripped.</li>
402: * <li>Elements in the list may be delimited by single or double quotes.
403: * Within a quoted elements, the normal Java escape sequences are valid.</li>
404: * </ul>
405: *
406: * @param type The type to convert the value to
407: * @param value String value to be parsed
408: * @return List of parsed elements.
409: *
410: * @throws ConversionException if the syntax of <code>svalue</code>
411: * is not syntactically valid
412: * @throws NullPointerException if <code>svalue</code>
413: * is <code>null</code>
414: */
415: private List parseElements(Class type, String value) {
416:
417: if (log().isDebugEnabled()) {
418: log().debug(
419: "Parsing elements, delimiter=[" + delimiter
420: + "], value=[" + value + "]");
421: }
422:
423: // Trim any matching '{' and '}' delimiters
424: value = value.trim();
425: if (value.startsWith("{") && value.endsWith("}")) {
426: value = value.substring(1, value.length() - 1);
427: }
428:
429: try {
430:
431: // Set up a StreamTokenizer on the characters in this String
432: StreamTokenizer st = new StreamTokenizer(new StringReader(
433: value));
434: st.whitespaceChars(delimiter, delimiter); // Set the delimiters
435: st.ordinaryChars('0', '9'); // Needed to turn off numeric flag
436: st.wordChars('0', '9'); // Needed to make part of tokens
437: for (int i = 0; i < allowedChars.length; i++) {
438: st.ordinaryChars(allowedChars[i], allowedChars[i]);
439: st.wordChars(allowedChars[i], allowedChars[i]);
440: }
441:
442: // Split comma-delimited tokens into a List
443: List list = null;
444: while (true) {
445: int ttype = st.nextToken();
446: if ((ttype == StreamTokenizer.TT_WORD) || (ttype > 0)) {
447: if (list == null) {
448: list = new ArrayList();
449: }
450: list.add(st.sval.trim());
451: } else if (ttype == StreamTokenizer.TT_EOF) {
452: break;
453: } else {
454: throw new ConversionException(
455: "Encountered token of type " + ttype
456: + " parsing elements to '"
457: + toString(type) + ".");
458: }
459: }
460:
461: if (list == null) {
462: list = Collections.EMPTY_LIST;
463: }
464: if (log().isDebugEnabled()) {
465: log().debug(list.size() + " elements parsed");
466: }
467:
468: // Return the completed list
469: return (list);
470:
471: } catch (IOException e) {
472:
473: throw new ConversionException(
474: "Error converting from String to '"
475: + toString(type) + "': " + e.getMessage(),
476: e);
477:
478: }
479:
480: }
481:
482: }
|