| /* |
| * 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.beanutils2.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.beanutils2.ConversionException; |
| import org.apache.commons.beanutils2.Converter; |
| |
| /** |
| * 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} 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 specific |
| * 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} 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} |
| * to a multi-dimensional arrays, by embedding {@link ArrayConverter} |
| * within each other - see example below.</li> |
| * <li><b>Default Value</b> |
| * <ul> |
| * <li><b><i>No Default</i></b> - 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</i></b> - 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: |
| * <ul> |
| * <li><i>defaultSize < 0</i> - default is {@code null}</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} (N.B. elements |
| * in the array will be {@code null})</li> |
| * </ul> |
| * </li> |
| * </ul> |
| * </li> |
| * </ul> |
| * |
| * <h3>Parsing Delimited Lists</h3> |
| * This implementation can convert a delimited list in {@code 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: |
| * <ul> |
| * <li>{@code setDelimiter(char)} - allows the character used as |
| * the delimiter to be configured [default is a comma].</li> |
| * <li>{@code setAllowedChars(char[])} - 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} 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} 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 preceding 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> |
| * |
| * @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; |
| |
| |
| |
| /** |
| * Construct an <b>array</b> {@code Converter} with the specified |
| * <b>component</b> {@code Converter} that throws a |
| * {@code ConversionException} if an error occurs. |
| * |
| * @param defaultType The default array type this |
| * {@code Converter} 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 <b>array</b> {@code Converter} with the specified |
| * <b>component</b> {@code Converter} that returns a default |
| * array of the specified size (or {@code null}) if an error occurs. |
| * |
| * @param defaultType The default array type this |
| * {@code Converter} 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} 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. |
| * |
| * @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. |
| * |
| * @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. |
| * |
| * @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 delimited list (default |
| * is {@code true} |
| */ |
| public void setOnlyFirstToString(final boolean onlyFirstToString) { |
| this.onlyFirstToString = onlyFirstToString; |
| } |
| |
| /** |
| * Return the default type this {@code Converter} handles. |
| * |
| * @return The default type this {@code Converter} handles. |
| */ |
| @Override |
| protected Class<?> getDefaultType() { |
| return defaultType; |
| } |
| |
| /** |
| * 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. |
| * |
| * @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. |
| * |
| * @param value The value to convert |
| * @return The value unchanged |
| */ |
| @Override |
| protected Object convertArray(final Object value) { |
| return value; |
| } |
| |
| /** |
| * <p> |
| * Converts non-array values to a Collection prior |
| * to being converted either to an array or a String. |
| * <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<>(1); |
| list.add(value); |
| return list; |
| } |
| |
| return parseElements(type, value.toString()); |
| } |
| |
| /** |
| * 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; |
| } |
| return Array.newInstance(type.getComponentType(), defaultSize); |
| |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| /** |
| * <p>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, 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 value} |
| * is not syntactically valid |
| * @throws NullPointerException if {@code value} |
| * is {@code null} |
| */ |
| 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 (final 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<>(); |
| } |
| 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); |
| |
| } |
| |
| } |
| |
| } |