    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 &lt; 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 &gt; 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    }