diff -r 000000000000 -r fb9019fb1bf7 src/net/fortuna/ical4j/model/Recur.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/net/fortuna/ical4j/model/Recur.java Tue Feb 10 18:12:00 2015 +0100 @@ -0,0 +1,1246 @@ +/** + * 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 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; + +import net.fortuna.ical4j.model.parameter.Value; +import net.fortuna.ical4j.util.Configurator; +import net.fortuna.ical4j.util.Dates; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * $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 Map experimentalValues = new HashMap(); + + // Calendar field we increment based on frequency. + private int calIncField; + + /** + * Default constructor. + */ + public Recur() { + } + + /** + * 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 { + 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); + } + // assume experimental value.. + else { + experimentalValues.put(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) { + 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) { + 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; + } + + /** + * {@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 = Dates.getCalendarInstance(seed); + cal.setTime(seed); + + // 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 = Dates.getCalendarInstance(seed); + cal.setTime(seed); + + // 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 todates
. 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 = Dates.getCalendarInstance(date);
+ cal.setTime(date);
+ 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 = Dates.getCalendarInstance(date);
+ cal.setTime(date);
+ 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 = Dates.getCalendarInstance(date);
+ cal.setTime(date);
+ 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 = Dates.getCalendarInstance(date);
+ cal.setLenient(false);
+ cal.setTime(date);
+ 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 = Dates.getCalendarInstance(date);
+ cal.setTime(date);
+ 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 = Dates.getCalendarInstance(date);
+ // default week start is Monday per RFC5545
+ int calendarWeekStartDay = Calendar.MONDAY;
+ if (weekStartDay != null) {
+ calendarWeekStartDay = WeekDay.getCalendarDay(new WeekDay(weekStartDay));
+ }
+ cal.setFirstDayOfWeek(calendarWeekStartDay);
+ cal.setTime(date);
+
+ 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 list
+ * @param offset
+ * @param sublist
+ */
+ 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 = Dates.getCalendarInstance(date);
+ cal.setTime(date);
+ 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 = Dates.getCalendarInstance(date);
+ cal.setTime(date);
+ 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 = Dates.getCalendarInstance(date);
+ cal.setTime(date);
+ 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;
+ }
+
+ /**
+ * @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 final 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;
+ }
+
+}