
net.fortuna.ical4j.model.Recur Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bw-ical4j-cl Show documentation
Show all versions of bw-ical4j-cl Show documentation
A fork of iCal4j with customizations for Bedework.
The newest version!
/**
* 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;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.util.CompatibilityHints;
import net.fortuna.ical4j.util.Configurator;
import net.fortuna.ical4j.util.Dates;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.io.Serializable;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
/**
* $Id$ [18-Apr-2004]
*
* Defines a recurrence.
* @version 2.0
* @author Ben Fortuna
*/
public class Recur implements Serializable {
private static final long serialVersionUID = -7333226591784095142L;
private static final String FREQ = "FREQ";
private static final String UNTIL = "UNTIL";
private static final String COUNT = "COUNT";
private static final String INTERVAL = "INTERVAL";
private static final String BYSECOND = "BYSECOND";
private static final String BYMINUTE = "BYMINUTE";
private static final String BYHOUR = "BYHOUR";
private static final String BYDAY = "BYDAY";
private static final String BYMONTHDAY = "BYMONTHDAY";
private static final String BYYEARDAY = "BYYEARDAY";
private static final String BYWEEKNO = "BYWEEKNO";
private static final String BYMONTH = "BYMONTH";
private static final String BYSETPOS = "BYSETPOS";
private static final String WKST = "WKST";
/**
* Second frequency resolution.
*/
public static final String SECONDLY = "SECONDLY";
/**
* Minute frequency resolution.
*/
public static final String MINUTELY = "MINUTELY";
/**
* Hour frequency resolution.
*/
public static final String HOURLY = "HOURLY";
/**
* Day frequency resolution.
*/
public static final String DAILY = "DAILY";
/**
* Week frequency resolution.
*/
public static final String WEEKLY = "WEEKLY";
/**
* Month frequency resolution.
*/
public static final String MONTHLY = "MONTHLY";
/**
* Year frequency resolution.
*/
public static final String YEARLY = "YEARLY";
/**
* When calculating dates matching this recur ({@code getDates()} or {@code getNextDate}),
* this property defines the maximum number of attempt to find a matching date by
* incrementing the seed.
* The default value is 1000. A value of -1 corresponds to no maximum.
*/
public static final String KEY_MAX_INCREMENT_COUNT = "net.fortuna.ical4j.recur.maxincrementcount";
private static int maxIncrementCount;
static {
final String value = Configurator.getProperty(KEY_MAX_INCREMENT_COUNT);
if (value != null && value.length() > 0) {
maxIncrementCount = Integer.parseInt(value);
} else {
maxIncrementCount = 1000;
}
}
private transient Log log = LogFactory.getLog(Recur.class);
private String frequency;
private Date until;
private int count = -1;
private int interval = -1;
private NumberList secondList;
private NumberList minuteList;
private NumberList hourList;
private WeekDayList dayList;
private NumberList monthDayList;
private NumberList yearDayList;
private NumberList weekNoList;
private NumberList monthList;
private NumberList setPosList;
private String weekStartDay;
private int calendarWeekStartDay;
private Map experimentalValues = new HashMap();
// Calendar field we increment based on frequency.
private int calIncField;
/**
* Default constructor.
*/
public Recur() {
// default week start is Monday per RFC5545
calendarWeekStartDay = Calendar.MONDAY;
}
/**
* Constructs a new instance from the specified string value.
* @param aValue a string representation of a recurrence.
* @throws ParseException thrown when the specified string contains an invalid representation of an UNTIL date value
*/
public Recur(final String aValue) throws ParseException {
// default week start is Monday per RFC5545
calendarWeekStartDay = Calendar.MONDAY;
final StringTokenizer t = new StringTokenizer(aValue, ";=");
while (t.hasMoreTokens()) {
final String token = t.nextToken();
if (FREQ.equals(token)) {
frequency = nextToken(t, token);
}
else if (UNTIL.equals(token)) {
final String untilString = nextToken(t, token);
if (untilString != null && untilString.indexOf("T") >= 0) {
until = new DateTime(untilString);
// UNTIL must be specified in UTC time..
((DateTime) until).setUtc(true);
}
else {
until = new Date(untilString);
}
}
else if (COUNT.equals(token)) {
count = Integer.parseInt(nextToken(t, token));
}
else if (INTERVAL.equals(token)) {
interval = Integer.parseInt(nextToken(t, token));
}
else if (BYSECOND.equals(token)) {
secondList = new NumberList(nextToken(t, token), 0, 59, false);
}
else if (BYMINUTE.equals(token)) {
minuteList = new NumberList(nextToken(t, token), 0, 59, false);
}
else if (BYHOUR.equals(token)) {
hourList = new NumberList(nextToken(t, token), 0, 23, false);
}
else if (BYDAY.equals(token)) {
dayList = new WeekDayList(nextToken(t, token));
}
else if (BYMONTHDAY.equals(token)) {
monthDayList = new NumberList(nextToken(t, token), 1, 31, true);
}
else if (BYYEARDAY.equals(token)) {
yearDayList = new NumberList(nextToken(t, token), 1, 366, true);
}
else if (BYWEEKNO.equals(token)) {
weekNoList = new NumberList(nextToken(t, token), 1, 53, true);
}
else if (BYMONTH.equals(token)) {
monthList = new NumberList(nextToken(t, token), 1, 12, false);
}
else if (BYSETPOS.equals(token)) {
setPosList = new NumberList(nextToken(t, token), 1, 366, true);
}
else if (WKST.equals(token)) {
weekStartDay = nextToken(t, token);
calendarWeekStartDay = WeekDay.getCalendarDay(new WeekDay(weekStartDay));
}
else {
if (CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING)) {
// assume experimental value..
experimentalValues.put(token, nextToken(t, token));
}
else {
throw new IllegalArgumentException("Invalid recurrence rule part: " +
token + "=" + nextToken(t, token));
}
}
}
validateFrequency();
}
private String nextToken(StringTokenizer t, String lastToken) {
try {
return t.nextToken();
}
catch (NoSuchElementException e) {
throw new IllegalArgumentException("Missing expected token, last token: " + lastToken);
}
}
/**
* @param frequency a recurrence frequency string
* @param until maximum recurrence date
*/
public Recur(final String frequency, final Date until) {
// default week start is Monday per RFC5545
calendarWeekStartDay = Calendar.MONDAY;
this.frequency = frequency;
this.until = until;
validateFrequency();
}
/**
* @param frequency a recurrence frequency string
* @param count maximum recurrence count
*/
public Recur(final String frequency, final int count) {
// default week start is Monday per RFC5545
calendarWeekStartDay = Calendar.MONDAY;
this.frequency = frequency;
this.count = count;
validateFrequency();
}
/**
* @return Returns the dayList.
*/
public final WeekDayList getDayList() {
if (dayList == null) {
dayList = new WeekDayList();
}
return dayList;
}
/**
* @return Returns the hourList.
*/
public final NumberList getHourList() {
if (hourList == null) {
hourList = new NumberList(0, 23, false);
}
return hourList;
}
/**
* @return Returns the minuteList.
*/
public final NumberList getMinuteList() {
if (minuteList == null) {
minuteList = new NumberList(0, 59, false);
}
return minuteList;
}
/**
* @return Returns the monthDayList.
*/
public final NumberList getMonthDayList() {
if (monthDayList == null) {
monthDayList = new NumberList(1, 31, true);
}
return monthDayList;
}
/**
* @return Returns the monthList.
*/
public final NumberList getMonthList() {
if (monthList == null) {
monthList = new NumberList(1, 12, false);
}
return monthList;
}
/**
* @return Returns the secondList.
*/
public final NumberList getSecondList() {
if (secondList == null) {
secondList = new NumberList(0, 59, false);
}
return secondList;
}
/**
* @return Returns the setPosList.
*/
public final NumberList getSetPosList() {
if (setPosList == null) {
setPosList = new NumberList(1, 366, true);
}
return setPosList;
}
/**
* @return Returns the weekNoList.
*/
public final NumberList getWeekNoList() {
if (weekNoList == null) {
weekNoList = new NumberList(1, 53, true);
}
return weekNoList;
}
/**
* @return Returns the yearDayList.
*/
public final NumberList getYearDayList() {
if (yearDayList == null) {
yearDayList = new NumberList(1, 366, true);
}
return yearDayList;
}
/**
* @return Returns the count or -1 if the rule does not have a count.
*/
public final int getCount() {
return count;
}
/**
* @return Returns the experimentalValues.
*/
public final Map getExperimentalValues() {
return experimentalValues;
}
/**
* @return Returns the frequency.
*/
public final String getFrequency() {
return frequency;
}
/**
* @return Returns the interval or -1 if the rule does not have an interval defined.
*/
public final int getInterval() {
return interval;
}
/**
* @return Returns the until or null if there is none.
*/
public final Date getUntil() {
return until;
}
/**
* @return Returns the weekStartDay or null if there is none.
*/
public final String getWeekStartDay() {
return weekStartDay;
}
/**
* @param weekStartDay The weekStartDay to set.
*/
public final void setWeekStartDay(final String weekStartDay) {
this.weekStartDay = weekStartDay;
if (weekStartDay != null) {
calendarWeekStartDay = WeekDay.getCalendarDay(new WeekDay(weekStartDay));
}
}
/**
* {@inheritDoc}
*/
public final String toString() {
final StringBuffer b = new StringBuffer();
b.append(FREQ);
b.append('=');
b.append(frequency);
if (weekStartDay != null) {
b.append(';');
b.append(WKST);
b.append('=');
b.append(weekStartDay);
}
if (until != null) {
b.append(';');
b.append(UNTIL);
b.append('=');
// Note: date-time representations should always be in UTC time.
b.append(until);
}
if (count >= 1) {
b.append(';');
b.append(COUNT);
b.append('=');
b.append(count);
}
if (interval >= 1) {
b.append(';');
b.append(INTERVAL);
b.append('=');
b.append(interval);
}
if (!getMonthList().isEmpty()) {
b.append(';');
b.append(BYMONTH);
b.append('=');
b.append(monthList);
}
if (!getWeekNoList().isEmpty()) {
b.append(';');
b.append(BYWEEKNO);
b.append('=');
b.append(weekNoList);
}
if (!getYearDayList().isEmpty()) {
b.append(';');
b.append(BYYEARDAY);
b.append('=');
b.append(yearDayList);
}
if (!getMonthDayList().isEmpty()) {
b.append(';');
b.append(BYMONTHDAY);
b.append('=');
b.append(monthDayList);
}
if (!getDayList().isEmpty()) {
b.append(';');
b.append(BYDAY);
b.append('=');
b.append(dayList);
}
if (!getHourList().isEmpty()) {
b.append(';');
b.append(BYHOUR);
b.append('=');
b.append(hourList);
}
if (!getMinuteList().isEmpty()) {
b.append(';');
b.append(BYMINUTE);
b.append('=');
b.append(minuteList);
}
if (!getSecondList().isEmpty()) {
b.append(';');
b.append(BYSECOND);
b.append('=');
b.append(secondList);
}
if (!getSetPosList().isEmpty()) {
b.append(';');
b.append(BYSETPOS);
b.append('=');
b.append(setPosList);
}
return b.toString();
}
/**
* Returns a list of start dates in the specified period represented by this recur. Any date fields not specified by
* this recur are retained from the period start, and as such you should ensure the period start is initialised
* correctly.
* @param periodStart the start of the period
* @param periodEnd the end of the period
* @param value the type of dates to generate (i.e. date/date-time)
* @return a list of dates
*/
public final DateList getDates(final Date periodStart,
final Date periodEnd, final Value value) {
return getDates(periodStart, periodStart, periodEnd, value, -1);
}
/**
* Convenience method for retrieving recurrences in a specified period.
* @param seed a seed date for generating recurrence instances
* @param period the period of returned recurrence dates
* @param value type of dates to generate
* @return a list of dates
*/
public final DateList getDates(final Date seed, final Period period,
final Value value) {
return getDates(seed, period.getStart(), period.getEnd(), value, -1);
}
/**
* Returns a list of start dates in the specified period represented by this recur. This method includes a base date
* argument, which indicates the start of the fist occurrence of this recurrence. The base date is used to inject
* default values to return a set of dates in the correct format. For example, if the search start date (start) is
* Wed, Mar 23, 12:19PM, but the recurrence is Mon - Fri, 9:00AM - 5:00PM, the start dates returned should all be at
* 9:00AM, and not 12:19PM.
* @return a list of dates represented by this recur instance
* @param seed the start date of this Recurrence's first instance
* @param periodStart the start of the period
* @param periodEnd the end of the period
* @param value the type of dates to generate (i.e. date/date-time)
*/
public final DateList getDates(final Date seed, final Date periodStart,
final Date periodEnd, final Value value) {
return getDates(seed, periodStart, periodEnd, value, -1);
}
/**
* Returns a list of start dates in the specified period represented by this recur. This method includes a base date
* argument, which indicates the start of the fist occurrence of this recurrence. The base date is used to inject
* default values to return a set of dates in the correct format. For example, if the search start date (start) is
* Wed, Mar 23, 12:19PM, but the recurrence is Mon - Fri, 9:00AM - 5:00PM, the start dates returned should all be at
* 9:00AM, and not 12:19PM.
* @return a list of dates represented by this recur instance
* @param seed the start date of this Recurrence's first instance
* @param periodStart the start of the period
* @param periodEnd the end of the period
* @param value the type of dates to generate (i.e. date/date-time)
* @param maxCount limits the number of instances returned. Up to one years
* worth extra may be returned. Less than 0 means no limit
*/
public final DateList getDates(final Date seed, final Date periodStart,
final Date periodEnd, final Value value,
final int maxCount) {
final DateList dates = new DateList(value);
if (seed instanceof DateTime) {
if (((DateTime) seed).isUtc()) {
dates.setUtc(true);
}
else {
dates.setTimeZone(((DateTime) seed).getTimeZone());
}
}
final Calendar cal = getCalendarInstance(seed, true);
// optimize the start time for selecting candidates
// (only applicable where a COUNT is not specified)
if (getCount() < 1) {
final Calendar seededCal = (Calendar) cal.clone();
while (seededCal.getTime().before(periodStart)) {
cal.setTime(seededCal.getTime());
increment(seededCal);
}
}
int invalidCandidateCount = 0;
int noCandidateIncrementCount = 0;
Date candidate = null;
while ((maxCount < 0) || (dates.size() < maxCount)) {
final Date candidateSeed = Dates.getInstance(cal.getTime(), value);
if (getUntil() != null && candidate != null
&& candidate.after(getUntil())) {
break;
}
if (periodEnd != null && candidate != null
&& candidate.after(periodEnd)) {
break;
}
if (getCount() >= 1
&& (dates.size() + invalidCandidateCount) >= getCount()) {
break;
}
// if (Value.DATE_TIME.equals(value)) {
if (candidateSeed instanceof DateTime) {
if (dates.isUtc()) {
((DateTime) candidateSeed).setUtc(true);
}
else {
((DateTime) candidateSeed).setTimeZone(dates.getTimeZone());
}
}
final DateList candidates = getCandidates(candidateSeed, value);
if (!candidates.isEmpty()) {
noCandidateIncrementCount = 0;
// sort candidates for identifying when UNTIL date is exceeded..
Collections.sort(candidates);
for (final Iterator i = candidates.iterator(); i.hasNext();) {
candidate = (Date) i.next();
// don't count candidates that occur before the seed date..
if (!candidate.before(seed)) {
// candidates exclusive of periodEnd..
if (candidate.before(periodStart)
|| !candidate.before(periodEnd)) {
invalidCandidateCount++;
} else if (getCount() >= 1
&& (dates.size() + invalidCandidateCount) >= getCount()) {
break;
} else if (!(getUntil() != null
&& candidate.after(getUntil()))) {
dates.add(candidate);
}
}
}
} else {
noCandidateIncrementCount++;
if ((maxIncrementCount > 0) && (noCandidateIncrementCount > maxIncrementCount)) {
break;
}
}
increment(cal);
}
// sort final list..
Collections.sort(dates);
return dates;
}
/**
* Returns the the next date of this recurrence given a seed date
* and start date. The seed date indicates the start of the fist
* occurrence of this recurrence. The start date is the
* starting date to search for the next recurrence. Return null
* if there is no occurrence date after start date.
* @return the next date in the recurrence series after startDate
* @param seed the start date of this Recurrence's first instance
* @param startDate the date to start the search
*/
public final Date getNextDate(final Date seed, final Date startDate) {
final Calendar cal = getCalendarInstance(seed, true);
// optimize the start time for selecting candidates
// (only applicable where a COUNT is not specified)
if (getCount() < 1) {
final Calendar seededCal = (Calendar) cal.clone();
while (seededCal.getTime().before(startDate)) {
cal.setTime(seededCal.getTime());
increment(seededCal);
}
}
int invalidCandidateCount = 0;
int noCandidateIncrementCount = 0;
Date candidate = null;
final Value value = seed instanceof DateTime ? Value.DATE_TIME : Value.DATE;
while (true) {
final Date candidateSeed = Dates.getInstance(cal.getTime(), value);
if (getUntil() != null && candidate != null && candidate.after(getUntil())) {
break;
}
if (getCount() > 0 && invalidCandidateCount >= getCount()) {
break;
}
if (Value.DATE_TIME.equals(value)) {
if (((DateTime) seed).isUtc()) {
((DateTime) candidateSeed).setUtc(true);
}
else {
((DateTime) candidateSeed).setTimeZone(((DateTime) seed).getTimeZone());
}
}
final DateList candidates = getCandidates(candidateSeed, value);
if (!candidates.isEmpty()) {
noCandidateIncrementCount = 0;
// sort candidates for identifying when UNTIL date is exceeded..
Collections.sort(candidates);
for (final Iterator i = candidates.iterator(); i.hasNext();) {
candidate = (Date) i.next();
// don't count candidates that occur before the seed date..
if (!candidate.before(seed)) {
// Candidate must be after startDate because
// we want the NEXT occurrence
if (!candidate.after(startDate)) {
invalidCandidateCount++;
} else if (getCount() > 0
&& invalidCandidateCount >= getCount()) {
break;
} else if (!(getUntil() != null
&& candidate.after(getUntil()))) {
return candidate;
}
}
}
} else {
noCandidateIncrementCount++;
if ((maxIncrementCount > 0) && (noCandidateIncrementCount > maxIncrementCount)) {
break;
}
}
increment(cal);
}
return null;
}
/**
* Increments the specified calendar according to the frequency and interval specified in this recurrence rule.
* @param cal a java.util.Calendar to increment
*/
private void increment(final Calendar cal) {
// initialise interval..
final int calInterval = (getInterval() >= 1) ? getInterval() : 1;
cal.add(calIncField, calInterval);
}
/**
* Returns a list of possible dates generated from the applicable BY* rules, using the specified date as a seed.
* @param date the seed date
* @param value the type of date list to return
* @return a DateList
*/
private DateList getCandidates(final Date date, final Value value) {
DateList dates = new DateList(value);
if (date instanceof DateTime) {
if (((DateTime) date).isUtc()) {
dates.setUtc(true);
}
else {
dates.setTimeZone(((DateTime) date).getTimeZone());
}
}
dates.add(date);
dates = getMonthVariants(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYMONTH processing: " + dates);
}
dates = getWeekNoVariants(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYWEEKNO processing: " + dates);
}
dates = getYearDayVariants(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYYEARDAY processing: " + dates);
}
dates = getMonthDayVariants(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYMONTHDAY processing: " + dates);
}
dates = getDayVariants(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYDAY processing: " + dates);
}
dates = getHourVariants(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYHOUR processing: " + dates);
}
dates = getMinuteVariants(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYMINUTE processing: " + dates);
}
dates = getSecondVariants(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYSECOND processing: " + dates);
}
dates = applySetPosRules(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after SETPOS processing: " + dates);
}
return dates;
}
/**
* Applies BYSETPOS rules to dates
. Valid positions are from 1 to the size of the date list. Invalid
* positions are ignored.
* @param dates
*/
private DateList applySetPosRules(final DateList dates) {
// return if no SETPOS rules specified..
if (getSetPosList().isEmpty()) {
return dates;
}
// sort the list before processing..
Collections.sort(dates);
final DateList setPosDates = getDateListInstance(dates);
final int size = dates.size();
for (final Iterator i = getSetPosList().iterator(); i.hasNext();) {
final Integer setPos = (Integer) i.next();
final int pos = setPos.intValue();
if (pos > 0 && pos <= size) {
setPosDates.add(dates.get(pos - 1));
}
else if (pos < 0 && pos >= -size) {
setPosDates.add(dates.get(size + pos));
}
}
return setPosDates;
}
/**
* Applies BYMONTH rules specified in this Recur instance to the specified date list. If no BYMONTH rules are
* specified the date list is returned unmodified.
* @param dates
* @return
*/
private DateList getMonthVariants(final DateList dates) {
if (getMonthList().isEmpty()) {
return dates;
}
final DateList monthlyDates = getDateListInstance(dates);
for (final Iterator i = dates.iterator(); i.hasNext();) {
final Date date = (Date) i.next();
final Calendar cal = getCalendarInstance(date, true);
for (final Iterator j = getMonthList().iterator(); j.hasNext();) {
final Integer month = (Integer) j.next();
// Java months are zero-based..
// cal.set(Calendar.MONTH, month.intValue() - 1);
cal.roll(Calendar.MONTH, (month.intValue() - 1) - cal.get(Calendar.MONTH));
monthlyDates.add(Dates.getInstance(cal.getTime(), monthlyDates.getType()));
}
}
return monthlyDates;
}
/**
* Applies BYWEEKNO rules specified in this Recur instance to the specified date list. If no BYWEEKNO rules are
* specified the date list is returned unmodified.
* @param dates
* @return
*/
private DateList getWeekNoVariants(final DateList dates) {
if (getWeekNoList().isEmpty()) {
return dates;
}
final DateList weekNoDates = getDateListInstance(dates);
for (final Iterator i = dates.iterator(); i.hasNext();) {
final Date date = (Date) i.next();
final Calendar cal = getCalendarInstance(date, true);
for (final Iterator j = getWeekNoList().iterator(); j.hasNext();) {
final Integer weekNo = (Integer) j.next();
cal.set(Calendar.WEEK_OF_YEAR, Dates.getAbsWeekNo(cal.getTime(), weekNo.intValue()));
weekNoDates.add(Dates.getInstance(cal.getTime(), weekNoDates.getType()));
}
}
return weekNoDates;
}
/**
* Applies BYYEARDAY rules specified in this Recur instance to the specified date list. If no BYYEARDAY rules are
* specified the date list is returned unmodified.
* @param dates
* @return
*/
private DateList getYearDayVariants(final DateList dates) {
if (getYearDayList().isEmpty()) {
return dates;
}
final DateList yearDayDates = getDateListInstance(dates);
for (final Iterator i = dates.iterator(); i.hasNext();) {
final Date date = (Date) i.next();
final Calendar cal = getCalendarInstance(date, true);
for (final Iterator j = getYearDayList().iterator(); j.hasNext();) {
final Integer yearDay = (Integer) j.next();
cal.set(Calendar.DAY_OF_YEAR, Dates.getAbsYearDay(cal.getTime(), yearDay.intValue()));
yearDayDates.add(Dates.getInstance(cal.getTime(), yearDayDates.getType()));
}
}
return yearDayDates;
}
/**
* Applies BYMONTHDAY rules specified in this Recur instance to the specified date list. If no BYMONTHDAY rules are
* specified the date list is returned unmodified.
* @param dates
* @return
*/
private DateList getMonthDayVariants(final DateList dates) {
if (getMonthDayList().isEmpty()) {
return dates;
}
final DateList monthDayDates = getDateListInstance(dates);
for (final Iterator i = dates.iterator(); i.hasNext();) {
final Date date = (Date) i.next();
final Calendar cal = getCalendarInstance(date, false);
for (final Iterator j = getMonthDayList().iterator(); j.hasNext();) {
final Integer monthDay = (Integer) j.next();
try {
cal.set(Calendar.DAY_OF_MONTH, Dates.getAbsMonthDay(cal.getTime(), monthDay.intValue()));
monthDayDates.add(Dates.getInstance(cal.getTime(), monthDayDates.getType()));
}
catch (IllegalArgumentException iae) {
if (log.isTraceEnabled()) {
log.trace("Invalid day of month: " + Dates.getAbsMonthDay(cal
.getTime(), monthDay.intValue()));
}
}
}
}
return monthDayDates;
}
/**
* Applies BYDAY rules specified in this Recur instance to the specified date list. If no BYDAY rules are specified
* the date list is returned unmodified.
* @param dates
* @return
*/
private DateList getDayVariants(final DateList dates) {
if (getDayList().isEmpty()) {
return dates;
}
final DateList weekDayDates = getDateListInstance(dates);
for (final Iterator i = dates.iterator(); i.hasNext();) {
final Date date = (Date) i.next();
for (final Iterator j = getDayList().iterator(); j.hasNext();) {
final WeekDay weekDay = (WeekDay) j.next();
// if BYYEARDAY or BYMONTHDAY is specified filter existing
// list..
if (!getYearDayList().isEmpty() || !getMonthDayList().isEmpty()) {
final Calendar cal = getCalendarInstance(date, true);
if (weekDay.equals(WeekDay.getWeekDay(cal))) {
weekDayDates.add(date);
}
}
else {
weekDayDates.addAll(getAbsWeekDays(date, dates.getType(), weekDay));
}
}
}
return weekDayDates;
}
/**
* Returns a list of applicable dates corresponding to the specified week day in accordance with the frequency
* specified by this recurrence rule.
* @param date
* @param weekDay
* @return
*/
private List getAbsWeekDays(final Date date, final Value type, final WeekDay weekDay) {
final Calendar cal = getCalendarInstance(date, true);
final DateList days = new DateList(type);
if (date instanceof DateTime) {
if (((DateTime) date).isUtc()) {
days.setUtc(true);
}
else {
days.setTimeZone(((DateTime) date).getTimeZone());
}
}
final int calDay = WeekDay.getCalendarDay(weekDay);
if (calDay == -1) {
// a matching weekday cannot be identified..
return days;
}
if (DAILY.equals(getFrequency())) {
if (cal.get(Calendar.DAY_OF_WEEK) == calDay) {
days.add(Dates.getInstance(cal.getTime(), type));
}
}
else if (WEEKLY.equals(getFrequency()) || !getWeekNoList().isEmpty()) {
final int weekNo = cal.get(Calendar.WEEK_OF_YEAR);
// construct a list of possible week days..
cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
while (cal.get(Calendar.DAY_OF_WEEK) != calDay) {
cal.add(Calendar.DAY_OF_WEEK, 1);
}
// final int weekNo = cal.get(Calendar.WEEK_OF_YEAR);
if (cal.get(Calendar.WEEK_OF_YEAR) == weekNo) {
days.add(Dates.getInstance(cal.getTime(), type));
// cal.add(Calendar.DAY_OF_WEEK, Dates.DAYS_PER_WEEK);
}
}
else if (MONTHLY.equals(getFrequency()) || !getMonthList().isEmpty()) {
final int month = cal.get(Calendar.MONTH);
// construct a list of possible month days..
cal.set(Calendar.DAY_OF_MONTH, 1);
while (cal.get(Calendar.DAY_OF_WEEK) != calDay) {
cal.add(Calendar.DAY_OF_MONTH, 1);
}
while (cal.get(Calendar.MONTH) == month) {
days.add(Dates.getInstance(cal.getTime(), type));
cal.add(Calendar.DAY_OF_MONTH, Dates.DAYS_PER_WEEK);
}
}
else if (YEARLY.equals(getFrequency())) {
final int year = cal.get(Calendar.YEAR);
// construct a list of possible year days..
cal.set(Calendar.DAY_OF_YEAR, 1);
while (cal.get(Calendar.DAY_OF_WEEK) != calDay) {
cal.add(Calendar.DAY_OF_YEAR, 1);
}
while (cal.get(Calendar.YEAR) == year) {
days.add(Dates.getInstance(cal.getTime(), type));
cal.add(Calendar.DAY_OF_YEAR, Dates.DAYS_PER_WEEK);
}
}
return getOffsetDates(days, weekDay.getOffset());
}
/**
* Returns a single-element sublist containing the element of list
at offset
. Valid
* offsets are from 1 to the size of the list. If an invalid offset is supplied, all elements from list
* are added to sublist
.
* @param offset
*/
private List getOffsetDates(final DateList dates, final int offset) {
if (offset == 0) {
return dates;
}
final List offsetDates = getDateListInstance(dates);
final int size = dates.size();
if (offset < 0 && offset >= -size) {
offsetDates.add(dates.get(size + offset));
}
else if (offset > 0 && offset <= size) {
offsetDates.add(dates.get(offset - 1));
}
return offsetDates;
}
/**
* Applies BYHOUR rules specified in this Recur instance to the specified date list. If no BYHOUR rules are
* specified the date list is returned unmodified.
* @param dates
* @return
*/
private DateList getHourVariants(final DateList dates) {
if (getHourList().isEmpty()) {
return dates;
}
final DateList hourlyDates = getDateListInstance(dates);
for (final Iterator i = dates.iterator(); i.hasNext();) {
final Date date = (Date) i.next();
final Calendar cal = getCalendarInstance(date, true);
for (final Iterator j = getHourList().iterator(); j.hasNext();) {
final Integer hour = (Integer) j.next();
cal.set(Calendar.HOUR_OF_DAY, hour.intValue());
hourlyDates.add(Dates.getInstance(cal.getTime(), hourlyDates.getType()));
}
}
return hourlyDates;
}
/**
* Applies BYMINUTE rules specified in this Recur instance to the specified date list. If no BYMINUTE rules are
* specified the date list is returned unmodified.
* @param dates
* @return
*/
private DateList getMinuteVariants(final DateList dates) {
if (getMinuteList().isEmpty()) {
return dates;
}
final DateList minutelyDates = getDateListInstance(dates);
for (final Iterator i = dates.iterator(); i.hasNext();) {
final Date date = (Date) i.next();
final Calendar cal = getCalendarInstance(date, true);
for (final Iterator j = getMinuteList().iterator(); j.hasNext();) {
final Integer minute = (Integer) j.next();
cal.set(Calendar.MINUTE, minute.intValue());
minutelyDates.add(Dates.getInstance(cal.getTime(), minutelyDates.getType()));
}
}
return minutelyDates;
}
/**
* Applies BYSECOND rules specified in this Recur instance to the specified date list. If no BYSECOND rules are
* specified the date list is returned unmodified.
* @param dates
* @return
*/
private DateList getSecondVariants(final DateList dates) {
if (getSecondList().isEmpty()) {
return dates;
}
final DateList secondlyDates = getDateListInstance(dates);
for (final Iterator i = dates.iterator(); i.hasNext();) {
final Date date = (Date) i.next();
final Calendar cal = getCalendarInstance(date, true);
for (final Iterator j = getSecondList().iterator(); j.hasNext();) {
final Integer second = (Integer) j.next();
cal.set(Calendar.SECOND, second.intValue());
secondlyDates.add(Dates.getInstance(cal.getTime(), secondlyDates.getType()));
}
}
return secondlyDates;
}
private void validateFrequency() {
if (frequency == null) {
throw new IllegalArgumentException(
"A recurrence rule MUST contain a FREQ rule part.");
}
if (SECONDLY.equals(getFrequency())) {
calIncField = Calendar.SECOND;
}
else if (MINUTELY.equals(getFrequency())) {
calIncField = Calendar.MINUTE;
}
else if (HOURLY.equals(getFrequency())) {
calIncField = Calendar.HOUR_OF_DAY;
}
else if (DAILY.equals(getFrequency())) {
calIncField = Calendar.DAY_OF_YEAR;
}
else if (WEEKLY.equals(getFrequency())) {
calIncField = Calendar.WEEK_OF_YEAR;
}
else if (MONTHLY.equals(getFrequency())) {
calIncField = Calendar.MONTH;
}
else if (YEARLY.equals(getFrequency())) {
calIncField = Calendar.YEAR;
}
else {
throw new IllegalArgumentException("Invalid FREQ rule part '"
+ frequency + "' in recurrence rule");
}
}
/**
* @param count The count to set.
*/
public final void setCount(final int count) {
this.count = count;
this.until = null;
}
/**
* @param frequency The frequency to set.
*/
public final void setFrequency(final String frequency) {
this.frequency = frequency;
validateFrequency();
}
/**
* @param interval The interval to set.
*/
public final void setInterval(final int interval) {
this.interval = interval;
}
/**
* @param until The until to set.
*/
public final void setUntil(final Date until) {
this.until = until;
this.count = -1;
}
/**
* Construct a Calendar object and sets the time.
* @param date
* @param lenient
* @return
*/
private Calendar getCalendarInstance(final Date date, final boolean lenient) {
Calendar cal = Dates.getCalendarInstance(date);
// A week should have at least 4 days to be considered as such per RFC5545
cal.setMinimalDaysInFirstWeek(4);
cal.setFirstDayOfWeek(calendarWeekStartDay);
cal.setLenient(lenient);
cal.setTime(date);
return cal;
}
/**
* @param stream
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(final java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
log = LogFactory.getLog(Recur.class);
}
/**
* Instantiate a new datelist with the same type, timezone and utc settings
* as the origList.
* @param origList
* @return a new empty list.
*/
private static DateList getDateListInstance(final DateList origList) {
final DateList list = new DateList(origList.getType());
if (origList.isUtc()) {
list.setUtc(true);
} else {
list.setTimeZone(origList.getTimeZone());
}
return list;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy