org.eclipse.smarthome.core.scheduler.RecurrenceExpression.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.smarthome.core.scheduler.RecurrenceExpression.java

Source

/**
 * 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;
    }

}