diff -r 000000000000 -r fb9019fb1bf7 src/net/fortuna/ical4j/model/component/Observance.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/net/fortuna/ical4j/model/component/Observance.java Tue Feb 10 18:12:00 2015 +0100 @@ -0,0 +1,334 @@ +/** + * Copyright (c) 2012, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.fortuna.ical4j.model.component; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + +import net.fortuna.ical4j.model.Component; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.DateList; +import net.fortuna.ical4j.model.DateTime; +import net.fortuna.ical4j.model.Property; +import net.fortuna.ical4j.model.PropertyList; +import net.fortuna.ical4j.model.ValidationException; +import net.fortuna.ical4j.model.parameter.Value; +import net.fortuna.ical4j.model.property.DtStart; +import net.fortuna.ical4j.model.property.RDate; +import net.fortuna.ical4j.model.property.RRule; +import net.fortuna.ical4j.model.property.TzOffsetFrom; +import net.fortuna.ical4j.model.property.TzOffsetTo; +import net.fortuna.ical4j.util.Dates; +import net.fortuna.ical4j.util.PropertyValidator; +import net.fortuna.ical4j.util.TimeZones; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * $Id$ [05-Apr-2004] + * + * Defines an iCalendar sub-component representing a timezone observance. Class made abstract such that only Standard + * and Daylight instances are valid. + * @author Ben Fortuna + */ +public abstract class Observance extends Component { + + /** + * + */ + private static final long serialVersionUID = 2523330383042085994L; + + /** + * one of 'standardc' or 'daylightc' MUST occur and each MAY occur more than once. + */ + public static final String STANDARD = "STANDARD"; + + /** + * Token for daylight observance. + */ + public static final String DAYLIGHT = "DAYLIGHT"; + + // TODO: clear cache when observance definition changes (??) + private long[] onsetsMillisec; + private DateTime[] onsetsDates; + private Map onsets = new TreeMap(); + private Date initialOnset = null; + + /** + * Used for parsing times in a UTC date-time representation. + */ + private static final String UTC_PATTERN = "yyyyMMdd'T'HHmmss"; + private static final DateFormat UTC_FORMAT = new SimpleDateFormat( + UTC_PATTERN); + + static { + UTC_FORMAT.setTimeZone(TimeZones.getUtcTimeZone()); + UTC_FORMAT.setLenient(false); + } + + /* If this is set we have rrules. If we get a date after this rebuild onsets */ + private Date onsetLimit; + + /** + * Constructs a timezone observance with the specified name and no properties. + * @param name the name of this observance component + */ + protected Observance(final String name) { + super(name); + } + + /** + * Constructor protected to enforce use of sub-classes from this library. + * @param name the name of the time type + * @param properties a list of properties + */ + protected Observance(final String name, final PropertyList properties) { + super(name, properties); + } + + /** + * {@inheritDoc} + */ + public final void validate(final boolean recurse) throws ValidationException { + + // From "4.8.3.3 Time Zone Offset From": + // Conformance: This property MUST be specified in a "VTIMEZONE" + // calendar component. + PropertyValidator.getInstance().assertOne(Property.TZOFFSETFROM, + getProperties()); + + // From "4.8.3.4 Time Zone Offset To": + // Conformance: This property MUST be specified in a "VTIMEZONE" + // calendar component. + PropertyValidator.getInstance().assertOne(Property.TZOFFSETTO, + getProperties()); + + /* + * ; the following are each REQUIRED, ; but MUST NOT occur more than once dtstart / tzoffsetto / tzoffsetfrom / + */ + PropertyValidator.getInstance().assertOne(Property.DTSTART, + getProperties()); + + /* + * ; the following are optional, ; and MAY occur more than once comment / rdate / rrule / tzname / x-prop + */ + + if (recurse) { + validateProperties(); + } + } + + /** + * Returns the latest applicable onset of this observance for the specified date. + * @param date the latest date that an observance onset may occur + * @return the latest applicable observance date or null if there is no applicable observance onset for the + * specified date + */ + public final Date getLatestOnset(final Date date) { + + if (initialOnset == null) { + try { + initialOnset = applyOffsetFrom(calculateOnset(((DtStart) getProperty(Property.DTSTART)).getDate())); + } catch (ParseException e) { + Log log = LogFactory.getLog(Observance.class); + log.error("Unexpected error calculating initial onset", e); + // XXX: is this correct? + return null; + } + } + + // observance not applicable if date is before the effective date of this observance.. + if (date.before(initialOnset)) { + return null; + } + + if ((onsetsMillisec != null) && (onsetLimit == null || date.before(onsetLimit))) { + return getCachedOnset(date); + } + + Date onset = initialOnset; + Date initialOnsetUTC; + // get first onset without adding TZFROM as this may lead to a day boundary + // change which would be incompatible with BYDAY RRULES + // we will have to add the offset to all cacheable onsets + try { + initialOnsetUTC = calculateOnset(((DtStart) getProperty(Property.DTSTART)).getDate()); + } catch (ParseException e) { + Log log = LogFactory.getLog(Observance.class); + log.error("Unexpected error calculating initial onset", e); + // XXX: is this correct? + return null; + } + // collect all onsets for the purposes of caching.. + final DateList cacheableOnsets = new DateList(); + cacheableOnsets.setUtc(true); + cacheableOnsets.add(initialOnset); + + // check rdates for latest applicable onset.. + final PropertyList rdates = getProperties(Property.RDATE); + for (final Iterator i = rdates.iterator(); i.hasNext();) { + final RDate rdate = (RDate) i.next(); + for (final Iterator j = rdate.getDates().iterator(); j.hasNext();) { + try { + final DateTime rdateOnset = applyOffsetFrom(calculateOnset((Date) j.next())); + if (!rdateOnset.after(date) && rdateOnset.after(onset)) { + onset = rdateOnset; + } + /* + * else if (rdateOnset.after(date) && rdateOnset.after(onset) && (nextOnset == null || + * rdateOnset.before(nextOnset))) { nextOnset = rdateOnset; } + */ + cacheableOnsets.add(rdateOnset); + } catch (ParseException e) { + Log log = LogFactory.getLog(Observance.class); + log.error("Unexpected error calculating onset", e); + } + } + } + + // check recurrence rules for latest applicable onset.. + final PropertyList rrules = getProperties(Property.RRULE); + for (final Iterator i = rrules.iterator(); i.hasNext();) { + final RRule rrule = (RRule) i.next(); + // include future onsets to determine onset period.. + final Calendar cal = Dates.getCalendarInstance(date); + cal.setTime(date); + cal.add(Calendar.YEAR, 10); + onsetLimit = Dates.getInstance(cal.getTime(), Value.DATE_TIME); + final DateList recurrenceDates = rrule.getRecur().getDates(initialOnsetUTC, + onsetLimit, Value.DATE_TIME); + for (final Iterator j = recurrenceDates.iterator(); j.hasNext();) { + final DateTime rruleOnset = applyOffsetFrom((DateTime) j.next()); + if (!rruleOnset.after(date) && rruleOnset.after(onset)) { + onset = rruleOnset; + } + /* + * else if (rruleOnset.after(date) && rruleOnset.after(onset) && (nextOnset == null || + * rruleOnset.before(nextOnset))) { nextOnset = rruleOnset; } + */ + cacheableOnsets.add(rruleOnset); + } + } + + // cache onsets.. + Collections.sort(cacheableOnsets); + DateTime cacheableOnset = null; + this.onsetsMillisec = new long[cacheableOnsets.size()]; + this.onsetsDates = new DateTime[onsetsMillisec.length]; + + for (int i = 0; i < onsetsMillisec.length; i++) { + cacheableOnset = (DateTime)cacheableOnsets.get(i); + onsetsMillisec[i] = cacheableOnset.getTime(); + onsetsDates[i] = cacheableOnset; + } + + return onset; + } + + /** + * Returns a cached onset for the specified date. + * @param date + * @return a cached onset date or null if no cached onset is applicable for the specified date + */ + private DateTime getCachedOnset(final Date date) { + int index = Arrays.binarySearch(onsetsMillisec, date.getTime()); + if (index >= 0) { + return onsetsDates[index]; + } else { + int insertionIndex = -index -1; + return onsetsDates[insertionIndex -1]; + } + } + + /** + * Returns the mandatory dtstart property. + * @return the DTSTART property or null if not specified + */ + public final DtStart getStartDate() { + return (DtStart) getProperty(Property.DTSTART); + } + + /** + * Returns the mandatory tzoffsetfrom property. + * @return the TZOFFSETFROM property or null if not specified + */ + public final TzOffsetFrom getOffsetFrom() { + return (TzOffsetFrom) getProperty(Property.TZOFFSETFROM); + } + + /** + * Returns the mandatory tzoffsetto property. + * @return the TZOFFSETTO property or null if not specified + */ + public final TzOffsetTo getOffsetTo() { + return (TzOffsetTo) getProperty(Property.TZOFFSETTO); + } + +// private Date calculateOnset(DateProperty dateProperty) { +// return calculateOnset(dateProperty.getValue()); +// } +// + private DateTime calculateOnset(Date date) throws ParseException { + return calculateOnset(date.toString()); + } + + private DateTime calculateOnset(String dateStr) throws ParseException { + + // Translate local onset into UTC time by parsing local time + // as GMT and adjusting by TZOFFSETFROM if required + long utcOnset; + + synchronized (UTC_FORMAT) { + utcOnset = UTC_FORMAT.parse(dateStr).getTime(); + } + + // return a UTC + DateTime onset = new DateTime(true); + onset.setTime(utcOnset); + return onset; + } + + private DateTime applyOffsetFrom(DateTime orig) { + DateTime withOffset = new DateTime(true); + withOffset.setTime(orig.getTime() - getOffsetFrom().getOffset().getOffset()); + return withOffset; + } +} \ No newline at end of file