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 */ 017package org.apache.commons.beanutils.converters; 018 019import java.io.IOException; 020import java.io.StreamTokenizer; 021import java.io.StringReader; 022import java.lang.reflect.Array; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.Iterator; 027import java.util.List; 028 029import org.apache.commons.beanutils.ConversionException; 030import org.apache.commons.beanutils.Converter; 031 032/** 033 * Generic {@link Converter} implementation that handles conversion 034 * to and from <b>array</b> objects. 035 * <p> 036 * Can be configured to either return a <i>default value</i> or throw a 037 * <code>ConversionException</code> if a conversion error occurs. 038 * <p> 039 * The main features of this implementation are: 040 * <ul> 041 * <li><b>Element Conversion</b> - delegates to a {@link Converter}, 042 * appropriate for the type, to convert individual elements 043 * of the array. This leverages the power of existing converters 044 * without having to replicate their functionality for converting 045 * to the element type and removes the need to create a specifc 046 * array type converters.</li> 047 * <li><b>Arrays or Collections</b> - can convert from either arrays or 048 * Collections to an array, limited only by the capability 049 * of the delegate {@link Converter}.</li> 050 * <li><b>Delimited Lists</b> - can Convert <b>to</b> and <b>from</b> a 051 * delimited list in String format.</li> 052 * <li><b>Conversion to String</b> - converts an array to a 053 * <code>String</code> in one of two ways: as a <i>delimited list</i> 054 * or by converting the first element in the array to a String - this 055 * is controlled by the {@link ArrayConverter#setOnlyFirstToString(boolean)} 056 * parameter.</li> 057 * <li><b>Multi Dimensional Arrays</b> - it is possible to convert a <code>String</code> 058 * to a multi-dimensional arrays, by embedding {@link ArrayConverter} 059 * within each other - see example below.</li> 060 * <li><b>Default Value</b> 061 * <ul> 062 * <li><b><i>No Default</i></b> - use the 063 * {@link ArrayConverter#ArrayConverter(Class, Converter)} 064 * constructor to create a converter which throws a 065 * {@link ConversionException} if the value is missing or 066 * invalid.</li> 067 * <li><b><i>Default values</i></b> - use the 068 * {@link ArrayConverter#ArrayConverter(Class, Converter, int)} 069 * constructor to create a converter which returns a <i>default 070 * value</i>. The <i>defaultSize</i> parameter controls the 071 * <i>default value</i> in the following way: 072 * <ul> 073 * <li><i>defaultSize < 0</i> - default is <code>null</code></li> 074 * <li><i>defaultSize = 0</i> - default is an array of length zero</li> 075 * <li><i>defaultSize > 0</i> - default is an array with a 076 * length specified by <code>defaultSize</code> (N.B. elements 077 * in the array will be <code>null</code>)</li> 078 * </ul> 079 * </li> 080 * </ul> 081 * </li> 082 * </ul> 083 * 084 * <h3>Parsing Delimited Lists</h3> 085 * This implementation can convert a delimited list in <code>String</code> format 086 * into an array of the appropriate type. By default, it uses a comma as the delimiter 087 * but the following methods can be used to configure parsing: 088 * <ul> 089 * <li><code>setDelimiter(char)</code> - allows the character used as 090 * the delimiter to be configured [default is a comma].</li> 091 * <li><code>setAllowedChars(char[])</code> - adds additional characters 092 * (to the default alphabetic/numeric) to those considered to be 093 * valid token characters. 094 * </ul> 095 * 096 * <h3>Multi Dimensional Arrays</h3> 097 * It is possible to convert a <code>String</code> to mulit-dimensional arrays by using 098 * {@link ArrayConverter} as the element {@link Converter} 099 * within another {@link ArrayConverter}. 100 * <p> 101 * For example, the following code demonstrates how to construct a {@link Converter} 102 * to convert a delimited <code>String</code> into a two dimensional integer array: 103 * </p> 104 * <pre> 105 * // Construct an Integer Converter 106 * IntegerConverter integerConverter = new IntegerConverter(); 107 * 108 * // Construct an array Converter for an integer array (i.e. int[]) using 109 * // an IntegerConverter as the element converter. 110 * // N.B. Uses the default comma (i.e. ",") as the delimiter between individual numbers 111 * ArrayConverter arrayConverter = new ArrayConverter(int[].class, integerConverter); 112 * 113 * // Construct a "Matrix" Converter which converts arrays of integer arrays using 114 * // the pre-ceeding ArrayConverter as the element Converter. 115 * // N.B. Uses a semi-colon (i.e. ";") as the delimiter to separate the different sets of numbers. 116 * // Also the delimiter used by the first ArrayConverter needs to be added to the 117 * // "allowed characters" for this one. 118 * ArrayConverter matrixConverter = new ArrayConverter(int[][].class, arrayConverter); 119 * matrixConverter.setDelimiter(';'); 120 * matrixConverter.setAllowedChars(new char[] {','}); 121 * 122 * // Do the Conversion 123 * String matrixString = "11,12,13 ; 21,22,23 ; 31,32,33 ; 41,42,43"; 124 * int[][] result = (int[][])matrixConverter.convert(int[][].class, matrixString); 125 * </pre> 126 * 127 * @version $Id$ 128 * @since 1.8.0 129 */ 130public class ArrayConverter extends AbstractConverter { 131 132 private final Class<?> defaultType; 133 private final Converter elementConverter; 134 private int defaultSize; 135 private char delimiter = ','; 136 private char[] allowedChars = new char[] {'.', '-'}; 137 private boolean onlyFirstToString = true; 138 139 // ----------------------------------------------------------- Constructors 140 141 /** 142 * Construct an <b>array</b> <code>Converter</code> with the specified 143 * <b>component</b> <code>Converter</code> that throws a 144 * <code>ConversionException</code> if an error occurs. 145 * 146 * @param defaultType The default array type this 147 * <code>Converter</code> handles 148 * @param elementConverter Converter used to convert 149 * individual array elements. 150 */ 151 public ArrayConverter(final Class<?> defaultType, final Converter elementConverter) { 152 super(); 153 if (defaultType == null) { 154 throw new IllegalArgumentException("Default type is missing"); 155 } 156 if (!defaultType.isArray()) { 157 throw new IllegalArgumentException("Default type must be an array."); 158 } 159 if (elementConverter == null) { 160 throw new IllegalArgumentException("Component Converter is missing."); 161 } 162 this.defaultType = defaultType; 163 this.elementConverter = elementConverter; 164 } 165 166 /** 167 * Construct an <b>array</b> <code>Converter</code> with the specified 168 * <b>component</b> <code>Converter</code> that returns a default 169 * array of the specified size (or <code>null</code>) if an error occurs. 170 * 171 * @param defaultType The default array type this 172 * <code>Converter</code> handles 173 * @param elementConverter Converter used to convert 174 * individual array elements. 175 * @param defaultSize Specifies the size of the default array value or if less 176 * than zero indicates that a <code>null</code> default value should be used. 177 */ 178 public ArrayConverter(final Class<?> defaultType, final Converter elementConverter, final int defaultSize) { 179 this(defaultType, elementConverter); 180 this.defaultSize = defaultSize; 181 Object defaultValue = null; 182 if (defaultSize >= 0) { 183 defaultValue = Array.newInstance(defaultType.getComponentType(), defaultSize); 184 } 185 setDefaultValue(defaultValue); 186 } 187 188 /** 189 * Set the delimiter to be used for parsing a delimited String. 190 * 191 * @param delimiter The delimiter [default ','] 192 */ 193 public void setDelimiter(final char delimiter) { 194 this.delimiter = delimiter; 195 } 196 197 /** 198 * Set the allowed characters to be used for parsing a delimited String. 199 * 200 * @param allowedChars Characters which are to be considered as part of 201 * the tokens when parsing a delimited String [default is '.' and '-'] 202 */ 203 public void setAllowedChars(final char[] allowedChars) { 204 this.allowedChars = allowedChars; 205 } 206 207 /** 208 * Indicates whether converting to a String should create 209 * a delimited list or just convert the first value. 210 * 211 * @param onlyFirstToString <code>true</code> converts only 212 * the first value in the array to a String, <code>false</code> 213 * converts all values in the array into a delimited list (default 214 * is <code>true</code> 215 */ 216 public void setOnlyFirstToString(final boolean onlyFirstToString) { 217 this.onlyFirstToString = onlyFirstToString; 218 } 219 220 /** 221 * Return the default type this <code>Converter</code> handles. 222 * 223 * @return The default type this <code>Converter</code> handles. 224 */ 225 @Override 226 protected Class<?> getDefaultType() { 227 return defaultType; 228 } 229 230 /** 231 * Handles conversion to a String. 232 * 233 * @param value The value to be converted. 234 * @return the converted String value. 235 * @throws Throwable if an error occurs converting to a String 236 */ 237 @Override 238 protected String convertToString(final Object value) throws Throwable { 239 240 int size = 0; 241 Iterator<?> iterator = null; 242 final Class<?> type = value.getClass(); 243 if (type.isArray()) { 244 size = Array.getLength(value); 245 } else { 246 final Collection<?> collection = convertToCollection(type, value); 247 size = collection.size(); 248 iterator = collection.iterator(); 249 } 250 251 if (size == 0) { 252 return (String)getDefault(String.class); 253 } 254 255 if (onlyFirstToString) { 256 size = 1; 257 } 258 259 // Create a StringBuffer containing a delimited list of the values 260 final StringBuilder buffer = new StringBuilder(); 261 for (int i = 0; i < size; i++) { 262 if (i > 0) { 263 buffer.append(delimiter); 264 } 265 Object element = iterator == null ? Array.get(value, i) : iterator.next(); 266 element = elementConverter.convert(String.class, element); 267 if (element != null) { 268 buffer.append(element); 269 } 270 } 271 272 return buffer.toString(); 273 274 } 275 276 /** 277 * Handles conversion to an array of the specified type. 278 * 279 * @param <T> Target type of the conversion. 280 * @param type The type to which this value should be converted. 281 * @param value The input value to be converted. 282 * @return The converted value. 283 * @throws Throwable if an error occurs converting to the specified type 284 */ 285 @Override 286 protected <T> T convertToType(final Class<T> type, final Object value) throws Throwable { 287 288 if (!type.isArray()) { 289 throw new ConversionException(toString(getClass()) 290 + " cannot handle conversion to '" 291 + toString(type) + "' (not an array)."); 292 } 293 294 // Handle the source 295 int size = 0; 296 Iterator<?> iterator = null; 297 if (value.getClass().isArray()) { 298 size = Array.getLength(value); 299 } else { 300 final Collection<?> collection = convertToCollection(type, value); 301 size = collection.size(); 302 iterator = collection.iterator(); 303 } 304 305 // Allocate a new Array 306 final Class<?> componentType = type.getComponentType(); 307 final Object newArray = Array.newInstance(componentType, size); 308 309 // Convert and set each element in the new Array 310 for (int i = 0; i < size; i++) { 311 Object element = iterator == null ? Array.get(value, i) : iterator.next(); 312 // TODO - probably should catch conversion errors and throw 313 // new exception providing better info back to the user 314 element = elementConverter.convert(componentType, element); 315 Array.set(newArray, i, element); 316 } 317 318 @SuppressWarnings("unchecked") 319 final 320 // This is safe because T is an array type and newArray is an array of 321 // T's component type 322 T result = (T) newArray; 323 return result; 324 } 325 326 /** 327 * Returns the value unchanged. 328 * 329 * @param value The value to convert 330 * @return The value unchanged 331 */ 332 @Override 333 protected Object convertArray(final Object value) { 334 return value; 335 } 336 337 /** 338 * <p> 339 * Converts non-array values to a Collection prior 340 * to being converted either to an array or a String. 341 * <ul> 342 * <li>{@link Collection} values are returned unchanged</li> 343 * <li>{@link Number}, {@link Boolean} and {@link java.util.Date} 344 * values returned as a the only element in a List.</li> 345 * <li>All other types are converted to a String and parsed 346 * as a delimited list.</li> 347 * </ul> 348 * 349 * <strong>N.B.</strong> The method is called by both the 350 * {@link ArrayConverter#convertToType(Class, Object)} and 351 * {@link ArrayConverter#convertToString(Object)} methods for 352 * <i>non-array</i> types. 353 * 354 * @param type The type to convert the value to 355 * @param value value to be converted 356 * @return Collection elements. 357 */ 358 protected Collection<?> convertToCollection(final Class<?> type, final Object value) { 359 if (value instanceof Collection) { 360 return (Collection<?>)value; 361 } 362 if (value instanceof Number || 363 value instanceof Boolean || 364 value instanceof java.util.Date) { 365 final List<Object> list = new ArrayList<Object>(1); 366 list.add(value); 367 return list; 368 } 369 370 return parseElements(type, value.toString()); 371 } 372 373 /** 374 * Return the default value for conversions to the specified 375 * type. 376 * @param type Data type to which this value should be converted. 377 * @return The default value for the specified type. 378 */ 379 @Override 380 protected Object getDefault(final Class<?> type) { 381 if (type.equals(String.class)) { 382 return null; 383 } 384 385 final Object defaultValue = super.getDefault(type); 386 if (defaultValue == null) { 387 return null; 388 } 389 390 if (defaultValue.getClass().equals(type)) { 391 return defaultValue; 392 } else { 393 return Array.newInstance(type.getComponentType(), defaultSize); 394 } 395 396 } 397 398 /** 399 * Provide a String representation of this array converter. 400 * 401 * @return A String representation of this array converter 402 */ 403 @Override 404 public String toString() { 405 final StringBuilder buffer = new StringBuilder(); 406 buffer.append(toString(getClass())); 407 buffer.append("[UseDefault="); 408 buffer.append(isUseDefault()); 409 buffer.append(", "); 410 buffer.append(elementConverter.toString()); 411 buffer.append(']'); 412 return buffer.toString(); 413 } 414 415 /** 416 * <p>Parse an incoming String of the form similar to an array initializer 417 * in the Java language into a <code>List</code> individual Strings 418 * for each element, according to the following rules.</p> 419 * <ul> 420 * <li>The string is expected to be a comma-separated list of values.</li> 421 * <li>The string may optionally have matching '{' and '}' delimiters 422 * around the list.</li> 423 * <li>Whitespace before and after each element is stripped.</li> 424 * <li>Elements in the list may be delimited by single or double quotes. 425 * Within a quoted elements, the normal Java escape sequences are valid.</li> 426 * </ul> 427 * 428 * @param type The type to convert the value to 429 * @param value String value to be parsed 430 * @return List of parsed elements. 431 * 432 * @throws ConversionException if the syntax of <code>svalue</code> 433 * is not syntactically valid 434 * @throws NullPointerException if <code>svalue</code> 435 * is <code>null</code> 436 */ 437 private List<String> parseElements(final Class<?> type, String value) { 438 439 if (log().isDebugEnabled()) { 440 log().debug("Parsing elements, delimiter=[" + delimiter + "], value=[" + value + "]"); 441 } 442 443 // Trim any matching '{' and '}' delimiters 444 value = value.trim(); 445 if (value.startsWith("{") && value.endsWith("}")) { 446 value = value.substring(1, value.length() - 1); 447 } 448 449 try { 450 451 // Set up a StreamTokenizer on the characters in this String 452 final StreamTokenizer st = new StreamTokenizer(new StringReader(value)); 453 st.whitespaceChars(delimiter , delimiter); // Set the delimiters 454 st.ordinaryChars('0', '9'); // Needed to turn off numeric flag 455 st.wordChars('0', '9'); // Needed to make part of tokens 456 for (char allowedChar : allowedChars) { 457 st.ordinaryChars(allowedChar, allowedChar); 458 st.wordChars(allowedChar, allowedChar); 459 } 460 461 // Split comma-delimited tokens into a List 462 List<String> list = null; 463 while (true) { 464 final int ttype = st.nextToken(); 465 if ((ttype == StreamTokenizer.TT_WORD) || (ttype > 0)) { 466 if (st.sval != null) { 467 if (list == null) { 468 list = new ArrayList<String>(); 469 } 470 list.add(st.sval); 471 } 472 } else if (ttype == StreamTokenizer.TT_EOF) { 473 break; 474 } else { 475 throw new ConversionException("Encountered token of type " 476 + ttype + " parsing elements to '" + toString(type) + "."); 477 } 478 } 479 480 if (list == null) { 481 list = Collections.emptyList(); 482 } 483 if (log().isDebugEnabled()) { 484 log().debug(list.size() + " elements parsed"); 485 } 486 487 // Return the completed list 488 return (list); 489 490 } catch (final IOException e) { 491 492 throw new ConversionException("Error converting from String to '" 493 + toString(type) + "': " + e.getMessage(), e); 494 495 } 496 497 } 498 499}