    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     */
    017    package org.apache.commons.beanutils.converters;
    018
    019    import java.util.Calendar;
    020    import java.util.Date;
    021    import java.util.Locale;
    022    import java.math.BigDecimal;
    023    import java.math.BigInteger;
    024    import java.text.NumberFormat;
    025    import java.text.DecimalFormat;
    026    import java.text.DecimalFormatSymbols;
    027    import java.text.ParsePosition;
    028
    029    import org.apache.commons.beanutils.ConversionException;
    030
    031    /**
    032     * {@link org.apache.commons.beanutils.Converter} implementaion that handles conversion
    033     * to and from <b>java.lang.Number</b> objects.
    034     * <p>
    035     * This implementation handles conversion for the following
    036     * <code>java.lang.Number</code> types.
    037     * <ul>
    038     *     <li><code>java.lang.Byte</code></li>
    039     *     <li><code>java.lang.Short</code></li>
    040     *     <li><code>java.lang.Integer</code></li>
    041     *     <li><code>java.lang.Long</code></li>
    042     *     <li><code>java.lang.Float</code></li>
    043     *     <li><code>java.lang.Double</code></li>
    044     *     <li><code>java.math.BigDecimal</code></li>
    045     *     <li><code>java.math.BigInteger</code></li>
    046     * </ul>
    047     *
    048     * <h3>String Conversions (to and from)</h3>
    049     * This class provides a number of ways in which number
    050     * conversions to/from Strings can be achieved:
    051     * <ul>
    052     *    <li>Using the default format for the default Locale, configure using:</li>
    053     *        <ul>
    054     *           <li><code>setUseLocaleFormat(true)</code></li>
    055     *        </ul>
    056     *    <li>Using the default format for a specified Locale, configure using:</li>
    057     *        <ul>
    058     *           <li><code>setLocale(Locale)</code></li>
    059     *        </ul>
    060     *    <li>Using a specified pattern for the default Locale, configure using:</li>
    061     *        <ul>
    062     *           <li><code>setPattern(String)</code></li>
    063     *        </ul>
    064     *    <li>Using a specified pattern for a specified Locale, configure using:</li>
    065     *        <ul>
    066     *           <li><code>setPattern(String)</code></li>
    067     *           <li><code>setLocale(Locale)</code></li>
    068     *        </ul>
    069     *    <li>If none of the above are configured the
    070     *        <code>toNumber(String)</code> method is used to convert
    071     *        from String to Number and the Number's
    072     *        <code>toString()</code> method used to convert from
    073     *        Number to String.</li>
    074     * </ul>
    075     *
    076     * <p>
    077     * <strong>N.B.</strong>Patterns can only be specified used the <i>standard</i>
    078     * pattern characters and NOT in <i>localized</i> form (see <code>java.text.SimpleDateFormat</code>).
    079     * For example to cater for number styles used in Germany such as <code>0.000,00</code> the pattern
    080     * is specified in the normal form <code>0,000.00</code> and the locale set to <code>Locale.GERMANY</code>.
    081     *
    082     * @version $Revision: 745081 $ $Date: 2009-02-17 14:05:20 +0000 (Tue, 17 Feb 2009) $
    083     * @since 1.8.0
    084     */
    085    public abstract class NumberConverter extends AbstractConverter {
    086
    087        private static final Integer ZERO = new Integer(0);
    088        private static final Integer ONE  = new Integer(1);
    089
    090        private String pattern;
    091        private boolean allowDecimals;
    092        private boolean useLocaleFormat;
    093        private Locale locale;
    094
    095        // ----------------------------------------------------------- Constructors
    096
    097        /**
    098         * Construct a <b>java.lang.Number</b> <i>Converter</i>
    099         * that throws a <code>ConversionException</code> if a error occurs.
    100         *
    101         * @param allowDecimals Indicates whether decimals are allowed
    102         */
    103        public NumberConverter(boolean allowDecimals) {
    104            super();
    105            this.allowDecimals = allowDecimals;
    106        }
    107
    108        /**
    109         * Construct a <code>java.lang.Number</code> <i>Converter</i> that returns
    110         * a default value if an error occurs.
    111         *
    112         * @param allowDecimals Indicates whether decimals are allowed
    113         * @param defaultValue The default value to be returned
    114         */
    115        public NumberConverter(boolean allowDecimals, Object defaultValue) {
    116            super();
    117            this.allowDecimals = allowDecimals;
    118            setDefaultValue(defaultValue);
    119        }
    120
    121        // --------------------------------------------------------- Public Methods
    122
    123        /**
    124         * Return whether decimals are allowed in the number.
    125         *
    126         * @return Whether decimals are allowed in the number
    127         */
    128        public boolean isAllowDecimals() {
    129            return allowDecimals;
    130        }
    131
    132        /**
    133         * Set whether a format should be used to convert
    134         * the Number.
    135         *
    136         * @param useLocaleFormat <code>true</code> if a number format
    137         * should be used.
    138         */
    139        public void setUseLocaleFormat(boolean useLocaleFormat) {
    140            this.useLocaleFormat = useLocaleFormat;
    141        }
    142
    143        /**
    144         * Return the number format pattern used to convert
    145         * Numbers to/from a <code>java.lang.String</code>
    146         * (or <code>null</code> if none specified).
    147         * <p>
    148         * See <code>java.text.SimpleDateFormat</code> for details
    149         * of how to specify the pattern.
    150         *
    151         * @return The format pattern.
    152         */
    153        public String getPattern() {
    154            return pattern;
    155        }
    156
    157        /**
    158         * Set a number format pattern to use to convert
    159         * Numbers to/from a <code>java.lang.String</code>.
    160         * <p>
    161         * See <code>java.text.SimpleDateFormat</code> for details
    162         * of how to specify the pattern.
    163         *
    164         * @param pattern The format pattern.
    165         */
    166        public void setPattern(String pattern) {
    167            this.pattern = pattern;
    168            setUseLocaleFormat(true);
    169        }
    170
    171        /**
    172         * Return the Locale for the <i>Converter</i>
    173         * (or <code>null</code> if none specified).
    174         *
    175         * @return The locale to use for conversion
    176         */
    177        public Locale getLocale() {
    178            return locale;
    179        }
    180
    181        /**
    182         * Set the Locale for the <i>Converter</i>.
    183         *
    184         * @param locale The locale to use for conversion
    185         */
    186        public void setLocale(Locale locale) {
    187            this.locale = locale;
    188            setUseLocaleFormat(true);
    189        }
    190
    191        // ------------------------------------------------------ Protected Methods
    192
    193        /**
    194         * Convert an input Number object into a String.
    195         *
    196         * @param value The input value to be converted
    197         * @return the converted String value.
    198         * @throws Throwable if an error occurs converting to a String
    199         */
    200        protected String convertToString(Object value) throws Throwable {
    201
    202            String result = null;
    203            if (useLocaleFormat && value instanceof Number) {
    204                NumberFormat format = getFormat();
    205                format.setGroupingUsed(false);
    206                result = format.format(value);
    207                if (log().isDebugEnabled()) {
    208                    log().debug("    Converted  to String using format '" + result + "'");
    209                }
    210
    211            } else {
    212                result = value.toString();
    213                if (log().isDebugEnabled()) {
    214                    log().debug("    Converted  to String using toString() '" + result + "'");
    215                }
    216            }
    217            return result;
    218
    219        }
    220
    221        /**
    222         * Convert the input object into a Number object of the
    223         * specified type.
    224         *
    225         * @param targetType Data type to which this value should be converted.
    226         * @param value The input value to be converted.
    227         * @return The converted value.
    228         * @throws Throwable if an error occurs converting to the specified type
    229         */
    230        protected Object convertToType(Class targetType, Object value) throws Throwable {
    231
    232            Class sourceType = value.getClass();
    233            // Handle Number
    234            if (value instanceof Number) {
    235                return toNumber(sourceType, targetType, (Number)value);
    236            }
    237
    238            // Handle Boolean
    239            if (value instanceof Boolean) {
    240                return toNumber(sourceType, targetType, ((Boolean)value).booleanValue() ? ONE : ZERO);
    241            }
    242
    243            // Handle Date --> Long
    244            if (value instanceof Date && Long.class.equals(targetType)) {
    245                return new Long(((Date)value).getTime());
    246            }
    247
    248            // Handle Calendar --> Long
    249            if (value instanceof Calendar  && Long.class.equals(targetType)) {
    250                return new Long(((Calendar)value).getTime().getTime());
    251            }
    252
    253            // Convert all other types to String & handle
    254            String stringValue = value.toString().trim();
    255            if (stringValue.length() == 0) {
    256                return handleMissing(targetType);
    257            }
    258
    259            // Convert/Parse a String
    260            Number number = null;
    261            if (useLocaleFormat) {
    262                NumberFormat format = getFormat();
    263                number = parse(sourceType, targetType, stringValue, format);
    264            } else {
    265                if (log().isDebugEnabled()) {
    266                    log().debug("    No NumberFormat, using default conversion");
    267                }
    268                number = toNumber(sourceType, targetType, stringValue);
    269            }
    270
    271            // Ensure the correct number type is returned
    272            return toNumber(sourceType, targetType, number);
    273
    274        }
    275
    276        /**
    277         * Convert any Number object to the specified type for this
    278         * <i>Converter</i>.
    279         * <p>
    280         * This method handles conversion to the following types:
    281         * <ul>
    282         *     <li><code>java.lang.Byte</code></li>
    283         *     <li><code>java.lang.Short</code></li>
    284         *     <li><code>java.lang.Integer</code></li>
    285         *     <li><code>java.lang.Long</code></li>
    286         *     <li><code>java.lang.Float</code></li>
    287         *     <li><code>java.lang.Double</code></li>
    288         *     <li><code>java.math.BigDecimal</code></li>
    289         *     <li><code>java.math.BigInteger</code></li>
    290         * </ul>
    291         * @param sourceType The type being converted from
    292         * @param targetType The Number type to convert to
    293         * @param value The Number to convert.
    294         *
    295         * @return The converted value.
    296         */
    297        private Number toNumber(Class sourceType, Class targetType, Number value) {
    298
    299            // Correct Number type already
    300            if (targetType.equals(value.getClass())) {
    301                return value;
    302            }
    303
    304            // Byte
    305            if (targetType.equals(Byte.class)) {
    306                long longValue = value.longValue();
    307                if (longValue > Byte.MAX_VALUE) {
    308                    throw new ConversionException(toString(sourceType) + " value '" + value
    309                            + "' is too large for " + toString(targetType));
    310                }
    311                if (longValue < Byte.MIN_VALUE) {
    312                    throw new ConversionException(toString(sourceType) + " value '" + value
    313                            + "' is too small " + toString(targetType));
    314                }
    315                return new Byte(value.byteValue());
    316            }
    317
    318            // Short
    319            if (targetType.equals(Short.class)) {
    320                long longValue = value.longValue();
    321                if (longValue > Short.MAX_VALUE) {
    322                    throw new ConversionException(toString(sourceType) + " value '" + value
    323                            + "' is too large for " + toString(targetType));
    324                }
    325                if (longValue < Short.MIN_VALUE) {
    326                    throw new ConversionException(toString(sourceType) + " value '" + value
    327                            + "' is too small " + toString(targetType));
    328                }
    329                return new Short(value.shortValue());
    330            }
    331
    332            // Integer
    333            if (targetType.equals(Integer.class)) {
    334                long longValue = value.longValue();
    335                if (longValue > Integer.MAX_VALUE) {
    336                    throw new ConversionException(toString(sourceType) + " value '" + value
    337                            + "' is too large for " + toString(targetType));
    338                }
    339                if (longValue < Integer.MIN_VALUE) {
    340                    throw new ConversionException(toString(sourceType) + " value '" + value
    341                            + "' is too small " + toString(targetType));
    342                }
    343                return new Integer(value.intValue());
    344            }
    345
    346            // Long
    347            if (targetType.equals(Long.class)) {
    348                return new Long(value.longValue());
    349            }
    350
    351            // Float
    352            if (targetType.equals(Float.class)) {
    353                if (value.doubleValue() > Float.MAX_VALUE) {
    354                    throw new ConversionException(toString(sourceType) + " value '" + value
    355                            + "' is too large for " + toString(targetType));
    356                }
    357                return new Float(value.floatValue());
    358            }
    359
    360            // Double
    361            if (targetType.equals(Double.class)) {
    362                return new Double(value.doubleValue());
    363            }
    364
    365            // BigDecimal
    366            if (targetType.equals(BigDecimal.class)) {
    367                if (value instanceof Float || value instanceof Double) {
    368                    return new BigDecimal(value.toString());
    369                } else if (value instanceof BigInteger) {
    370                    return new BigDecimal((BigInteger)value);
    371                } else {
    372                    return BigDecimal.valueOf(value.longValue());
    373                }
    374            }
    375
    376            // BigInteger
    377            if (targetType.equals(BigInteger.class)) {
    378                if (value instanceof BigDecimal) {
    379                    return ((BigDecimal)value).toBigInteger();
    380                } else {
    381                    return BigInteger.valueOf(value.longValue());
    382                }
    383            }
    384
    385            String msg = toString(getClass()) + " cannot handle conversion to '"
    386                       + toString(targetType) + "'";
    387            if (log().isWarnEnabled()) {
    388                log().warn("    " + msg);
    389            }
    390            throw new ConversionException(msg);
    391
    392        }
    393
    394        /**
    395         * Default String to Number conversion.
    396         * <p>
    397         * This method handles conversion from a String to the following types:
    398         * <ul>
    399         *     <li><code>java.lang.Byte</code></li>
    400         *     <li><code>java.lang.Short</code></li>
    401         *     <li><code>java.lang.Integer</code></li>
    402         *     <li><code>java.lang.Long</code></li>
    403         *     <li><code>java.lang.Float</code></li>
    404         *     <li><code>java.lang.Double</code></li>
    405         *     <li><code>java.math.BigDecimal</code></li>
    406         *     <li><code>java.math.BigInteger</code></li>
    407         * </ul>
    408         * @param sourceType The type being converted from
    409         * @param targetType The Number type to convert to
    410         * @param value The String value to convert.
    411         *
    412         * @return The converted Number value.
    413         */
    414        private Number toNumber(Class sourceType, Class targetType, String value) {
    415
    416            // Byte
    417            if (targetType.equals(Byte.class)) {
    418                return new Byte(value);
    419            }
    420
    421            // Short
    422            if (targetType.equals(Short.class)) {
    423                return new Short(value);
    424            }
    425
    426            // Integer
    427            if (targetType.equals(Integer.class)) {
    428                return new Integer(value);
    429            }
    430
    431            // Long
    432            if (targetType.equals(Long.class)) {
    433                return new Long(value);
    434            }
    435
    436            // Float
    437            if (targetType.equals(Float.class)) {
    438                return new Float(value);
    439            }
    440
    441            // Double
    442            if (targetType.equals(Double.class)) {
    443                return new Double(value);
    444            }
    445
    446            // BigDecimal
    447            if (targetType.equals(BigDecimal.class)) {
    448                return new BigDecimal(value);
    449            }
    450
    451            // BigInteger
    452            if (targetType.equals(BigInteger.class)) {
    453                return new BigInteger(value);
    454            }
    455
    456            String msg = toString(getClass()) + " cannot handle conversion from '" +
    457                         toString(sourceType) + "' to '" + toString(targetType) + "'";
    458            if (log().isWarnEnabled()) {
    459                log().warn("    " + msg);
    460            }
    461            throw new ConversionException(msg);
    462        }
    463
    464        /**
    465         * Provide a String representation of this number converter.
    466         *
    467         * @return A String representation of this number converter
    468         */
    469        public String toString() {
    470            StringBuffer buffer = new StringBuffer();
    471            buffer.append(toString(getClass()));
    472            buffer.append("[UseDefault=");
    473            buffer.append(isUseDefault());
    474            buffer.append(", UseLocaleFormat=");
    475            buffer.append(useLocaleFormat);
    476            if (pattern != null) {
    477                buffer.append(", Pattern=");
    478                buffer.append(pattern);
    479            }
    480            if (locale != null) {
    481                buffer.append(", Locale=");
    482                buffer.append(locale);
    483            }
    484            buffer.append(']');
    485            return buffer.toString();
    486        }
    487
    488        /**
    489         * Return a NumberFormat to use for Conversion.
    490         *
    491         * @return The NumberFormat.
    492         */
    493        private NumberFormat getFormat() {
    494            NumberFormat format = null;
    495            if (pattern != null) {
    496                if (locale == null) {
    497                    if (log().isDebugEnabled()) {
    498                        log().debug("    Using pattern '" + pattern + "'");
    499                    }
    500                    format = new DecimalFormat(pattern);
    501                } else {
    502                    if (log().isDebugEnabled()) {
    503                        log().debug("    Using pattern '" + pattern + "'" +
    504                                  " with Locale[" + locale + "]");
    505                    }
    506                    DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
    507                    format = new DecimalFormat(pattern, symbols);
    508                }
    509            } else {
    510                if (locale == null) {
    511                    if (log().isDebugEnabled()) {
    512                        log().debug("    Using default Locale format");
    513                    }
    514                    format = NumberFormat.getInstance();
    515                } else {
    516                    if (log().isDebugEnabled()) {
    517                        log().debug("    Using Locale[" + locale + "] format");
    518                    }
    519                    format = NumberFormat.getInstance(locale);
    520                }
    521            }
    522            if (!allowDecimals) {
    523                format.setParseIntegerOnly(true);
    524            }
    525            return format;
    526        }
    527
    528        /**
    529         * Convert a String into a <code>Number</code> object.
    530         * @param sourceType TODO
    531         * @param targetType The type to convert the value to
    532         * @param value The String date value.
    533         * @param format The NumberFormat to parse the String value.
    534         *
    535         * @return The converted Number object.
    536         * @throws ConversionException if the String cannot be converted.
    537         */
    538        private Number parse(Class sourceType, Class targetType, String value, NumberFormat format) {
    539            ParsePosition pos = new ParsePosition(0);
    540            Number parsedNumber = format.parse(value, pos);
    541            if (pos.getErrorIndex() >= 0 || pos.getIndex() != value.length() || parsedNumber == null) {
    542                String msg = "Error converting from '" + toString(sourceType) + "' to '" + toString(targetType) + "'";
    543                if (format instanceof DecimalFormat) {
    544                    msg += " using pattern '" + ((DecimalFormat)format).toPattern() + "'";
    545                }
    546                if (locale != null) {
    547                    msg += " for locale=[" + locale + "]";
    548                }
    549                if (log().isDebugEnabled()) {
    550                    log().debug("    " + msg);
    551                }
    552                throw new ConversionException(msg);
    553            }
    554            return parsedNumber;
    555        }
    556
    557    }