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 * https://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} (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 * // 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 * // 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 }