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 < 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 > 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 }