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