michael@0: /** michael@0: * Copyright (c) 2012, Ben Fortuna michael@0: * All rights reserved. michael@0: * michael@0: * Redistribution and use in source and binary forms, with or without michael@0: * modification, are permitted provided that the following conditions michael@0: * are met: michael@0: * michael@0: * o Redistributions of source code must retain the above copyright michael@0: * notice, this list of conditions and the following disclaimer. michael@0: * michael@0: * o Redistributions in binary form must reproduce the above copyright michael@0: * notice, this list of conditions and the following disclaimer in the michael@0: * documentation and/or other materials provided with the distribution. michael@0: * michael@0: * o Neither the name of Ben Fortuna nor the names of any other contributors michael@0: * may be used to endorse or promote products derived from this software michael@0: * without specific prior written permission. michael@0: * michael@0: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS michael@0: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT michael@0: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR michael@0: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR michael@0: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, michael@0: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, michael@0: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR michael@0: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF michael@0: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING michael@0: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS michael@0: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. michael@0: */ michael@0: package net.fortuna.ical4j.model; michael@0: michael@0: import java.text.DateFormat; michael@0: import java.text.ParseException; michael@0: import java.text.SimpleDateFormat; michael@0: import java.util.Map; michael@0: import java.util.WeakHashMap; michael@0: michael@0: import net.fortuna.ical4j.util.CompatibilityHints; michael@0: import net.fortuna.ical4j.util.Dates; michael@0: import net.fortuna.ical4j.util.TimeZones; michael@0: michael@4: import org.apache.commons.lang3.builder.EqualsBuilder; michael@0: michael@0: /** michael@0: * $Id$ michael@0: * michael@0: * Created on 26/06/2005 michael@0: * michael@0: * Represents a time of day on a specific date. michael@0: * michael@0: *
michael@0: * 4.3.5 Date-Time michael@0: * michael@0: * Value Name: DATE-TIME michael@0: * michael@0: * Purpose: This value type is used to identify values that specify a michael@0: * precise calendar date and time of day. michael@0: * michael@0: * Formal Definition: The value type is defined by the following michael@0: * notation: michael@0: * michael@0: * date-time = date "T" time ;As specified in the date and time michael@0: * ;value definitions michael@0: * michael@0: * Description: If the property permits, multiple "date-time" values are michael@0: * specified as a COMMA character (US-ASCII decimal 44) separated list michael@0: * of values. No additional content value encoding (i.e., BACKSLASH michael@0: * character encoding) is defined for this value type. michael@0: * michael@0: * The "DATE-TIME" data type is used to identify values that contain a michael@0: * precise calendar date and time of day. The format is based on the michael@0: * [ISO 8601] complete representation, basic format for a calendar date michael@0: * and time of day. The text format is a concatenation of the "date", michael@0: * followed by the LATIN CAPITAL LETTER T character (US-ASCII decimal michael@0: * 84) time designator, followed by the "time" format. michael@0: * michael@0: * The "DATE-TIME" data type expresses time values in three forms: michael@0: * michael@0: * The form of date and time with UTC offset MUST NOT be used. For michael@0: * example, the following is not valid for a date-time value: michael@0: * michael@0: * DTSTART:19980119T230000-0800 ;Invalid time format michael@0: * michael@0: * FORM #1: DATE WITH LOCAL TIME michael@0: * michael@0: * The date with local time form is simply a date-time value that does michael@0: * not contain the UTC designator nor does it reference a time zone. For michael@0: * example, the following represents Janurary 18, 1998, at 11 PM: michael@0: * michael@0: * DTSTART:19980118T230000 michael@0: * michael@0: * Date-time values of this type are said to be "floating" and are not michael@0: * bound to any time zone in particular. They are used to represent the michael@0: * same hour, minute, and second value regardless of which time zone is michael@0: * currently being observed. For example, an event can be defined that michael@0: * indicates that an individual will be busy from 11:00 AM to 1:00 PM michael@0: * every day, no matter which time zone the person is in. In these michael@0: * cases, a local time can be specified. The recipient of an iCalendar michael@0: * object with a property value consisting of a local time, without any michael@0: * relative time zone information, SHOULD interpret the value as being michael@0: * fixed to whatever time zone the ATTENDEE is in at any given moment. michael@0: * This means that two ATTENDEEs, in different time zones, receiving the michael@0: * same event definition as a floating time, may be participating in the michael@0: * event at different actual times. Floating time SHOULD only be used michael@0: * where that is the reasonable behavior. michael@0: * michael@0: * In most cases, a fixed time is desired. To properly communicate a michael@0: * fixed time in a property value, either UTC time or local time with michael@0: * time zone reference MUST be specified. michael@0: * michael@0: * The use of local time in a DATE-TIME value without the TZID property michael@0: * parameter is to be interpreted as floating time, regardless of the michael@0: * existence of "VTIMEZONE" calendar components in the iCalendar object. michael@0: * michael@0: * FORM #2: DATE WITH UTC TIME michael@0: * michael@0: * The date with UTC time, or absolute time, is identified by a LATIN michael@0: * CAPITAL LETTER Z suffix character (US-ASCII decimal 90), the UTC michael@0: * designator, appended to the time value. For example, the following michael@0: * represents January 19, 1998, at 0700 UTC: michael@0: * michael@0: * DTSTART:19980119T070000Z michael@0: * michael@0: * The TZID property parameter MUST NOT be applied to DATE-TIME michael@0: * properties whose time values are specified in UTC. michael@0: * michael@0: * FORM #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE michael@0: * michael@0: * The date and local time with reference to time zone information is michael@0: * identified by the use the TZID property parameter to reference the michael@0: * appropriate time zone definition. TZID is discussed in detail in the michael@0: * section on Time Zone. For example, the following represents 2 AM in michael@0: * New York on Janurary 19, 1998: michael@0: * michael@0: * DTSTART;TZID=US-Eastern:19980119T020000 michael@0: * michael@0: * Example: The following represents July 14, 1997, at 1:30 PM in New michael@0: * York City in each of the three time formats, using the "DTSTART" michael@0: * property. michael@0: * michael@0: * DTSTART:19970714T133000 ;Local time michael@0: * DTSTART:19970714T173000Z ;UTC time michael@0: * DTSTART;TZID=US-Eastern:19970714T133000 ;Local time and time michael@0: * ; zone reference michael@0: * michael@0: * A time value MUST ONLY specify 60 seconds when specifying the michael@0: * periodic "leap second" in the time value. For example: michael@0: * michael@0: * COMPLETED:19970630T235960Z michael@0: *michael@0: * michael@0: * @author Ben Fortuna michael@0: */ michael@0: public class DateTime extends Date { michael@0: michael@0: private static final long serialVersionUID = -6407231357919440387L; michael@0: michael@0: private static final String DEFAULT_PATTERN = "yyyyMMdd'T'HHmmss"; michael@0: michael@0: private static final String UTC_PATTERN = "yyyyMMdd'T'HHmmss'Z'"; michael@3: michael@3: private static final String VCARD_PATTERN = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"; michael@0: michael@0: private static final String RELAXED_PATTERN = "yyyyMMdd"; michael@0: michael@0: /** michael@0: * Used for parsing times in a UTC date-time representation. michael@0: */ michael@0: private static final DateFormatCache UTC_FORMAT; michael@0: static { michael@0: final DateFormat format = new SimpleDateFormat(UTC_PATTERN); michael@0: format.setTimeZone(TimeZones.getUtcTimeZone()); michael@0: format.setLenient(false); michael@0: michael@0: UTC_FORMAT = new DateFormatCache(format); michael@0: } michael@0: michael@0: /** michael@0: * Used for parsing times in a local date-time representation. michael@0: */ michael@0: private static final DateFormatCache DEFAULT_FORMAT; michael@0: static { michael@0: final DateFormat format = new SimpleDateFormat(DEFAULT_PATTERN); michael@0: format.setLenient(false); michael@0: DEFAULT_FORMAT = new DateFormatCache(format); michael@0: } michael@0: michael@0: private static final DateFormatCache LENIENT_DEFAULT_FORMAT; michael@0: static { michael@0: final DateFormat format = new SimpleDateFormat(DEFAULT_PATTERN); michael@0: LENIENT_DEFAULT_FORMAT = new DateFormatCache(format); michael@0: } michael@0: michael@0: private static final DateFormatCache RELAXED_FORMAT; michael@0: static { michael@0: final DateFormat format = new SimpleDateFormat(RELAXED_PATTERN); michael@3: format.setLenient(true); michael@0: RELAXED_FORMAT = new DateFormatCache(format); michael@0: } michael@0: michael@3: private static final DateFormatCache VCARD_FORMAT; michael@3: static { michael@3: final DateFormat format = new SimpleDateFormat(VCARD_PATTERN); michael@3: VCARD_FORMAT = new DateFormatCache(format); michael@3: } michael@3: michael@0: private Time time; michael@0: michael@0: private TimeZone timezone; michael@0: michael@0: /** michael@0: * Default constructor. michael@0: */ michael@0: public DateTime() { michael@0: super(Dates.PRECISION_SECOND, java.util.TimeZone.getDefault()); michael@0: this.time = new Time(getTime(), getFormat().getTimeZone()); michael@0: } michael@0: michael@0: /** michael@0: * @param utc michael@0: * indicates if the date is in UTC time michael@0: */ michael@0: public DateTime(final boolean utc) { michael@0: this(); michael@0: setUtc(utc); michael@0: } michael@0: michael@0: /** michael@0: * @param time michael@0: * a date-time value in milliseconds michael@0: */ michael@0: public DateTime(final long time) { michael@0: super(time, Dates.PRECISION_SECOND, java.util.TimeZone.getDefault()); michael@0: this.time = new Time(time, getFormat().getTimeZone()); michael@0: } michael@0: michael@0: /** michael@0: * @param date michael@0: * a date-time value michael@0: */ michael@0: public DateTime(final java.util.Date date) { michael@0: super(date.getTime(), Dates.PRECISION_SECOND, java.util.TimeZone.getDefault()); michael@0: this.time = new Time(date.getTime(), getFormat().getTimeZone()); michael@0: // copy timezone information if applicable.. michael@0: if (date instanceof DateTime) { michael@0: final DateTime dateTime = (DateTime) date; michael@0: if (dateTime.isUtc()) { michael@0: setUtc(true); michael@0: } else { michael@0: setTimeZone(dateTime.getTimeZone()); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new DateTime instance from parsing the specified string michael@0: * representation in the default (local) timezone. michael@0: * michael@0: * @param value michael@0: * a string representation of a date-time michael@0: * @throws ParseException michael@0: * where the specified string is not a valid date-time michael@0: */ michael@0: public DateTime(final String value) throws ParseException { michael@0: this(value, null); michael@0: /* michael@0: * long time = 0; try { synchronized (UTC_FORMAT) { time = michael@0: * UTC_FORMAT.parse(value).getTime(); } setUtc(true); } catch michael@0: * (ParseException pe) { synchronized (DEFAULT_FORMAT) { michael@0: * DEFAULT_FORMAT.setTimeZone(getFormat().getTimeZone()); time = michael@0: * DEFAULT_FORMAT.parse(value).getTime(); } this.time = new Time(time, michael@0: * getFormat().getTimeZone()); } setTime(time); michael@0: */ michael@0: } michael@0: michael@0: /** michael@0: * Creates a new date-time instance from the specified value in the given michael@0: * timezone. If a timezone is not specified, the default timezone (as michael@0: * returned by {@link java.util.TimeZone#getDefault()}) is used. michael@0: * michael@0: * @param value michael@0: * a string representation of a date-time michael@0: * @param timezone michael@0: * the timezone for the date-time instance michael@0: * @throws ParseException michael@0: * where the specified string is not a valid date-time michael@0: */ michael@0: public DateTime(final String value, final TimeZone timezone) michael@0: throws ParseException { michael@0: // setting the time to 0 since we are going to reset it anyway michael@0: super(0, Dates.PRECISION_SECOND, timezone != null ? timezone michael@0: : java.util.TimeZone.getDefault()); michael@0: this.time = new Time(getTime(), getFormat().getTimeZone()); michael@0: michael@0: try { michael@0: if (value.endsWith("Z")) { michael@0: setTime(value, (DateFormat) UTC_FORMAT.get(), null); michael@0: setUtc(true); michael@0: } else { michael@0: if (timezone != null) { michael@0: setTime(value, (DateFormat) DEFAULT_FORMAT.get(), timezone); michael@0: } else { michael@0: // Use lenient parsing for floating times. This is to michael@0: // overcome michael@0: // the problem of parsing VTimeZone dates that specify dates michael@0: // that the strict parser does not accept. michael@0: setTime(value, (DateFormat) LENIENT_DEFAULT_FORMAT.get(), michael@0: getFormat().getTimeZone()); michael@0: } michael@0: setTimeZone(timezone); michael@0: } michael@0: } catch (ParseException pe) { michael@3: if (CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_VCARD_COMPATIBILITY)) { michael@0: michael@3: try { michael@3: setTime(value, (DateFormat) VCARD_FORMAT.get(), timezone); michael@3: setTimeZone(timezone); michael@3: } catch (ParseException pe2) { michael@3: if (CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING)) { michael@3: setTime(value, (DateFormat) RELAXED_FORMAT.get(), timezone); michael@3: setTimeZone(timezone); michael@3: } michael@3: } michael@3: } else if (CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING)) { michael@0: setTime(value, (DateFormat) RELAXED_FORMAT.get(), timezone); michael@0: setTimeZone(timezone); michael@0: } else { michael@0: throw pe; michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @param value michael@0: * a string representation of a date-time michael@0: * @param pattern michael@0: * a pattern to apply when parsing the date-time value michael@0: * @param timezone michael@0: * the timezone for the date-time instance michael@0: * @throws ParseException michael@0: * where the specified string is not a valid date-time michael@0: */ michael@0: public DateTime(String value, String pattern, TimeZone timezone) michael@0: throws ParseException { michael@0: // setting the time to 0 since we are going to reset it anyway michael@0: super(0, Dates.PRECISION_SECOND, timezone != null ? timezone michael@0: : java.util.TimeZone.getDefault()); michael@0: this.time = new Time(getTime(), getFormat().getTimeZone()); michael@0: michael@0: final DateFormat format = CalendarDateFormatFactory michael@0: .getInstance(pattern); michael@0: setTime(value, format, timezone); michael@0: } michael@0: michael@0: /** michael@0: * @param value michael@0: * a string representation of a date-time michael@0: * @param pattern michael@0: * a pattern to apply when parsing the date-time value michael@0: * @param utc michael@0: * indicates whether the date-time is in UTC time michael@0: * @throws ParseException michael@0: * where the specified string is not a valid date-time michael@0: */ michael@0: public DateTime(String value, String pattern, boolean utc) michael@0: throws ParseException { michael@0: // setting the time to 0 since we are going to reset it anyway michael@0: this(0); michael@0: final DateFormat format = CalendarDateFormatFactory michael@0: .getInstance(pattern); michael@0: if (utc) { michael@0: setTime(value, format, michael@0: ((DateFormat) UTC_FORMAT.get()).getTimeZone()); michael@0: } else { michael@0: setTime(value, format, null); michael@0: } michael@0: setUtc(utc); michael@0: } michael@0: michael@0: /** michael@0: * Internal set of time by parsing value string. michael@0: * michael@0: * @param value michael@0: * @param format michael@0: * a {@code DateFormat}, protected by the use of a ThreadLocal. michael@0: * @param tz michael@0: * @throws ParseException michael@0: */ michael@0: private void setTime(final String value, final DateFormat format, michael@0: final java.util.TimeZone tz) throws ParseException { michael@0: michael@0: if (tz != null) { michael@0: format.setTimeZone(tz); michael@0: } michael@0: setTime(format.parse(value).getTime()); michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public final void setTime(final long time) { michael@0: super.setTime(time); michael@0: // need to check for null time due to Android java.util.Date(long) michael@0: // constructor michael@0: // calling this method.. michael@0: if (this.time != null) { michael@0: this.time.setTime(time); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @return Returns the utc. michael@0: */ michael@0: public final boolean isUtc() { michael@0: return time.isUtc(); michael@0: } michael@0: michael@0: /** michael@0: * Updates this date-time to display in UTC time if the argument is true. michael@0: * Otherwise, resets to the default timezone. michael@0: * michael@0: * @param utc michael@0: * The utc to set. michael@0: */ michael@0: public final void setUtc(final boolean utc) { michael@0: // reset the timezone associated with this instance.. michael@0: this.timezone = null; michael@0: if (utc) { michael@0: getFormat().setTimeZone(TimeZones.getUtcTimeZone()); michael@0: } else { michael@0: resetTimeZone(); michael@0: } michael@0: time = new Time(time, getFormat().getTimeZone(), utc); michael@0: } michael@0: michael@0: /** michael@0: * Sets the timezone associated with this date-time instance. If the michael@0: * specified timezone is null, it will reset to the default timezone. If the michael@0: * date-time instance is utc, it will turn into either a floating (no michael@0: * timezone) date-time, or a date-time with a timezone. michael@0: * michael@0: * @param timezone michael@0: * a timezone to apply to the instance michael@0: */ michael@0: public final void setTimeZone(final TimeZone timezone) { michael@0: this.timezone = timezone; michael@0: if (timezone != null) { michael@0: getFormat().setTimeZone(timezone); michael@0: } else { michael@0: resetTimeZone(); michael@0: } michael@0: time = new Time(time, getFormat().getTimeZone(), false); michael@0: } michael@0: michael@0: /** michael@0: * Reset the timezone to default. michael@0: */ michael@0: private void resetTimeZone() { michael@0: // use GMT timezone to avoid daylight savings rules affecting floating michael@0: // time values.. michael@0: getFormat().setTimeZone(TimeZone.getDefault()); michael@0: // getFormat().setTimeZone(TimeZone.getTimeZone(TimeZones.GMT_ID)); michael@0: } michael@0: michael@0: /** michael@0: * Returns the current timezone associated with this date-time value. michael@0: * michael@0: * @return a Java timezone michael@0: */ michael@0: public final TimeZone getTimeZone() { michael@0: return timezone; michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public final String toString() { michael@0: final StringBuffer b = new StringBuffer(super.toString()); michael@0: b.append('T'); michael@0: b.append(time.toString()); michael@0: return b.toString(); michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public boolean equals(final Object arg0) { michael@0: // TODO: what about compareTo, before, after, etc.? michael@0: michael@0: if (arg0 instanceof DateTime) { michael@0: return new EqualsBuilder().append(time, ((DateTime) arg0).time) michael@0: .isEquals(); michael@0: } michael@0: return super.equals(arg0); michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public int hashCode() { michael@0: return super.hashCode(); michael@0: } michael@0: michael@0: private static class DateFormatCache { michael@0: michael@0: private final Map threadMap = new WeakHashMap(); michael@0: michael@0: private final DateFormat templateFormat; michael@0: michael@0: private DateFormatCache(DateFormat dateFormat) { michael@0: this.templateFormat = dateFormat; michael@0: } michael@0: michael@0: public DateFormat get() { michael@0: DateFormat dateFormat = (DateFormat) threadMap.get(Thread michael@0: .currentThread()); michael@0: if (dateFormat == null) { michael@0: dateFormat = (DateFormat) templateFormat.clone(); michael@0: threadMap.put(Thread.currentThread(), dateFormat); michael@0: } michael@0: return dateFormat; michael@0: } michael@0: } michael@0: }