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    }