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.CsvDataTypeMismatchException;
20  import org.apache.commons.lang3.StringUtils;
21  
22  import javax.xml.datatype.DatatypeConfigurationException;
23  import javax.xml.datatype.DatatypeFactory;
24  import javax.xml.datatype.XMLGregorianCalendar;
25  import java.lang.reflect.Field;
26  import java.lang.reflect.InvocationTargetException;
27  import java.text.ParseException;
28  import java.text.SimpleDateFormat;
29  import java.util.*;
30  
31  /**
32   * This class converts an input to a date type.
33   * I would dearly love to use Apache Commons BeanUtils to make this class smaller
34   * and easier, but BeanUtils is abysmal with dates of all types.
35   * 
36   * @param <T> Type of the bean to be manipulated
37   * 
38   * @author Andrew Rucker Jones
39   * @since 3.8
40   * @see com.opencsv.bean.CsvDate
41   */
42  public class BeanFieldDate<T> extends AbstractBeanField<T> {
43  
44      private final String formatString;
45      private final String locale;
46  
47      /**
48       * @param field        A {@link java.lang.reflect.Field} object.
49       * @param required     True if the field is required to contain a value, false
50       *                     if it is allowed to be null or a blank string.
51       * @param formatString The string to use for formatting the date. See
52       *                     {@link com.opencsv.bean.CsvDate#value()}
53       * @param locale       If not null or empty, specifies the locale used for
54       *                     converting locale-specific data types
55       * @param errorLocale The locale to use for error messages.
56       */
57      public BeanFieldDate(Field field, boolean required, String formatString, String locale, Locale errorLocale) {
58          super(field, required, errorLocale);
59          this.formatString = formatString;
60          this.locale = locale;
61      }
62      
63      /**
64       * @return A {@link java.text.SimpleDateFormat} primed with the proper
65       *   format string and a locale, if one has been set.
66       */
67      private SimpleDateFormat getFormat() {
68          SimpleDateFormat sdf;
69          if (StringUtils.isNotEmpty(locale)) {
70              Locale l = Locale.forLanguageTag(locale);
71              sdf = new SimpleDateFormat(formatString, l);
72          } else {
73              sdf = new SimpleDateFormat(formatString);
74          }
75          return sdf;
76      }
77      
78      /**
79       * Converts the input to/from a date object.
80       * <p>This method should work with any type derived from {@link java.util.Date}
81       * as long as it has a constructor taking one long that specifies the number
82       * of milliseconds since the epoch. The following types are explicitly
83       * supported:
84       * <ul><li>java.util.Date</li>
85       * <li>java.sql.Date</li>
86       * <li>java.sql.Time</li>
87       * <li>java.sql.Timestamp</li></ul></p>
88       *
89       * @param <U> The type to be converted to
90       * @param value The string to be converted into a date/time type or vice
91       *   versa
92       * @param fieldType The class of the destination field
93       * @return The object resulting from the conversion
94       * @throws CsvDataTypeMismatchException If the conversion fails
95       */
96      private <U> U convertDate(Object value, Class<U> fieldType)
97              throws CsvDataTypeMismatchException {
98          U o;
99  
100         if(value instanceof String) {
101             Date d;
102             try {
103                 d = getFormat().parse((String)value);
104                 o = fieldType.getConstructor(Long.TYPE).newInstance(d.getTime());
105             }
106             // I would have prefered a CsvBeanIntrospectionException, but that
107             // would have broken backward compatibility. This is not completely
108             // illogical: I know all of the data types I expect here, and they
109             // should all be instantiated with no problems. Ergo, this must be
110             // the wrong data type.
111             catch(ParseException | InstantiationException
112                     | IllegalAccessException | NoSuchMethodException
113                     | InvocationTargetException e) {
114                 CsvDataTypeMismatchException csve = new CsvDataTypeMismatchException(value, fieldType);
115                 csve.initCause(e);
116                 throw csve;
117             }
118         }
119         else if(Date.class.isAssignableFrom(value.getClass())) {
120             o = fieldType.cast(getFormat().format((Date)value));
121         }
122         else {
123             throw new CsvDataTypeMismatchException(value, fieldType,
124                     ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("csvdate.not.date"));
125         }
126         
127         return o;
128     }
129     
130     /**
131      * Converts the input to/from a calendar object.
132      * <p>This method should work for any type that implements
133      * {@link java.util.Calendar} or is derived from
134      * {@link javax.xml.datatype.XMLGregorianCalendar}. The following types are
135      * explicitly supported:
136      * <ul><li>Calendar (always a GregorianCalendar)</li>
137      * <li>GregorianCalendar</li>
138      * <li>XMLGregorianCalendar</li></ul>
139      * It is also known to work with
140      * org.apache.xerces.jaxp.datatype.XMLGregorianCalendarImpl.</p>
141      *
142      * @param <U> The type to be converted to
143      * @param value The string to be converted into a date/time type or vice
144      *   versa
145      * @param fieldType The class of the destination field
146      * @return The object resulting from the conversion
147      * @throws CsvDataTypeMismatchException If the conversion fails
148      */
149     private <U> U convertCalendar(Object value, Class<U> fieldType)
150             throws CsvDataTypeMismatchException {
151         U o;
152 
153         if(value instanceof String) {
154             // Parse input
155             Date d;
156             try {
157                 d = getFormat().parse((String)value);
158             } catch (ParseException e) {
159                 CsvDataTypeMismatchException csve = new CsvDataTypeMismatchException(value, fieldType);
160                 csve.initCause(e);
161                 throw csve;
162             }
163 
164             // Make a GregorianCalendar out of it, because this works for all
165             // supported types, at least as an intermediate step.
166             GregorianCalendar gc = new GregorianCalendar();
167             gc.setTime(d);
168 
169             // XMLGregorianCalendar requires special processing.
170             if (fieldType == XMLGregorianCalendar.class) {
171                 try {
172                     o = fieldType.cast(DatatypeFactory
173                             .newInstance()
174                             .newXMLGregorianCalendar(gc));
175                 } catch (DatatypeConfigurationException e) {
176                     // I've never known how to handle this exception elegantly,
177                     // especially since I can't conceive of the circumstances
178                     // under which it is thrown.
179                     CsvDataTypeMismatchException ex = new CsvDataTypeMismatchException(
180                             ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("xmlgregoriancalendar.impossible"));
181                     ex.initCause(e);
182                     throw ex;
183                 }
184             }
185             else {
186                 o = fieldType.cast(gc);
187             }
188         }
189         else {
190             Calendar c;
191             if(value instanceof XMLGregorianCalendar) {
192                 c = ((XMLGregorianCalendar)value).toGregorianCalendar();
193             }
194             else if (value instanceof Calendar) {
195                 c = (Calendar)value;
196             }
197             else {
198                 throw new CsvDataTypeMismatchException(value, fieldType,
199                         ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("csvdate.not.date"));
200             }
201             o = fieldType.cast(getFormat().format(c.getTime()));
202         }
203 
204         return o;
205     }
206 
207     /**
208      * Splits the conversion into date-based and calendar-based.
209      * 
210      * @param <U> The type to be converted to
211      * @param value The string to be converted into a date/time type or vice
212      *   versa
213      * @param fieldType The class of the destination field
214      * @return The object resulting from the conversion
215      * @throws CsvDataTypeMismatchException If a non-convertable type is
216      *                                      passed in, or if the conversion fails
217      */
218     private <U> U convertCommon(Object value, Class<U> fieldType)
219             throws CsvDataTypeMismatchException {
220         U o;
221         Class conversionClass = (fieldType == String.class)?value.getClass():fieldType;
222         
223         // Send to the proper submethod
224         if (Date.class.isAssignableFrom(conversionClass)) {
225             o = convertDate(value, fieldType);
226         } else if (Calendar.class.isAssignableFrom(conversionClass)
227                 || XMLGregorianCalendar.class.isAssignableFrom(conversionClass)) {
228             o = convertCalendar(value, fieldType);
229         } else {
230             throw new CsvDataTypeMismatchException(value, fieldType,
231                     ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("csvdate.not.date"));
232         }
233         
234         return o;
235     }
236 
237     @Override
238     protected Object convert(String value) throws CsvDataTypeMismatchException {
239         return StringUtils.isBlank(value) ? null : convertCommon(value, field.getType());
240     }
241 
242     /**
243      * This method converts the encapsulated date type to a string, respecting
244      * any locales and conversion patterns that have been set through opencsv
245      * annotations.
246      * 
247      * @param value The object containing a date of one of the supported types
248      * @return A string representation of the date. If a
249      *   {@link CsvBindByName#locale() locale} or {@link CsvDate#value() conversion
250      *   pattern} has been specified through annotations, these are used when
251      *   creating the return value.
252      * @throws CsvDataTypeMismatchException If an unsupported type as been
253      *   improperly annotated
254      */
255     @Override
256     protected String convertToWrite(Object value)
257             throws CsvDataTypeMismatchException {
258         return value == null ? null : convertCommon(value, String.class);
259     }
260 }