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.beanutils2.converters; 18 19 import java.io.IOException; 20 import java.io.StreamTokenizer; 21 import java.io.StringReader; 22 import java.lang.reflect.Array; 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.Collections; 26 import java.util.Date; 27 import java.util.Iterator; 28 import java.util.List; 29 import java.util.Objects; 30 31 import org.apache.commons.beanutils2.ConversionException; 32 import org.apache.commons.beanutils2.Converter; 33 34 /** 35 * Generic {@link Converter} implementation that handles conversion to and from <strong>array</strong> objects. 36 * <p> 37 * Can be configured to either return a <em>default value</em> or throw a {@code ConversionException} if a conversion error occurs. 38 * <p> 39 * The main features of this implementation are: 40 * <ul> 41 * <li><strong>Element Conversion</strong> - delegates to a {@link Converter}, appropriate for the type, to convert individual elements of the array. This 42 * leverages the power of existing converters without having to replicate their functionality for converting to the element type and removes the need to create 43 * a specific array type converters.</li> 44 * <li><strong>Arrays or Collections</strong> - can convert from either arrays or Collections to an array, limited only by the capability of the delegate 45 * {@link Converter}.</li> 46 * <li><strong>Delimited Lists</strong> - can Convert <strong>to</strong> and <strong>from</strong> a delimited list in String format.</li> 47 * <li><strong>Conversion to String</strong> - converts an array to a {@code String} in one of two ways: as a <em>delimited list</em> or by converting the first 48 * element in the array to a String - this is controlled by the {@link ArrayConverter#setOnlyFirstToString(boolean)} parameter.</li> 49 * <li><strong>Multi Dimensional Arrays</strong> - it is possible to convert a {@code String} to a multi-dimensional arrays, by embedding {@link ArrayConverter} 50 * within each other - see example below.</li> 51 * <li><strong>Default Value</strong> 52 * <ul> 53 * <li><strong><em>No Default</em></strong> - use the {@link ArrayConverter#ArrayConverter(Class, Converter)} constructor to create a converter which throws a 54 * {@link ConversionException} if the value is missing or invalid.</li> 55 * <li><strong><em>Default values</em></strong> - use the {@link ArrayConverter#ArrayConverter(Class, Converter, int)} constructor to create a converter which 56 * returns a <em>default value</em>. The <em>defaultSize</em> parameter controls the <em>default value</em> in the following way: 57 * <ul> 58 * <li><em>defaultSize < 0</em> - default is {@code null}</li> 59 * <li><em>defaultSize = 0</em> - default is an array of length zero</li> 60 * <li><em>defaultSize > 0</em> - default is an array with a length specified by {@code defaultSize} (N.B. elements in the array will be {@code null})</li> 61 * </ul> 62 * </li> 63 * </ul> 64 * </li> 65 * </ul> 66 * 67 * <h2>Parsing Delimited Lists</h2> This implementation can convert a delimited list in {@code String} format into an array of the appropriate type. By default, 68 * it uses a comma as the delimiter but the following methods can be used to configure parsing: 69 * <ul> 70 * <li>{@code setDelimiter(char)} - allows the character used as the delimiter to be configured [default is a comma].</li> 71 * <li>{@code setAllowedChars(char[])} - adds additional characters (to the default alphabetic/numeric) to those considered to be valid token characters. 72 * </ul> 73 * 74 * <h2>Multi Dimensional Arrays</h2> It is possible to convert a {@code String} to multi-dimensional arrays by using {@link ArrayConverter} as the element 75 * {@link Converter} within another {@link ArrayConverter}. 76 * <p> 77 * For example, the following code demonstrates how to construct a {@link Converter} to convert a delimited {@code String} into a two dimensional integer array: 78 * </p> 79 * 80 * <pre> 81 * // Construct an Integer Converter 82 * IntegerConverter integerConverter = new IntegerConverter(); 83 * 84 * // Construct an array Converter for an integer array (i.e. int[]) using 85 * // an IntegerConverter as the element converter. 86 * // N.B. Uses the default comma (i.e. ",") as the delimiter between individual numbers 87 * ArrayConverter arrayConverter = new ArrayConverter(int[].class, integerConverter); 88 * 89 * // Construct a "Matrix" Converter which converts arrays of integer arrays using 90 * // the preceding ArrayConverter as the element Converter. 91 * // N.B. Uses a semicolon (i.e. ";") as the delimiter to separate the different sets of numbers. 92 * // Also the delimiter used by the first ArrayConverter needs to be added to the 93 * // "allowed characters" for this one. 94 * ArrayConverter matrixConverter = new ArrayConverter(int[][].class, arrayConverter); 95 * matrixConverter.setDelimiter(';'); 96 * matrixConverter.setAllowedChars(new char[] { ',' }); 97 * 98 * // Do the Conversion 99 * String matrixString = "11,12,13 ; 21,22,23 ; 31,32,33 ; 41,42,43"; 100 * int[][] result = (int[][]) matrixConverter.convert(int[][].class, matrixString); 101 * </pre> 102 * 103 * @param <C> The converter type. 104 * @since 1.8.0 105 */ 106 public class ArrayConverter<C> extends AbstractConverter<C> { 107 108 private final Class<C> defaultType; 109 private final Converter elementConverter; 110 private int defaultSize; 111 private char delimiter = ','; 112 private char[] allowedChars = { '.', '-' }; 113 private boolean onlyFirstToString = true; 114 115 /** 116 * Constructs an <strong>array</strong> {@code Converter} with the specified <strong>component</strong> {@code Converter} that throws a 117 * {@code ConversionException} if an error occurs. 118 * 119 * @param defaultType The default array type this {@code Converter} handles 120 * @param elementConverter Converter used to convert individual array elements. 121 */ 122 public ArrayConverter(final Class<C> defaultType, final Converter elementConverter) { 123 Objects.requireNonNull(defaultType, "defaultType"); 124 if (!defaultType.isArray()) { 125 throw new IllegalArgumentException("Default type must be an array."); 126 } 127 this.elementConverter = Objects.requireNonNull(elementConverter, "elementConverter"); 128 this.defaultType = defaultType; 129 } 130 131 /** 132 * Constructs an <strong>array</strong> {@code Converter} with the specified <strong>component</strong> {@code Converter} that returns a default array of 133 * the specified size (or {@code null}) if an error occurs. 134 * 135 * @param defaultType The default array type this {@code Converter} handles 136 * @param elementConverter Converter used to convert individual array elements. 137 * @param defaultSize Specifies the size of the default array value or if less than zero indicates that a {@code null} default value should be used. 138 */ 139 public ArrayConverter(final Class<C> defaultType, final Converter elementConverter, final int defaultSize) { 140 this(defaultType, elementConverter); 141 this.defaultSize = defaultSize; 142 C defaultValue = null; 143 if (defaultSize >= 0) { 144 defaultValue = (C) Array.newInstance(defaultType.getComponentType(), defaultSize); 145 } 146 setDefaultValue(defaultValue); 147 } 148 149 /** 150 * Returns the value unchanged. 151 * 152 * @param value The value to convert 153 * @return The value unchanged 154 */ 155 @Override 156 protected Object convertArray(final Object value) { 157 return value; 158 } 159 160 /** 161 * Converts non-array values to a Collection prior to being converted either to an array or a String. 162 * <ul> 163 * <li>{@link Collection} values are returned unchanged</li> 164 * <li>{@link Number}, {@link Boolean} and {@link java.util.Date} values returned as a the only element in a List.</li> 165 * <li>All other types are converted to a String and parsed as a delimited list.</li> 166 * </ul> 167 * 168 * <strong>N.B.</strong> The method is called by both the {@link ArrayConverter#convertToType(Class, Object)} and 169 * {@link ArrayConverter#convertToString(Object)} methods for <em>non-array</em> types. 170 * 171 * @param value value to be converted 172 * @return Collection elements. 173 */ 174 protected Collection<?> convertToCollection(final Object value) { 175 if (value instanceof Collection) { 176 return (Collection<?>) value; 177 } 178 if (value instanceof Number || value instanceof Boolean || value instanceof Date) { 179 final List<Object> list = new ArrayList<>(1); 180 list.add(value); 181 return list; 182 } 183 184 return parseElements(value.toString()); 185 } 186 187 /** 188 * Handles conversion to a String. 189 * 190 * @param value The value to be converted. 191 * @return the converted String value. 192 * @throws IllegalArgumentException if an error occurs converting to a String 193 */ 194 @Override 195 protected String convertToString(final Object value) { 196 int size = 0; 197 Iterator<?> iterator = null; 198 final Class<?> type = value.getClass(); 199 if (type.isArray()) { 200 size = Array.getLength(value); 201 } else { 202 final Collection<?> collection = convertToCollection(value); 203 size = collection.size(); 204 iterator = collection.iterator(); 205 } 206 207 if (size == 0) { 208 return (String) getDefault(String.class); 209 } 210 211 if (onlyFirstToString) { 212 size = 1; 213 } 214 215 // Create a StringBuilder containing a delimited list of the values 216 final StringBuilder buffer = new StringBuilder(); 217 for (int i = 0; i < size; i++) { 218 if (i > 0) { 219 buffer.append(delimiter); 220 } 221 Object element = iterator == null ? Array.get(value, i) : iterator.next(); 222 element = elementConverter.convert(String.class, element); 223 if (element != null) { 224 buffer.append(element); 225 } 226 } 227 228 return buffer.toString(); 229 } 230 231 /** 232 * Handles conversion to an array of the specified type. 233 * 234 * @param <T> Target type of the conversion. 235 * @param type The type to which this value should be converted. 236 * @param value The input value to be converted. 237 * @return The converted value. 238 * @throws Throwable if an error occurs converting to the specified type 239 */ 240 @Override 241 protected <T> T convertToType(final Class<T> type, final Object value) throws Throwable { 242 if (!type.isArray()) { 243 throw ConversionException.format("%s cannot handle conversion to '%s' (not an array).", toString(getClass()), toString(type)); 244 } 245 246 // Handle the source 247 int size = 0; 248 Iterator<?> iterator = null; 249 if (value.getClass().isArray()) { 250 size = Array.getLength(value); 251 } else { 252 final Collection<?> collection = convertToCollection(value); 253 size = collection.size(); 254 iterator = collection.iterator(); 255 } 256 257 // Allocate a new Array 258 final Class<?> componentType = type.getComponentType(); 259 final Object newArray = Array.newInstance(componentType, size); 260 261 // Convert and set each element in the new Array 262 for (int i = 0; i < size; i++) { 263 Object element = iterator == null ? Array.get(value, i) : iterator.next(); 264 // TODO - probably should catch conversion errors and throw 265 // new exception providing better info back to the user 266 element = elementConverter.convert(componentType, element); 267 Array.set(newArray, i, element); 268 } 269 // This is safe because T is an array type and newArray is an array of 270 // T's component type 271 return (T) newArray; 272 } 273 274 /** 275 * Gets the default value for conversions to the specified type. 276 * 277 * @param type Data type to which this value should be converted. 278 * @return The default value for the specified type. 279 */ 280 @Override 281 protected Object getDefault(final Class<?> type) { 282 if (type.equals(String.class)) { 283 return null; 284 } 285 286 final Object defaultValue = super.getDefault(type); 287 if (defaultValue == null) { 288 return null; 289 } 290 291 if (defaultValue.getClass().equals(type)) { 292 return defaultValue; 293 } 294 return Array.newInstance(type.getComponentType(), defaultSize); 295 } 296 297 /** 298 * Gets the default type this {@code Converter} handles. 299 * 300 * @return The default type this {@code Converter} handles. 301 */ 302 @Override 303 protected Class<C> getDefaultType() { 304 return defaultType; 305 } 306 307 /** 308 * <p> 309 * Parse an incoming String of the form similar to an array initializer in the Java language into a {@code List} individual Strings for each element, 310 * according to the following rules. 311 * </p> 312 * <ul> 313 * <li>The string is expected to be a comma-separated list of values.</li> 314 * <li>The string may optionally have matching '{' and '}' delimiters around the list.</li> 315 * <li>Whitespace before and after each element is stripped.</li> 316 * <li>Elements in the list may be delimited by single or double quotes. Within a quoted elements, the normal Java escape sequences are valid.</li> 317 * </ul> 318 * 319 * @param value String value to be parsed 320 * @return List of parsed elements. 321 * @throws ConversionException if the syntax of {@code value} is not syntactically valid 322 * @throws NullPointerException if {@code value} is {@code null} 323 */ 324 private List<String> parseElements(String value) { 325 if (log().isDebugEnabled()) { 326 log().debug("Parsing elements, delimiter=[" + delimiter + "], value=[" + value + "]"); 327 } 328 329 // Trim any matching '{' and '}' delimiters 330 value = toTrim(value); 331 if (value.startsWith("{") && value.endsWith("}")) { 332 value = value.substring(1, value.length() - 1); 333 } 334 335 final String typeName = toString(String.class); 336 try { 337 338 // Set up a StreamTokenizer on the characters in this String 339 final StreamTokenizer st = new StreamTokenizer(new StringReader(value)); 340 st.whitespaceChars(delimiter, delimiter); // Set the delimiters 341 st.ordinaryChars('0', '9'); // Needed to turn off numeric flag 342 st.wordChars('0', '9'); // Needed to make part of tokens 343 for (final char allowedChar : allowedChars) { 344 st.ordinaryChars(allowedChar, allowedChar); 345 st.wordChars(allowedChar, allowedChar); 346 } 347 348 // Split comma-delimited tokens into a List 349 List<String> list = null; 350 while (true) { 351 final int ttype = st.nextToken(); 352 if (ttype == StreamTokenizer.TT_WORD || ttype > 0) { 353 if (st.sval != null) { 354 if (list == null) { 355 list = new ArrayList<>(); 356 } 357 list.add(st.sval); 358 } 359 } else if (ttype == StreamTokenizer.TT_EOF) { 360 break; 361 } else { 362 throw ConversionException.format("Encountered token of type %s parsing elements to '%s'.", ttype, typeName); 363 } 364 } 365 366 if (list == null) { 367 list = Collections.emptyList(); 368 } 369 if (log().isDebugEnabled()) { 370 log().debug(list.size() + " elements parsed"); 371 } 372 373 // Return the completed list 374 return list; 375 376 } catch (final IOException e) { 377 throw new ConversionException("Error converting from String to '" + typeName + "': " + e.getMessage(), e); 378 } 379 } 380 381 /** 382 * Sets the allowed characters to be used for parsing a delimited String. 383 * 384 * @param allowedChars Characters which are to be considered as part of the tokens when parsing a delimited String [default is '.' and '-'] 385 */ 386 public void setAllowedChars(final char[] allowedChars) { 387 this.allowedChars = Objects.requireNonNull(allowedChars, "allowedChars").clone(); 388 } 389 390 /** 391 * Sets the delimiter to be used for parsing a delimited String. 392 * 393 * @param delimiter The delimiter [default ','] 394 */ 395 public void setDelimiter(final char delimiter) { 396 this.delimiter = delimiter; 397 } 398 399 /** 400 * Indicates whether converting to a String should create a delimited list or just convert the first value. 401 * 402 * @param onlyFirstToString {@code true} converts only the first value in the array to a String, {@code false} converts all values in the array into a 403 * delimited list (default is {@code true} 404 */ 405 public void setOnlyFirstToString(final boolean onlyFirstToString) { 406 this.onlyFirstToString = onlyFirstToString; 407 } 408 409 /** 410 * Provide a String representation of this array converter. 411 * 412 * @return A String representation of this array converter 413 */ 414 @Override 415 public String toString() { 416 final StringBuilder buffer = new StringBuilder(); 417 buffer.append(toString(getClass())); 418 buffer.append("[UseDefault="); 419 buffer.append(isUseDefault()); 420 buffer.append(", "); 421 buffer.append(elementConverter.toString()); 422 buffer.append(']'); 423 return buffer.toString(); 424 } 425 426 }