/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.core.scheduler; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.StringTokenizer; import java.util.TimeZone; import org.apache.commons.lang.StringUtils; import org.eclipse.smarthome.core.scheduler.RecurrenceExpression.RecurrenceExpressionPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>RecurrenceExpression</code> is an implementation of {@link Expression} that provides a parser and evaluator for * iCalendar recurrence rule expressions as defined in the * <ahref="http://tools.ietf.org/html/rfc5545#section-3.3.10">RFC 5545</a>. * <p> * Recurrence rules provide the ability to specify complex time combinations * based on the standard format for defining calendar and scheduling information * (iCalendar). For instance : "every Sunday in January at 8:30 AM and 9:30 AM, * every other year". More examples can be found <a * href="http://http://tools.ietf.org/html/rfc5545#section-3.8.5.3">here</a>. * <p> * A recurrence rule is composed of 1 required part that defines the frequency * and 13 optional ones separated by a semi-colon. * <p> * The recur definition of the RFC 5545 is as follows:. * * <pre> * <code> * recur = recur-rule-part *( ";" recur-rule-part ) * ; * ; The rule parts are not ordered in any * ; particular sequence. * ; * ; The FREQ rule part is REQUIRED, * ; but MUST NOT occur more than once. * ; * ; The UNTIL or COUNT rule parts are OPTIONAL, * ; but they MUST NOT occur in the same 'recur'. * ; * ; The other rule parts are OPTIONAL, * ; but MUST NOT occur more than once. * * recur-rule-part = ( "FREQ" "=" freq ) * / ( "UNTIL" "=" enddate ) * / ( "COUNT" "=" 1*DIGIT ) * / ( "INTERVAL" "=" 1*DIGIT ) * / ( "BYSECOND" "=" byseclist ) * / ( "BYMINUTE" "=" byminlist ) * / ( "BYHOUR" "=" byhrlist ) * / ( "BYDAY" "=" bywdaylist ) * / ( "BYMONTHDAY" "=" bymodaylist ) * / ( "BYYEARDAY" "=" byyrdaylist ) * / ( "BYWEEKNO" "=" bywknolist ) * / ( "BYMONTH" "=" bymolist ) * / ( "BYSETPOS" "=" bysplist ) * / ( "WKST" "=" weekday ) * freq = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY" * / "WEEKLY" / "MONTHLY" / "YEARLY" * enddate = date / date-time * byseclist = ( seconds *("," seconds) ) * seconds = 1*2DIGIT ;0 to 60 * byminlist = ( minutes *("," minutes) ) * minutes = 1*2DIGIT ;0 to 59 * byhrlist = ( hour *("," hour) ) * hour = 1*2DIGIT ;0 to 23 * bywdaylist = ( weekdaynum *("," weekdaynum) ) * weekdaynum = [[plus / minus] ordwk] weekday * plus = "+" * minus = "-" * ordwk = 1*2DIGIT ;1 to 53 * weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA" * ; Corresponding to SUNDAY, MONDAY, TUESDAY, * ; WEDNESDAY, THURSDAY, FRIDAY, and SATURDAY days of the week. * bymodaylist = ( monthdaynum *("," monthdaynum) ) * monthdaynum = [plus / minus] ordmoday * ordmoday = 1*2DIGIT ;1 to 31 * byyrdaylist = ( yeardaynum *("," yeardaynum) ) * yeardaynum = [plus / minus] ordyrday * ordyrday = 1*3DIGIT ;1 to 366 * bywknolist = ( weeknum *("," weeknum) ) * weeknum = [plus / minus] ordwk * bymolist = ( monthnum *("," monthnum) ) * monthnum = 1*2DIGIT ;1 to 12 * bysplist = ( setposday *("," setposday) ) * setposday = yeardaynum * </code> * </pre> * * Description: This value type is a structured value consisting of a * list of one or more recurrence grammar parts. Each rule part is * defined by a NAME=VALUE pair. The rule parts are separated from * each other by the SEMICOLON character. The rule parts are not * ordered in any particular sequence. Individual rule parts MUST * only be specified once. Compliant applications MUST accept rule * parts ordered in any sequence, but to ensure backward * compatibility with applications that pre-date this revision of * iCalendar the FREQ rule part MUST be the first rule part specified * in a RECUR value. * * The FREQ rule part identifies the type of recurrence rule. This * rule part MUST be specified in the recurrence rule. Valid values * include SECONDLY, to specify repeating events based on an interval * of a second or more; MINUTELY, to specify repeating events based * on an interval of a minute or more; HOURLY, to specify repeating * events based on an interval of an hour or more; DAILY, to specify * repeating events based on an interval of a day or more; WEEKLY, to * specify repeating events based on an interval of a week or more; * MONTHLY, to specify repeating events based on an interval of a * month or more; and YEARLY, to specify repeating events based on an * interval of a year or more. * * The INTERVAL rule part contains a positive integer representing at * which intervals the recurrence rule repeats. The default value is * "1", meaning every second for a SECONDLY rule, every minute for a * MINUTELY rule, every hour for an HOURLY rule, every day for a * DAILY rule, every week for a WEEKLY rule, every month for a * MONTHLY rule, and every year for a YEARLY rule. For example, * within a DAILY rule, a value of "8" means every eight days. * * The UNTIL rule part defines a DATE or DATE-TIME value that bounds * the recurrence rule in an inclusive manner. If the value * specified by UNTIL is synchronized with the specified recurrence, * this DATE or DATE-TIME becomes the last instance of the * recurrence. The value of the UNTIL rule part MUST have the same * value type as the "DTSTART" property. Furthermore, if the * "DTSTART" property is specified as a date with local time, then * the UNTIL rule part MUST also be specified as a date with local * time. If the "DTSTART" property is specified as a date with UTC * time or a date with local time and time zone reference, then the * UNTIL rule part MUST be specified as a date with UTC time. In the * case of the "STANDARD" and "DAYLIGHT" sub-components the UNTIL * rule part MUST always be specified as a date with UTC time. If * specified as a DATE-TIME value, then it MUST be specified in a UTC * time format. If not present, and the COUNT rule part is also not * present, the "RRULE" is considered to repeat forever. * * The COUNT rule part defines the number of occurrences at which to * range-bound the recurrence. The "DTSTART" property value always * counts as the first occurrence. * * The BYSECOND rule part specifies a COMMA-separated list of seconds * within a minute. Valid values are 0 to 60. The BYMINUTE rule * part specifies a COMMA-separated list of minutes within an hour. * Valid values are 0 to 59. The BYHOUR rule part specifies a COMMA- * separated list of hours of the day. Valid values are 0 to 23. * The BYSECOND, BYMINUTE and BYHOUR rule parts MUST NOT be specified * when the associated "DTSTART" property has a DATE value type. * These rule parts MUST be ignored in RECUR value that violate the * above requirement (e.g., generated by applications that pre-date * this revision of iCalendar). * * The BYDAY rule part specifies a COMMA-separated list of days of * the week; SU indicates Sunday; MO indicates Monday; TU indicates * Tuesday; WE indicates Wednesday; TH indicates Thursday; FR * indicates Friday; and SA indicates Saturday. * * Each BYDAY value can also be preceded by a positive (+n) or * negative (-n) integer. If present, this indicates the nth * occurrence of a specific day within the MONTHLY or YEARLY "RRULE". * * For example, within a MONTHLY rule, +1MO (or simply 1MO) * represents the first Monday within the month, whereas -1MO * represents the last Monday of the month. The numeric value in a * BYDAY rule part with the FREQ rule part set to YEARLY corresponds * to an offset within the month when the BYMONTH rule part is * present, and corresponds to an offset within the year when the * BYWEEKNO or BYMONTH rule parts are present. If an integer * modifier is not present, it means all days of this type within the * specified frequency. For example, within a MONTHLY rule, MO * represents all Mondays within the month. The BYDAY rule part MUST * NOT be specified with a numeric value when the FREQ rule part is * not set to MONTHLY or YEARLY. Furthermore, the BYDAY rule part * MUST NOT be specified with a numeric value with the FREQ rule part * set to YEARLY when the BYWEEKNO rule part is specified. * * The BYMONTHDAY rule part specifies a COMMA-separated list of days * of the month. Valid values are 1 to 31 or -31 to -1. For * example, -10 represents the tenth to the last day of the month. * The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule * part is set to WEEKLY. * * The BYYEARDAY rule part specifies a COMMA-separated list of days * of the year. Valid values are 1 to 366 or -366 to -1. For * example, -1 represents the last day of the year (December 31st) * and -306 represents the 306th to the last day of the year (March * 1st). The BYYEARDAY rule part MUST NOT be specified when the FREQ * rule part is set to DAILY, WEEKLY, or MONTHLY. * * The BYWEEKNO rule part specifies a COMMA-separated list of * ordinals specifying weeks of the year. Valid values are 1 to 53 * or -53 to -1. This corresponds to weeks according to week * numbering as defined in [ISO.8601.2004]. A week is defined as a * seven day period, starting on the day of the week defined to be * the week start (see WKST). Week number one of the calendar year * is the first week that contains at least four (4) days in that * calendar year. This rule part MUST NOT be used when the FREQ rule * part is set to anything other than YEARLY. For example, 3 * represents the third week of the year. * * Note: Assuming a Monday week start, week 53 can only occur when * Thursday is January 1 or if it is a leap year and Wednesday is * January 1. * * The BYMONTH rule part specifies a COMMA-separated list of months * of the year. Valid values are 1 to 12. * * The WKST rule part specifies the day on which the workweek starts. * Valid values are MO, TU, WE, TH, FR, SA, and SU. This is * significant when a WEEKLY "RRULE" has an interval greater than 1, * and a BYDAY rule part is specified. This is also significant when * in a YEARLY "RRULE" when a BYWEEKNO rule part is specified. The * default value is MO. * * The BYSETPOS rule part specifies a COMMA-separated list of values * that corresponds to the nth occurrence within the set of * recurrence instances specified by the rule. BYSETPOS operates on * a set of recurrence instances in one interval of the recurrence * rule. For example, in a WEEKLY rule, the interval would be one * week A set of recurrence instances starts at the beginning of the * interval defined by the FREQ rule part. Valid values are 1 to 366 * or -366 to -1. It MUST only be used in conjunction with another * BYxxx rule part. For example "the last work day of the month" * could be represented as: * * FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1 * * Each BYSETPOS value can include a positive (+n) or negative (-n) * integer. If present, this indicates the nth occurrence of the * specific occurrence within the set of occurrences specified by the * rule. * * Recurrence rules may generate recurrence instances with an invalid * date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM * on a day where the local time is moved forward by an hour at 1:00 * AM). Such recurrence instances MUST be ignored and MUST NOT be * counted as part of the recurrence set. * * Information, not contained in the rule, necessary to determine the * various recurrence instance start time and dates are derived from * the Start Time ("DTSTART") component attribute. For example, * "FREQ=YEARLY;BYMONTH=1" doesn't specify a specific day within the * month or a time. This information would be the same as what is * specified for "DTSTART". * * BYxxx rule parts modify the recurrence in some manner. BYxxx rule * parts for a period of time that is the same or greater than the * frequency generally reduce or limit the number of occurrences of * the recurrence generated. For example, "FREQ=DAILY;BYMONTH=1" * reduces the number of recurrence instances from all days (if * BYMONTH rule part is not present) to all days in January. BYxxx * rule parts for a period of time less than the frequency generally * increase or expand the number of occurrences of the recurrence. * For example, "FREQ=YEARLY;BYMONTH=1,2" increases the number of * days within the yearly recurrence set from 1 (if BYMONTH rule part * is not present) to 2. * * If multiple BYxxx rule parts are specified, then after evaluating * the specified FREQ and INTERVAL rule parts, the BYxxx rule parts * are applied to the current set of evaluated occurrences in the * following order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, * BYHOUR, BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are * evaluated. * * The table below summarizes the dependency of BYxxx rule part * expand or limit behavior on the FREQ rule part value. * * The term "N/A" means that the corresponding BYxxx rule part MUST * NOT be used with the corresponding FREQ value. * * BYDAY has some special behavior depending on the FREQ value and * this is described in separate notes below the table. * * +----------+--------+--------+-------+-------+------+-------+------+ * | |SECONDLY|MINUTELY|HOURLY |DAILY |WEEKLY|MONTHLY|YEARLY| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYMONTH |Limit |Limit |Limit |Limit |Limit |Limit |Expand| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYWEEKNO |N/A |N/A |N/A |N/A |N/A |N/A |Expand| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYYEARDAY |Limit |Limit |Limit |N/A |N/A |N/A |Expand| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYMONTHDAY|Limit |Limit |Limit |Limit |N/A |Expand |Expand| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYDAY |Limit |Limit |Limit |Limit |Expand|Note 1 |Note 2| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYHOUR |Limit |Limit |Limit |Expand |Expand|Expand |Expand| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYMINUTE |Limit |Limit |Expand |Expand |Expand|Expand |Expand| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYSECOND |Limit |Expand |Expand |Expand |Expand|Expand |Expand| * +----------+--------+--------+-------+-------+------+-------+------+ * |BYSETPOS |Limit |Limit |Limit |Limit |Limit |Limit |Limit | * +----------+--------+--------+-------+-------+------+-------+------+ * * Note 1: Limit if BYMONTHDAY is present; otherwise, special expand * for MONTHLY. * * Note 2: Limit if BYYEARDAY or BYMONTHDAY is present; otherwise, * special expand for WEEKLY if BYWEEKNO present; otherwise, * special expand for MONTHLY if BYMONTH present; otherwise, * special expand for YEARLY. * * Here is an example of evaluating multiple BYxxx rule parts. * * DTSTART;TZID=America/New_York:19970105T083000 * RRULE:FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9; * BYMINUTE=30 * * First, the "INTERVAL=2" would be applied to "FREQ=YEARLY" to * arrive at "every other year". Then, "BYMONTH=1" would be applied * to arrive at "every January, every other year". Then, "BYDAY=SU" * would be applied to arrive at "every Sunday in January, every * other year". Then, "BYHOUR=8,9" would be applied to arrive at * "every Sunday in January at 8 AM and 9 AM, every other year". * Then, "BYMINUTE=30" would be applied to arrive at "every Sunday in * January at 8:30 AM and 9:30 AM, every other year". Then, lacking * information from "RRULE", the second is derived from "DTSTART", to * end up in "every Sunday in January at 8:30:00 AM and 9:30:00 AM, * every other year". Similarly, if the BYMINUTE, BYHOUR, BYDAY, * BYMONTHDAY, or BYMONTH rule part were missing, the appropriate * minute, hour, day, or month would have been retrieved from the * "DTSTART" property. * * If the computed local start time of a recurrence instance does not * exist, or occurs more than once, for the specified time zone, the * time of the recurrence instance is interpreted in the same manner * as an explicit DATE-TIME value describing that date and time, as * specified in Section 3.3.5. * * No additional content value encoding (i.e., BACKSLASH character * encoding, see Section 3.3.11) is defined for this value type. * * Example: The following is a rule that specifies 10 occurrences that * occur every other day: * * FREQ=DAILY;COUNT=10;INTERVAL=2 * * * @author Karel Goderis - Initial contribution * */ public class RecurrenceExpression extends AbstractExpression<RecurrenceExpressionPart> { 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"; private final Logger logger = LoggerFactory.getLogger(RecurrenceExpression.class); public enum Frequency { SECONDLY("SECONDLY", Calendar.SECOND), MINUTELY("MINUTELY", Calendar.MINUTE), HOURLY("HOURLY", Calendar.HOUR_OF_DAY), DAILY("DAILY", Calendar.DAY_OF_YEAR), WEEKLY("WEEKLY", Calendar.WEEK_OF_YEAR), MONTHLY("MONTHLY", Calendar.MONTH), YEARLY("YEARLY", Calendar.YEAR); private final String identifier; private final int calendarField; private Frequency(final String id, final int field) { this.identifier = id; this.calendarField = field; } public static Frequency getFrequency(final String id) { for (Frequency aFrequency : Frequency.values()) { if (aFrequency.toString().equals(id)) { return aFrequency; } } throw new IllegalArgumentException("Invalid frequency value " + id); } public int getCalendarField() { return calendarField; } @Override public String toString() { return identifier; } } public enum WeekDay { SUNDAY("SU", Calendar.SUNDAY), MONDAY("MO", Calendar.MONDAY), TUESDAY("TU", Calendar.TUESDAY), WEDNESDAY("WE", Calendar.WEDNESDAY), THURSDAY("TH", Calendar.THURSDAY), FRIDAY("FR", Calendar.FRIDAY), SATURDAY("SA", Calendar.SATURDAY); private final String identifier; private final int calendarDay; public static WeekDay getWeekDay(final int calendar) { return WeekDay.values()[calendar]; } public static WeekDay getWeekDay(final String id) { for (WeekDay aDay : WeekDay.values()) { if (aDay.toString().equals(id)) { return aDay; } } throw new IllegalArgumentException("Invalid calendar value " + id); } private WeekDay(final String code, final int day) { this.identifier = code; this.calendarDay = day; } public int getCalendarDay() { return calendarDay; } @Override public String toString() { return identifier; } }; /** * Constructs a new <CODE>RecurrenceExpression</CODE> based on the specified * parameter. * * @param recurrenceRule string representation of the RFC 5545 recurrence rule the new object should represent. * @throws ParseException if the string expression cannot be parsed into a valid <code>RecurrenceExpression</code> * . */ public RecurrenceExpression(final String recurrenceRule) throws ParseException { this(recurrenceRule, Calendar.getInstance().getTime(), TimeZone.getDefault()); } /** * Constructs a new <CODE>RecurrenceExpression</CODE> based on the specified * parameter. * * @param recurrenceRule string representation of the RFC 5545 recurrence rule the new object should represent. * @param startTime the start time to consider for the recurrence rule. * @throws ParseException if the string expression cannot be parsed into a valid <code>RecurrenceExpression</code> * . */ public RecurrenceExpression(final String recurrenceRule, final Date startTime) throws ParseException { this(recurrenceRule, startTime, TimeZone.getDefault()); } /** * Constructs a new <CODE>RecurrenceExpression</CODE> based on the specified * parameter. * * @param recurrenceRule string representation of the RFC 5545 recurrence rule the new object should represent. * @param startTime the start time to consider for the recurrence rule. * @param zone the timezone for which this recurrence rule will be resolved. * @throws ParseException if the string expression cannot be parsed into a valid <code>RecurrenceExpression</code> * . */ public RecurrenceExpression(final String recurrenceRule, final Date startTime, final TimeZone zone) throws ParseException { super(recurrenceRule, ";", startTime, zone, 0, 366); } @Override public void setStartDate(Date startDate) throws IllegalArgumentException, ParseException { if (startDate == null) { throw new IllegalArgumentException("The start date of the rule can not be null"); } UntilExpressionPart until = (UntilExpressionPart) getExpressionPart(UntilExpressionPart.class); if (until != null && until.getUntil().before(startDate)) { throw new IllegalArgumentException("Start date cannot be after until"); } // We set the real start date to the next second; milliseconds are not supported by Recurrence expressions // anyways Calendar calendar = Calendar.getInstance(getTimeZone()); calendar.setTime(startDate); if (calendar.get(Calendar.MILLISECOND) != 0) { calendar.add(Calendar.SECOND, 1); calendar.set(Calendar.MILLISECOND, 0); } super.setStartDate(calendar.getTime()); } @Override public boolean isSatisfiedBy(final Date test) { getTimeAfter(test); Collections.sort(getCandidates()); for (Date aDate : getCandidates()) { if (aDate.after(test)) { return false; } if (aDate.equals(test)) { return true; } } return false; } /** * Indicates whether the specified expression can be parsed into a * valid <code>RecurrencExpression</code> * * @param expression the expression to evaluate * @return a boolean indicating whether the given expression will yield a valid <code>RecurrencExpression</code> */ public static boolean isValidExpression(String expression) { try { new RecurrenceExpression(expression); } catch (ParseException pe) { return false; } return true; } @Override public final Date getFinalFireTime() { boolean isUntil = getExpressionPart(UntilExpressionPart.class) != null ? true : false; boolean isCount = getExpressionPart(CountExpressionPart.class) != null ? true : false; if (!(isUntil || isCount)) { return null; } else { return super.getFinalFireTime(); } } @Override protected void validateExpression() throws IllegalArgumentException { boolean isFrequency = getExpressionPart(FrequencyExpressionPart.class) != null ? true : false; boolean isUntil = getExpressionPart(UntilExpressionPart.class) != null ? true : false; boolean isCount = getExpressionPart(CountExpressionPart.class) != null ? true : false; boolean isByDay = getExpressionPart(DayExpressionPart.class) != null ? true : false; boolean isByWeekNumber = getExpressionPart(WeekNumberExpressionPart.class) != null ? true : false; boolean isByMonthDay = getExpressionPart(MonthDayExpressionPart.class) != null ? true : false; boolean isByYearDay = getExpressionPart(YearDayExpressionPart.class) != null ? true : false; boolean isByPosition = getExpressionPart(PositionExpressionPart.class) != null ? true : false; boolean isBySecond = getExpressionPart(SecondExpressionPart.class) != null ? true : false; boolean isByHour = getExpressionPart(HourExpressionPart.class) != null ? true : false; boolean isByMinute = getExpressionPart(MinuteExpressionPart.class) != null ? true : false; boolean isByMonth = getExpressionPart(MonthExpressionPart.class) != null ? true : false; if (!isFrequency) { throw new IllegalArgumentException("A recurrence rule MUST contain a FREQ rule part."); } if (isUntil && isCount) { throw new IllegalArgumentException( "The UNTIL and COUNT rule parts MUST NOT occur in the same recurrence rule."); } if (isByDay && isFrequency) { Frequency frequency = ((FrequencyExpressionPart) getExpressionPart(FrequencyExpressionPart.class)) .getFrequency(); if (((DayExpressionPart) getExpressionPart(DayExpressionPart.class)).isNumeric() && (frequency == Frequency.MONTHLY || frequency == Frequency.YEARLY)) { throw new IllegalArgumentException("The BYDAY rule part MUST NOT be specified with a numeric value " + "when the FREQ rule part is not set to MONTHLY or YEARLY."); } } if (isByDay && isFrequency && isByWeekNumber) { Frequency frequency = ((FrequencyExpressionPart) getExpressionPart(FrequencyExpressionPart.class)) .getFrequency(); if (((DayExpressionPart) getExpressionPart(DayExpressionPart.class)).isNumeric() && frequency == Frequency.YEARLY) { throw new IllegalArgumentException( "The BYDAY rule part MUST NOT be specified with a numeric value with the FREQ rule part set to YEARLY when the BYWEEKNO rule part is specified."); } } if (isByMonthDay && isFrequency) { Frequency frequency = ((FrequencyExpressionPart) getExpressionPart(FrequencyExpressionPart.class)) .getFrequency(); if (frequency == Frequency.WEEKLY) { throw new IllegalArgumentException( "The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule part is set to WEEKLY."); } } if (isByYearDay && isFrequency) { Frequency frequency = ((FrequencyExpressionPart) getExpressionPart(FrequencyExpressionPart.class)) .getFrequency(); if (frequency == Frequency.WEEKLY || frequency == Frequency.DAILY || frequency == Frequency.MONTHLY) { throw new IllegalArgumentException( "The BYYEARDAY rule part MUST NOT be specified when the FREQ rule part is set to DAILY, WEEKLY, or MONTHLY."); } } if (isByWeekNumber && isFrequency) { Frequency frequency = ((FrequencyExpressionPart) getExpressionPart(FrequencyExpressionPart.class)) .getFrequency(); if (frequency != Frequency.YEARLY) { throw new IllegalArgumentException( "The BYWEEKNO rule part MUST NOT be used when the FREQ rule part is set to anything other than YEARLY."); } } if (isByPosition && !(isByDay || isByHour || isByMinute || isByMonth || isByMonthDay || isBySecond || isByWeekNumber || isByYearDay)) { throw new IllegalArgumentException( "The BYSETPOS rule part MUST only be used in conjunction with another BYxxx rule part."); } } @Override protected void populateWithSeeds() { // Nothing to do here, as the mandatory FREQ part of the Recurrence Rule is a defacto source of seeds } @Override protected void pruneFarthest() { Collections.sort(getCandidates()); ArrayList<Date> beforeDates = new ArrayList<Date>(); for (Date candidate : getCandidates()) { if (candidate.before(getStartDate())) { beforeDates.add(candidate); } } getCandidates().removeAll(beforeDates); } @Override protected RecurrenceExpressionPart parseToken(String token, int position) throws ParseException { String key = StringUtils.substringBefore(token, "="); String value = StringUtils.substringAfter(token, "="); switch (key) { case FREQ: { return new FrequencyExpressionPart(value); } case INTERVAL: { return new IntervalExpressionPart(value); } case BYSECOND: { return new SecondExpressionPart(value); } case BYMINUTE: { return new MinuteExpressionPart(value); } case BYHOUR: { return new HourExpressionPart(value); } case BYDAY: { return new DayExpressionPart(value); } case BYMONTHDAY: { return new MonthDayExpressionPart(value); } case BYMONTH: { return new MonthExpressionPart(value); } case BYYEARDAY: { return new YearDayExpressionPart(value); } case BYWEEKNO: { return new WeekNumberExpressionPart(value); } case BYSETPOS: { return new PositionExpressionPart(value); } case WKST: { return new WeekStartExpressionPart(value); } case UNTIL: { return new UntilExpressionPart(value); } case COUNT: { return new CountExpressionPart(value); } default: throw new IllegalArgumentException("Unknown expression part"); } } protected abstract class RecurrenceExpressionPart extends AbstractExpressionPart { public RecurrenceExpressionPart(String s) throws ParseException { super(s); } } protected abstract class IntegerListRecurrenceExpressionPart extends RecurrenceExpressionPart { public IntegerListRecurrenceExpressionPart(String s) throws ParseException { super(s); } @Override public void parse() throws ParseException { setValueSet(initializeValueSet()); StringTokenizer valueTokenizer = new StringTokenizer(getPart(), ","); while (valueTokenizer.hasMoreTokens()) { String v = valueTokenizer.nextToken(); try { try { getValueSet().add(Integer.parseInt(v)); } catch (NumberFormatException e) { throw new ParseException("Invalid integer value : " + v, 0); } getValueSet().add(Integer.parseInt(v)); } catch (NumberFormatException e) { throw new ParseException("Invalid integer value : " + v, 0); } } } } protected abstract class DayListRecurrenceExpressionPart extends RecurrenceExpressionPart { protected HashMap<WeekDay, Integer> dayList; public DayListRecurrenceExpressionPart(String s) throws ParseException { super(s); } @Override public void parse() throws ParseException { dayList = new HashMap<WeekDay, Integer>(); StringTokenizer valueTokenizer = new StringTokenizer(getPart(), ","); while (valueTokenizer.hasMoreTokens()) { String v = valueTokenizer.nextToken(); try { try { WeekDay day = WeekDay.getWeekDay(v); dayList.put(day, 0); } catch (IllegalArgumentException e) { String dayname = StringUtils.right(v, 2); String occurrence = StringUtils.left(v, v.length() - 2); if (occurrence.length() == 0) { dayList.put(WeekDay.getWeekDay(dayname), 0); } else { dayList.put(WeekDay.getWeekDay(dayname), Integer.parseInt(occurrence)); } } } catch (Exception f) { throw new ParseException("Invalid day/occurence value : " + v, 0); } } } public boolean isNumeric() { if (dayList != null) { for (Integer number : dayList.values()) { if (number != 0) { return true; } } } return false; } } public class FrequencyExpressionPart extends RecurrenceExpressionPart { protected static final int SEEDS = 100; public FrequencyExpressionPart(String s) throws ParseException { super(s); } protected Frequency frequency; public Frequency getFrequency() { return frequency; } @Override public void parse() throws ParseException { frequency = Frequency.getFrequency(getPart()); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { IntervalExpressionPart intervalPart = (IntervalExpressionPart) getExpressionPart( IntervalExpressionPart.class); Calendar cal = Calendar.getInstance(getTimeZone()); cal.setLenient(false); cal.setTime(getStartDate()); candidates = new ArrayList<Date>(); ArrayList<Date> newCandidates = new ArrayList<Date>(); newCandidates.add(cal.getTime()); int interval = intervalPart != null ? intervalPart.getInterval() : 1; for (int i = 1; i < SEEDS; i++) { cal.add(frequency.getCalendarField(), interval); newCandidates.add(cal.getTime()); } candidates.addAll(newCandidates); return candidates; } @Override BoundedIntegerSet initializeValueSet() { return null; } @Override public int order() { return 3; } } public class IntervalExpressionPart extends RecurrenceExpressionPart { public IntervalExpressionPart(String s) throws ParseException { super(s); } int interval; public int getInterval() { return interval; } @Override public void parse() throws ParseException { try { interval = Integer.parseInt(getPart()); if (interval <= 0) { throw new IllegalArgumentException("Inteval must be a postive integer"); } } catch (NumberFormatException pe) { throw new ParseException("Invalid integer value for INTERVAL : " + getPart(), 0); } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { return candidates; } @Override BoundedIntegerSet initializeValueSet() { return null; } @Override public int order() { return 1; } } public class WeekStartExpressionPart extends RecurrenceExpressionPart { public WeekStartExpressionPart(String s) throws ParseException { super(s); } WeekDay weekStart; public WeekDay getWeekStart() { return weekStart; } @Override public void parse() throws ParseException { try { weekStart = WeekDay.getWeekDay(getPart()); } catch (Exception f) { throw new ParseException("Invalid value for WKST: " + getPart(), 0); } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { return candidates; } @Override BoundedIntegerSet initializeValueSet() { return null; } @Override public int order() { return 2; } } public class MonthExpressionPart extends IntegerListRecurrenceExpressionPart { protected static final int MIN_MONTH = 1; protected static final int MAX_MONTH = 12; public MonthExpressionPart(String s) throws ParseException { super(s); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); if (frequency == Frequency.YEARLY) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { cal.setTime(date); for (Integer element : getValueSet()) { cal.roll(Calendar.MONTH, (element - 1) - cal.get(Calendar.MONTH)); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!getValueSet().contains(cal.get(Calendar.MONTH) + (getValueSet().is1indexed ? 1 : 0))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } return candidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_MONTH, MAX_MONTH, false, true); } @Override public int order() { return 4; } } public class WeekNumberExpressionPart extends IntegerListRecurrenceExpressionPart { private static final int MIN_WEEKNO = 1; private static final int MAX_WEEKNO = 53; public WeekNumberExpressionPart(String s) throws ParseException { super(s); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); WeekStartExpressionPart weekStartPart = (WeekStartExpressionPart) getExpressionPart( WeekStartExpressionPart.class); WeekDay weekStart = weekStartPart != null ? weekStartPart.getWeekStart() : WeekDay.MONDAY; if (frequency == Frequency.YEARLY) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); cal.setFirstDayOfWeek(weekStart.getCalendarDay()); for (Date date : candidates) { for (Integer element : getValueSet()) { cal.setTime(date); if (element > 0 && element <= cal.getMaximum(Calendar.WEEK_OF_YEAR)) { cal.set(Calendar.WEEK_OF_YEAR, element); newCandidates.add(cal.getTime()); } else { if (cal.getMaximum(MAX_WEEKNO) + element + 1 > 0) { cal.set(Calendar.WEEK_OF_YEAR, cal.getMaximum(MAX_WEEKNO) + element + 1); newCandidates.add(cal.getTime()); } } } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else { logger.warn("BYWEEKNO can only be used together with YEARLY"); } return candidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_WEEKNO, MAX_WEEKNO, true, true); } @Override public int order() { return 5; } } public class YearDayExpressionPart extends IntegerListRecurrenceExpressionPart { private static final int MIN_YEARDAY = 1; private static final int MAX_YEARDAY = 366; public YearDayExpressionPart(String s) throws ParseException { super(s); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); if (frequency == Frequency.YEARLY) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { for (Integer element : getValueSet()) { cal.setTime(date); if (element > 0 && element <= cal.getMaximum(Calendar.DAY_OF_YEAR)) { cal.set(Calendar.DAY_OF_YEAR, element); newCandidates.add(cal.getTime()); } else { if (cal.getMaximum(Calendar.DAY_OF_YEAR) + element + 1 > 0) { cal.set(Calendar.DAY_OF_YEAR, cal.getMaximum(Calendar.DAY_OF_YEAR) + element + 1); newCandidates.add(cal.getTime()); } } } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else if (frequency == Frequency.SECONDLY || frequency == Frequency.MINUTELY || frequency == Frequency.HOURLY) { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!getValueSet().contains(cal.get(Calendar.DAY_OF_YEAR))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } return candidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_YEARDAY, MAX_YEARDAY, true, true); } @Override public int order() { return 6; } } public class MonthDayExpressionPart extends IntegerListRecurrenceExpressionPart { protected static final int MIN_MONTHDAY = 1; protected static final int MAX_MONTHDAY = 31; public MonthDayExpressionPart(String s) throws ParseException { super(s); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); if (frequency == Frequency.YEARLY || frequency == Frequency.MONTHLY) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { for (Integer element : getValueSet()) { cal.setTime(date); if (element > 0 && element <= cal.getMaximum(Calendar.DAY_OF_MONTH)) { cal.set(Calendar.DAY_OF_MONTH, element); newCandidates.add(cal.getTime()); } else { if (cal.getMaximum(Calendar.DAY_OF_MONTH) + element + 1 > 0) { cal.set(Calendar.DAY_OF_MONTH, cal.getMaximum(Calendar.DAY_OF_MONTH) + element + 1); newCandidates.add(cal.getTime()); } } } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else if (frequency == Frequency.SECONDLY || frequency == Frequency.MINUTELY || frequency == Frequency.HOURLY || frequency == Frequency.DAILY) { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!getValueSet().contains(cal.get(Calendar.DAY_OF_MONTH))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } return candidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_MONTHDAY, MAX_MONTHDAY, true, true); } @Override public int order() { return 7; } } public class DayExpressionPart extends DayListRecurrenceExpressionPart { protected static final int MIN_MONTHDAY = 1; protected static final int MAX_MONTHDAY = 31; public DayExpressionPart(String s) throws ParseException { super(s); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); WeekStartExpressionPart weekStartPart = (WeekStartExpressionPart) getExpressionPart( WeekStartExpressionPart.class); WeekDay weekStart = weekStartPart != null ? weekStartPart.getWeekStart() : WeekDay.MONDAY; boolean isByYearDay = getExpressionPart(YearDayExpressionPart.class) != null ? true : false; boolean isByMonthDay = getExpressionPart(MonthDayExpressionPart.class) != null ? true : false; boolean isByWeekNumber = getExpressionPart(WeekNumberExpressionPart.class) != null ? true : false; boolean isByMonth = getExpressionPart(MonthExpressionPart.class) != null ? true : false; if (frequency == Frequency.YEARLY) { if (isByYearDay || isByMonthDay) { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!dayList.keySet().contains(WeekDay.getWeekDay(cal.get(Calendar.DAY_OF_WEEK) - 1))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } else if (isByWeekNumber) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { for (WeekDay aDay : dayList.keySet()) { cal.setTime(date); cal.setFirstDayOfWeek(weekStart.getCalendarDay()); cal.set(Calendar.DAY_OF_WEEK, aDay.getCalendarDay()); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else if (isByMonth) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { for (WeekDay aDay : dayList.keySet()) { cal.setTime(date); cal.set(Calendar.DAY_OF_MONTH, 1); List<Date> datesInMonth = new ArrayList<Date>(); int setMonth = cal.get(Calendar.MONTH); while (cal.get(Calendar.DAY_OF_MONTH) <= cal.getMaximum(Calendar.DAY_OF_MONTH) && cal.get(Calendar.MONTH) == setMonth) { if (cal.get(Calendar.DAY_OF_WEEK) == aDay.getCalendarDay()) { datesInMonth.add(cal.getTime()); } cal.add(Calendar.DAY_OF_YEAR, 1); } cal.setTime(date); logger.debug("date is {}, weekday is {}, offset is {}, datesInMonth {}", new Object[] { date, aDay, dayList.get(aDay), datesInMonth.size() }); if (dayList.get(aDay) > 0 && dayList.get(aDay) <= datesInMonth.size()) { newCandidates.add(datesInMonth.get(dayList.get(aDay) - 1)); } else if (dayList.get(aDay) < 0 && dayList.get(aDay) >= -datesInMonth.size()) { logger.debug("Adding new candidate {}", datesInMonth.get(datesInMonth.size() + dayList.get(aDay))); newCandidates.add(datesInMonth.get(datesInMonth.size() + dayList.get(aDay))); } else if (dayList.get(aDay) == 0) { newCandidates.addAll(datesInMonth); } } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { for (WeekDay aDay : dayList.keySet()) { cal.setTime(date); cal.set(Calendar.MONTH, 0); cal.set(Calendar.DAY_OF_MONTH, 1); int setYear = cal.get(Calendar.YEAR); List<Date> datesInYear = new ArrayList<Date>(); while (cal.get(Calendar.DAY_OF_YEAR) <= cal.getMaximum(Calendar.DAY_OF_YEAR) && cal.get(Calendar.YEAR) == setYear) { if (cal.get(Calendar.DAY_OF_WEEK) == aDay.getCalendarDay()) { datesInYear.add(cal.getTime()); } cal.add(Calendar.DAY_OF_YEAR, 1); } cal.setTime(date); if (dayList.get(aDay) > 0 && dayList.get(aDay) <= datesInYear.size()) { newCandidates.add(datesInYear.get(dayList.get(aDay) - 1)); } else if (dayList.get(aDay) < 0 && dayList.get(aDay) >= -datesInYear.size()) { newCandidates.add(datesInYear.get(datesInYear.size() + dayList.get(aDay))); } else if (dayList.get(aDay) == 0) { newCandidates.addAll(datesInYear); } } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } } else if (frequency == Frequency.MONTHLY) { if (!isByMonthDay) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { for (WeekDay aDay : dayList.keySet()) { cal.setTime(date); cal.set(Calendar.DAY_OF_MONTH, 1); List<Date> datesInMonth = new ArrayList<Date>(); int setMonth = cal.get(Calendar.MONTH); while (cal.get(Calendar.DAY_OF_MONTH) <= cal.getMaximum(Calendar.DAY_OF_MONTH) && cal.get(Calendar.MONTH) == setMonth) { if (cal.get(Calendar.DAY_OF_WEEK) == aDay.getCalendarDay()) { datesInMonth.add(cal.getTime()); } cal.add(Calendar.DAY_OF_YEAR, 1); } cal.setTime(date); if (dayList.get(aDay) > 0 && dayList.get(aDay) <= datesInMonth.size()) { newCandidates.add(datesInMonth.get(dayList.get(aDay) - 1)); } else if (dayList.get(aDay) < 0 && dayList.get(aDay) >= -datesInMonth.size()) { newCandidates.add(datesInMonth.get(datesInMonth.size() + dayList.get(aDay))); } else if (dayList.get(aDay) == 0) { newCandidates.addAll(datesInMonth); } } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!dayList.keySet().contains(WeekDay.getWeekDay(cal.get(Calendar.DAY_OF_WEEK) - 1))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } } else if (frequency == Frequency.WEEKLY) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { for (WeekDay aDay : dayList.keySet()) { cal.setTime(date); cal.setFirstDayOfWeek(weekStart.getCalendarDay()); cal.set(Calendar.DAY_OF_WEEK, aDay.getCalendarDay()); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!dayList.keySet().contains(WeekDay.getWeekDay(cal.get(Calendar.DAY_OF_WEEK) - 1))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } return candidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_MONTHDAY, MAX_MONTHDAY, true, true); } @Override public int order() { return 8; } } public class HourExpressionPart extends IntegerListRecurrenceExpressionPart { protected static final int MIN_HOUR = 0; protected static final int MAX_HOUR = 23; public HourExpressionPart(String s) throws ParseException { super(s); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); if (frequency == Frequency.YEARLY || frequency == Frequency.MONTHLY || frequency == Frequency.WEEKLY || frequency == Frequency.DAILY) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { cal.setTime(date); for (Integer element : getValueSet()) { cal.set(Calendar.HOUR_OF_DAY, element); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!getValueSet().contains(cal.get(Calendar.HOUR_OF_DAY))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } return candidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_HOUR, MAX_HOUR, false, false); } @Override public int order() { return 9; } } public class MinuteExpressionPart extends IntegerListRecurrenceExpressionPart { protected static final int MIN_MINUTE = 0; protected static final int MAX_MINUTE = 59; public MinuteExpressionPart(String s) throws ParseException { super(s); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); if (frequency == Frequency.YEARLY || frequency == Frequency.MONTHLY || frequency == Frequency.WEEKLY || frequency == Frequency.DAILY || frequency == Frequency.HOURLY) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { cal.setTime(date); for (Integer element : getValueSet()) { cal.set(Calendar.MINUTE, element); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!getValueSet().contains(cal.get(Calendar.MINUTE))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } return candidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_MINUTE, MAX_MINUTE, false, true); } @Override public int order() { return 10; } } public class SecondExpressionPart extends IntegerListRecurrenceExpressionPart { protected static final int MIN_SECOND = 0; protected static final int MAX_SECOND = 59; public SecondExpressionPart(String s) throws ParseException { super(s); } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); if (frequency == Frequency.YEARLY || frequency == Frequency.MONTHLY || frequency == Frequency.WEEKLY || frequency == Frequency.DAILY || frequency == Frequency.HOURLY || frequency == Frequency.MINUTELY) { ArrayList<Date> newCandidates = new ArrayList<Date>(); ArrayList<Date> oldCandidates = candidates; final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date date : candidates) { cal.setTime(date); for (Integer element : getValueSet()) { cal.set(Calendar.SECOND, element); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } else { List<Date> pruneCandidates = new ArrayList<Date>(); final Calendar cal = Calendar.getInstance(getTimeZone()); for (Date aDate : candidates) { cal.setTime(aDate); if (!getValueSet().contains(cal.get(Calendar.SECOND))) { pruneCandidates.add(aDate); } } candidates.removeAll(pruneCandidates); } return candidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_SECOND, MAX_SECOND, false, false); } @Override public int order() { return 11; } } public class PositionExpressionPart extends IntegerListRecurrenceExpressionPart { private static final int MIN_SETPOS = 1; private static final int MAX_SETPOS = 366; public PositionExpressionPart(String s) throws ParseException { super(s); } @SuppressWarnings("null") @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { ArrayList<Date> selectCandidates = new ArrayList<Date>(); FrequencyExpressionPart freqpart = (FrequencyExpressionPart) getExpressionPart( FrequencyExpressionPart.class); Frequency frequency = freqpart.getFrequency(); Collections.sort(candidates); ArrayList<ArrayList<Date>> segments = new ArrayList<ArrayList<Date>>(); ArrayList<Date> segment = null; Calendar segmentStart = null; boolean segmentEnded = false; for (Date aDate : candidates) { Calendar cal = Calendar.getInstance(); cal.setTime(aDate); if (segmentStart == null || segmentEnded) { if (segmentEnded) { segmentEnded = false; segments.add(segment); segment = new ArrayList<Date>(); segment.add(segmentStart.getTime()); } else { segmentStart = cal; segment = new ArrayList<Date>(); segment.add(aDate); } } else { switch (frequency) { case DAILY: if (segmentStart.get(Calendar.DAY_OF_WEEK) == cal.get(Calendar.DAY_OF_WEEK)) { segment.add(aDate); } else { segmentEnded = true; segmentStart = cal; } break; case HOURLY: if (segmentStart.get(Calendar.HOUR) == cal.get(Calendar.HOUR)) { segment.add(aDate); } else { segmentEnded = true; segmentStart = cal; } break; case MINUTELY: if (segmentStart.get(Calendar.MINUTE) == cal.get(Calendar.MINUTE)) { segment.add(aDate); } else { segmentEnded = true; segmentStart = cal; } break; case MONTHLY: if (segmentStart.get(Calendar.MONTH) == cal.get(Calendar.MONTH)) { segment.add(aDate); } else { segmentEnded = true; segmentStart = cal; } break; case SECONDLY: if (segmentStart.get(Calendar.SECOND) == cal.get(Calendar.SECOND)) { segment.add(aDate); } else { segmentEnded = true; segmentStart = cal; } break; case WEEKLY: if (segmentStart.get(Calendar.WEEK_OF_YEAR) == cal.get(Calendar.WEEK_OF_YEAR)) { segment.add(aDate); } else { segmentEnded = true; segmentStart = cal; } break; case YEARLY: if (segmentStart.get(Calendar.YEAR) == cal.get(Calendar.YEAR)) { segment.add(aDate); } else { segmentEnded = true; segmentStart = cal; } break; default: break; } } } segments.add(segment); for (ArrayList<Date> sublist : segments) { for (Integer position : getValueSet()) { if (position > 0 && position <= sublist.size()) { selectCandidates.add(sublist.get(position - 1)); } else if (position < 0 && position >= -sublist.size()) { selectCandidates.add(sublist.get(sublist.size() + position)); } } } return selectCandidates; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_SETPOS, MAX_SETPOS, true, true); } @Override public int order() { return 12; } } public class CountExpressionPart extends RecurrenceExpressionPart { public CountExpressionPart(String s) throws ParseException { super(s); } int count; public int getCount() { return count; } @Override public void parse() throws ParseException { try { count = Integer.parseInt(getPart()); if (count <= 0) { throw new IllegalArgumentException("Count must be a postive integer"); } } catch (NumberFormatException pe) { throw new ParseException("Invalid integer value for COUNT : " + getPart(), 0); } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { ArrayList<Date> countedCandidates = new ArrayList<Date>(); Collections.sort(candidates); int maxToCount = Math.min(candidates.size(), count); for (int i = 0; i < maxToCount; i++) { countedCandidates.add(candidates.get(i)); } return countedCandidates; } @Override BoundedIntegerSet initializeValueSet() { return null; } @Override public int order() { return 13; } } public class UntilExpressionPart extends RecurrenceExpressionPart { private static final String LOCALTIME_FORMAT = "yyyyMMdd'T'HHmmss"; private static final String UTCTIME_FORMAT = "yyyyMMdd'T'HHmmss'Z'"; private static final String DEFAULT_FORMAT = "yyyyMMdd"; public UntilExpressionPart(String s) throws ParseException { super(s); } Date until; public Date getUntil() { return until; } @Override public void parse() throws ParseException { DateFormat format = null; if (getPart().contains("T")) { if (getPart().contains("Z")) { format = new SimpleDateFormat(UTCTIME_FORMAT); format.setLenient(false); format.setTimeZone(TimeZone.getTimeZone("GMT")); } else { format = new SimpleDateFormat(LOCALTIME_FORMAT); format.setLenient(false); format.setTimeZone(getTimeZone()); } } else { format = new SimpleDateFormat(DEFAULT_FORMAT); format.setLenient(false); format.setTimeZone(getTimeZone()); } try { until = format.parse(getPart()); } catch (Exception e) { throw new ParseException("Invalid date format for UNTIL : " + getPart(), 0); } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { ArrayList<Date> filtered = new ArrayList<Date>(); Collections.sort(candidates); for (Date aDate : candidates) { if (aDate.before(until) || aDate.equals(until)) { filtered.add(aDate); } } return filtered; } @Override BoundedIntegerSet initializeValueSet() { return null; } @Override public int order() { return 13; } } @Override public boolean hasFloatingStartDate() { return false; } }