    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}