/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/calendar/trunk/calendar-impl/impl/src/java/org/sakaiproject/calendar/impl/RecurrenceRuleBase.java $
* $Id: RecurrenceRuleBase.java 105079 2012-02-24 23:08:11Z ottenhoff@longsight.com $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**********************************************************************************/
package org.sakaiproject.calendar.impl;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.Vector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.calendar.api.RecurrenceRule;
import org.sakaiproject.time.api.Time;
import org.sakaiproject.time.api.TimeBreakdown;
import org.sakaiproject.time.api.TimeRange;
import org.sakaiproject.time.cover.TimeService;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import org.sakaiproject.util.ResourceLoader;
/**
* This is a common base for the daily, weekly, monthly, and yearly recurrence rules.
*/
public abstract class RecurrenceRuleBase implements RecurrenceRule
{
protected static final Log M_log = LogFactory.getLog(RecurrenceRuleBase.class);
/** Every this many number of units: 1 would be daily/monthly/annually. */
private int interval = 1;
/** For this many occurrences - if 0, does not limit. */
private int count = 0;
/** No time ranges past this time are generated - if null, does not limit. */
private Time until = null;
/*For i18n of recurrence rule description*/
protected ResourceLoader rb = new ResourceLoader("calendar");
/**
* Construct.
*/
public RecurrenceRuleBase()
{
} // RecurrenceRuleBase
/**
* Construct with no limits.
* @param interval Every this many number of days: 1 would be daily.
*/
public RecurrenceRuleBase(int interval)
{
this.interval = interval;
} // RecurrenceRuleBase
/**
* Construct with count limit.
* @param interval Every this many number of days: 1 would be daily.
* @param count For this many occurrences - if 0, does not limit.
*/
public RecurrenceRuleBase(int interval, int count)
{
this.interval = interval;
this.count = count;
} // RecurrenceRuleBase
/**
* Construct with time limit.
* @param interval Every this many number of days: 1 would be daily.
* @param until No time ranges past this time are generated - if null, does not limit.
*/
public RecurrenceRuleBase(int interval, Time until)
{
this.interval = interval;
this.until = (Time) until.clone();
} // RecurrenceRuleBase
/**
* Return a List of all RecurrenceInstance objects generated by this rule within the given time range, based on the
* prototype first range, in time order.
* @param prototype The prototype first TimeRange.
* @param range A time range to limit the generated ranges.
* @param timeZone The time zone to use for displaying times.
* %%% Note: this is currently not implemented, and always uses the "local" zone.
* @return a List of RecurrenceInstance generated by this rule in this range.
*/
public List generateInstances(TimeRange prototype, TimeRange range, TimeZone timeZone)
{
// these calendars are used if local time zone and the time zone where the first event was created (timeZone) are different
GregorianCalendar firstEventCalendarDate = null;
GregorianCalendar nextFirstEventCalendarDate = null;
// %%% Note: base the breakdonw on the "timeZone" parameter to support multiple timeZone displays -ggolden
TimeBreakdown startBreakdown = prototype.firstTime().breakdownLocal();
GregorianCalendar startCalendarDate = TimeService.getCalendar(TimeService.getLocalTimeZone(),0,0,0,0,0,0,0);
startCalendarDate.set(
startBreakdown.getYear(),
startBreakdown.getMonth() - 1,
startBreakdown.getDay(),
startBreakdown.getHour(),
startBreakdown.getMin(),
startBreakdown.getSec());
// if local time zone and first event time zone are different
// a new calendar is generated to calculate the re-occurring events
// if not, the local time zone calendar is used
boolean differentTimeZone = false;
if (TimeService.getLocalTimeZone().getID().equals(timeZone.getID()))
{
differentTimeZone = false;
}
else
{
differentTimeZone = true;
}
if (differentTimeZone)
{
firstEventCalendarDate = TimeService.getCalendar(timeZone,0,0,0,0,0,0,0);
firstEventCalendarDate.setTimeInMillis(startCalendarDate.getTimeInMillis());
nextFirstEventCalendarDate = (GregorianCalendar) firstEventCalendarDate.clone();
}
List rv = new Vector();
GregorianCalendar nextCalendarDate =
(GregorianCalendar) startCalendarDate.clone();
int currentCount = 1;
do
{
if (differentTimeZone)
{
// next time is calculated according to the first event time zone, not the local one
nextCalendarDate.setTimeInMillis(nextFirstEventCalendarDate.getTimeInMillis());
}
Time nextTime = TimeService.newTime(nextCalendarDate);
// is this past count?
if ((getCount() > 0) && (currentCount > getCount()))
break;
// is this past until?
if ((getUntil() != null) && isAfter(nextTime, getUntil()) )
break;
TimeRange nextTimeRange = TimeService.newTimeRange(nextTime.getTime(), prototype.duration());
//
// Is this out of the range?
//
if (isOverlap(range, nextTimeRange))
{
TimeRange eventTimeRange = null;
// Single time cases require special handling.
if ( prototype.isSingleTime() )
{
eventTimeRange = TimeService.newTimeRange(nextTimeRange.firstTime());
}
else
{
eventTimeRange = TimeService.newTimeRange(nextTimeRange.firstTime(), nextTimeRange.lastTime(), true, false);
}
// use this one
rv.add(new RecurrenceInstance(eventTimeRange, currentCount));
}
// if next starts after the range, stop generating
else if (isAfter(nextTime, range.lastTime()))
break;
// advance interval years.
if (differentTimeZone)
{
nextFirstEventCalendarDate = (GregorianCalendar) firstEventCalendarDate.clone();
nextFirstEventCalendarDate.add(getRecurrenceType(), getInterval()*currentCount);
}
else
{
nextCalendarDate = (GregorianCalendar) startCalendarDate.clone();
nextCalendarDate.add(getRecurrenceType(), getInterval()*currentCount);
}
currentCount++;
}
while (true);
return rv;
}
protected abstract int getRecurrenceType();
/**
* Checks for overlap.
* @param range1
* @param range2
*/
protected final boolean isOverlap(TimeRange range1, TimeRange range2)
{
return range1.overlaps(range2);
}
/**
* Returns true if time1 is after time2
* @param time1
* @param time2
*/
protected final boolean isAfter(Time time1, Time time2)
{
return (time1.getTime() > time2.getTime());
}
/**
* Gets the number of times (years) that this event should repeat.
*/
public final int getCount()
{
return count;
}
/**
* Gets the end time for recurring events.
*/
public final Time getUntil()
{
return until;
}
/**
* Gets the interval (in years) for this event.
*/
public final int getInterval()
{
return interval;
}
/**
* Sets the number of times that this event will repeat. This object is immutable, but we need
* the "set" method for unit testing.
* @param i
*/
protected final void setCount(int i)
{
this.count = i;
setUntil(null);
}
/**
* Sets the interval (in months) which this event will repeat. This object is immutable, but we need
* the "set" method for unit testing.
* @param i
*/
protected final void setInterval(int i)
{
this.interval = i;
setCount(0);
}
/**
* Sets the end time for this recurring event. This object is immutable, but we need
* the "set" method for unit testing.
* @param time
*/
protected final void setUntil(Time time)
{
this.until = time;
}
/**
* Take values from this xml element
* @param el The xml element.
*/
public void set(Element el)
{
// read the interval
try
{
setInterval(Integer.parseInt(el.getAttribute("interval")));
}
catch (Exception any) {}
// read the count
try
{
setCount(Integer.parseInt(el.getAttribute("count")));
}
catch (Exception any) {}
// read the until
try
{
setUntil(TimeService.newTimeGmt(el.getAttribute("until")));
}
catch (Exception any) {}
}
/*
* (non-Javadoc)
*
* @see org.sakaiproject.calendar.api.RecurrenceRule#getContentHandler()
*/
public ContentHandler getContentHandler(final Map<String,Object> services)
{
return new DefaultHandler()
{
/*
* (non-Javadoc)
*
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String,
* java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException
{
// read the interval
try
{
setInterval(Integer.parseInt(attributes.getValue("interval")));
}
catch (Exception any)
{
}
// read the count
try
{
setCount(Integer.parseInt(attributes.getValue("count")));
// M_log.debug("Set count to "+attributes.getValue("count"));
}
catch (Exception any)
{
}
// read the until
try
{
setUntil(((org.sakaiproject.time.api.TimeService)services.get("timeservice")).newTimeGmt(attributes.getValue("until")));
// M_log.debug("Set until to "+attributes.getValue("until"));
}
catch (Exception any)
{
}
}
};
}
/**
* Remove from the ranges list any RecurrenceInstance excluded by this rule.
* @param ranges The list (RecurrenceInstance) of ranges.
*/
public final void excludeInstances(List ranges)
{
}
/**
* Set base class attributes in the Element rule during XML serialization.
* @param rule
*/
protected void setBaseClassXML(Element rule)
{
// set the interval
rule.setAttribute("interval", Integer.toString(getInterval()));
// set either count or until (or neither), not both
if (getCount() > 0)
{
rule.setAttribute("count", Integer.toString(getCount()));
}
else
if (getUntil() != null)
{
rule.setAttribute("until", getUntil().toString());
}
}
}