/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.beanutils.converters;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.Converter;
Generic Converter
implementation that handles conversion to and from array objects.
Can be configured to either return a default value or throw a
ConversionException
if a conversion error occurs.
The main features of this implementation are:
- Element Conversion - delegates to a
Converter
, appropriate for the type, to convert individual elements of the array. This leverages the power of existing converters without having to replicate their functionality for converting to the element type and removes the need to create a specifc array type converters.
- Arrays or Collections - can convert from either arrays or Collections to an array, limited only by the capability of the delegate
Converter
.
- Delimited Lists - can Convert to and from a
delimited list in String format.
- Conversion to String - converts an array to a
String
in one of two ways: as a delimited list or by converting the first element in the array to a String - this is controlled by the setOnlyFirstToString(boolean)
parameter.
- Multi Dimensional Arrays - it is possible to convert a
String
to a multi-dimensional arrays, by embedding ArrayConverter
within each other - see example below.
- Default Value
- No Default - use the
ArrayConverter(Class<?>, Converter)
constructor to create a converter which throws a ConversionException
if the value is missing or invalid.
- Default values - use the
ArrayConverter(Class<?>, Converter, int)
constructor to create a converter which returns a default
value. The defaultSize parameter controls the
default value in the following way:
- defaultSize < 0 - default is
null
- defaultSize = 0 - default is an array of length zero
- defaultSize > 0 - default is an array with a
length specified by
defaultSize
(N.B. elements
in the array will be null
)
Parsing Delimited Lists
This implementation can convert a delimited list in String
format
into an array of the appropriate type. By default, it uses a comma as the delimiter
but the following methods can be used to configure parsing:
setDelimiter(char)
- allows the character used as
the delimiter to be configured [default is a comma].
setAllowedChars(char[])
- adds additional characters
(to the default alphabetic/numeric) to those considered to be
valid token characters.
Multi Dimensional Arrays
It is possible to convert a String
to mulit-dimensional arrays by using ArrayConverter
as the element Converter
within another ArrayConverter
. For example, the following code demonstrates how to construct a Converter
to convert a delimited String
into a two dimensional integer array:
// Construct an Integer Converter
IntegerConverter integerConverter = new IntegerConverter();
// Construct an array Converter for an integer array (i.e. int[]) using
// an IntegerConverter as the element converter.
// N.B. Uses the default comma (i.e. ",") as the delimiter between individual numbers
ArrayConverter arrayConverter = new ArrayConverter(int[].class, integerConverter);
// Construct a "Matrix" Converter which converts arrays of integer arrays using
// the pre-ceeding ArrayConverter as the element Converter.
// N.B. Uses a semi-colon (i.e. ";") as the delimiter to separate the different sets of numbers.
// Also the delimiter used by the first ArrayConverter needs to be added to the
// "allowed characters" for this one.
ArrayConverter matrixConverter = new ArrayConverter(int[][].class, arrayConverter);
matrixConverter.setDelimiter(';');
matrixConverter.setAllowedChars(new char[] {','});
// Do the Conversion
String matrixString = "11,12,13 ; 21,22,23 ; 31,32,33 ; 41,42,43";
int[][] result = (int[][])matrixConverter.convert(int[][].class, matrixString);
Version: $Id$ Since: 1.8.0
/**
* Generic {@link Converter} implementation that handles conversion
* to and from <b>array</b> objects.
* <p>
* Can be configured to either return a <i>default value</i> or throw a
* <code>ConversionException</code> if a conversion error occurs.
* <p>
* The main features of this implementation are:
* <ul>
* <li><b>Element Conversion</b> - delegates to a {@link Converter},
* appropriate for the type, to convert individual elements
* of the array. This leverages the power of existing converters
* without having to replicate their functionality for converting
* to the element type and removes the need to create a specifc
* array type converters.</li>
* <li><b>Arrays or Collections</b> - can convert from either arrays or
* Collections to an array, limited only by the capability
* of the delegate {@link Converter}.</li>
* <li><b>Delimited Lists</b> - can Convert <b>to</b> and <b>from</b> a
* delimited list in String format.</li>
* <li><b>Conversion to String</b> - converts an array to a
* <code>String</code> in one of two ways: as a <i>delimited list</i>
* or by converting the first element in the array to a String - this
* is controlled by the {@link ArrayConverter#setOnlyFirstToString(boolean)}
* parameter.</li>
* <li><b>Multi Dimensional Arrays</b> - it is possible to convert a <code>String</code>
* to a multi-dimensional arrays, by embedding {@link ArrayConverter}
* within each other - see example below.</li>
* <li><b>Default Value</b></li>
* <ul>
* <li><b><i>No Default</b></i> - use the
* {@link ArrayConverter#ArrayConverter(Class, Converter)}
* constructor to create a converter which throws a
* {@link ConversionException} if the value is missing or
* invalid.</li>
* <li><b><i>Default values</b></i> - use the
* {@link ArrayConverter#ArrayConverter(Class, Converter, int)}
* constructor to create a converter which returns a <i>default
* value</i>. The <i>defaultSize</i> parameter controls the
* <i>default value</i> in the following way:</li>
* <ul>
* <li><i>defaultSize < 0</i> - default is <code>null</code></li>
* <li><i>defaultSize = 0</i> - default is an array of length zero</li>
* <li><i>defaultSize > 0</i> - default is an array with a
* length specified by <code>defaultSize</code> (N.B. elements
* in the array will be <code>null</code>)</li>
* </ul>
* </ul>
* </ul>
*
* <h3>Parsing Delimited Lists</h3>
* This implementation can convert a delimited list in <code>String</code> format
* into an array of the appropriate type. By default, it uses a comma as the delimiter
* but the following methods can be used to configure parsing:
* <ul>
* <li><code>setDelimiter(char)</code> - allows the character used as
* the delimiter to be configured [default is a comma].</li>
* <li><code>setAllowedChars(char[])</code> - adds additional characters
* (to the default alphabetic/numeric) to those considered to be
* valid token characters.
* </ul>
*
* <h3>Multi Dimensional Arrays</h3>
* It is possible to convert a <code>String</code> to mulit-dimensional arrays by using
* {@link ArrayConverter} as the element {@link Converter}
* within another {@link ArrayConverter}.
* <p>
* For example, the following code demonstrates how to construct a {@link Converter}
* to convert a delimited <code>String</code> into a two dimensional integer array:
* <p>
* <pre>
* // Construct an Integer Converter
* IntegerConverter integerConverter = new IntegerConverter();
*
* // Construct an array Converter for an integer array (i.e. int[]) using
* // an IntegerConverter as the element converter.
* // N.B. Uses the default comma (i.e. ",") as the delimiter between individual numbers
* ArrayConverter arrayConverter = new ArrayConverter(int[].class, integerConverter);
*
* // Construct a "Matrix" Converter which converts arrays of integer arrays using
* // the pre-ceeding ArrayConverter as the element Converter.
* // N.B. Uses a semi-colon (i.e. ";") as the delimiter to separate the different sets of numbers.
* // Also the delimiter used by the first ArrayConverter needs to be added to the
* // "allowed characters" for this one.
* ArrayConverter matrixConverter = new ArrayConverter(int[][].class, arrayConverter);
* matrixConverter.setDelimiter(';');
* matrixConverter.setAllowedChars(new char[] {','});
*
* // Do the Conversion
* String matrixString = "11,12,13 ; 21,22,23 ; 31,32,33 ; 41,42,43";
* int[][] result = (int[][])matrixConverter.convert(int[][].class, matrixString);
* </pre>
*
* @version $Id$
* @since 1.8.0
*/
public class ArrayConverter extends AbstractConverter {
private final Class<?> defaultType;
private final Converter elementConverter;
private int defaultSize;
private char delimiter = ',';
private char[] allowedChars = new char[] {'.', '-'};
private boolean onlyFirstToString = true;
// ----------------------------------------------------------- Constructors
Construct an array Converter
with the specified
component Converter
that throws a
ConversionException
if an error occurs.
Params: - defaultType – The default array type this
Converter
handles - elementConverter – Converter used to convert
individual array elements.
/**
* Construct an <b>array</b> <code>Converter</code> with the specified
* <b>component</b> <code>Converter</code> that throws a
* <code>ConversionException</code> if an error occurs.
*
* @param defaultType The default array type this
* <code>Converter</code> handles
* @param elementConverter Converter used to convert
* individual array elements.
*/
public ArrayConverter(final Class<?> defaultType, final Converter elementConverter) {
super();
if (defaultType == null) {
throw new IllegalArgumentException("Default type is missing");
}
if (!defaultType.isArray()) {
throw new IllegalArgumentException("Default type must be an array.");
}
if (elementConverter == null) {
throw new IllegalArgumentException("Component Converter is missing.");
}
this.defaultType = defaultType;
this.elementConverter = elementConverter;
}
Construct an array Converter
with the specified
component Converter
that returns a default
array of the specified size (or null
) if an error occurs.
Params: - defaultType – The default array type this
Converter
handles - elementConverter – Converter used to convert
individual array elements.
- defaultSize – Specifies the size of the default array value or if less
than zero indicates that a
null
default value should be used.
/**
* Construct an <b>array</b> <code>Converter</code> with the specified
* <b>component</b> <code>Converter</code> that returns a default
* array of the specified size (or <code>null</code>) if an error occurs.
*
* @param defaultType The default array type this
* <code>Converter</code> handles
* @param elementConverter Converter used to convert
* individual array elements.
* @param defaultSize Specifies the size of the default array value or if less
* than zero indicates that a <code>null</code> default value should be used.
*/
public ArrayConverter(final Class<?> defaultType, final Converter elementConverter, final int defaultSize) {
this(defaultType, elementConverter);
this.defaultSize = defaultSize;
Object defaultValue = null;
if (defaultSize >= 0) {
defaultValue = Array.newInstance(defaultType.getComponentType(), defaultSize);
}
setDefaultValue(defaultValue);
}
Set the delimiter to be used for parsing a delimited String.
Params: - delimiter – The delimiter [default ',']
/**
* Set the delimiter to be used for parsing a delimited String.
*
* @param delimiter The delimiter [default ',']
*/
public void setDelimiter(final char delimiter) {
this.delimiter = delimiter;
}
Set the allowed characters to be used for parsing a delimited String.
Params: - allowedChars – Characters which are to be considered as part of
the tokens when parsing a delimited String [default is '.' and '-']
/**
* Set the allowed characters to be used for parsing a delimited String.
*
* @param allowedChars Characters which are to be considered as part of
* the tokens when parsing a delimited String [default is '.' and '-']
*/
public void setAllowedChars(final char[] allowedChars) {
this.allowedChars = allowedChars;
}
Indicates whether converting to a String should create
a delimited list or just convert the first value.
Params: - onlyFirstToString –
true
converts only
the first value in the array to a String, false
converts all values in the array into a delimited list (default
is true
/**
* Indicates whether converting to a String should create
* a delimited list or just convert the first value.
*
* @param onlyFirstToString <code>true</code> converts only
* the first value in the array to a String, <code>false</code>
* converts all values in the array into a delimited list (default
* is <code>true</code>
*/
public void setOnlyFirstToString(final boolean onlyFirstToString) {
this.onlyFirstToString = onlyFirstToString;
}
Return the default type this Converter
handles.
Returns: The default type this Converter
handles.
/**
* Return the default type this <code>Converter</code> handles.
*
* @return The default type this <code>Converter</code> handles.
*/
@Override
protected Class<?> getDefaultType() {
return defaultType;
}
Handles conversion to a String.
Params: - value – The value to be converted.
Throws: - Throwable – if an error occurs converting to a String
Returns: the converted String value.
/**
* Handles conversion to a String.
*
* @param value The value to be converted.
* @return the converted String value.
* @throws Throwable if an error occurs converting to a String
*/
@Override
protected String convertToString(final Object value) throws Throwable {
int size = 0;
Iterator<?> iterator = null;
final Class<?> type = value.getClass();
if (type.isArray()) {
size = Array.getLength(value);
} else {
final Collection<?> collection = convertToCollection(type, value);
size = collection.size();
iterator = collection.iterator();
}
if (size == 0) {
return (String)getDefault(String.class);
}
if (onlyFirstToString) {
size = 1;
}
// Create a StringBuffer containing a delimited list of the values
final StringBuilder buffer = new StringBuilder();
for (int i = 0; i < size; i++) {
if (i > 0) {
buffer.append(delimiter);
}
Object element = iterator == null ? Array.get(value, i) : iterator.next();
element = elementConverter.convert(String.class, element);
if (element != null) {
buffer.append(element);
}
}
return buffer.toString();
}
Handles conversion to an array of the specified type.
Params: - type – The type to which this value should be converted.
- value – The input value to be converted.
Type parameters: - <T> – Target type of the conversion.
Throws: - Throwable – if an error occurs converting to the specified type
Returns: The converted value.
/**
* Handles conversion to an array of the specified type.
*
* @param <T> Target type of the conversion.
* @param type The type to which this value should be converted.
* @param value The input value to be converted.
* @return The converted value.
* @throws Throwable if an error occurs converting to the specified type
*/
@Override
protected <T> T convertToType(final Class<T> type, final Object value) throws Throwable {
if (!type.isArray()) {
throw new ConversionException(toString(getClass())
+ " cannot handle conversion to '"
+ toString(type) + "' (not an array).");
}
// Handle the source
int size = 0;
Iterator<?> iterator = null;
if (value.getClass().isArray()) {
size = Array.getLength(value);
} else {
final Collection<?> collection = convertToCollection(type, value);
size = collection.size();
iterator = collection.iterator();
}
// Allocate a new Array
final Class<?> componentType = type.getComponentType();
final Object newArray = Array.newInstance(componentType, size);
// Convert and set each element in the new Array
for (int i = 0; i < size; i++) {
Object element = iterator == null ? Array.get(value, i) : iterator.next();
// TODO - probably should catch conversion errors and throw
// new exception providing better info back to the user
element = elementConverter.convert(componentType, element);
Array.set(newArray, i, element);
}
@SuppressWarnings("unchecked")
final
// This is safe because T is an array type and newArray is an array of
// T's component type
T result = (T) newArray;
return result;
}
Returns the value unchanged.
Params: - value – The value to convert
Returns: The value unchanged
/**
* Returns the value unchanged.
*
* @param value The value to convert
* @return The value unchanged
*/
@Override
protected Object convertArray(final Object value) {
return value;
}
Converts non-array values to a Collection prior
to being converted either to an array or a String.
Collection
values are returned unchanged
Number
, Boolean
and Date
values returned as a the only element in a List.
- All other types are converted to a String and parsed
as a delimited list.
N.B. The method is called by both the convertToType(Class<Object>, Object)
and convertToString(Object)
methods for non-array types.
Params: - type – The type to convert the value to
- value – value to be converted
Returns: Collection elements.
/**
* Converts non-array values to a Collection prior
* to being converted either to an array or a String.
* </p>
* <ul>
* <li>{@link Collection} values are returned unchanged</li>
* <li>{@link Number}, {@link Boolean} and {@link java.util.Date}
* values returned as a the only element in a List.</li>
* <li>All other types are converted to a String and parsed
* as a delimited list.</li>
* </ul>
*
* <strong>N.B.</strong> The method is called by both the
* {@link ArrayConverter#convertToType(Class, Object)} and
* {@link ArrayConverter#convertToString(Object)} methods for
* <i>non-array</i> types.
*
* @param type The type to convert the value to
* @param value value to be converted
* @return Collection elements.
*/
protected Collection<?> convertToCollection(final Class<?> type, final Object value) {
if (value instanceof Collection) {
return (Collection<?>)value;
}
if (value instanceof Number ||
value instanceof Boolean ||
value instanceof java.util.Date) {
final List<Object> list = new ArrayList<Object>(1);
list.add(value);
return list;
}
return parseElements(type, value.toString());
}
Return the default value for conversions to the specified
type.
Params: - type – Data type to which this value should be converted.
Returns: The default value for the specified type.
/**
* Return the default value for conversions to the specified
* type.
* @param type Data type to which this value should be converted.
* @return The default value for the specified type.
*/
@Override
protected Object getDefault(final Class<?> type) {
if (type.equals(String.class)) {
return null;
}
final Object defaultValue = super.getDefault(type);
if (defaultValue == null) {
return null;
}
if (defaultValue.getClass().equals(type)) {
return defaultValue;
} else {
return Array.newInstance(type.getComponentType(), defaultSize);
}
}
Provide a String representation of this array converter.
Returns: A String representation of this array converter
/**
* Provide a String representation of this array converter.
*
* @return A String representation of this array converter
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder();
buffer.append(toString(getClass()));
buffer.append("[UseDefault=");
buffer.append(isUseDefault());
buffer.append(", ");
buffer.append(elementConverter.toString());
buffer.append(']');
return buffer.toString();
}
Parse an incoming String of the form similar to an array initializer
in the Java language into a List
individual Strings
for each element, according to the following rules.
- The string is expected to be a comma-separated list of values.
- The string may optionally have matching '{' and '}' delimiters
around the list.
- Whitespace before and after each element is stripped.
- Elements in the list may be delimited by single or double quotes.
Within a quoted elements, the normal Java escape sequences are valid.
Params: - type – The type to convert the value to
- value – String value to be parsed
Throws: - ConversionException – if the syntax of
svalue
is not syntactically valid - NullPointerException – if
svalue
is null
Returns: List of parsed elements.
/**
* <p>Parse an incoming String of the form similar to an array initializer
* in the Java language into a <code>List</code> individual Strings
* for each element, according to the following rules.</p>
* <ul>
* <li>The string is expected to be a comma-separated list of values.</li>
* <li>The string may optionally have matching '{' and '}' delimiters
* around the list.</li>
* <li>Whitespace before and after each element is stripped.</li>
* <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>
* </ul>
*
* @param type The type to convert the value to
* @param value String value to be parsed
* @return List of parsed elements.
*
* @throws ConversionException if the syntax of <code>svalue</code>
* is not syntactically valid
* @throws NullPointerException if <code>svalue</code>
* is <code>null</code>
*/
private List<String> parseElements(final Class<?> type, String value) {
if (log().isDebugEnabled()) {
log().debug("Parsing elements, delimiter=[" + delimiter + "], value=[" + value + "]");
}
// Trim any matching '{' and '}' delimiters
value = value.trim();
if (value.startsWith("{") && value.endsWith("}")) {
value = value.substring(1, value.length() - 1);
}
try {
// Set up a StreamTokenizer on the characters in this String
final StreamTokenizer st = new StreamTokenizer(new StringReader(value));
st.whitespaceChars(delimiter , delimiter); // Set the delimiters
st.ordinaryChars('0', '9'); // Needed to turn off numeric flag
st.wordChars('0', '9'); // Needed to make part of tokens
for (char allowedChar : allowedChars) {
st.ordinaryChars(allowedChar, allowedChar);
st.wordChars(allowedChar, allowedChar);
}
// Split comma-delimited tokens into a List
List<String> list = null;
while (true) {
final int ttype = st.nextToken();
if ((ttype == StreamTokenizer.TT_WORD) || (ttype > 0)) {
if (st.sval != null) {
if (list == null) {
list = new ArrayList<String>();
}
list.add(st.sval);
}
} else if (ttype == StreamTokenizer.TT_EOF) {
break;
} else {
throw new ConversionException("Encountered token of type "
+ ttype + " parsing elements to '" + toString(type) + ".");
}
}
if (list == null) {
list = Collections.emptyList();
}
if (log().isDebugEnabled()) {
log().debug(list.size() + " elements parsed");
}
// Return the completed list
return (list);
} catch (final IOException e) {
throw new ConversionException("Error converting from String to '"
+ toString(type) + "': " + e.getMessage(), e);
}
}
}