1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/net/fortuna/ical4j/model/component/Observance.java Tue Feb 10 18:12:00 2015 +0100 1.3 @@ -0,0 +1,334 @@ 1.4 +/** 1.5 + * Copyright (c) 2012, Ben Fortuna 1.6 + * All rights reserved. 1.7 + * 1.8 + * Redistribution and use in source and binary forms, with or without 1.9 + * modification, are permitted provided that the following conditions 1.10 + * are met: 1.11 + * 1.12 + * o Redistributions of source code must retain the above copyright 1.13 + * notice, this list of conditions and the following disclaimer. 1.14 + * 1.15 + * o Redistributions in binary form must reproduce the above copyright 1.16 + * notice, this list of conditions and the following disclaimer in the 1.17 + * documentation and/or other materials provided with the distribution. 1.18 + * 1.19 + * o Neither the name of Ben Fortuna nor the names of any other contributors 1.20 + * may be used to endorse or promote products derived from this software 1.21 + * without specific prior written permission. 1.22 + * 1.23 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 1.24 + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 1.25 + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 1.26 + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 1.27 + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 1.28 + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 1.29 + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 1.30 + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 1.31 + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 1.32 + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 1.33 + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 1.34 + */ 1.35 +package net.fortuna.ical4j.model.component; 1.36 + 1.37 +import java.io.IOException; 1.38 +import java.text.DateFormat; 1.39 +import java.text.ParseException; 1.40 +import java.text.SimpleDateFormat; 1.41 +import java.util.Arrays; 1.42 +import java.util.Calendar; 1.43 +import java.util.Collections; 1.44 +import java.util.Iterator; 1.45 +import java.util.Map; 1.46 +import java.util.TreeMap; 1.47 + 1.48 +import net.fortuna.ical4j.model.Component; 1.49 +import net.fortuna.ical4j.model.Date; 1.50 +import net.fortuna.ical4j.model.DateList; 1.51 +import net.fortuna.ical4j.model.DateTime; 1.52 +import net.fortuna.ical4j.model.Property; 1.53 +import net.fortuna.ical4j.model.PropertyList; 1.54 +import net.fortuna.ical4j.model.ValidationException; 1.55 +import net.fortuna.ical4j.model.parameter.Value; 1.56 +import net.fortuna.ical4j.model.property.DtStart; 1.57 +import net.fortuna.ical4j.model.property.RDate; 1.58 +import net.fortuna.ical4j.model.property.RRule; 1.59 +import net.fortuna.ical4j.model.property.TzOffsetFrom; 1.60 +import net.fortuna.ical4j.model.property.TzOffsetTo; 1.61 +import net.fortuna.ical4j.util.Dates; 1.62 +import net.fortuna.ical4j.util.PropertyValidator; 1.63 +import net.fortuna.ical4j.util.TimeZones; 1.64 + 1.65 +import org.apache.commons.logging.Log; 1.66 +import org.apache.commons.logging.LogFactory; 1.67 + 1.68 +/** 1.69 + * $Id$ [05-Apr-2004] 1.70 + * 1.71 + * Defines an iCalendar sub-component representing a timezone observance. Class made abstract such that only Standard 1.72 + * and Daylight instances are valid. 1.73 + * @author Ben Fortuna 1.74 + */ 1.75 +public abstract class Observance extends Component { 1.76 + 1.77 + /** 1.78 + * 1.79 + */ 1.80 + private static final long serialVersionUID = 2523330383042085994L; 1.81 + 1.82 + /** 1.83 + * one of 'standardc' or 'daylightc' MUST occur and each MAY occur more than once. 1.84 + */ 1.85 + public static final String STANDARD = "STANDARD"; 1.86 + 1.87 + /** 1.88 + * Token for daylight observance. 1.89 + */ 1.90 + public static final String DAYLIGHT = "DAYLIGHT"; 1.91 + 1.92 + // TODO: clear cache when observance definition changes (??) 1.93 + private long[] onsetsMillisec; 1.94 + private DateTime[] onsetsDates; 1.95 + private Map onsets = new TreeMap(); 1.96 + private Date initialOnset = null; 1.97 + 1.98 + /** 1.99 + * Used for parsing times in a UTC date-time representation. 1.100 + */ 1.101 + private static final String UTC_PATTERN = "yyyyMMdd'T'HHmmss"; 1.102 + private static final DateFormat UTC_FORMAT = new SimpleDateFormat( 1.103 + UTC_PATTERN); 1.104 + 1.105 + static { 1.106 + UTC_FORMAT.setTimeZone(TimeZones.getUtcTimeZone()); 1.107 + UTC_FORMAT.setLenient(false); 1.108 + } 1.109 + 1.110 + /* If this is set we have rrules. If we get a date after this rebuild onsets */ 1.111 + private Date onsetLimit; 1.112 + 1.113 + /** 1.114 + * Constructs a timezone observance with the specified name and no properties. 1.115 + * @param name the name of this observance component 1.116 + */ 1.117 + protected Observance(final String name) { 1.118 + super(name); 1.119 + } 1.120 + 1.121 + /** 1.122 + * Constructor protected to enforce use of sub-classes from this library. 1.123 + * @param name the name of the time type 1.124 + * @param properties a list of properties 1.125 + */ 1.126 + protected Observance(final String name, final PropertyList properties) { 1.127 + super(name, properties); 1.128 + } 1.129 + 1.130 + /** 1.131 + * {@inheritDoc} 1.132 + */ 1.133 + public final void validate(final boolean recurse) throws ValidationException { 1.134 + 1.135 + // From "4.8.3.3 Time Zone Offset From": 1.136 + // Conformance: This property MUST be specified in a "VTIMEZONE" 1.137 + // calendar component. 1.138 + PropertyValidator.getInstance().assertOne(Property.TZOFFSETFROM, 1.139 + getProperties()); 1.140 + 1.141 + // From "4.8.3.4 Time Zone Offset To": 1.142 + // Conformance: This property MUST be specified in a "VTIMEZONE" 1.143 + // calendar component. 1.144 + PropertyValidator.getInstance().assertOne(Property.TZOFFSETTO, 1.145 + getProperties()); 1.146 + 1.147 + /* 1.148 + * ; the following are each REQUIRED, ; but MUST NOT occur more than once dtstart / tzoffsetto / tzoffsetfrom / 1.149 + */ 1.150 + PropertyValidator.getInstance().assertOne(Property.DTSTART, 1.151 + getProperties()); 1.152 + 1.153 + /* 1.154 + * ; the following are optional, ; and MAY occur more than once comment / rdate / rrule / tzname / x-prop 1.155 + */ 1.156 + 1.157 + if (recurse) { 1.158 + validateProperties(); 1.159 + } 1.160 + } 1.161 + 1.162 + /** 1.163 + * Returns the latest applicable onset of this observance for the specified date. 1.164 + * @param date the latest date that an observance onset may occur 1.165 + * @return the latest applicable observance date or null if there is no applicable observance onset for the 1.166 + * specified date 1.167 + */ 1.168 + public final Date getLatestOnset(final Date date) { 1.169 + 1.170 + if (initialOnset == null) { 1.171 + try { 1.172 + initialOnset = applyOffsetFrom(calculateOnset(((DtStart) getProperty(Property.DTSTART)).getDate())); 1.173 + } catch (ParseException e) { 1.174 + Log log = LogFactory.getLog(Observance.class); 1.175 + log.error("Unexpected error calculating initial onset", e); 1.176 + // XXX: is this correct? 1.177 + return null; 1.178 + } 1.179 + } 1.180 + 1.181 + // observance not applicable if date is before the effective date of this observance.. 1.182 + if (date.before(initialOnset)) { 1.183 + return null; 1.184 + } 1.185 + 1.186 + if ((onsetsMillisec != null) && (onsetLimit == null || date.before(onsetLimit))) { 1.187 + return getCachedOnset(date); 1.188 + } 1.189 + 1.190 + Date onset = initialOnset; 1.191 + Date initialOnsetUTC; 1.192 + // get first onset without adding TZFROM as this may lead to a day boundary 1.193 + // change which would be incompatible with BYDAY RRULES 1.194 + // we will have to add the offset to all cacheable onsets 1.195 + try { 1.196 + initialOnsetUTC = calculateOnset(((DtStart) getProperty(Property.DTSTART)).getDate()); 1.197 + } catch (ParseException e) { 1.198 + Log log = LogFactory.getLog(Observance.class); 1.199 + log.error("Unexpected error calculating initial onset", e); 1.200 + // XXX: is this correct? 1.201 + return null; 1.202 + } 1.203 + // collect all onsets for the purposes of caching.. 1.204 + final DateList cacheableOnsets = new DateList(); 1.205 + cacheableOnsets.setUtc(true); 1.206 + cacheableOnsets.add(initialOnset); 1.207 + 1.208 + // check rdates for latest applicable onset.. 1.209 + final PropertyList rdates = getProperties(Property.RDATE); 1.210 + for (final Iterator i = rdates.iterator(); i.hasNext();) { 1.211 + final RDate rdate = (RDate) i.next(); 1.212 + for (final Iterator j = rdate.getDates().iterator(); j.hasNext();) { 1.213 + try { 1.214 + final DateTime rdateOnset = applyOffsetFrom(calculateOnset((Date) j.next())); 1.215 + if (!rdateOnset.after(date) && rdateOnset.after(onset)) { 1.216 + onset = rdateOnset; 1.217 + } 1.218 + /* 1.219 + * else if (rdateOnset.after(date) && rdateOnset.after(onset) && (nextOnset == null || 1.220 + * rdateOnset.before(nextOnset))) { nextOnset = rdateOnset; } 1.221 + */ 1.222 + cacheableOnsets.add(rdateOnset); 1.223 + } catch (ParseException e) { 1.224 + Log log = LogFactory.getLog(Observance.class); 1.225 + log.error("Unexpected error calculating onset", e); 1.226 + } 1.227 + } 1.228 + } 1.229 + 1.230 + // check recurrence rules for latest applicable onset.. 1.231 + final PropertyList rrules = getProperties(Property.RRULE); 1.232 + for (final Iterator i = rrules.iterator(); i.hasNext();) { 1.233 + final RRule rrule = (RRule) i.next(); 1.234 + // include future onsets to determine onset period.. 1.235 + final Calendar cal = Dates.getCalendarInstance(date); 1.236 + cal.setTime(date); 1.237 + cal.add(Calendar.YEAR, 10); 1.238 + onsetLimit = Dates.getInstance(cal.getTime(), Value.DATE_TIME); 1.239 + final DateList recurrenceDates = rrule.getRecur().getDates(initialOnsetUTC, 1.240 + onsetLimit, Value.DATE_TIME); 1.241 + for (final Iterator j = recurrenceDates.iterator(); j.hasNext();) { 1.242 + final DateTime rruleOnset = applyOffsetFrom((DateTime) j.next()); 1.243 + if (!rruleOnset.after(date) && rruleOnset.after(onset)) { 1.244 + onset = rruleOnset; 1.245 + } 1.246 + /* 1.247 + * else if (rruleOnset.after(date) && rruleOnset.after(onset) && (nextOnset == null || 1.248 + * rruleOnset.before(nextOnset))) { nextOnset = rruleOnset; } 1.249 + */ 1.250 + cacheableOnsets.add(rruleOnset); 1.251 + } 1.252 + } 1.253 + 1.254 + // cache onsets.. 1.255 + Collections.sort(cacheableOnsets); 1.256 + DateTime cacheableOnset = null; 1.257 + this.onsetsMillisec = new long[cacheableOnsets.size()]; 1.258 + this.onsetsDates = new DateTime[onsetsMillisec.length]; 1.259 + 1.260 + for (int i = 0; i < onsetsMillisec.length; i++) { 1.261 + cacheableOnset = (DateTime)cacheableOnsets.get(i); 1.262 + onsetsMillisec[i] = cacheableOnset.getTime(); 1.263 + onsetsDates[i] = cacheableOnset; 1.264 + } 1.265 + 1.266 + return onset; 1.267 + } 1.268 + 1.269 + /** 1.270 + * Returns a cached onset for the specified date. 1.271 + * @param date 1.272 + * @return a cached onset date or null if no cached onset is applicable for the specified date 1.273 + */ 1.274 + private DateTime getCachedOnset(final Date date) { 1.275 + int index = Arrays.binarySearch(onsetsMillisec, date.getTime()); 1.276 + if (index >= 0) { 1.277 + return onsetsDates[index]; 1.278 + } else { 1.279 + int insertionIndex = -index -1; 1.280 + return onsetsDates[insertionIndex -1]; 1.281 + } 1.282 + } 1.283 + 1.284 + /** 1.285 + * Returns the mandatory dtstart property. 1.286 + * @return the DTSTART property or null if not specified 1.287 + */ 1.288 + public final DtStart getStartDate() { 1.289 + return (DtStart) getProperty(Property.DTSTART); 1.290 + } 1.291 + 1.292 + /** 1.293 + * Returns the mandatory tzoffsetfrom property. 1.294 + * @return the TZOFFSETFROM property or null if not specified 1.295 + */ 1.296 + public final TzOffsetFrom getOffsetFrom() { 1.297 + return (TzOffsetFrom) getProperty(Property.TZOFFSETFROM); 1.298 + } 1.299 + 1.300 + /** 1.301 + * Returns the mandatory tzoffsetto property. 1.302 + * @return the TZOFFSETTO property or null if not specified 1.303 + */ 1.304 + public final TzOffsetTo getOffsetTo() { 1.305 + return (TzOffsetTo) getProperty(Property.TZOFFSETTO); 1.306 + } 1.307 + 1.308 +// private Date calculateOnset(DateProperty dateProperty) { 1.309 +// return calculateOnset(dateProperty.getValue()); 1.310 +// } 1.311 +// 1.312 + private DateTime calculateOnset(Date date) throws ParseException { 1.313 + return calculateOnset(date.toString()); 1.314 + } 1.315 + 1.316 + private DateTime calculateOnset(String dateStr) throws ParseException { 1.317 + 1.318 + // Translate local onset into UTC time by parsing local time 1.319 + // as GMT and adjusting by TZOFFSETFROM if required 1.320 + long utcOnset; 1.321 + 1.322 + synchronized (UTC_FORMAT) { 1.323 + utcOnset = UTC_FORMAT.parse(dateStr).getTime(); 1.324 + } 1.325 + 1.326 + // return a UTC 1.327 + DateTime onset = new DateTime(true); 1.328 + onset.setTime(utcOnset); 1.329 + return onset; 1.330 + } 1.331 + 1.332 + private DateTime applyOffsetFrom(DateTime orig) { 1.333 + DateTime withOffset = new DateTime(true); 1.334 + withOffset.setTime(orig.getTime() - getOffsetFrom().getOffset().getOffset()); 1.335 + return withOffset; 1.336 + } 1.337 +} 1.338 \ No newline at end of file