View Javadoc
1   /*
2    * Copyright 2016 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.StringUtils;
22  
23  import javax.xml.datatype.DatatypeConfigurationException;
24  import javax.xml.datatype.DatatypeFactory;
25  import javax.xml.datatype.XMLGregorianCalendar;
26  import java.lang.reflect.InvocationTargetException;
27  import java.text.ParseException;
28  import java.text.SimpleDateFormat;
29  import java.time.*;
30  import java.time.chrono.*;
31  import java.time.format.DateTimeFormatter;
32  import java.time.temporal.ChronoField;
33  import java.time.temporal.Temporal;
34  import java.time.temporal.TemporalAccessor;
35  import java.util.*;
36  import java.util.function.BiFunction;
37  
38  /**
39   * This class converts an input to a date type.
40   * <p>This class should work with any type derived from {@link java.util.Date}
41   * as long as it has a constructor taking one long that specifies the number
42   * of milliseconds since the epoch. The following types are explicitly
43   * supported:
44   * <ul><li>java.util.Date</li>
45   * <li>java.sql.Date</li>
46   * <li>java.sql.Time</li>
47   * <li>java.sql.Timestamp</li></ul></p>
48   * <p>This class should work for any type that implements
49   * {@link java.util.Calendar} or is derived from
50   * {@link javax.xml.datatype.XMLGregorianCalendar}. The following types are
51   * explicitly supported:
52   * <ul><li>Calendar (always a GregorianCalendar)</li>
53   * <li>GregorianCalendar</li>
54   * <li>XMLGregorianCalendar</li></ul>
55   * It is also known to work with
56   * org.apache.xerces.jaxp.datatype.XMLGregorianCalendarImpl.</p>
57   * <p>This class works for all types from the JDK that implement
58   * {@link java.time.temporal.TemporalAccessor}.</p>
59   *
60   * @author Andrew Rucker Jones
61   * @see com.opencsv.bean.CsvDate
62   * @since 4.2 (previously BeanFieldDate since 3.8)
63   */
64  public class ConverterDate extends AbstractCsvConverter {
65  
66      private static final String CSVDATE_NOT_DATE = "csvdate.not.date";
67  
68      /**
69       * The formatter for all inputs to old-style date representations.
70       * <em>It is absolutely critical that access to this member variable is always
71       * synchronized!</em>
72       */
73      private final SimpleDateFormat readSdf;
74  
75      /**
76       * The formatter for all outputs from old-style date representations.
77       * <em>It is absolutely critical that access to this member variable is always
78       * synchronized!</em>
79       */
80      private final SimpleDateFormat writeSdf;
81  
82      /**
83       * The formatter for all inputs to
84       * {@link java.time.temporal.TemporalAccessor} representations.
85       */
86      private final DateTimeFormatter readDtf;
87  
88      /**
89       * The formatter for all outputs from
90       * {@link java.time.temporal.TemporalAccessor} representations.
91       */
92      private final DateTimeFormatter writeDtf;
93  
94      /**
95       * A reference to the function to use when converting from strings to
96       * {@link java.time.temporal.TemporalAccessor}-based values.
97       */
98      private final BiFunction<DateTimeFormatter, String, TemporalAccessor> readTemporalConversionFunction;
99  
100     /**
101      * A reference to the function to use when converting from
102      * {@link java.time.temporal.TemporalAccessor}-based values to strings.
103      */
104     private final BiFunction<DateTimeFormatter, TemporalAccessor, String> writeTemporalConversionFunction;
105 
106     /**
107      * Initializes the class.
108      * This includes initializing the locales for reading and writing, the
109      * format strings for reading and writing, and the chronologies for
110      * reading and writing, all as necessary based on the type to be converted.
111      *
112      * @param type            The type of the field being populated
113      * @param readFormat      The string to use for parsing the date. See
114      *                        {@link com.opencsv.bean.CsvDate#value()}
115      * @param writeFormat     The string to use for formatting the date. See
116      *                        {@link CsvDate#writeFormat()}
117      * @param locale          If not null or empty, specifies the locale used for
118      *                        converting locale-specific data types
119      * @param writeLocale     If not null or empty, specifies the locale used for
120      *                        converting locale-specific data types for writing
121      * @param errorLocale     The locale to use for error messages
122      * @param readChronology  The {@link java.time.chrono.Chronology} to be used
123      *                        for reading if
124      *                        {@link java.time.temporal.TemporalAccessor}-based
125      *                        fields are in use
126      * @param writeChronology The {@link java.time.chrono.Chronology} to be
127      *                        used for writing if
128      *                        {@link java.time.temporal.TemporalAccessor}-based
129      *                        fields are in use
130      */
131     public ConverterDate(Class<?> type, String locale, String writeLocale, Locale errorLocale, String readFormat, String writeFormat, String readChronology, String writeChronology) {
132         super(type, locale, writeLocale, errorLocale);
133 
134         // Chronology
135         Chronology readChrono = getChronology(readChronology, this.locale);
136         Chronology writeChrono = getChronology(writeChronology, this.writeLocale);
137 
138         // Format string, locale, and conversion function for reading
139         try {
140             if (TemporalAccessor.class.isAssignableFrom(type)) {
141                 readSdf = null;
142                 DateTimeFormatter dtfWithoutChronology = setDateTimeFormatter(readFormat, this.locale);
143                 readDtf = dtfWithoutChronology.withChronology(readChrono);
144 
145                 readTemporalConversionFunction = determineReadTemporalConversionFunction(type);
146 
147             } else {
148                 readDtf = null;
149                 readTemporalConversionFunction = null;
150                 readSdf = setDateFormat(readFormat, this.locale);
151             }
152         } catch (IllegalArgumentException e) {
153             CsvBadConverterException csve = new CsvBadConverterException(getClass(), String.format(
154                     ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, this.errorLocale)
155                             .getString("invalid.date.format.string"), readFormat));
156             csve.initCause(e);
157             throw csve;
158         }
159 
160         // Format string, locale, and conversion function for writing
161         try {
162             if (TemporalAccessor.class.isAssignableFrom(type)) {
163                 writeSdf = null;
164                 DateTimeFormatter dtfWithoutChronology = setDateTimeFormatter(writeFormat, this.writeLocale);
165                 writeDtf = dtfWithoutChronology.withChronology(writeChrono);
166                 writeTemporalConversionFunction = determineWriteTemporalConversionFunction(type);
167             } else {
168                 writeDtf = null;
169                 writeTemporalConversionFunction = null;
170                 writeSdf = setDateFormat(writeFormat, this.writeLocale);
171             }
172         } catch (IllegalArgumentException e) {
173             CsvBadConverterException csve = new CsvBadConverterException(getClass(), String.format(
174                     ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, this.errorLocale)
175                             .getString("invalid.date.format.string"), writeFormat));
176             csve.initCause(e);
177             throw csve;
178         }
179     }
180 
181     private BiFunction<DateTimeFormatter, TemporalAccessor, String> determineWriteTemporalConversionFunction(Class<?> type) {
182         if (Instant.class.equals(type)) {
183             return (writeDtf, value) -> {
184                 LocalDateTime ldt = LocalDateTime.ofInstant((Instant) value, ZoneId.of("UTC"));
185                 return writeDtf.format(ldt);
186             };
187         } else {
188             return (writeDtf, value) -> writeDtf.format((TemporalAccessor) value);
189         }
190     }
191 
192     private BiFunction<DateTimeFormatter, String, TemporalAccessor> determineReadTemporalConversionFunction(Class<?> type) {
193 
194         if (TemporalAccessor.class.equals(type)) {
195             return DateTimeFormatter::parse;
196         } else if (ChronoLocalDateTime.class.equals(type)
197                 || LocalDateTime.class.equals(type)) {
198             return (readDtf, s) -> readDtf.parse(s, LocalDateTime::from);
199         } else if (ChronoZonedDateTime.class.equals(type)
200                 || ZonedDateTime.class.equals(type)) {
201             return (readDtf, s) -> readDtf.parse(s, ZonedDateTime::from);
202         } else if (Temporal.class.equals(type)) {
203             return (readDtf, s) -> readDtf.parseBest(s, ZonedDateTime::from,
204                     OffsetDateTime::from, Instant::from,
205                     LocalDateTime::from, LocalDate::from, OffsetTime::from,
206                     LocalTime::from);
207         } else if (Era.class.equals(type) || IsoEra.class.equals(type)) {
208             return (readDtf, s) -> IsoEra.of(readDtf.parse(s).get(ChronoField.ERA));
209         } else if (DayOfWeek.class.equals(type)) {
210             return (readDtf, s) -> readDtf.parse(s, DayOfWeek::from);
211         } else if (HijrahEra.class.equals(type)) {
212             return (readDtf, s) -> HijrahEra.of(readDtf.parse(s).get(ChronoField.ERA));
213         } else if (Instant.class.equals(type)) {
214             return (readDtf, s) -> readDtf.parse(s, Instant::from);
215         } else if (ChronoLocalDate.class.isAssignableFrom(type)) {
216             return (readDtf, s) -> readDtf.parse(s, ChronoLocalDate::from);
217         } else if (JapaneseEra.class.equals(type)) {
218             return (readDtf, s) -> JapaneseEra.of(readDtf.parse(s).get(ChronoField.ERA));
219         } else if (LocalTime.class.equals(type)) {
220             return (readDtf, s) -> readDtf.parse(s, LocalTime::from);
221         } else if (MinguoEra.class.equals(type)) {
222             return (readDtf, s) -> MinguoEra.of(readDtf.parse(s).get(ChronoField.ERA));
223         } else if (Month.class.equals(type)) {
224             return (readDtf, s) -> readDtf.parse(s, Month::from);
225         } else if (MonthDay.class.equals(type)) {
226             return (readDtf, s) -> readDtf.parse(s, MonthDay::from);
227         } else if (OffsetDateTime.class.equals(type)) {
228             return (readDtf, s) -> readDtf.parse(s, OffsetDateTime::from);
229         } else if (OffsetTime.class.equals(type)) {
230             return (readDtf, s) -> readDtf.parse(s, OffsetTime::from);
231         } else if (ThaiBuddhistEra.class.equals(type)) {
232             return (readDtf, s) -> ThaiBuddhistEra.of(readDtf.parse(s).get(ChronoField.ERA));
233         } else if (Year.class.equals(type)) {
234             return (readDtf, s) -> readDtf.parse(s, Year::from);
235         } else if (YearMonth.class.equals(type)) {
236             return (readDtf, s) -> readDtf.parse(s, YearMonth::from);
237         } else if (ZoneOffset.class.equals(type)) {
238             return (readDtf, s) -> readDtf.parse(s, ZoneOffset::from);
239         } else {
240             throw new CsvBadConverterException(getClass(), String.format(
241                     ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, this.errorLocale)
242                             .getString(CSVDATE_NOT_DATE), type));
243         }
244     }
245 
246     private SimpleDateFormat setDateFormat(String format, Locale formatLocale) {
247         if (formatLocale != null) {
248             return new SimpleDateFormat(format, formatLocale);
249         }
250         return new SimpleDateFormat(format);
251     }
252 
253     private DateTimeFormatter setDateTimeFormatter(String format, Locale formatLocale) {
254         if (this.writeLocale != null) {
255             return DateTimeFormatter.ofPattern(format, formatLocale);
256         }
257         return DateTimeFormatter.ofPattern(format);
258     }
259 
260     private Chronology getChronology(String readChronology, Locale locale2) {
261         Chronology readChrono;
262         try {
263             readChrono = StringUtils.isNotBlank(readChronology) ?
264                     Chronology.of(readChronology) :
265                     Chronology.ofLocale(locale2);
266         } catch (DateTimeException e) {
267             CsvBadConverterException csve = new CsvBadConverterException(getClass(),
268                     String.format(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, this.errorLocale)
269                             .getString("chronology.not.found"), readChronology));
270             csve.initCause(e);
271             throw csve;
272         }
273         return readChrono;
274     }
275 
276     @Override
277     public Object convertToRead(String value) throws CsvDataTypeMismatchException {
278         Object returnValue = null;
279         if (StringUtils.isNotBlank(value)) {
280 
281             // Convert Date-based types
282             if (Date.class.isAssignableFrom(type)) {
283                 Date d;
284                 try {
285                     synchronized (readSdf) {
286                         d = readSdf.parse(value);
287                     }
288 
289                     returnValue = type.getConstructor(Long.TYPE).newInstance(d.getTime());
290                 }
291                 // I would have preferred a CsvBeanIntrospectionException, but that
292                 // would have broken backward compatibility. This is not completely
293                 // illogical: I know all of the data types I expect here, and they
294                 // should all be instantiated with no problems. Ergo, this must be
295                 // the wrong data type.
296                 catch (ParseException | InstantiationException
297                         | IllegalAccessException | NoSuchMethodException
298                         | InvocationTargetException e) {
299                     CsvDataTypeMismatchException csve = new CsvDataTypeMismatchException(value, type);
300                     csve.initCause(e);
301                     throw csve;
302                 }
303                 // Convert TemporalAccessor-based types
304             } else if (TemporalAccessor.class.isAssignableFrom(type)) {
305                 try {
306                     returnValue = type.cast(readTemporalConversionFunction.apply(readDtf, value));
307                 } catch (DateTimeException | ArithmeticException e) {
308                     CsvDataTypeMismatchException csve = new CsvDataTypeMismatchException(value, type);
309                     csve.initCause(e);
310                     throw csve;
311                 }
312                 // Convert Calendar-based types
313             } else if (Calendar.class.isAssignableFrom(type)
314                     || XMLGregorianCalendar.class.isAssignableFrom(type)) {
315                 // Parse input
316                 Date d;
317                 try {
318                     synchronized (readSdf) {
319                         d = readSdf.parse(value);
320                     }
321                 } catch (ParseException e) {
322                     CsvDataTypeMismatchException csve = new CsvDataTypeMismatchException(value, type);
323                     csve.initCause(e);
324                     throw csve;
325                 }
326 
327                 // Make a GregorianCalendar out of it, because this works for all
328                 // supported types, at least as an intermediate step.
329                 GregorianCalendar gc = new GregorianCalendar();
330                 gc.setTime(d);
331 
332                 // XMLGregorianCalendar requires special processing.
333                 if (type == XMLGregorianCalendar.class) {
334                     try {
335                         returnValue = type.cast(DatatypeFactory
336                                 .newInstance()
337                                 .newXMLGregorianCalendar(gc));
338                     } catch (DatatypeConfigurationException e) {
339                         // I've never known how to handle this exception elegantly,
340                         // especially since I can't conceive of the circumstances
341                         // under which it is thrown.
342                         CsvDataTypeMismatchException ex = new CsvDataTypeMismatchException(
343                                 ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
344                                         .getString("xmlgregoriancalendar.impossible"));
345                         ex.initCause(e);
346                         throw ex;
347                     }
348                 } else {
349                     returnValue = type.cast(gc);
350                 }
351             } else {
352                 throw new CsvDataTypeMismatchException(value, type, String.format(
353                         ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString(CSVDATE_NOT_DATE), type));
354             }
355         }
356 
357         return returnValue;
358     }
359 
360     /**
361      * This method converts the encapsulated date type to a string, respecting
362      * any locales and conversion patterns that have been set through opencsv
363      * annotations.
364      *
365      * @param value The object containing a date of one of the supported types
366      * @return A string representation of the date. If a
367      * {@link CsvBindByName#locale() locale} or {@link CsvDate#value() conversion
368      * pattern} has been specified through annotations, these are used when
369      * creating the return value.
370      * @throws CsvDataTypeMismatchException If an unsupported type as been
371      *                                      improperly annotated
372      */
373     @Override
374     public String convertToWrite(Object value)
375             throws CsvDataTypeMismatchException {
376         String returnValue = null;
377         if (value != null) {
378 
379             // For Date-based conversions
380             if (Date.class.isAssignableFrom(type)) {
381                 synchronized (writeSdf) {
382                     returnValue = writeSdf.format((Date) value);
383                 }
384                 // For TemporalAccessor-based conversions
385             } else if (TemporalAccessor.class.isAssignableFrom(type)) {
386                 try {
387                     returnValue = writeTemporalConversionFunction.apply(writeDtf, (TemporalAccessor) value);
388                 } catch (DateTimeException | ArithmeticException e) {
389                     CsvDataTypeMismatchException csve = new CsvDataTypeMismatchException(value, type);
390                     csve.initCause(e);
391                     throw csve;
392                 }
393                 // For Calendar-based conversions
394             } else if (Calendar.class.isAssignableFrom(type)
395                     || XMLGregorianCalendar.class.isAssignableFrom(type)) {
396                 Calendar c;
397                 if (value instanceof XMLGregorianCalendar) {
398                     c = ((XMLGregorianCalendar) value).toGregorianCalendar();
399                 } else {
400                     c = (Calendar) value;
401                 }
402                 synchronized (writeSdf) {
403                     returnValue = writeSdf.format(c.getTime());
404                 }
405             } else {
406                 throw new CsvDataTypeMismatchException(value, type, String.format(
407                         ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString(CSVDATE_NOT_DATE), type));
408             }
409         }
410         return returnValue;
411     }
412 }