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