View Javadoc
1   /*
2    * Redistribution and use of this software and associated documentation ("Software"), with or
3    * without modification, are permitted provided that the following conditions are met:
4    *
5    * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
6    * must also contain a copy of this document.
7    *
8    * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
9    * conditions and the following disclaimer in the documentation and/or other materials provided with
10   * the distribution.
11   *
12   * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
13   * without prior written permission of Intalio, Inc. For written permission, please contact
14   * info@exolab.org.
15   *
16   * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
17   * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
18   * Intalio, Inc.
19   *
20   * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
21   *
22   * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
23   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
24   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
25   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28   * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
29   * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30   *
31   * Copyright 1999-2004 (C) Intalio, Inc. All Rights Reserved.
32   *
33   * $Id$
34   */
35  package org.exolab.castor.xml.handlers;
36  
37  import java.lang.reflect.Array;
38  import java.text.ParseException;
39  import java.text.SimpleDateFormat;
40  import java.util.ArrayList;
41  import java.util.Calendar;
42  import java.util.Date;
43  import java.util.Enumeration;
44  import java.util.GregorianCalendar;
45  import java.util.List;
46  import java.util.TimeZone;
47  
48  import org.exolab.castor.mapping.FieldHandler;
49  import org.exolab.castor.types.DateTime;
50  import org.exolab.castor.xml.XMLFieldHandler;
51  
52  /**
53   * A specialized FieldHandler for the XML Schema Date/Time related types.
54   *
55   * @author <a href="kvisco-at-intalio.com">Keith Visco</a>
56   * @version $Revision$ $Date: 2005-02-09 13:04:19 -0700 (Wed, 09 Feb 2005) $
57   */
58  public class DateFieldHandler extends XMLFieldHandler {
59  
60    /** The default length of the date string, used by the format method. */
61    private static final byte DEFAULT_DATE_LENGTH = 25;
62    /** The error message prefix. */
63    private static final String INVALID_DATE = "Invalid dateTime format: ";
64    /** The default parse options when none are specified. */
65    private static final ParseOptions DEFAULT_PARSE_OPTIONS = new ParseOptions();
66  
67    /** The local timezone offset from UTC. */
68    private static TimeZone _timezone = TimeZone.getDefault();
69    /**
70     * A boolean to indicate that the TimeZone can be suppressed if the TimeZone is equivalent to the
71     * "default" timezone.
72     */
73    private static boolean _allowTimeZoneSuppression = false;
74    /** if true, milliseconds should be suppressed upon formatting. */
75    private static boolean _suppressMillis = false;
76  
77    /** The nested FieldHandler. */
78    private final FieldHandler _handler;
79    /** The current set of parse options. */
80    private ParseOptions _options = new ParseOptions();
81    /** A flag to indicate that java.sql.Date should be returned instead. */
82    private boolean _useSQLDate = false;
83  
84    // ----------------/
85    // - Constructors -/
86    // ----------------/
87  
88    /**
89     * Creates a new DateFieldHandler using the given FieldHandler for delegation.
90     *
91     * @param fieldHandler the fieldHandler for delegation.
92     */
93    public DateFieldHandler(final FieldHandler fieldHandler) {
94      if (fieldHandler == null) {
95        String err = "The FieldHandler argument passed to "
96            + "the constructor of DateFieldHandler must not be null.";
97        throw new IllegalArgumentException(err);
98      }
99      _handler = fieldHandler;
100   } // -- DateFieldHandler
101 
102   // ------------------/
103   // - Public Methods -/
104   // ------------------/
105 
106   /**
107    * Returns the value of the field associated with this descriptor from the given target object.
108    *
109    * @param target the object to get the value from
110    * @return the value of the field associated with this descriptor from the given target object.
111    */
112   public Object getValue(final Object target) {
113     Object val = _handler.getValue(target);
114     if (val == null) {
115       return val;
116     }
117 
118     Object formatted = null;
119 
120     Class type = val.getClass();
121 
122     if (java.util.Date.class.isAssignableFrom(type)) {
123       formatted = format((Date) val);
124     } else if (type.isArray()) {
125       int size = Array.getLength(val);
126       String[] values = new String[size];
127       for (int i = 0; i < size; i++) {
128         values[i] = format(Array.get(val, i));
129       }
130       formatted = values;
131     } else if (java.util.Enumeration.class.isAssignableFrom(type)) {
132       Enumeration enumeration = (Enumeration) val;
133       List<String> values = new ArrayList<>();
134       while (enumeration.hasMoreElements()) {
135         values.add(format(enumeration.nextElement()));
136       }
137       formatted = values.toArray(new String[values.size()]);
138     } else {
139       formatted = val.toString();
140     }
141     return formatted;
142   } // -- getValue
143 
144   /**
145    * Sets the value of the field associated with this descriptor.
146    *
147    * @param target the object in which to set the value
148    * @param value the value of the field
149    * @throws IllegalStateException if the value provided cannot be parsed into a legal date/time.
150    */
151   public void setValue(final Object target, final Object value)
152       throws java.lang.IllegalStateException {
153     Date date = null;
154 
155     if (value == null || value instanceof Date) {
156       date = (Date) value;
157     } else {
158       try {
159         date = parse(value.toString(), _options);
160         // -- java.sql.Date?
161         if (_useSQLDate && date != null) {
162           date = new java.sql.Date(date.getTime());
163         }
164       } catch (java.text.ParseException px) {
165         // -- invalid dateTime
166         throw new IllegalStateException(px.getMessage());
167       }
168     }
169 
170     _handler.setValue(target, date);
171   } // -- setValue
172 
173   /**
174    * Sets the value of the field to a default value.
175    *
176    * @param target The object
177    * @throws IllegalStateException The Java object has changed and is no longer supported by this
178    *         handler, or the handler is not compatiable with the Java object
179    */
180   public void resetValue(final Object target) throws java.lang.IllegalStateException {
181     _handler.resetValue(target);
182   }
183 
184   /**
185    * Creates a new instance of the object described by this field.
186    *
187    * @param parent The object for which the field is created
188    * @return A new instance of the field's value
189    * @throws IllegalStateException This field is a simple type and cannot be instantiated
190    */
191   public Object newInstance(final Object parent) throws IllegalStateException {
192     Object obj = _handler.newInstance(parent);
193     if (obj == null) {
194       obj = new Date();
195     }
196     return obj;
197   } // -- newInstance
198 
199   /**
200    * Returns true if the given object is an XMLFieldHandler that is equivalent to the delegated
201    * handler. An equivalent XMLFieldHandler is an XMLFieldHandler that is an instances of the same
202    * class.
203    *
204    * @param obj The object to compare against <code>this</code>
205    * @return true if the given object is an XMLFieldHandler that is equivalent to this one.
206    */
207   public boolean equals(final Object obj) {
208     if (obj == null) {
209       return false;
210     }
211     if (obj == this) {
212       return true;
213     }
214     if (!(obj instanceof FieldHandler)) {
215       return false;
216     }
217     return (_handler.getClass().isInstance(obj) || getClass().isInstance(obj));
218   } // -- equals
219 
220   /**
221    * Sets whether or not the time zone should always be displayed when marshaling xsd:dateTime
222    * values. If true, then the time zone will not be displayed if the time zone is the current local
223    * time zone.
224    *
225    * @param allowTimeZoneSuppression if true, the time zone will not be displayed if it is the
226    *        current local time zone.
227    */
228   public static void setAllowTimeZoneSuppression(final boolean allowTimeZoneSuppression) {
229     _allowTimeZoneSuppression = allowTimeZoneSuppression;
230   } // -- setAlwaysUseUTCTime
231 
232   /**
233    * Sets the default TimeZone used for comparing dates when marshaling xsd:dateTime values using
234    * this handler. This is used when determining if the timezone can be omitted when marshaling.
235    *
236    * Default is JVM default returned by TimeZone.getDefault()
237    *
238    * @param timeZone TimeZone to use instead of JVM default
239    * @see #setAllowTimeZoneSuppression
240    */
241   public static void setDefaultTimeZone(final TimeZone timeZone) {
242     if (timeZone == null) {
243       // -- reset timezone to the default
244       _timezone = TimeZone.getDefault();
245     } else {
246       _timezone = (TimeZone) timeZone.clone();
247     }
248   } // -- setDefaultTimeZone
249 
250   /**
251    * Sets a flag indicating whether or not Milliseconds should be suppressed upon formatting a
252    * dateTime as a String.
253    *
254    * @param suppressMillis a boolean when true indicates that millis should be suppressed during
255    *        conversion of a dateTime to a String
256    */
257   public static void setSuppressMillis(final boolean suppressMillis) {
258     _suppressMillis = suppressMillis;
259   } // -- setAlwaysUseUTCTime
260 
261   /**
262    * Specifies that this DateFieldHandler should use java.sql.Date when creating new Date instances.
263    *
264    * @param useSQLDate a boolean that when true indicates that java.sql.Date should be used instead
265    *        of java.util.Date.
266    */
267   public void setUseSQLDate(final boolean useSQLDate) {
268     _useSQLDate = useSQLDate;
269     _options._allowNoTime = _useSQLDate;
270   } // -- setUseSQLDate
271 
272   // -------------------/
273   // - Private Methods -/
274   // -------------------/
275 
276   /**
277    * Parses the given string, which must be in the following format: <b>CCYY-MM-DDThh:mm:ss</b> or
278    * <b>CCYY-MM-DDThh:mm:ss.sss</b> where "CC" represents the century, "YY" the year, "MM" the month
279    * and "DD" the day. The letter "T" is the date/time separator and "hh", "mm", "ss" represent
280    * hour, minute and second respectively.
281    * <p>
282    * CCYY represents the Year and each 'C' and 'Y' must be a digit from 0-9. A minimum of 4 digits
283    * must be present.
284    * <p>
285    * MM represents the month and each 'M' must be a digit from 0-9, but together "MM" must not
286    * represent a value greater than 12. "MM" must be 2 digits, use of leading zero is required for
287    * all values less than 10.
288    * <p>
289    * DD represents the day of the month and each 'D' must be a digit from 0-9. DD must be 2 digits
290    * (use a leading zero if necessary) and must not be greater than 31.
291    * <p>
292    * 'T' is the date/time separator and must exist!
293    * <p>
294    * hh represents the hour using 0-23. mm represents the minute using 0-59. ss represents the
295    * second using 0-60. (60 for leap second) sss represents the millisecond using 0-999.
296    *
297    * @param dateTime the string to convert to a Date
298    * @return a new Date that represents the given string.
299    * @throws ParseException when the given string does not conform to the above string.
300    */
301   protected static Date parse(final String dateTime) throws ParseException {
302     return parse(dateTime, DEFAULT_PARSE_OPTIONS);
303   } // -- parse
304 
305   /**
306    * Parses the given string, which must be in the following format: <b>CCYY-MM-DDThh:mm:ss</b> or
307    * <b>CCYY-MM-DDThh:mm:ss.sss</b> where "CC" represents the century, "YY" the year, "MM" the month
308    * and "DD" the day. The letter "T" is the date/time separator and "hh", "mm", "ss" represent
309    * hour, minute and second respectively.
310    * <p>
311    * CCYY represents the Year and each 'C' and 'Y' must be a digit from 0-9. A minimum of 4 digits
312    * must be present.
313    * <p>
314    * MM represents the month and each 'M' must be a digit from 0-9, but together "MM" must not
315    * represent a value greater than 12. "MM" must be 2 digits, use of leading zero is required for
316    * all values less than 10.
317    * <p>
318    * DD represents the day of the month and each 'D' must be a digit from 0-9. DD must be 2 digits
319    * (use a leading zero if necessary) and must not be greater than 31.
320    * <p>
321    * 'T' is the date/time separator and must exist!
322    * <p>
323    * hh represents the hour using 0-23. mm represents the minute using 0-59. ss represents the
324    * second using 0-60. (60 for leap second) sss represents the millisecond using 0-999.
325    *
326    * @param dateTime the string to convert to a Date
327    * @param options the parsing options to use
328    * @return a new Date that represents the given string.
329    * @throws ParseException when the given string does not conform to the above string.
330    */
331   protected static Date parse(final String dateTime, final ParseOptions options)
332       throws ParseException {
333     if (dateTime == null) {
334       throw new ParseException(INVALID_DATE + "null", 0);
335     }
336 
337     ParseOptions pOptions = (options != null) ? options : DEFAULT_PARSE_OPTIONS;
338 
339     String trimmed = dateTime.trim();
340 
341     // If no time is present and we don't require time, use org.exolab.castor.types.Date
342     if (pOptions._allowNoTime && trimmed.indexOf('T') == -1) {
343       org.exolab.castor.types.Date parsedDate = new org.exolab.castor.types.Date(trimmed);
344       return parsedDate.toDate();
345     }
346 
347     DateTime parsedDateTime = new DateTime(trimmed);
348     return parsedDateTime.toDate();
349   } // -- parse
350 
351   /**
352    * Returns the given date in a String format, using the ISO8601 format as specified in the W3C XML
353    * Schema 1.0 Recommendation (Part 2: Datatypes) for dateTime.
354    *
355    * @param date the Date to format
356    * @return the formatted string
357    */
358   protected static String format(final Date date) {
359     final SimpleDateFormat formatter;
360     if (_suppressMillis) {
361       formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
362     } else {
363       formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
364     }
365 
366     /* ensure the formatter does not use the default system timezone */
367     formatter.setTimeZone(_timezone);
368 
369     GregorianCalendar cal = new GregorianCalendar();
370     cal.setTime(date);
371     cal.setTimeZone(_timezone);
372 
373     StringBuilder buffer = new StringBuilder(DEFAULT_DATE_LENGTH);
374     if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
375       buffer.append('-');
376     }
377 
378     buffer.append(formatter.format(date));
379     formatTimeZone(cal, buffer);
380     return buffer.toString();
381   } // -- format
382 
383   /**
384    * Format the time zone information (only) from the provided Calendar.
385    * 
386    * @param cal a calendar containing a time and time zone
387    * @param buffer the StringBuilder to which to format the time zone
388    */
389   private static void formatTimeZone(final Calendar cal, final StringBuilder buffer) {
390     int value = cal.get(Calendar.ZONE_OFFSET);
391     int dstOffset = cal.get(Calendar.DST_OFFSET);
392 
393     if (value == 0 && dstOffset == 0) {
394       buffer.append('Z'); // UTC
395       return;
396     }
397 
398     if (_allowTimeZoneSuppression && value == _timezone.getRawOffset()) {
399       return;
400     }
401 
402     // -- adjust for Daylight Savings Time
403     value = value + dstOffset;
404 
405     if (value > 0) {
406       buffer.append('+');
407     } else {
408       value = -value;
409       buffer.append('-');
410     }
411 
412     // -- convert to minutes from milliseconds
413     int minutes = value / 60000;
414 
415     // -- hours: hh
416     value = minutes / 60;
417     if (value < 10) {
418       buffer.append('0');
419     }
420     buffer.append(value);
421     buffer.append(':');
422 
423     // -- remaining minutes: mm
424     value = minutes % 60;
425     if (value < 10) {
426       buffer.append('0');
427     }
428     buffer.append(value);
429   }
430 
431   /**
432    * Formats the given object. If the object is a java.util.Date, it will be formatted by a call to
433    * {@link #format(Date)}, otherwise the toString() method is called on the object.
434    * 
435    * @param object The object to be formatted
436    *
437    * @return the formatted object.
438    */
439   private static String format(final Object object) {
440     if (object == null) {
441       return null;
442     }
443     if (object instanceof java.util.Date) {
444       return format((Date) object);
445     }
446     return object.toString();
447   } // -- format
448 
449   /**
450    * A class for controlling the parse options. There is currently only one parse option.
451    */
452   static class ParseOptions {
453     /** If true and the 'T' field is not present, a xsd:date is parsed, else xsd:dateTime. */
454     public boolean _allowNoTime = false;
455   }
456 
457 } // -- DateFieldHandler