View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    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 &lt; 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 &gt; 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     // ----------------------------------------------------------- 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("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         // Create a StringBuffer containing a delimited list of the values
    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         // Handle the source
    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         // Allocate a new Array
    285         Class componentType = type.getComponentType();
    286         Object newArray = Array.newInstance(componentType, size);
    287
    288         // Convert and set each element in the new Array
    289         for (int i = 0; i < size; i++) {
    290             Object element = iterator == null ? Array.get(value, i) : iterator.next();
    291             // TODO - probably should catch conversion errors and throw
    292             //        new exception providing better info back to the user
    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         // Trim any matching '{' and '}' delimiters
    415         value = value.trim();
    416         if (value.startsWith("{") && value.endsWith("}")) {
    417             value = value.substring(1, value.length() - 1);
    418         }
    419
    420         try {
    421
    422             // Set up a StreamTokenizer on the characters in this String
    423             StreamTokenizer st = new StreamTokenizer(new StringReader(value));
    424             st.whitespaceChars(delimiter , delimiter); // Set the delimiters
    425             st.ordinaryChars('0', '9');  // Needed to turn off numeric flag
    426             st.wordChars('0', '9');      // Needed to make part of tokens
    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             // Split comma-delimited tokens into a List
    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             // Return the completed list
    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 }