|
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.component; |
|
33 |
|
34 import java.io.IOException; |
|
35 import java.text.DateFormat; |
|
36 import java.text.ParseException; |
|
37 import java.text.SimpleDateFormat; |
|
38 import java.util.Arrays; |
|
39 import java.util.Calendar; |
|
40 import java.util.Collections; |
|
41 import java.util.Iterator; |
|
42 import java.util.Map; |
|
43 import java.util.TreeMap; |
|
44 |
|
45 import net.fortuna.ical4j.model.Component; |
|
46 import net.fortuna.ical4j.model.Date; |
|
47 import net.fortuna.ical4j.model.DateList; |
|
48 import net.fortuna.ical4j.model.DateTime; |
|
49 import net.fortuna.ical4j.model.Property; |
|
50 import net.fortuna.ical4j.model.PropertyList; |
|
51 import net.fortuna.ical4j.model.ValidationException; |
|
52 import net.fortuna.ical4j.model.parameter.Value; |
|
53 import net.fortuna.ical4j.model.property.DtStart; |
|
54 import net.fortuna.ical4j.model.property.RDate; |
|
55 import net.fortuna.ical4j.model.property.RRule; |
|
56 import net.fortuna.ical4j.model.property.TzOffsetFrom; |
|
57 import net.fortuna.ical4j.model.property.TzOffsetTo; |
|
58 import net.fortuna.ical4j.util.Dates; |
|
59 import net.fortuna.ical4j.util.PropertyValidator; |
|
60 import net.fortuna.ical4j.util.TimeZones; |
|
61 |
|
62 import org.apache.commons.logging.Log; |
|
63 import org.apache.commons.logging.LogFactory; |
|
64 |
|
65 /** |
|
66 * $Id$ [05-Apr-2004] |
|
67 * |
|
68 * Defines an iCalendar sub-component representing a timezone observance. Class made abstract such that only Standard |
|
69 * and Daylight instances are valid. |
|
70 * @author Ben Fortuna |
|
71 */ |
|
72 public abstract class Observance extends Component { |
|
73 |
|
74 /** |
|
75 * |
|
76 */ |
|
77 private static final long serialVersionUID = 2523330383042085994L; |
|
78 |
|
79 /** |
|
80 * one of 'standardc' or 'daylightc' MUST occur and each MAY occur more than once. |
|
81 */ |
|
82 public static final String STANDARD = "STANDARD"; |
|
83 |
|
84 /** |
|
85 * Token for daylight observance. |
|
86 */ |
|
87 public static final String DAYLIGHT = "DAYLIGHT"; |
|
88 |
|
89 // TODO: clear cache when observance definition changes (??) |
|
90 private long[] onsetsMillisec; |
|
91 private DateTime[] onsetsDates; |
|
92 private Map onsets = new TreeMap(); |
|
93 private Date initialOnset = null; |
|
94 |
|
95 /** |
|
96 * Used for parsing times in a UTC date-time representation. |
|
97 */ |
|
98 private static final String UTC_PATTERN = "yyyyMMdd'T'HHmmss"; |
|
99 private static final DateFormat UTC_FORMAT = new SimpleDateFormat( |
|
100 UTC_PATTERN); |
|
101 |
|
102 static { |
|
103 UTC_FORMAT.setTimeZone(TimeZones.getUtcTimeZone()); |
|
104 UTC_FORMAT.setLenient(false); |
|
105 } |
|
106 |
|
107 /* If this is set we have rrules. If we get a date after this rebuild onsets */ |
|
108 private Date onsetLimit; |
|
109 |
|
110 /** |
|
111 * Constructs a timezone observance with the specified name and no properties. |
|
112 * @param name the name of this observance component |
|
113 */ |
|
114 protected Observance(final String name) { |
|
115 super(name); |
|
116 } |
|
117 |
|
118 /** |
|
119 * Constructor protected to enforce use of sub-classes from this library. |
|
120 * @param name the name of the time type |
|
121 * @param properties a list of properties |
|
122 */ |
|
123 protected Observance(final String name, final PropertyList properties) { |
|
124 super(name, properties); |
|
125 } |
|
126 |
|
127 /** |
|
128 * {@inheritDoc} |
|
129 */ |
|
130 public final void validate(final boolean recurse) throws ValidationException { |
|
131 |
|
132 // From "4.8.3.3 Time Zone Offset From": |
|
133 // Conformance: This property MUST be specified in a "VTIMEZONE" |
|
134 // calendar component. |
|
135 PropertyValidator.getInstance().assertOne(Property.TZOFFSETFROM, |
|
136 getProperties()); |
|
137 |
|
138 // From "4.8.3.4 Time Zone Offset To": |
|
139 // Conformance: This property MUST be specified in a "VTIMEZONE" |
|
140 // calendar component. |
|
141 PropertyValidator.getInstance().assertOne(Property.TZOFFSETTO, |
|
142 getProperties()); |
|
143 |
|
144 /* |
|
145 * ; the following are each REQUIRED, ; but MUST NOT occur more than once dtstart / tzoffsetto / tzoffsetfrom / |
|
146 */ |
|
147 PropertyValidator.getInstance().assertOne(Property.DTSTART, |
|
148 getProperties()); |
|
149 |
|
150 /* |
|
151 * ; the following are optional, ; and MAY occur more than once comment / rdate / rrule / tzname / x-prop |
|
152 */ |
|
153 |
|
154 if (recurse) { |
|
155 validateProperties(); |
|
156 } |
|
157 } |
|
158 |
|
159 /** |
|
160 * Returns the latest applicable onset of this observance for the specified date. |
|
161 * @param date the latest date that an observance onset may occur |
|
162 * @return the latest applicable observance date or null if there is no applicable observance onset for the |
|
163 * specified date |
|
164 */ |
|
165 public final Date getLatestOnset(final Date date) { |
|
166 |
|
167 if (initialOnset == null) { |
|
168 try { |
|
169 initialOnset = applyOffsetFrom(calculateOnset(((DtStart) getProperty(Property.DTSTART)).getDate())); |
|
170 } catch (ParseException e) { |
|
171 Log log = LogFactory.getLog(Observance.class); |
|
172 log.error("Unexpected error calculating initial onset", e); |
|
173 // XXX: is this correct? |
|
174 return null; |
|
175 } |
|
176 } |
|
177 |
|
178 // observance not applicable if date is before the effective date of this observance.. |
|
179 if (date.before(initialOnset)) { |
|
180 return null; |
|
181 } |
|
182 |
|
183 if ((onsetsMillisec != null) && (onsetLimit == null || date.before(onsetLimit))) { |
|
184 return getCachedOnset(date); |
|
185 } |
|
186 |
|
187 Date onset = initialOnset; |
|
188 Date initialOnsetUTC; |
|
189 // get first onset without adding TZFROM as this may lead to a day boundary |
|
190 // change which would be incompatible with BYDAY RRULES |
|
191 // we will have to add the offset to all cacheable onsets |
|
192 try { |
|
193 initialOnsetUTC = calculateOnset(((DtStart) getProperty(Property.DTSTART)).getDate()); |
|
194 } catch (ParseException e) { |
|
195 Log log = LogFactory.getLog(Observance.class); |
|
196 log.error("Unexpected error calculating initial onset", e); |
|
197 // XXX: is this correct? |
|
198 return null; |
|
199 } |
|
200 // collect all onsets for the purposes of caching.. |
|
201 final DateList cacheableOnsets = new DateList(); |
|
202 cacheableOnsets.setUtc(true); |
|
203 cacheableOnsets.add(initialOnset); |
|
204 |
|
205 // check rdates for latest applicable onset.. |
|
206 final PropertyList rdates = getProperties(Property.RDATE); |
|
207 for (final Iterator i = rdates.iterator(); i.hasNext();) { |
|
208 final RDate rdate = (RDate) i.next(); |
|
209 for (final Iterator j = rdate.getDates().iterator(); j.hasNext();) { |
|
210 try { |
|
211 final DateTime rdateOnset = applyOffsetFrom(calculateOnset((Date) j.next())); |
|
212 if (!rdateOnset.after(date) && rdateOnset.after(onset)) { |
|
213 onset = rdateOnset; |
|
214 } |
|
215 /* |
|
216 * else if (rdateOnset.after(date) && rdateOnset.after(onset) && (nextOnset == null || |
|
217 * rdateOnset.before(nextOnset))) { nextOnset = rdateOnset; } |
|
218 */ |
|
219 cacheableOnsets.add(rdateOnset); |
|
220 } catch (ParseException e) { |
|
221 Log log = LogFactory.getLog(Observance.class); |
|
222 log.error("Unexpected error calculating onset", e); |
|
223 } |
|
224 } |
|
225 } |
|
226 |
|
227 // check recurrence rules for latest applicable onset.. |
|
228 final PropertyList rrules = getProperties(Property.RRULE); |
|
229 for (final Iterator i = rrules.iterator(); i.hasNext();) { |
|
230 final RRule rrule = (RRule) i.next(); |
|
231 // include future onsets to determine onset period.. |
|
232 final Calendar cal = Dates.getCalendarInstance(date); |
|
233 cal.setTime(date); |
|
234 cal.add(Calendar.YEAR, 10); |
|
235 onsetLimit = Dates.getInstance(cal.getTime(), Value.DATE_TIME); |
|
236 final DateList recurrenceDates = rrule.getRecur().getDates(initialOnsetUTC, |
|
237 onsetLimit, Value.DATE_TIME); |
|
238 for (final Iterator j = recurrenceDates.iterator(); j.hasNext();) { |
|
239 final DateTime rruleOnset = applyOffsetFrom((DateTime) j.next()); |
|
240 if (!rruleOnset.after(date) && rruleOnset.after(onset)) { |
|
241 onset = rruleOnset; |
|
242 } |
|
243 /* |
|
244 * else if (rruleOnset.after(date) && rruleOnset.after(onset) && (nextOnset == null || |
|
245 * rruleOnset.before(nextOnset))) { nextOnset = rruleOnset; } |
|
246 */ |
|
247 cacheableOnsets.add(rruleOnset); |
|
248 } |
|
249 } |
|
250 |
|
251 // cache onsets.. |
|
252 Collections.sort(cacheableOnsets); |
|
253 DateTime cacheableOnset = null; |
|
254 this.onsetsMillisec = new long[cacheableOnsets.size()]; |
|
255 this.onsetsDates = new DateTime[onsetsMillisec.length]; |
|
256 |
|
257 for (int i = 0; i < onsetsMillisec.length; i++) { |
|
258 cacheableOnset = (DateTime)cacheableOnsets.get(i); |
|
259 onsetsMillisec[i] = cacheableOnset.getTime(); |
|
260 onsetsDates[i] = cacheableOnset; |
|
261 } |
|
262 |
|
263 return onset; |
|
264 } |
|
265 |
|
266 /** |
|
267 * Returns a cached onset for the specified date. |
|
268 * @param date |
|
269 * @return a cached onset date or null if no cached onset is applicable for the specified date |
|
270 */ |
|
271 private DateTime getCachedOnset(final Date date) { |
|
272 int index = Arrays.binarySearch(onsetsMillisec, date.getTime()); |
|
273 if (index >= 0) { |
|
274 return onsetsDates[index]; |
|
275 } else { |
|
276 int insertionIndex = -index -1; |
|
277 return onsetsDates[insertionIndex -1]; |
|
278 } |
|
279 } |
|
280 |
|
281 /** |
|
282 * Returns the mandatory dtstart property. |
|
283 * @return the DTSTART property or null if not specified |
|
284 */ |
|
285 public final DtStart getStartDate() { |
|
286 return (DtStart) getProperty(Property.DTSTART); |
|
287 } |
|
288 |
|
289 /** |
|
290 * Returns the mandatory tzoffsetfrom property. |
|
291 * @return the TZOFFSETFROM property or null if not specified |
|
292 */ |
|
293 public final TzOffsetFrom getOffsetFrom() { |
|
294 return (TzOffsetFrom) getProperty(Property.TZOFFSETFROM); |
|
295 } |
|
296 |
|
297 /** |
|
298 * Returns the mandatory tzoffsetto property. |
|
299 * @return the TZOFFSETTO property or null if not specified |
|
300 */ |
|
301 public final TzOffsetTo getOffsetTo() { |
|
302 return (TzOffsetTo) getProperty(Property.TZOFFSETTO); |
|
303 } |
|
304 |
|
305 // private Date calculateOnset(DateProperty dateProperty) { |
|
306 // return calculateOnset(dateProperty.getValue()); |
|
307 // } |
|
308 // |
|
309 private DateTime calculateOnset(Date date) throws ParseException { |
|
310 return calculateOnset(date.toString()); |
|
311 } |
|
312 |
|
313 private DateTime calculateOnset(String dateStr) throws ParseException { |
|
314 |
|
315 // Translate local onset into UTC time by parsing local time |
|
316 // as GMT and adjusting by TZOFFSETFROM if required |
|
317 long utcOnset; |
|
318 |
|
319 synchronized (UTC_FORMAT) { |
|
320 utcOnset = UTC_FORMAT.parse(dateStr).getTime(); |
|
321 } |
|
322 |
|
323 // return a UTC |
|
324 DateTime onset = new DateTime(true); |
|
325 onset.setTime(utcOnset); |
|
326 return onset; |
|
327 } |
|
328 |
|
329 private DateTime applyOffsetFrom(DateTime orig) { |
|
330 DateTime withOffset = new DateTime(true); |
|
331 withOffset.setTime(orig.getTime() - getOffsetFrom().getOffset().getOffset()); |
|
332 return withOffset; |
|
333 } |
|
334 } |