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    }