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