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