View Javadoc
1   /*
2    * Copyright 2018 Andrew Rucker Jones.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.opencsv.bean;
17  
18  import com.opencsv.ICSVParser;
19  import com.opencsv.exceptions.CsvBadConverterException;
20  import com.opencsv.exceptions.CsvDataTypeMismatchException;
21  import org.apache.commons.lang3.ClassUtils;
22  import org.apache.commons.lang3.ObjectUtils;
23  import org.apache.commons.lang3.StringUtils;
24  
25  import java.math.BigDecimal;
26  import java.math.BigInteger;
27  import java.math.RoundingMode;
28  import java.text.DecimalFormat;
29  import java.text.NumberFormat;
30  import java.text.ParseException;
31  import java.util.Locale;
32  import java.util.ResourceBundle;
33  import java.util.function.UnaryOperator;
34  
35  /**
36   * This converter class is used in combination with {@link CsvNumber}, that is,
37   * when number inputs and outputs should be formatted.
38   *
39   * @author Andrew Rucker Jones
40   * @since 4.2
41   */
42  public class ConverterNumber extends AbstractCsvConverter {
43  
44      private final DecimalFormat readFormatter, writeFormatter;
45      private final UnaryOperator<Number> readConversionFunction;
46  
47      /**
48       * @param type    The class of the type of the data being processed
49       * @param locale   If not null or empty, specifies the locale used for
50       *                 converting locale-specific data types for reading
51       * @param writeLocale   If not null or empty, specifies the locale used for
52       *                 converting locale-specific data types for writing
53       * @param errorLocale The locale to use for error messages
54       * @param readFormat The string to use for parsing the number.
55       * @param writeFormat The string to use for formatting the number.
56       * @param roundingMode The rounding mode used when converting {@link java.math.BigDecimal}s.
57       * @throws CsvBadConverterException If the information given to initialize the converter are inconsistent (e.g.
58       *   the annotation {@link com.opencsv.bean.CsvNumber} has been applied to a non-{@link java.lang.Number} type.
59       * @see com.opencsv.bean.CsvNumber#value()
60       */
61      public ConverterNumber(Class<?> type, String locale, String writeLocale, Locale errorLocale,
62              String readFormat, String writeFormat, RoundingMode roundingMode)
63              throws CsvBadConverterException {
64          super(type, locale, writeLocale, errorLocale);
65  
66          // Check that the bean member is of an applicable type
67          if(!Number.class.isAssignableFrom(
68                  this.type.isPrimitive()
69                          ? ClassUtils.primitiveToWrapper(this.type)
70                          : this.type)) {
71              throw new CsvBadConverterException(
72                      ConverterNumber.class,
73                      ResourceBundle.getBundle(
74                              ICSVParser.DEFAULT_BUNDLE_NAME,
75                              this.errorLocale)
76                              .getString("csvnumber.not.number"));
77          }
78  
79          // Set up the read formatter
80          readFormatter = createDecimalFormat(readFormat, this.locale, roundingMode);
81  
82          // Account for BigDecimal and BigInteger, which require special
83          // processing
84          if(this.type == BigInteger.class || this.type == BigDecimal.class) {
85              readFormatter.setParseBigDecimal(true);
86          }
87  
88          // Save the read conversion function for later
89          if(this.type == Byte.class || this.type == Byte.TYPE) {
90              readConversionFunction = Number::byteValue;
91          }
92          else if(this.type == Short.class || this.type == Short.TYPE) {
93              readConversionFunction = Number::shortValue;
94          }
95          else if(this.type == Integer.class || this.type == Integer.TYPE) {
96              readConversionFunction = Number::intValue;
97          }
98          else if(this.type == Long.class || this.type == Long.TYPE) {
99              readConversionFunction = Number::longValue;
100         }
101         else if(this.type == Float.class || this.type == Float.TYPE) {
102             readConversionFunction = Number::floatValue;
103         }
104         else if(this.type == Double.class || this.type == Double.TYPE) {
105             readConversionFunction = Number::doubleValue;
106         }
107         else if(this.type == BigInteger.class) {
108             readConversionFunction = n -> ((BigDecimal) n).toBigInteger();
109         }
110         else {
111             // Either it's already a BigDecimal and nothing need be done,
112             // or it's some derivative of java.lang.Number that we couldn't be
113             // expected to know and accommodate for. In the latter case, a class
114             // cast exception will be thrown later on assignment.
115             readConversionFunction = n -> n;
116         }
117 
118         // Set up the write formatter
119         writeFormatter = createDecimalFormat(writeFormat, this.writeLocale, roundingMode);
120     }
121 
122     private DecimalFormat createDecimalFormat(String format, Locale locale, RoundingMode roundingMode) {
123         NumberFormat nf = NumberFormat.getInstance(ObjectUtils.defaultIfNull(locale, Locale.getDefault(Locale.Category.FORMAT)));
124         if (!(nf instanceof DecimalFormat)) {
125             throw new CsvBadConverterException(
126                     ConverterNumber.class,
127                     ResourceBundle.getBundle(
128                             ICSVParser.DEFAULT_BUNDLE_NAME,
129                             this.errorLocale)
130                             .getString("numberformat.not.decimalformat"));
131         }
132         DecimalFormat formatter = (DecimalFormat) nf;
133 
134         try {
135             formatter.applyLocalizedPattern(format);
136         } catch (IllegalArgumentException e) {
137             CsvBadConverterException csve = new CsvBadConverterException(
138                     ConverterNumber.class,
139                     String.format(ResourceBundle.getBundle(
140                             ICSVParser.DEFAULT_BUNDLE_NAME,
141                             this.errorLocale)
142                                     .getString("invalid.number.pattern"),
143                             format));
144             csve.initCause(e);
145             throw csve;
146         }
147         
148         formatter.setRoundingMode(roundingMode);
149 
150         return formatter;
151     }
152 
153     @Override
154     public Object convertToRead(String value) throws CsvDataTypeMismatchException {
155         Number n = null;
156         if(StringUtils.isNotEmpty(value)) {
157             try {
158                 synchronized (readFormatter) {
159                     n = readFormatter.parse(value);
160                 }
161             }
162             catch(ParseException | ArithmeticException e) {
163                 CsvDataTypeMismatchException csve = new CsvDataTypeMismatchException(
164                         value, type,
165                         String.format(ResourceBundle.getBundle(
166                                 ICSVParser.DEFAULT_BUNDLE_NAME,
167                                 errorLocale)
168                                 .getString("unparsable.number"), value, readFormatter.toPattern()));
169                 csve.initCause(e);
170                 throw csve;
171             }
172 
173             n = readConversionFunction.apply(n);
174         }
175         return n;
176     }
177 
178     /**
179      *  Formats the number in question according to the pattern that has been
180      *  provided.
181      */
182     // The rest of the Javadoc is inherited.
183     @Override
184     public String convertToWrite(Object value) throws CsvDataTypeMismatchException {
185         synchronized (writeFormatter) {
186             try {
187                 return value != null ? writeFormatter.format(value) : null;
188             }
189             catch (ArithmeticException e) {
190                 CsvDataTypeMismatchException csve = new CsvDataTypeMismatchException(
191                         value, type,
192                         String.format(ResourceBundle.getBundle(
193                                 ICSVParser.DEFAULT_BUNDLE_NAME,
194                                 errorLocale)
195                                 .getString("unparsable.number"), value, writeFormatter.toPattern()));
196                 csve.initCause(e);
197                 throw csve;
198             }
199         }
200     }
201 }