View Javadoc
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.math.BigDecimal;
20  import java.math.BigInteger;
21  import java.text.DecimalFormat;
22  import java.text.DecimalFormatSymbols;
23  import java.text.NumberFormat;
24  import java.text.ParsePosition;
25  import java.util.Calendar;
26  import java.util.Date;
27  import java.util.Locale;
28  
29  import org.apache.commons.beanutils2.ConversionException;
30  
31  /**
32   * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from <strong>java.lang.Number</strong> objects.
33   * <p>
34   * This implementation handles conversion for the following {@link Number} types.
35   * <ul>
36   * <li>{@link Byte}</li>
37   * <li>{@link Short}</li>
38   * <li>{@link Integer}</li>
39   * <li>{@link Long}</li>
40   * <li>{@link Float}</li>
41   * <li>{@link Double}</li>
42   * <li>{@link java.math.BigDecimal}</li>
43   * <li>{@link java.math.BigInteger}</li>
44   * </ul>
45   *
46   * <h2>String Conversions (to and from)</h2> This class provides a number of ways in which number conversions to/from Strings can be achieved:
47   * <ul>
48   * <li>Using the default format for the default Locale, configure using:
49   * <ul>
50   * <li>{@code setUseLocaleFormat(true)}</li>
51   * </ul>
52   * </li>
53   * <li>Using the default format for a specified Locale, configure using:
54   * <ul>
55   * <li>{@code setLocale(Locale)}</li>
56   * </ul>
57   * </li>
58   * <li>Using a specified pattern for the default Locale, configure using:
59   * <ul>
60   * <li>{@code setPattern(String)}</li>
61   * </ul>
62   * </li>
63   * <li>Using a specified pattern for a specified Locale, configure using:
64   * <ul>
65   * <li>{@code setPattern(String)}</li>
66   * <li>{@code setLocale(Locale)}</li>
67   * </ul>
68   * </li>
69   * <li>If none of the above are configured the {@code toNumber(String)} method is used to convert from String to Number and the Number's {@code toString()}
70   * method used to convert from Number to String.</li>
71   * </ul>
72   *
73   * <p>
74   * <strong>N.B.</strong>Patterns can only be specified using the <em>standard</em> pattern characters and NOT in <em>localized</em> form (see
75   * {@link java.text.DecimalFormat}). For example to cater for number styles used in Germany such as {@code 0.000,00} the pattern is specified in the normal form
76   * {@code 0,000.00</code> and the locale set to <code>Locale.GERMANY}
77   *
78   * @param <N> The default value type.
79   * @since 1.8.0
80   */
81  public abstract class NumberConverter<N extends Number> extends AbstractConverter<N> {
82  
83      private static final Integer ZERO = Integer.valueOf(0);
84      private static final Integer ONE = Integer.valueOf(1);
85  
86      private String pattern;
87      private final boolean allowDecimals;
88      private boolean useLocaleFormat;
89      private Locale locale;
90  
91      /**
92       * Constructs a <strong>java.lang.Number</strong> <em>Converter</em> that throws a {@code ConversionException} if a error occurs.
93       *
94       * @param allowDecimals Indicates whether decimals are allowed
95       */
96      public NumberConverter(final boolean allowDecimals) {
97          this.allowDecimals = allowDecimals;
98      }
99  
100     /**
101      * Constructs a {@link Number} <em>Converter</em> that returns a default value if an error occurs.
102      *
103      * @param allowDecimals Indicates whether decimals are allowed
104      * @param defaultValue  The default value to be returned
105      */
106     public NumberConverter(final boolean allowDecimals, final N defaultValue) {
107         this.allowDecimals = allowDecimals;
108         setDefaultValue(defaultValue);
109     }
110 
111     /**
112      * Convert an input Number object into a String.
113      *
114      * @param value The input value to be converted
115      * @return the converted String value.
116      * @throws IllegalArgumentException if an error occurs converting to a String
117      */
118     @Override
119     protected String convertToString(final Object value) {
120 
121         String result = null;
122         if (useLocaleFormat && value instanceof Number) {
123             final NumberFormat format = getFormat();
124             format.setGroupingUsed(false);
125             result = format.format(value);
126             if (log().isDebugEnabled()) {
127                 log().debug("    Converted  to String using format '" + result + "'");
128             }
129 
130         } else {
131             result = value.toString();
132             if (log().isDebugEnabled()) {
133                 log().debug("    Converted  to String using toString() '" + result + "'");
134             }
135         }
136         return result;
137 
138     }
139 
140     /**
141      * Convert the input object into a Number object of the specified type.
142      *
143      * @param <T>        Target type of the conversion.
144      * @param targetType Data type to which this value should be converted.
145      * @param value      The input value to be converted.
146      * @return The converted value.
147      * @throws Throwable if an error occurs converting to the specified type
148      */
149     @Override
150     protected <T> T convertToType(final Class<T> targetType, final Object value) throws Throwable {
151         final Class<?> sourceType = value.getClass();
152         // Handle Number
153         if (value instanceof Number) {
154             return toNumber(sourceType, targetType, (Number) value);
155         }
156 
157         // Handle Boolean
158         if (value instanceof Boolean) {
159             return toNumber(sourceType, targetType, ((Boolean) value).booleanValue() ? ONE : ZERO);
160         }
161 
162         // Handle Date --> Long
163         if (value instanceof Date && Long.class.equals(targetType)) {
164             return targetType.cast(Long.valueOf(((Date) value).getTime()));
165         }
166 
167         // Handle Calendar --> Long
168         if (value instanceof Calendar && Long.class.equals(targetType)) {
169             return targetType.cast(Long.valueOf(((Calendar) value).getTime().getTime()));
170         }
171 
172         // Convert all other types to String & handle
173         final String stringValue = toTrim(value);
174         if (stringValue.isEmpty()) {
175             return handleMissing(targetType);
176         }
177 
178         // Convert/Parse a String
179         Number number = null;
180         if (useLocaleFormat) {
181             final NumberFormat format = getFormat();
182             number = parse(sourceType, targetType, stringValue, format);
183         } else {
184             if (log().isDebugEnabled()) {
185                 log().debug("    No NumberFormat, using default conversion");
186             }
187             number = toNumber(sourceType, targetType, stringValue);
188         }
189 
190         // Ensure the correct number type is returned
191         return toNumber(sourceType, targetType, number);
192     }
193 
194     /**
195      * Gets a NumberFormat to use for Conversion.
196      *
197      * @return The NumberFormat.
198      */
199     private NumberFormat getFormat() {
200         NumberFormat format = null;
201         if (pattern != null) {
202             if (locale == null) {
203                 if (log().isDebugEnabled()) {
204                     log().debug("    Using pattern '" + pattern + "'");
205                 }
206                 format = new DecimalFormat(pattern);
207             } else {
208                 if (log().isDebugEnabled()) {
209                     log().debug("    Using pattern '" + pattern + "'" + " with Locale[" + locale + "]");
210                 }
211                 final DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
212                 format = new DecimalFormat(pattern, symbols);
213             }
214         } else if (locale == null) {
215             if (log().isDebugEnabled()) {
216                 log().debug("    Using default Locale format");
217             }
218             format = NumberFormat.getInstance();
219         } else {
220             if (log().isDebugEnabled()) {
221                 log().debug("    Using Locale[" + locale + "] format");
222             }
223             format = NumberFormat.getInstance(locale);
224         }
225         if (!allowDecimals) {
226             format.setParseIntegerOnly(true);
227         }
228         return format;
229     }
230 
231     /**
232      * Gets the Locale for the <em>Converter</em> (or {@code null} if none specified).
233      *
234      * @return The locale to use for conversion
235      */
236     public Locale getLocale() {
237         return locale;
238     }
239 
240     /**
241      * Gets the number format pattern used to convert Numbers to/from a {@link String} (or {@code null} if none specified).
242      * <p>
243      * See {@link java.text.DecimalFormat} for details of how to specify the pattern.
244      *
245      * @return The format pattern.
246      */
247     public String getPattern() {
248         return pattern;
249     }
250 
251     /**
252      * Gets whether decimals are allowed in the number.
253      *
254      * @return Whether decimals are allowed in the number
255      */
256     public boolean isAllowDecimals() {
257         return allowDecimals;
258     }
259 
260     /**
261      * Convert a String into a {@code Number} object.
262      *
263      * @param sourceType the source type of the conversion
264      * @param targetType The type to convert the value to
265      * @param value      The String date value.
266      * @param format     The NumberFormat to parse the String value.
267      * @return The converted Number object.
268      * @throws ConversionException if the String cannot be converted.
269      */
270     private Number parse(final Class<?> sourceType, final Class<?> targetType, final String value, final NumberFormat format) {
271         final ParsePosition pos = new ParsePosition(0);
272         final Number parsedNumber = format.parse(value, pos);
273         if (pos.getErrorIndex() >= 0 || pos.getIndex() != value.length() || parsedNumber == null) {
274             String msg = "Error converting from '" + toString(sourceType) + "' to '" + toString(targetType) + "'";
275             if (format instanceof DecimalFormat) {
276                 msg += " using pattern '" + ((DecimalFormat) format).toPattern() + "'";
277             }
278             if (locale != null) {
279                 msg += " for locale=[" + locale + "]";
280             }
281             if (log().isDebugEnabled()) {
282                 log().debug("    " + msg);
283             }
284             throw new ConversionException(msg);
285         }
286         return parsedNumber;
287     }
288 
289     /**
290      * Sets the Locale for the <em>Converter</em>.
291      *
292      * @param locale The locale to use for conversion
293      */
294     public void setLocale(final Locale locale) {
295         this.locale = locale;
296         setUseLocaleFormat(true);
297     }
298 
299     /**
300      * Sets a number format pattern to use to convert Numbers to/from a {@link String}.
301      * <p>
302      * See {@link java.text.DecimalFormat} for details of how to specify the pattern.
303      *
304      * @param pattern The format pattern.
305      */
306     public void setPattern(final String pattern) {
307         this.pattern = pattern;
308         setUseLocaleFormat(true);
309     }
310 
311     /**
312      * Sets whether a format should be used to convert the Number.
313      *
314      * @param useLocaleFormat {@code true} if a number format should be used.
315      */
316     public void setUseLocaleFormat(final boolean useLocaleFormat) {
317         this.useLocaleFormat = useLocaleFormat;
318     }
319 
320     /**
321      * Default String to Number conversion.
322      * <p>
323      * This method handles conversion from a String to the following types:
324      * <ul>
325      * <li>{@link Byte}</li>
326      * <li>{@link Short}</li>
327      * <li>{@link Integer}</li>
328      * <li>{@link Long}</li>
329      * <li>{@link Float}</li>
330      * <li>{@link Double}</li>
331      * <li>{@link java.math.BigDecimal}</li>
332      * <li>{@link java.math.BigInteger}</li>
333      * </ul>
334      *
335      * @param sourceType The type being converted from
336      * @param targetType The Number type to convert to
337      * @param value      The String value to convert.
338      * @return The converted Number value.
339      */
340     private Number toNumber(final Class<?> sourceType, final Class<?> targetType, final String value) {
341         // Byte
342         if (targetType.equals(Byte.class)) {
343             return Byte.valueOf(value);
344         }
345 
346         // Short
347         if (targetType.equals(Short.class)) {
348             return Short.valueOf(value);
349         }
350 
351         // Integer
352         if (targetType.equals(Integer.class)) {
353             return Integer.valueOf(value);
354         }
355 
356         // Long
357         if (targetType.equals(Long.class)) {
358             return Long.valueOf(value);
359         }
360 
361         // Float
362         if (targetType.equals(Float.class)) {
363             return Float.valueOf(value);
364         }
365 
366         // Double
367         if (targetType.equals(Double.class)) {
368             return Double.valueOf(value);
369         }
370 
371         // BigDecimal
372         if (targetType.equals(BigDecimal.class)) {
373             return new BigDecimal(value);
374         }
375 
376         // BigInteger
377         if (targetType.equals(BigInteger.class)) {
378             return new BigInteger(value);
379         }
380 
381         final String msg = toString(getClass()) + " cannot handle conversion from '" + toString(sourceType) + "' to '" + toString(targetType) + "'";
382         if (log().isWarnEnabled()) {
383             log().warn("    " + msg);
384         }
385         throw new ConversionException(msg);
386     }
387 
388     /**
389      * Convert any Number object to the specified type for this <em>Converter</em>.
390      * <p>
391      * This method handles conversion to the following types:
392      * <ul>
393      * <li>{@link Byte}</li>
394      * <li>{@link Short}</li>
395      * <li>{@link Integer}</li>
396      * <li>{@link Long}</li>
397      * <li>{@link Float}</li>
398      * <li>{@link Double}</li>
399      * <li>{@link java.math.BigDecimal}</li>
400      * <li>{@link java.math.BigInteger}</li>
401      * </ul>
402      *
403      * @param sourceType The type being converted from
404      * @param targetType The Number type to convert to
405      * @param value      The Number to convert.
406      * @return The converted value.
407      */
408     private <T> T toNumber(final Class<?> sourceType, final Class<T> targetType, final Number value) {
409         // Correct Number type already
410         if (targetType.equals(value.getClass())) {
411             return targetType.cast(value);
412         }
413 
414         // Byte
415         if (targetType.equals(Byte.class)) {
416             final long longValue = value.longValue();
417             if (longValue > Byte.MAX_VALUE) {
418                 throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType));
419             }
420             if (longValue < Byte.MIN_VALUE) {
421                 throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType));
422             }
423             return targetType.cast(Byte.valueOf(value.byteValue()));
424         }
425 
426         // Short
427         if (targetType.equals(Short.class)) {
428             final long longValue = value.longValue();
429             if (longValue > Short.MAX_VALUE) {
430                 throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType));
431             }
432             if (longValue < Short.MIN_VALUE) {
433                 throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType));
434             }
435             return targetType.cast(Short.valueOf(value.shortValue()));
436         }
437 
438         // Integer
439         if (targetType.equals(Integer.class)) {
440             final long longValue = value.longValue();
441             if (longValue > Integer.MAX_VALUE) {
442                 throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType));
443             }
444             if (longValue < Integer.MIN_VALUE) {
445                 throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType));
446             }
447             return targetType.cast(Integer.valueOf(value.intValue()));
448         }
449 
450         // Long
451         if (targetType.equals(Long.class)) {
452             return targetType.cast(Long.valueOf(value.longValue()));
453         }
454 
455         // Float
456         if (targetType.equals(Float.class)) {
457             if (value.doubleValue() > Float.MAX_VALUE) {
458                 throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType));
459             }
460             return targetType.cast(Float.valueOf(value.floatValue()));
461         }
462 
463         // Double
464         if (targetType.equals(Double.class)) {
465             return targetType.cast(Double.valueOf(value.doubleValue()));
466         }
467 
468         // BigDecimal
469         if (targetType.equals(BigDecimal.class)) {
470             if (value instanceof Float || value instanceof Double) {
471                 return targetType.cast(new BigDecimal(value.toString()));
472             }
473             if (value instanceof BigInteger) {
474                 return targetType.cast(new BigDecimal((BigInteger) value));
475             }
476             if (value instanceof BigDecimal) {
477                 return targetType.cast(new BigDecimal(value.toString()));
478             }
479             return targetType.cast(BigDecimal.valueOf(value.longValue()));
480         }
481 
482         // BigInteger
483         if (targetType.equals(BigInteger.class)) {
484             if (value instanceof BigDecimal) {
485                 return targetType.cast(((BigDecimal) value).toBigInteger());
486             }
487             return targetType.cast(BigInteger.valueOf(value.longValue()));
488         }
489 
490         final String msg = toString(getClass()) + " cannot handle conversion to '" + toString(targetType) + "'";
491         if (log().isWarnEnabled()) {
492             log().warn("    " + msg);
493         }
494         throw new ConversionException(msg);
495     }
496 
497     /**
498      * Provide a String representation of this number converter.
499      *
500      * @return A String representation of this number converter
501      */
502     @Override
503     public String toString() {
504         final StringBuilder buffer = new StringBuilder();
505         buffer.append(toString(getClass()));
506         buffer.append("[UseDefault=");
507         buffer.append(isUseDefault());
508         buffer.append(", UseLocaleFormat=");
509         buffer.append(useLocaleFormat);
510         if (pattern != null) {
511             buffer.append(", Pattern=");
512             buffer.append(pattern);
513         }
514         if (locale != null) {
515             buffer.append(", Locale=");
516             buffer.append(locale);
517         }
518         buffer.append(']');
519         return buffer.toString();
520     }
521 
522 }