/* * Redistribution and use of this software and associated documentation * ("Software"), with or without modification, are permitted provided that the * following conditions are met: * * 1. Redistributions of source code must retain copyright statements and * notices. Redistributions must also contain a copy of this document. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name "Exolab" must not be used to endorse or promote products derived * from this Software without prior written permission of Intalio, Inc. For * written permission, please contact info@exolab.org. * * 4. Products derived from this Software may not be called "Exolab" nor may * "Exolab" appear in their names without prior written permission of Intalio, * Inc. Exolab is a registered trademark of Intalio, Inc. * * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/). * * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * Copyright 1999-2004 (C) Intalio, Inc. All Rights Reserved. * * $Id$ */ package org.exolab.castor.xml.handlers; import java.lang.reflect.Array; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.GregorianCalendar; import java.util.TimeZone; import java.util.Vector; import org.exolab.castor.mapping.FieldHandler; import org.exolab.castor.types.DateTime; import org.exolab.castor.xml.XMLFieldHandler; /** * A specialized FieldHandler for the XML Schema Date/Time related types. * * @author <a href="kvisco-at-intalio.com">Keith Visco</a> * @version $Revision$ $Date: 2005-02-09 13:04:19 -0700 (Wed, 09 Feb * 2005) $ */ public class DateFieldHandler extends XMLFieldHandler { /** The default length of the date string, used by the format method. */ private static final byte DEFAULT_DATE_LENGTH = 25; /** The error message prefix. */ private static final String INVALID_DATE = "Invalid dateTime format: "; /** The default parse options when none are specified. */ private static final ParseOptions DEFAULT_PARSE_OPTIONS = new ParseOptions(); /** The local timezone offset from UTC. */ private static TimeZone _timezone = TimeZone.getDefault(); /** A boolean to indicate that the TimeZone can be suppressed if the TimeZone * is equivalent to the "default" timezone. */ private static boolean _allowTimeZoneSuppression = false; /** if true, milliseconds should be suppressed upon formatting. */ private static boolean _suppressMillis = false; /** The nested FieldHandler. */ private final FieldHandler _handler; /** The current set of parse options. */ private ParseOptions _options = new ParseOptions(); /** A flag to indicate that java.sql.Date should be returned instead. */ private boolean _useSQLDate = false; // ----------------/ // - Constructors -/ // ----------------/ /** * Creates a new DateFieldHandler using the given FieldHandler for * delegation. * * @param fieldHandler the fieldHandler for delegation. */ public DateFieldHandler(final FieldHandler fieldHandler) { if (fieldHandler == null) { String err = "The FieldHandler argument passed to " + "the constructor of DateFieldHandler must not be null."; throw new IllegalArgumentException(err); } _handler = fieldHandler; } // -- DateFieldHandler // ------------------/ // - Public Methods -/ // ------------------/ /** * Returns the value of the field associated with this descriptor from the * given target object. * * @param target the object to get the value from * @return the value of the field associated with this descriptor from the * given target object. */ public Object getValue(final Object target) { Object val = _handler.getValue(target); if (val == null) { return val; } Object formatted = null; Class type = val.getClass(); if (java.util.Date.class.isAssignableFrom(type)) { formatted = format((Date) val); } else if (type.isArray()) { int size = Array.getLength(val); String[] values = new String[size]; for (int i = 0; i < size; i++) { values[i] = format(Array.get(val, i)); } formatted = values; } else if (java.util.Enumeration.class.isAssignableFrom(type)) { Enumeration enumeration = (Enumeration) val; Vector values = new Vector(); while (enumeration.hasMoreElements()) { values.addElement(format(enumeration.nextElement())); } String[] valuesArray = new String[values.size()]; values.copyInto(valuesArray); formatted = valuesArray; } else { formatted = val.toString(); } return formatted; } // -- getValue /** * Sets the value of the field associated with this descriptor. * * @param target the object in which to set the value * @param value the value of the field * @throws IllegalStateException if the value provided cannot be parsed into * a legal date/time. */ public void setValue(final Object target, final Object value) throws java.lang.IllegalStateException { Date date = null; if (value == null || value instanceof Date) { date = (Date) value; } else { try { date = parse(value.toString(), _options); // -- java.sql.Date? if (_useSQLDate && date != null) { date = new java.sql.Date(date.getTime()); } } catch (java.text.ParseException px) { // -- invalid dateTime throw new IllegalStateException(px.getMessage()); } } _handler.setValue(target, date); } // -- setValue /** * Sets the value of the field to a default value. * * @param target The object * @throws IllegalStateException The Java object has changed and is no * longer supported by this handler, or the handler is not * compatiable with the Java object */ public void resetValue(final Object target) throws java.lang.IllegalStateException { _handler.resetValue(target); } /** * Creates a new instance of the object described by this field. * * @param parent The object for which the field is created * @return A new instance of the field's value * @throws IllegalStateException This field is a simple type and cannot be * instantiated */ public Object newInstance(final Object parent) throws IllegalStateException { Object obj = _handler.newInstance(parent); if (obj == null) { obj = new Date(); } return obj; } // -- newInstance /** * Returns true if the given object is an XMLFieldHandler that is equivalent * to the delegated handler. An equivalent XMLFieldHandler is an * XMLFieldHandler that is an instances of the same class. * * @param obj The object to compare against <code>this</code> * @return true if the given object is an XMLFieldHandler that is equivalent * to this one. */ public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (!(obj instanceof FieldHandler)) { return false; } return (_handler.getClass().isInstance(obj) || getClass().isInstance(obj)); } // -- equals /** * Sets whether or not the time zone should always be displayed when * marshaling xsd:dateTime values. If true, then the time zone will not be * displayed if the time zone is the current local time zone. * * @param allowTimeZoneSuppression if true, the time zone will not be * displayed if it is the current local time zone. */ public static void setAllowTimeZoneSuppression(final boolean allowTimeZoneSuppression) { _allowTimeZoneSuppression = allowTimeZoneSuppression; } // -- setAlwaysUseUTCTime /** * Sets the default TimeZone used for comparing dates when marshaling * xsd:dateTime values using this handler. This is used when determining if * the timezone can be omitted when marshaling. * * Default is JVM default returned by TimeZone.getDefault() * * @param timeZone TimeZone to use instead of JVM default * @see #setAllowTimeZoneSuppression */ public static void setDefaultTimeZone(final TimeZone timeZone) { if (timeZone == null) { // -- reset timezone to the default _timezone = TimeZone.getDefault(); } else { _timezone = (TimeZone) timeZone.clone(); } } // -- setDefaultTimeZone /** * Sets a flag indicating whether or not Milliseconds should be suppressed * upon formatting a dateTime as a String. * * @param suppressMillis a boolean when true indicates that millis should be * suppressed during conversion of a dateTime to a String */ public static void setSuppressMillis(final boolean suppressMillis) { _suppressMillis = suppressMillis; } // -- setAlwaysUseUTCTime /** * Specifies that this DateFieldHandler should use java.sql.Date when * creating new Date instances. * * @param useSQLDate a boolean that when true indicates that java.sql.Date * should be used instead of java.util.Date. */ public void setUseSQLDate(final boolean useSQLDate) { _useSQLDate = useSQLDate; _options._allowNoTime = _useSQLDate; } // -- setUseSQLDate // -------------------/ // - Private Methods -/ // -------------------/ /** * Parses the given string, which must be in the following format: * <b>CCYY-MM-DDThh:mm:ss</b> or <b>CCYY-MM-DDThh:mm:ss.sss</b> where "CC" * represents the century, "YY" the year, "MM" the month and "DD" the day. * The letter "T" is the date/time separator and "hh", "mm", "ss" represent * hour, minute and second respectively. * <p> * CCYY represents the Year and each 'C' and 'Y' must be a digit from 0-9. A * minimum of 4 digits must be present. * <p> * MM represents the month and each 'M' must be a digit from 0-9, but * together "MM" must not represent a value greater than 12. "MM" must be 2 * digits, use of leading zero is required for all values less than 10. * <p> * DD represents the day of the month and each 'D' must be a digit from 0-9. * DD must be 2 digits (use a leading zero if necessary) and must not be * greater than 31. * <p> * 'T' is the date/time separator and must exist! * <p> * hh represents the hour using 0-23. mm represents the minute using 0-59. * ss represents the second using 0-60. (60 for leap second) sss represents * the millisecond using 0-999. * * @param dateTime the string to convert to a Date * @return a new Date that represents the given string. * @throws ParseException when the given string does not conform to the * above string. */ protected static Date parse(final String dateTime) throws ParseException { return parse(dateTime, DEFAULT_PARSE_OPTIONS); } // -- parse /** * Parses the given string, which must be in the following format: * <b>CCYY-MM-DDThh:mm:ss</b> or <b>CCYY-MM-DDThh:mm:ss.sss</b> where "CC" * represents the century, "YY" the year, "MM" the month and "DD" the day. * The letter "T" is the date/time separator and "hh", "mm", "ss" represent * hour, minute and second respectively. * <p> * CCYY represents the Year and each 'C' and 'Y' must be a digit from 0-9. A * minimum of 4 digits must be present. * <p> * MM represents the month and each 'M' must be a digit from 0-9, but * together "MM" must not represent a value greater than 12. "MM" must be 2 * digits, use of leading zero is required for all values less than 10. * <p> * DD represents the day of the month and each 'D' must be a digit from 0-9. * DD must be 2 digits (use a leading zero if necessary) and must not be * greater than 31. * <p> * 'T' is the date/time separator and must exist! * <p> * hh represents the hour using 0-23. mm represents the minute using 0-59. * ss represents the second using 0-60. (60 for leap second) sss represents * the millisecond using 0-999. * * @param dateTime the string to convert to a Date * @param options the parsing options to use * @return a new Date that represents the given string. * @throws ParseException when the given string does not conform to the * above string. */ protected static Date parse(final String dateTime, final ParseOptions options) throws ParseException { if (dateTime == null) { throw new ParseException(INVALID_DATE + "null", 0); } ParseOptions pOptions = (options != null) ? options : DEFAULT_PARSE_OPTIONS; String trimmed = dateTime.trim(); // If no time is present and we don't require time, use org.exolab.castor.types.Date if (pOptions._allowNoTime && trimmed.indexOf('T') == -1) { org.exolab.castor.types.Date parsedDate = new org.exolab.castor.types.Date(trimmed); return parsedDate.toDate(); } DateTime parsedDateTime = new DateTime(trimmed); return parsedDateTime.toDate(); } // -- parse /** * Returns the given date in a String format, using the ISO8601 format as * specified in the W3C XML Schema 1.0 Recommendation (Part 2: Datatypes) * for dateTime. * * @param date the Date to format * @return the formatted string */ protected static String format(final Date date) { final SimpleDateFormat formatter; if (_suppressMillis) { formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); } else { formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); } /* ensure the formatter does not use the default system timezone */ formatter.setTimeZone(_timezone); GregorianCalendar cal = new GregorianCalendar(); cal.setTime(date); cal.setTimeZone(_timezone); StringBuffer buffer = new StringBuffer(DEFAULT_DATE_LENGTH); if (cal.get(Calendar.ERA) == GregorianCalendar.BC) { buffer.append("-"); } buffer.append(formatter.format(date)); formatTimeZone(cal, buffer); return buffer.toString(); } // -- format /** * Format the time zone information (only) from the provided Calendar. * @param cal a calendar containing a time and time zone * @param buffer the StringBuffer to which to format the time zone */ private static void formatTimeZone(final Calendar cal, final StringBuffer buffer) { int value = cal.get(Calendar.ZONE_OFFSET); int dstOffset = cal.get(Calendar.DST_OFFSET); if (value == 0 && dstOffset == 0) { buffer.append('Z'); // UTC return; } if (_allowTimeZoneSuppression && value == _timezone.getRawOffset()) { return; } // -- adjust for Daylight Savings Time value = value + dstOffset; if (value > 0) { buffer.append('+'); } else { value = -value; buffer.append('-'); } // -- convert to minutes from milliseconds int minutes = value / 60000; // -- hours: hh value = minutes / 60; if (value < 10) { buffer.append('0'); } buffer.append(value); buffer.append(':'); // -- remaining minutes: mm value = minutes % 60; if (value < 10) { buffer.append('0'); } buffer.append(value); } /** * Formats the given object. If the object is a java.util.Date, it will be * formatted by a call to {@link #format(Date)}, otherwise the toString() * method is called on the object. * @param object The object to be formatted * * @return the formatted object. */ private static String format(final Object object) { if (object == null) { return null; } if (object instanceof java.util.Date) { return format((Date) object); } return object.toString(); } //-- format /** * A class for controlling the parse options. There is currently only one * parse option. */ static class ParseOptions { /** If true and the 'T' field is not present, a xsd:date is parsed, else xsd:dateTime. */ public boolean _allowNoTime = false; } } //-- DateFieldHandler