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.beanutils.converters;
018
019import java.util.Calendar;
020import java.util.Date;
021import java.util.Locale;
022import java.math.BigDecimal;
023import java.math.BigInteger;
024import java.text.NumberFormat;
025import java.text.DecimalFormat;
026import java.text.DecimalFormatSymbols;
027import java.text.ParsePosition;
028
029import 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 */
085public 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}