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; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.Serializable; michael@0: import java.text.ParseException; michael@0: import java.util.Calendar; michael@0: import java.util.Collections; michael@0: import java.util.HashMap; michael@0: import java.util.Iterator; michael@0: import java.util.List; michael@0: import java.util.Map; michael@0: import java.util.NoSuchElementException; michael@0: import java.util.StringTokenizer; michael@0: michael@0: import net.fortuna.ical4j.model.parameter.Value; michael@3: import net.fortuna.ical4j.util.CompatibilityHints; michael@0: import net.fortuna.ical4j.util.Configurator; michael@0: import net.fortuna.ical4j.util.Dates; 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$ [18-Apr-2004] michael@0: * michael@0: * Defines a recurrence. michael@0: * @version 2.0 michael@0: * @author Ben Fortuna michael@0: */ michael@0: public class Recur implements Serializable { michael@0: michael@0: private static final long serialVersionUID = -7333226591784095142L; michael@0: michael@0: private static final String FREQ = "FREQ"; michael@0: michael@0: private static final String UNTIL = "UNTIL"; michael@0: michael@0: private static final String COUNT = "COUNT"; michael@0: michael@0: private static final String INTERVAL = "INTERVAL"; michael@0: michael@0: private static final String BYSECOND = "BYSECOND"; michael@0: michael@0: private static final String BYMINUTE = "BYMINUTE"; michael@0: michael@0: private static final String BYHOUR = "BYHOUR"; michael@0: michael@0: private static final String BYDAY = "BYDAY"; michael@0: michael@0: private static final String BYMONTHDAY = "BYMONTHDAY"; michael@0: michael@0: private static final String BYYEARDAY = "BYYEARDAY"; michael@0: michael@0: private static final String BYWEEKNO = "BYWEEKNO"; michael@0: michael@0: private static final String BYMONTH = "BYMONTH"; michael@0: michael@0: private static final String BYSETPOS = "BYSETPOS"; michael@0: michael@0: private static final String WKST = "WKST"; michael@0: michael@0: /** michael@0: * Second frequency resolution. michael@0: */ michael@0: public static final String SECONDLY = "SECONDLY"; michael@0: michael@0: /** michael@0: * Minute frequency resolution. michael@0: */ michael@0: public static final String MINUTELY = "MINUTELY"; michael@0: michael@0: /** michael@0: * Hour frequency resolution. michael@0: */ michael@0: public static final String HOURLY = "HOURLY"; michael@0: michael@0: /** michael@0: * Day frequency resolution. michael@0: */ michael@0: public static final String DAILY = "DAILY"; michael@0: michael@0: /** michael@0: * Week frequency resolution. michael@0: */ michael@0: public static final String WEEKLY = "WEEKLY"; michael@0: michael@0: /** michael@0: * Month frequency resolution. michael@0: */ michael@0: public static final String MONTHLY = "MONTHLY"; michael@0: michael@0: /** michael@0: * Year frequency resolution. michael@0: */ michael@0: public static final String YEARLY = "YEARLY"; michael@0: michael@0: /** michael@0: * When calculating dates matching this recur ({@code getDates()} or {@code getNextDate}), michael@0: * this property defines the maximum number of attempt to find a matching date by michael@0: * incrementing the seed. michael@0: *

The default value is 1000. A value of -1 corresponds to no maximum.

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