src/net/fortuna/ical4j/model/component/Observance.java

changeset 0
fb9019fb1bf7
equal deleted inserted replaced
-1:000000000000 0:9a23f38df870
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 }

mercurial