/*
* $Id: Observance.java,v 1.12 2006/07/23 08:26:06 fortuna Exp $ [05-Apr-2004]
*
* Copyright (c) 2005, 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.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.Period;
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;
/**
* 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 implements Comparable {
/**
* one of 'standardc' or 'daylightc' MUST occur and each MAY occur more than
* once.
*/
public static final String STANDARD = "STANDARD";
public static final String DAYLIGHT = "DAYLIGHT";
private Log log = LogFactory.getLog(Observance.class);
// TODO: clear cache when observance definition changes (??)
private Map onsets = new TreeMap();
private boolean rdatesCached = false;
/**
* 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);
}
/**
* @see net.fortuna.ical4j.model.Component#validate(boolean)
*/
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) {
// The initial on-set will be '16010101T090000 GMT' if '16010101T020000' DTSTART and '-700' offset were given.
DtStart dtStart = (DtStart) getProperty(Property.DTSTART);
Date initialOnset = new DateTime(dtStart.getDate().getTime() - getOffsetFrom().getOffset().getOffset());
// observance not applicable if date is before the effective date of this observance..
if (date.before(initialOnset)) {
return null;
}
long start = System.currentTimeMillis();
Date onset = getCachedOnset(date);
if (log.isDebugEnabled()) {
log.debug("Cache " + ((onset != null) ? "hit" : "miss") + " - retrieval time: " + (System.currentTimeMillis() - start) + "ms");
}
if (onset == null) {
onset = initialOnset;
// collect all onsets for the purposes of caching..
DateList cacheableOnsets = new DateList();
// Date nextOnset = null;
if (!rdatesCached) {
// check rdates for latest applicable onset..
PropertyList rdates = getProperties(Property.RDATE);
for (Iterator i = rdates.iterator(); i.hasNext();) {
RDate rdate = (RDate) i.next();
for (Iterator j = rdate.getDates().iterator(); j.hasNext();) {
Date rdateOnset = (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);
}
}
rdatesCached = true;
}
// check recurrence rules for latest applicable onset..
PropertyList rrules = getProperties(Property.RRULE);
Value dateType;
if (date instanceof DateTime) {
dateType = Value.DATE_TIME;
}
else {
dateType = Value.DATE;
}
for (Iterator i = rrules.iterator(); i.hasNext();) {
RRule rrule = (RRule) i.next();
// include future onsets to determine onset period..
Calendar cal = Dates.getCalendarInstance(date);
cal.setTime(date);
cal.add(Calendar.YEAR, 1);
Date endRecur = Dates.getInstance(cal.getTime(), dateType);
DateList recurrenceDates = rrule.getRecur().getDates(onset, endRecur, dateType);
for (Iterator j = recurrenceDates.iterator(); j.hasNext();) {
Date rruleOnset = (Date) 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);
Date cacheableOnset = null;
Date nextOnset = null;
for (Iterator i = cacheableOnsets.iterator(); i.hasNext();) {
cacheableOnset = nextOnset;
nextOnset = (Date) i.next();
if (cacheableOnset != null) {
onsets.put(new Period(new DateTime(cacheableOnset), new DateTime(nextOnset)), cacheableOnset);
}
}
// as we don't have an onset following the final onset, we must
// cache it with an arbitrary period length..
if (nextOnset != null) {
Calendar finalOnsetPeriodEnd = Calendar.getInstance();
finalOnsetPeriodEnd.setTime(nextOnset);
finalOnsetPeriodEnd.add(Calendar.YEAR, 100);
onsets.put(new Period(new DateTime(nextOnset),
new DateTime(finalOnsetPeriodEnd.getTime())),
nextOnset);
}
/*
Period onsetPeriod = null;
if (nextOnset != null) {
onsetPeriod = new Period(new DateTime(onset), new DateTime(nextOnset));
}
else {
onsetPeriod = new Period(new DateTime(onset), new DateTime(date));
}
onsets.put(onsetPeriod, onset);
*/
}
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 Date getCachedOnset(final Date date) {
for (Iterator i = onsets.keySet().iterator(); i.hasNext();) {
Period onsetPeriod = (Period) i.next();
if (onsetPeriod.includes(date)) {
return (Date) onsets.get(onsetPeriod);
}
}
return null;
}
/**
* Returns the mandatory dtstart property.
* @return
*/
public final DtStart getStartDate() {
return (DtStart) getProperty(Property.DTSTART);
}
/**
* Returns the mandatory tzoffsetfrom property.
* @return
*/
public final TzOffsetFrom getOffsetFrom() {
return (TzOffsetFrom) getProperty(Property.TZOFFSETFROM);
}
/**
* Returns the mandatory tzoffsetto property.
* @return
*/
public final TzOffsetTo getOffsetTo() {
return (TzOffsetTo) getProperty(Property.TZOFFSETTO);
}
/* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public final int compareTo(final Object arg0) {
return compareTo((Observance) arg0);
}
/**
* @param arg0
* @return
*/
public final int compareTo(final Observance arg0) {
// TODO: sort by RDATE??
DtStart dtStart = (DtStart) getProperty(Property.DTSTART);
DtStart dtStart0 = (DtStart) arg0.getProperty(Property.DTSTART);
return dtStart.getDate().compareTo(dtStart0.getDate());
}
}