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.beanutils2.converters;
018
019import java.math.BigDecimal;
020import java.math.BigInteger;
021import java.text.DecimalFormat;
022import java.text.DecimalFormatSymbols;
023import java.text.NumberFormat;
024import java.text.ParsePosition;
025import java.util.Calendar;
026import java.util.Date;
027import java.util.Locale;
028
029import org.apache.commons.beanutils2.ConversionException;
030
031/**
032 * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from <strong>java.lang.Number</strong> objects.
033 * <p>
034 * This implementation handles conversion for the following {@link Number} types.
035 * <ul>
036 * <li>{@link Byte}</li>
037 * <li>{@link Short}</li>
038 * <li>{@link Integer}</li>
039 * <li>{@link Long}</li>
040 * <li>{@link Float}</li>
041 * <li>{@link Double}</li>
042 * <li>{@link java.math.BigDecimal}</li>
043 * <li>{@link java.math.BigInteger}</li>
044 * </ul>
045 *
046 * <h2>String Conversions (to and from)</h2> This class provides a number of ways in which number conversions to/from Strings can be achieved:
047 * <ul>
048 * <li>Using the default format for the default Locale, configure using:
049 * <ul>
050 * <li>{@code setUseLocaleFormat(true)}</li>
051 * </ul>
052 * </li>
053 * <li>Using the default format for a specified Locale, configure using:
054 * <ul>
055 * <li>{@code setLocale(Locale)}</li>
056 * </ul>
057 * </li>
058 * <li>Using a specified pattern for the default Locale, configure using:
059 * <ul>
060 * <li>{@code setPattern(String)}</li>
061 * </ul>
062 * </li>
063 * <li>Using a specified pattern for a specified Locale, configure using:
064 * <ul>
065 * <li>{@code setPattern(String)}</li>
066 * <li>{@code setLocale(Locale)}</li>
067 * </ul>
068 * </li>
069 * <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()}
070 * method used to convert from Number to String.</li>
071 * </ul>
072 *
073 * <p>
074 * <strong>N.B.</strong>Patterns can only be specified using the <em>standard</em> pattern characters and NOT in <em>localized</em> form (see
075 * {@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
076 * {@code 0,000.00</code> and the locale set to <code>Locale.GERMANY}
077 *
078 * @param <N> The default value type.
079 * @since 1.8.0
080 */
081public abstract class NumberConverter<N extends Number> extends AbstractConverter<N> {
082
083    private static final Integer ZERO = Integer.valueOf(0);
084    private static final Integer ONE = Integer.valueOf(1);
085
086    private String pattern;
087    private final boolean allowDecimals;
088    private boolean useLocaleFormat;
089    private Locale locale;
090
091    /**
092     * Constructs a <strong>java.lang.Number</strong> <em>Converter</em> that throws a {@code ConversionException} if a error occurs.
093     *
094     * @param allowDecimals Indicates whether decimals are allowed
095     */
096    public NumberConverter(final boolean allowDecimals) {
097        this.allowDecimals = allowDecimals;
098    }
099
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}