|
1 /** |
|
2 * Copyright (c) 2012, Ben Fortuna |
|
3 * All rights reserved. |
|
4 * |
|
5 * Redistribution and use in source and binary forms, with or without |
|
6 * modification, are permitted provided that the following conditions |
|
7 * are met: |
|
8 * |
|
9 * o Redistributions of source code must retain the above copyright |
|
10 * notice, this list of conditions and the following disclaimer. |
|
11 * |
|
12 * o Redistributions in binary form must reproduce the above copyright |
|
13 * notice, this list of conditions and the following disclaimer in the |
|
14 * documentation and/or other materials provided with the distribution. |
|
15 * |
|
16 * o Neither the name of Ben Fortuna nor the names of any other contributors |
|
17 * may be used to endorse or promote products derived from this software |
|
18 * without specific prior written permission. |
|
19 * |
|
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
|
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
31 */ |
|
32 package net.fortuna.ical4j.model; |
|
33 |
|
34 import java.text.DateFormat; |
|
35 import java.text.ParseException; |
|
36 import java.text.SimpleDateFormat; |
|
37 import java.util.Map; |
|
38 import java.util.WeakHashMap; |
|
39 |
|
40 import net.fortuna.ical4j.util.CompatibilityHints; |
|
41 import net.fortuna.ical4j.util.Dates; |
|
42 import net.fortuna.ical4j.util.TimeZones; |
|
43 |
|
44 import org.apache.commons.lang.builder.EqualsBuilder; |
|
45 import org.apache.commons.lang.builder.HashCodeBuilder; |
|
46 |
|
47 /** |
|
48 * $Id$ |
|
49 * |
|
50 * Created on 26/06/2005 |
|
51 * |
|
52 * Represents a time of day on a specific date. |
|
53 * |
|
54 * <pre> |
|
55 * 4.3.5 Date-Time |
|
56 * |
|
57 * Value Name: DATE-TIME |
|
58 * |
|
59 * Purpose: This value type is used to identify values that specify a |
|
60 * precise calendar date and time of day. |
|
61 * |
|
62 * Formal Definition: The value type is defined by the following |
|
63 * notation: |
|
64 * |
|
65 * date-time = date "T" time ;As specified in the date and time |
|
66 * ;value definitions |
|
67 * |
|
68 * Description: If the property permits, multiple "date-time" values are |
|
69 * specified as a COMMA character (US-ASCII decimal 44) separated list |
|
70 * of values. No additional content value encoding (i.e., BACKSLASH |
|
71 * character encoding) is defined for this value type. |
|
72 * |
|
73 * The "DATE-TIME" data type is used to identify values that contain a |
|
74 * precise calendar date and time of day. The format is based on the |
|
75 * [ISO 8601] complete representation, basic format for a calendar date |
|
76 * and time of day. The text format is a concatenation of the "date", |
|
77 * followed by the LATIN CAPITAL LETTER T character (US-ASCII decimal |
|
78 * 84) time designator, followed by the "time" format. |
|
79 * |
|
80 * The "DATE-TIME" data type expresses time values in three forms: |
|
81 * |
|
82 * The form of date and time with UTC offset MUST NOT be used. For |
|
83 * example, the following is not valid for a date-time value: |
|
84 * |
|
85 * DTSTART:19980119T230000-0800 ;Invalid time format |
|
86 * |
|
87 * FORM #1: DATE WITH LOCAL TIME |
|
88 * |
|
89 * The date with local time form is simply a date-time value that does |
|
90 * not contain the UTC designator nor does it reference a time zone. For |
|
91 * example, the following represents Janurary 18, 1998, at 11 PM: |
|
92 * |
|
93 * DTSTART:19980118T230000 |
|
94 * |
|
95 * Date-time values of this type are said to be "floating" and are not |
|
96 * bound to any time zone in particular. They are used to represent the |
|
97 * same hour, minute, and second value regardless of which time zone is |
|
98 * currently being observed. For example, an event can be defined that |
|
99 * indicates that an individual will be busy from 11:00 AM to 1:00 PM |
|
100 * every day, no matter which time zone the person is in. In these |
|
101 * cases, a local time can be specified. The recipient of an iCalendar |
|
102 * object with a property value consisting of a local time, without any |
|
103 * relative time zone information, SHOULD interpret the value as being |
|
104 * fixed to whatever time zone the ATTENDEE is in at any given moment. |
|
105 * This means that two ATTENDEEs, in different time zones, receiving the |
|
106 * same event definition as a floating time, may be participating in the |
|
107 * event at different actual times. Floating time SHOULD only be used |
|
108 * where that is the reasonable behavior. |
|
109 * |
|
110 * In most cases, a fixed time is desired. To properly communicate a |
|
111 * fixed time in a property value, either UTC time or local time with |
|
112 * time zone reference MUST be specified. |
|
113 * |
|
114 * The use of local time in a DATE-TIME value without the TZID property |
|
115 * parameter is to be interpreted as floating time, regardless of the |
|
116 * existence of "VTIMEZONE" calendar components in the iCalendar object. |
|
117 * |
|
118 * FORM #2: DATE WITH UTC TIME |
|
119 * |
|
120 * The date with UTC time, or absolute time, is identified by a LATIN |
|
121 * CAPITAL LETTER Z suffix character (US-ASCII decimal 90), the UTC |
|
122 * designator, appended to the time value. For example, the following |
|
123 * represents January 19, 1998, at 0700 UTC: |
|
124 * |
|
125 * DTSTART:19980119T070000Z |
|
126 * |
|
127 * The TZID property parameter MUST NOT be applied to DATE-TIME |
|
128 * properties whose time values are specified in UTC. |
|
129 * |
|
130 * FORM #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE |
|
131 * |
|
132 * The date and local time with reference to time zone information is |
|
133 * identified by the use the TZID property parameter to reference the |
|
134 * appropriate time zone definition. TZID is discussed in detail in the |
|
135 * section on Time Zone. For example, the following represents 2 AM in |
|
136 * New York on Janurary 19, 1998: |
|
137 * |
|
138 * DTSTART;TZID=US-Eastern:19980119T020000 |
|
139 * |
|
140 * Example: The following represents July 14, 1997, at 1:30 PM in New |
|
141 * York City in each of the three time formats, using the "DTSTART" |
|
142 * property. |
|
143 * |
|
144 * DTSTART:19970714T133000 ;Local time |
|
145 * DTSTART:19970714T173000Z ;UTC time |
|
146 * DTSTART;TZID=US-Eastern:19970714T133000 ;Local time and time |
|
147 * ; zone reference |
|
148 * |
|
149 * A time value MUST ONLY specify 60 seconds when specifying the |
|
150 * periodic "leap second" in the time value. For example: |
|
151 * |
|
152 * COMPLETED:19970630T235960Z |
|
153 * </pre> |
|
154 * |
|
155 * @author Ben Fortuna |
|
156 */ |
|
157 public class DateTime extends Date { |
|
158 |
|
159 private static final long serialVersionUID = -6407231357919440387L; |
|
160 |
|
161 private static final String DEFAULT_PATTERN = "yyyyMMdd'T'HHmmss"; |
|
162 |
|
163 private static final String UTC_PATTERN = "yyyyMMdd'T'HHmmss'Z'"; |
|
164 |
|
165 private static final String RELAXED_PATTERN = "yyyyMMdd"; |
|
166 |
|
167 /** |
|
168 * Used for parsing times in a UTC date-time representation. |
|
169 */ |
|
170 private static final DateFormatCache UTC_FORMAT; |
|
171 static { |
|
172 final DateFormat format = new SimpleDateFormat(UTC_PATTERN); |
|
173 format.setTimeZone(TimeZones.getUtcTimeZone()); |
|
174 format.setLenient(false); |
|
175 |
|
176 UTC_FORMAT = new DateFormatCache(format); |
|
177 } |
|
178 |
|
179 /** |
|
180 * Used for parsing times in a local date-time representation. |
|
181 */ |
|
182 private static final DateFormatCache DEFAULT_FORMAT; |
|
183 static { |
|
184 final DateFormat format = new SimpleDateFormat(DEFAULT_PATTERN); |
|
185 format.setLenient(false); |
|
186 DEFAULT_FORMAT = new DateFormatCache(format); |
|
187 } |
|
188 |
|
189 private static final DateFormatCache LENIENT_DEFAULT_FORMAT; |
|
190 static { |
|
191 final DateFormat format = new SimpleDateFormat(DEFAULT_PATTERN); |
|
192 LENIENT_DEFAULT_FORMAT = new DateFormatCache(format); |
|
193 } |
|
194 |
|
195 private static final DateFormatCache RELAXED_FORMAT; |
|
196 static { |
|
197 final DateFormat format = new SimpleDateFormat(RELAXED_PATTERN); |
|
198 format.setLenient(false); |
|
199 RELAXED_FORMAT = new DateFormatCache(format); |
|
200 } |
|
201 |
|
202 private Time time; |
|
203 |
|
204 private TimeZone timezone; |
|
205 |
|
206 /** |
|
207 * Default constructor. |
|
208 */ |
|
209 public DateTime() { |
|
210 super(Dates.PRECISION_SECOND, java.util.TimeZone.getDefault()); |
|
211 this.time = new Time(getTime(), getFormat().getTimeZone()); |
|
212 } |
|
213 |
|
214 /** |
|
215 * @param utc |
|
216 * indicates if the date is in UTC time |
|
217 */ |
|
218 public DateTime(final boolean utc) { |
|
219 this(); |
|
220 setUtc(utc); |
|
221 } |
|
222 |
|
223 /** |
|
224 * @param time |
|
225 * a date-time value in milliseconds |
|
226 */ |
|
227 public DateTime(final long time) { |
|
228 super(time, Dates.PRECISION_SECOND, java.util.TimeZone.getDefault()); |
|
229 this.time = new Time(time, getFormat().getTimeZone()); |
|
230 } |
|
231 |
|
232 /** |
|
233 * @param date |
|
234 * a date-time value |
|
235 */ |
|
236 public DateTime(final java.util.Date date) { |
|
237 super(date.getTime(), Dates.PRECISION_SECOND, java.util.TimeZone.getDefault()); |
|
238 this.time = new Time(date.getTime(), getFormat().getTimeZone()); |
|
239 // copy timezone information if applicable.. |
|
240 if (date instanceof DateTime) { |
|
241 final DateTime dateTime = (DateTime) date; |
|
242 if (dateTime.isUtc()) { |
|
243 setUtc(true); |
|
244 } else { |
|
245 setTimeZone(dateTime.getTimeZone()); |
|
246 } |
|
247 } |
|
248 } |
|
249 |
|
250 /** |
|
251 * Constructs a new DateTime instance from parsing the specified string |
|
252 * representation in the default (local) timezone. |
|
253 * |
|
254 * @param value |
|
255 * a string representation of a date-time |
|
256 * @throws ParseException |
|
257 * where the specified string is not a valid date-time |
|
258 */ |
|
259 public DateTime(final String value) throws ParseException { |
|
260 this(value, null); |
|
261 /* |
|
262 * long time = 0; try { synchronized (UTC_FORMAT) { time = |
|
263 * UTC_FORMAT.parse(value).getTime(); } setUtc(true); } catch |
|
264 * (ParseException pe) { synchronized (DEFAULT_FORMAT) { |
|
265 * DEFAULT_FORMAT.setTimeZone(getFormat().getTimeZone()); time = |
|
266 * DEFAULT_FORMAT.parse(value).getTime(); } this.time = new Time(time, |
|
267 * getFormat().getTimeZone()); } setTime(time); |
|
268 */ |
|
269 } |
|
270 |
|
271 /** |
|
272 * Creates a new date-time instance from the specified value in the given |
|
273 * timezone. If a timezone is not specified, the default timezone (as |
|
274 * returned by {@link java.util.TimeZone#getDefault()}) is used. |
|
275 * |
|
276 * @param value |
|
277 * a string representation of a date-time |
|
278 * @param timezone |
|
279 * the timezone for the date-time instance |
|
280 * @throws ParseException |
|
281 * where the specified string is not a valid date-time |
|
282 */ |
|
283 public DateTime(final String value, final TimeZone timezone) |
|
284 throws ParseException { |
|
285 // setting the time to 0 since we are going to reset it anyway |
|
286 super(0, Dates.PRECISION_SECOND, timezone != null ? timezone |
|
287 : java.util.TimeZone.getDefault()); |
|
288 this.time = new Time(getTime(), getFormat().getTimeZone()); |
|
289 |
|
290 try { |
|
291 if (value.endsWith("Z")) { |
|
292 setTime(value, (DateFormat) UTC_FORMAT.get(), null); |
|
293 setUtc(true); |
|
294 } else { |
|
295 if (timezone != null) { |
|
296 setTime(value, (DateFormat) DEFAULT_FORMAT.get(), timezone); |
|
297 } else { |
|
298 // Use lenient parsing for floating times. This is to |
|
299 // overcome |
|
300 // the problem of parsing VTimeZone dates that specify dates |
|
301 // that the strict parser does not accept. |
|
302 setTime(value, (DateFormat) LENIENT_DEFAULT_FORMAT.get(), |
|
303 getFormat().getTimeZone()); |
|
304 } |
|
305 setTimeZone(timezone); |
|
306 } |
|
307 } catch (ParseException pe) { |
|
308 if (CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING)) { |
|
309 |
|
310 setTime(value, (DateFormat) RELAXED_FORMAT.get(), timezone); |
|
311 setTimeZone(timezone); |
|
312 } else { |
|
313 throw pe; |
|
314 } |
|
315 } |
|
316 } |
|
317 |
|
318 /** |
|
319 * @param value |
|
320 * a string representation of a date-time |
|
321 * @param pattern |
|
322 * a pattern to apply when parsing the date-time value |
|
323 * @param timezone |
|
324 * the timezone for the date-time instance |
|
325 * @throws ParseException |
|
326 * where the specified string is not a valid date-time |
|
327 */ |
|
328 public DateTime(String value, String pattern, TimeZone timezone) |
|
329 throws ParseException { |
|
330 // setting the time to 0 since we are going to reset it anyway |
|
331 super(0, Dates.PRECISION_SECOND, timezone != null ? timezone |
|
332 : java.util.TimeZone.getDefault()); |
|
333 this.time = new Time(getTime(), getFormat().getTimeZone()); |
|
334 |
|
335 final DateFormat format = CalendarDateFormatFactory |
|
336 .getInstance(pattern); |
|
337 setTime(value, format, timezone); |
|
338 } |
|
339 |
|
340 /** |
|
341 * @param value |
|
342 * a string representation of a date-time |
|
343 * @param pattern |
|
344 * a pattern to apply when parsing the date-time value |
|
345 * @param utc |
|
346 * indicates whether the date-time is in UTC time |
|
347 * @throws ParseException |
|
348 * where the specified string is not a valid date-time |
|
349 */ |
|
350 public DateTime(String value, String pattern, boolean utc) |
|
351 throws ParseException { |
|
352 // setting the time to 0 since we are going to reset it anyway |
|
353 this(0); |
|
354 final DateFormat format = CalendarDateFormatFactory |
|
355 .getInstance(pattern); |
|
356 if (utc) { |
|
357 setTime(value, format, |
|
358 ((DateFormat) UTC_FORMAT.get()).getTimeZone()); |
|
359 } else { |
|
360 setTime(value, format, null); |
|
361 } |
|
362 setUtc(utc); |
|
363 } |
|
364 |
|
365 /** |
|
366 * Internal set of time by parsing value string. |
|
367 * |
|
368 * @param value |
|
369 * @param format |
|
370 * a {@code DateFormat}, protected by the use of a ThreadLocal. |
|
371 * @param tz |
|
372 * @throws ParseException |
|
373 */ |
|
374 private void setTime(final String value, final DateFormat format, |
|
375 final java.util.TimeZone tz) throws ParseException { |
|
376 |
|
377 if (tz != null) { |
|
378 format.setTimeZone(tz); |
|
379 } |
|
380 setTime(format.parse(value).getTime()); |
|
381 } |
|
382 |
|
383 /** |
|
384 * {@inheritDoc} |
|
385 */ |
|
386 public final void setTime(final long time) { |
|
387 super.setTime(time); |
|
388 // need to check for null time due to Android java.util.Date(long) |
|
389 // constructor |
|
390 // calling this method.. |
|
391 if (this.time != null) { |
|
392 this.time.setTime(time); |
|
393 } |
|
394 } |
|
395 |
|
396 /** |
|
397 * @return Returns the utc. |
|
398 */ |
|
399 public final boolean isUtc() { |
|
400 return time.isUtc(); |
|
401 } |
|
402 |
|
403 /** |
|
404 * Updates this date-time to display in UTC time if the argument is true. |
|
405 * Otherwise, resets to the default timezone. |
|
406 * |
|
407 * @param utc |
|
408 * The utc to set. |
|
409 */ |
|
410 public final void setUtc(final boolean utc) { |
|
411 // reset the timezone associated with this instance.. |
|
412 this.timezone = null; |
|
413 if (utc) { |
|
414 getFormat().setTimeZone(TimeZones.getUtcTimeZone()); |
|
415 } else { |
|
416 resetTimeZone(); |
|
417 } |
|
418 time = new Time(time, getFormat().getTimeZone(), utc); |
|
419 } |
|
420 |
|
421 /** |
|
422 * Sets the timezone associated with this date-time instance. If the |
|
423 * specified timezone is null, it will reset to the default timezone. If the |
|
424 * date-time instance is utc, it will turn into either a floating (no |
|
425 * timezone) date-time, or a date-time with a timezone. |
|
426 * |
|
427 * @param timezone |
|
428 * a timezone to apply to the instance |
|
429 */ |
|
430 public final void setTimeZone(final TimeZone timezone) { |
|
431 this.timezone = timezone; |
|
432 if (timezone != null) { |
|
433 getFormat().setTimeZone(timezone); |
|
434 } else { |
|
435 resetTimeZone(); |
|
436 } |
|
437 time = new Time(time, getFormat().getTimeZone(), false); |
|
438 } |
|
439 |
|
440 /** |
|
441 * Reset the timezone to default. |
|
442 */ |
|
443 private void resetTimeZone() { |
|
444 // use GMT timezone to avoid daylight savings rules affecting floating |
|
445 // time values.. |
|
446 getFormat().setTimeZone(TimeZone.getDefault()); |
|
447 // getFormat().setTimeZone(TimeZone.getTimeZone(TimeZones.GMT_ID)); |
|
448 } |
|
449 |
|
450 /** |
|
451 * Returns the current timezone associated with this date-time value. |
|
452 * |
|
453 * @return a Java timezone |
|
454 */ |
|
455 public final TimeZone getTimeZone() { |
|
456 return timezone; |
|
457 } |
|
458 |
|
459 /** |
|
460 * {@inheritDoc} |
|
461 */ |
|
462 public final String toString() { |
|
463 final StringBuffer b = new StringBuffer(super.toString()); |
|
464 b.append('T'); |
|
465 b.append(time.toString()); |
|
466 return b.toString(); |
|
467 } |
|
468 |
|
469 /** |
|
470 * {@inheritDoc} |
|
471 */ |
|
472 public boolean equals(final Object arg0) { |
|
473 // TODO: what about compareTo, before, after, etc.? |
|
474 |
|
475 if (arg0 instanceof DateTime) { |
|
476 return new EqualsBuilder().append(time, ((DateTime) arg0).time) |
|
477 .isEquals(); |
|
478 } |
|
479 return super.equals(arg0); |
|
480 } |
|
481 |
|
482 /** |
|
483 * {@inheritDoc} |
|
484 */ |
|
485 public int hashCode() { |
|
486 return super.hashCode(); |
|
487 } |
|
488 |
|
489 private static class DateFormatCache { |
|
490 |
|
491 private final Map threadMap = new WeakHashMap(); |
|
492 |
|
493 private final DateFormat templateFormat; |
|
494 |
|
495 private DateFormatCache(DateFormat dateFormat) { |
|
496 this.templateFormat = dateFormat; |
|
497 } |
|
498 |
|
499 public DateFormat get() { |
|
500 DateFormat dateFormat = (DateFormat) threadMap.get(Thread |
|
501 .currentThread()); |
|
502 if (dateFormat == null) { |
|
503 dateFormat = (DateFormat) templateFormat.clone(); |
|
504 threadMap.put(Thread.currentThread(), dateFormat); |
|
505 } |
|
506 return dateFormat; |
|
507 } |
|
508 } |
|
509 } |