Coverage Report - org.apache.commons.beanutils.converters.ArrayConverter
 
Classes in this File Line Coverage Branch Coverage Complexity
ArrayConverter
94%
111/118
86%
57/66
4.692
 
 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: 640131 $ $Date: 2008-03-22 22:10:31 -0400 (Sat, 22 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  9736
     private char delimiter    = ',';
 133  9736
     private char[] allowedChars = new char[] {'.', '-'};
 134  9736
     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  9736
         super();
 150  9736
         if (defaultType == null) {
 151  1
             throw new IllegalArgumentException("Default type is missing");
 152  
         }
 153  9735
         if (!defaultType.isArray()) {
 154  1
             throw new IllegalArgumentException("Default type must be an array.");
 155  
         }
 156  9734
         if (elementConverter == null) {
 157  1
             throw new IllegalArgumentException("Component Converter is missing.");
 158  
         }
 159  9733
         this.defaultTypeInstance = Array.newInstance(defaultType.getComponentType(), 0);
 160  9733
         this.elementConverter = elementConverter;
 161  9733
     }
 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  9727
         this(defaultType, elementConverter);
 177  9727
         this.defaultSize = defaultSize;
 178  9727
         Object defaultValue = null;
 179  9727
         if (defaultSize >= 0) {
 180  9725
             defaultValue = Array.newInstance(defaultType.getComponentType(), defaultSize);
 181  
         }
 182  9727
         setDefaultValue(defaultValue);
 183  9727
     }
 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  2
         this.delimiter = delimiter;
 192  2
     }
 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  4
         this.allowedChars = allowedChars;
 202  4
     }
 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  1
         this.onlyFirstToString = onlyFirstToString;
 215  1
     }
 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  9725
         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  8
         int size = 0;
 236  8
         Iterator iterator = null;
 237  8
         Class type = value.getClass();
 238  8
         if (type.isArray()) {
 239  5
             size = Array.getLength(value);
 240  
         } else {
 241  3
             Collection collection = convertToCollection(type, value);
 242  3
             size = collection.size();
 243  3
             iterator = collection.iterator();
 244  
         }
 245  
 
 246  8
         if (size == 0) {
 247  0
             return (String)getDefault(String.class);
 248  
         }
 249  
 
 250  8
         if (onlyFirstToString) {
 251  6
             size = 1;
 252  
         }
 253  
 
 254  
         // Create a StringBuffer containing a delimited list of the values
 255  8
         StringBuffer buffer = new StringBuffer();
 256  22
         for (int i = 0; i < size; i++) {
 257  14
             if (i > 0) {
 258  6
                 buffer.append(delimiter);
 259  
             }
 260  14
             Object element = iterator == null ? Array.get(value, i) : iterator.next();
 261  14
             element = elementConverter.convert(String.class, element);
 262  14
             if (element != null) {
 263  14
                 buffer.append(element);
 264  
             }
 265  
         }
 266  
 
 267  8
         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  69
         if (!type.isArray()) {
 282  0
             throw new ConversionException(toString(getClass())
 283  
                     + " cannot handle conversion to '"
 284  
                     + toString(type) + "' (not an array).");
 285  
         }
 286  
 
 287  
         // Handle the source
 288  69
         int size = 0;
 289  69
         Iterator iterator = null;
 290  69
         if (value.getClass().isArray()) {
 291  14
             size = Array.getLength(value);
 292  
         } else {
 293  55
             Collection collection = convertToCollection(type, value);
 294  55
             size = collection.size();
 295  55
             iterator = collection.iterator();
 296  
         }
 297  
 
 298  
         // Allocate a new Array
 299  69
         Class componentType = type.getComponentType();
 300  69
         Object newArray = Array.newInstance(componentType, size);
 301  
 
 302  
         // Convert and set each element in the new Array
 303  217
         for (int i = 0; i < size; i++) {
 304  154
             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  154
             element = elementConverter.convert(componentType, element);
 308  148
             Array.set(newArray, i, element);
 309  
         }
 310  
 
 311  63
         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  9837
         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  58
         if (value instanceof Collection) {
 347  6
             return (Collection)value;
 348  
         }
 349  52
         if (value instanceof Number ||
 350  
             value instanceof Boolean ||
 351  
             value instanceof java.util.Date) {
 352  2
             List list = new ArrayList(1);
 353  2
             list.add(value);
 354  2
             return list;
 355  
         }
 356  
         
 357  50
         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  16
         if (type.equals(String.class)) {
 368  1
             return null;
 369  
         }
 370  
 
 371  15
         Object defaultValue = super.getDefault(type);
 372  15
         if (defaultValue == null) {
 373  1
             return null;
 374  
         }
 375  
 
 376  14
         if (defaultValue.getClass().equals(type)) {
 377  13
             return defaultValue;
 378  
         } else {
 379  1
             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  22
         StringBuffer buffer = new StringBuffer();
 391  22
         buffer.append(toString(getClass()));
 392  22
         buffer.append("[UseDefault=");
 393  22
         buffer.append(isUseDefault());
 394  22
         buffer.append(", ");
 395  22
         buffer.append(elementConverter.toString());
 396  22
         buffer.append(']');
 397  22
         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  50
         if (log().isDebugEnabled()) {
 425  0
             log().debug("Parsing elements, delimiter=[" + delimiter + "], value=[" + value + "]");
 426  
         }
 427  
 
 428  
         // Trim any matching '{' and '}' delimiters
 429  50
         value = value.trim();
 430  50
         if (value.startsWith("{") && value.endsWith("}")) {
 431  18
             value = value.substring(1, value.length() - 1);
 432  
         }
 433  
 
 434  
         try {
 435  
 
 436  
             // Set up a StreamTokenizer on the characters in this String
 437  50
             StreamTokenizer st = new StreamTokenizer(new StringReader(value));
 438  50
             st.whitespaceChars(delimiter , delimiter); // Set the delimiters
 439  50
             st.ordinaryChars('0', '9');  // Needed to turn off numeric flag
 440  50
             st.wordChars('0', '9');      // Needed to make part of tokens
 441  151
             for (int i = 0; i < allowedChars.length; i++) {
 442  101
                 st.ordinaryChars(allowedChars[i], allowedChars[i]);
 443  101
                 st.wordChars(allowedChars[i], allowedChars[i]);
 444  
             }
 445  
 
 446  
             // Split comma-delimited tokens into a List
 447  50
             List list = null;
 448  
             while (true) {
 449  150
                 int ttype = st.nextToken();
 450  150
                 if ((ttype == StreamTokenizer.TT_WORD) || (ttype > 0)) {
 451  100
                     if (st.sval != null) {
 452  96
                         if (list == null) {
 453  44
                             list = new ArrayList();
 454  
                         }
 455  96
                         list.add(st.sval);
 456  
                     }
 457  50
                 } else if (ttype == StreamTokenizer.TT_EOF) {
 458  50
                     break;
 459  
                 } else {
 460  0
                     throw new ConversionException("Encountered token of type "
 461  
                         + ttype + " parsing elements to '" + toString(type) + ".");
 462  
                 }
 463  100
             }
 464  
 
 465  50
             if (list == null) {
 466  6
                 list = Collections.EMPTY_LIST;
 467  
             }
 468  50
             if (log().isDebugEnabled()) {
 469  0
                 log().debug(list.size() + " elements parsed");
 470  
             }
 471  
 
 472  
             // Return the completed list
 473  50
             return (list);
 474  
 
 475  0
         } catch (IOException e) {
 476  
 
 477  0
             throw new ConversionException("Error converting from String to '"
 478  
                     + toString(type) + "': " + e.getMessage(), e);
 479  
 
 480  
         }
 481  
 
 482  
     }
 483  
 
 484  
 }