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