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