    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005//     http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.ioc.internal.services.cron;
    014
    015import java.io.Serializable;
    016import java.text.ParseException;
    017import java.util.*;
    018
    019/**
    020 * Provides a parser and evaluator for unix-like cron expressions. Cron
    021 * expressions provide the ability to specify complex time combinations such as
    022 * &quot;At 8:00am every Monday through Friday&quot; or &quot;At 1:30am every
    023 * last Friday of the month&quot;.
    024 *
    025 * Cron expressions are comprised of 6 required fields and one optional field
    026 * separated by white space. The fields respectively are described as follows:
    027 *
    028 * <table cellspacing="8">
    029 * <tr>
    030 * <th align="left">Field Name</th>
    031 * <th align="left">&nbsp;</th>
    032 * <th align="left">Allowed Values</th>
    033 * <th align="left">Allowed Special Characters</th>
    034 * </tr>
    035 * <tr>
    036 * <td align="left"><code>Seconds</code></td>
    037 * <td align="left">&nbsp;</td>
    038 * <td align="left"><code>0-59</code></td>
    039 * <td align="left"><code>, - * /</code></td>
    040 * </tr>
    041 * <tr>
    042 * <td align="left"><code>Minutes</code></td>
    043 * <td align="left">&nbsp;</td>
    044 * <td align="left"><code>0-59</code></td>
    045 * <td align="left"><code>, - * /</code></td>
    046 * </tr>
    047 * <tr>
    048 * <td align="left"><code>Hours</code></td>
    049 * <td align="left">&nbsp;</td>
    050 * <td align="left"><code>0-23</code></td>
    051 * <td align="left"><code>, - * /</code></td>
    052 * </tr>
    053 * <tr>
    054 * <td align="left"><code>Day-of-month</code></td>
    055 * <td align="left">&nbsp;</td>
    056 * <td align="left"><code>1-31</code></td>
    057 * <td align="left"><code>, - * ? / L W</code></td>
    058 * </tr>
    059 * <tr>
    060 * <td align="left"><code>Month</code></td>
    061 * <td align="left">&nbsp;</td>
    062 * <td align="left"><code>1-12 or JAN-DEC</code></td>
    063 * <td align="left"><code>, - * /</code></td>
    064 * </tr>
    065 * <tr>
    066 * <td align="left"><code>Day-of-Week</code></td>
    067 * <td align="left">&nbsp;</td>
    068 * <td align="left"><code>1-7 or SUN-SAT</code></td>
    069 * <td align="left"><code>, - * ? / L #</code></td>
    070 * </tr>
    071 * <tr>
    072 * <td align="left"><code>Year (Optional)</code></td>
    073 * <td align="left">&nbsp;</td>
    074 * <td align="left"><code>empty, 1970-2199</code></td>
    075 * <td align="left"><code>, - * /</code></td>
    076 * </tr>
    077 * <caption>Cron Expressions</caption>
    078 * </table>
    079 *
    080 * The '*' character is used to specify all values. For example, &quot;*&quot;
    081 * in the minute field means &quot;every minute&quot;.
    082 *
    083 * The '?' character is allowed for the day-of-month and day-of-week fields. It
    084 * is used to specify 'no specific value'. This is useful when you need to
    085 * specify something in one of the two fields, but not the other.
    086 *
    087 * The '-' character is used to specify ranges For example &quot;10-12&quot; in
    088 * the hour field means &quot;the hours 10, 11 and 12&quot;.
    089 *
    090 * The ',' character is used to specify additional values. For example
    091 * &quot;MON,WED,FRI&quot; in the day-of-week field means &quot;the days Monday,
    092 * Wednesday, and Friday&quot;.
    093 *
    094 * The '/' character is used to specify increments. For example &quot;0/15&quot;
    095 * in the seconds field means &quot;the seconds 0, 15, 30, and 45&quot;. And
    096 * &quot;5/15&quot; in the seconds field means &quot;the seconds 5, 20, 35, and
    097 * 50&quot;.  Specifying '*' before the  '/' is equivalent to specifying 0 is
    098 * the value to start with. Essentially, for each field in the expression, there
    099 * is a set of numbers that can be turned on or off. For seconds and minutes,
    100 * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
    101 * 31, and for months 1 to 12. The &quot;/&quot; character simply helps you turn
    102 * on every &quot;nth&quot; value in the given set. Thus &quot;7/6&quot; in the
    103 * month field only turns on month &quot;7&quot;, it does NOT mean every 6th
    104 * month, please note that subtlety.
    105 *
    106 * The 'L' character is allowed for the day-of-month and day-of-week fields.
    107 * This character is short-hand for &quot;last&quot;, but it has different
    108 * meaning in each of the two fields. For example, the value &quot;L&quot; in
    109 * the day-of-month field means &quot;the last day of the month&quot; - day 31
    110 * for January, day 28 for February on non-leap years. If used in the
    111 * day-of-week field by itself, it simply means &quot;7&quot; or
    112 * &quot;SAT&quot;. But if used in the day-of-week field after another value, it
    113 * means &quot;the last xxx day of the month&quot; - for example &quot;6L&quot;
    114 * means &quot;the last friday of the month&quot;. You can also specify an offset
    115 * from the last day of the month, such as "L-3" which would mean the third-to-last
    116 * day of the calendar month. <i>When using the 'L' option, it is important not to
    117 * specify lists, or ranges of values, as you'll get confusing/unexpected results.</i>
    118 *
    119 * The 'W' character is allowed for the day-of-month field.  This character
    120 * is used to specify the weekday (Monday-Friday) nearest the given day.  As an
    121 * example, if you were to specify &quot;15W&quot; as the value for the
    122 * day-of-month field, the meaning is: &quot;the nearest weekday to the 15th of
    123 * the month&quot;. So if the 15th is a Saturday, the trigger will fire on
    124 * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
    125 * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
    126 * However if you specify &quot;1W&quot; as the value for day-of-month, and the
    127 * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
    128 * 'jump' over the boundary of a month's days.  The 'W' character can only be
    129 * specified when the day-of-month is a single day, not a range or list of days.
    130 *
    131 * The 'L' and 'W' characters can also be combined for the day-of-month
    132 * expression to yield 'LW', which translates to &quot;last weekday of the
    133 * month&quot;.
    134 *
    135 * The '#' character is allowed for the day-of-week field. This character is
    136 * used to specify &quot;the nth&quot; XXX day of the month. For example, the
    137 * value of &quot;6#3&quot; in the day-of-week field means the third Friday of
    138 * the month (day 6 = Friday and &quot;#3&quot; = the 3rd one in the month).
    139 * Other examples: &quot;2#1&quot; = the first Monday of the month and
    140 * &quot;4#5&quot; = the fifth Wednesday of the month. Note that if you specify
    141 * &quot;#5&quot; and there is not 5 of the given day-of-week in the month, then
    142 * no firing will occur that month.  If the '#' character is used, there can
    143 * only be one expression in the day-of-week field (&quot;3#1,6#3&quot; is
    144 * not valid, since there are two expressions).
    145 *
    146 * <!--The 'C' character is allowed for the day-of-month and day-of-week fields.
    147 * This character is short-hand for "calendar". This means values are
    148 * calculated against the associated calendar, if any. If no calendar is
    149 * associated, then it is equivalent to having an all-inclusive calendar. A
    150 * value of "5C" in the day-of-month field means "the first day included by the
    151 * calendar on or after the 5th". A value of "1C" in the day-of-week field
    152 * means "the first day included by the calendar on or after Sunday".-->
    153 *
    154 * The legal characters and the names of months and days of the week are not
    155 * case sensitive.
    156 *
    157 *
    158 * <b>NOTES:</b>
    159 * <ul>
    160 * <li>Support for specifying both a day-of-week and a day-of-month value is
    161 * not complete (you'll need to use the '?' character in one of these fields).
    162 * </li>
    163 * <li>Overflowing ranges is supported - that is, having a larger number on
    164 * the left hand side than the right. You might do 22-2 to catch 10 o'clock
    165 * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
    166 * very important to note that overuse of overflowing ranges creates ranges
    167 * that don't make sense and no effort has been made to determine which
    168 * interpretation CronExpression chooses. An example would be
    169 * "0 0 14-6 ? * FRI-MON". </li>
    170 * </ul>
    171 *
    172 *
    173 * @author Sharada Jambula, James House
    174 * @author Contributions from Mads Henderson
    175 * @author Refactoring from CronTrigger to CronExpression by Aaron Craven
    176 */
    177public class CronExpression implements Serializable
    178{
    179
    180    private static final long serialVersionUID = 12423409423L;
    181
    182    protected static final int SECOND = 0;
    183    protected static final int MINUTE = 1;
    184    protected static final int HOUR = 2;
    185    protected static final int DAY_OF_MONTH = 3;
    186    protected static final int MONTH = 4;
    187    protected static final int DAY_OF_WEEK = 5;
    188    protected static final int YEAR = 6;
    189    protected static final int ALL_SPEC_INT = 99; // '*'
    190    protected static final int NO_SPEC_INT = 98; // '?'
    191    protected static final Integer ALL_SPEC = Integer.valueOf(ALL_SPEC_INT);
    192    protected static final Integer NO_SPEC = Integer.valueOf(NO_SPEC_INT);
    193
    194    protected static final Map monthMap = new HashMap(20);
    195    protected static final Map dayMap = new HashMap(60);
    196
    197    static
    198    {
    199        monthMap.put("JAN", Integer.valueOf(0));
    200        monthMap.put("FEB", Integer.valueOf(1));
    201        monthMap.put("MAR", Integer.valueOf(2));
    202        monthMap.put("APR", Integer.valueOf(3));
    203        monthMap.put("MAY", Integer.valueOf(4));
    204        monthMap.put("JUN", Integer.valueOf(5));
    205        monthMap.put("JUL", Integer.valueOf(6));
    206        monthMap.put("AUG", Integer.valueOf(7));
    207        monthMap.put("SEP", Integer.valueOf(8));
    208        monthMap.put("OCT", Integer.valueOf(9));
    209        monthMap.put("NOV", Integer.valueOf(10));
    210        monthMap.put("DEC", Integer.valueOf(11));
    211
    212        dayMap.put("SUN", Integer.valueOf(1));
    213        dayMap.put("MON", Integer.valueOf(2));
    214        dayMap.put("TUE", Integer.valueOf(3));
    215        dayMap.put("WED", Integer.valueOf(4));
    216        dayMap.put("THU", Integer.valueOf(5));
    217        dayMap.put("FRI", Integer.valueOf(6));
    218        dayMap.put("SAT", Integer.valueOf(7));
    219    }
    220
    221    private String cronExpression = null;
    222    private TimeZone timeZone = null;
    223    protected transient TreeSet<Integer> seconds;
    224    protected transient TreeSet<Integer> minutes;
    225    protected transient TreeSet<Integer> hours;
    226    protected transient TreeSet<Integer> daysOfMonth;
    227    protected transient TreeSet<Integer> months;
    228    protected transient TreeSet<Integer> daysOfWeek;
    229    protected transient TreeSet<Integer> years;
    230
    231    protected transient boolean lastdayOfWeek = false;
    232    protected transient int nthdayOfWeek = 0;
    233    protected transient boolean lastdayOfMonth = false;
    234    protected transient boolean nearestWeekday = false;
    235    protected transient int lastdayOffset = 0;
    236    protected transient boolean expressionParsed = false;
    237
    238    public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100;
    239
    240    /**
    241     * Constructs a new <CODE>CronExpression</CODE> based on the specified
    242     * parameter.
    243     *
    244     * @param cronExpression
    245     *         String representation of the cron expression the
    246     *         new object should represent
    247     * @throws java.text.ParseException
    248     *         if the string expression cannot be parsed into a valid
    249     *         <CODE>CronExpression</CODE>
    250     */
    251    public CronExpression(String cronExpression) throws ParseException
    252    {
    253        if (cronExpression == null)
    254        {
    255            throw new IllegalArgumentException("cronExpression cannot be null");
    256        }
    257
    258        this.cronExpression = cronExpression.toUpperCase(Locale.US);
    259
    260        buildExpression(this.cronExpression);
    261    }
    262
    263    /**
    264     * Indicates whether the given date satisfies the cron expression. Note that
    265     * milliseconds are ignored, so two Dates falling on different milliseconds
    266     * of the same second will always have the same result here.
    267     *
    268     * @param date
    269     *         the date to evaluate
    270     * @return a boolean indicating whether the given date satisfies the cron
    271     *         expression
    272     */
    273    public boolean isSatisfiedBy(Date date)
    274    {
    275        Calendar testDateCal = Calendar.getInstance(getTimeZone());
    276        testDateCal.setTime(date);
    277        testDateCal.set(Calendar.MILLISECOND, 0);
    278        Date originalDate = testDateCal.getTime();
    279
    280        testDateCal.add(Calendar.SECOND, -1);
    281
    282        Date timeAfter = getTimeAfter(testDateCal.getTime());
    283
    284        return ((timeAfter != null) && (timeAfter.equals(originalDate)));
    285    }
    286
    287    /**
    288     * Returns the next date/time <I>after</I> the given date/time which
    289     * satisfies the cron expression.
    290     *
    291     * @param date
    292     *         the date/time at which to begin the search for the next valid
    293     *         date/time
    294     * @return the next valid date/time
    295     */
    296    public Date getNextValidTimeAfter(Date date)
    297    {
    298        return getTimeAfter(date);
    299    }
    300
    301    /**
    302     * Returns the next date/time <I>after</I> the given date/time which does
    303     * <I>not</I> satisfy the expression
    304     *
    305     * @param date
    306     *         the date/time at which to begin the search for the next
    307     *         invalid date/time
    308     * @return the next valid date/time
    309     */
    310    public Date getNextInvalidTimeAfter(Date date)
    311    {
    312        long difference = 1000;
    313
    314        //move back to the nearest second so differences will be accurate
    315        Calendar adjustCal = Calendar.getInstance(getTimeZone());
    316        adjustCal.setTime(date);
    317        adjustCal.set(Calendar.MILLISECOND, 0);
    318        Date lastDate = adjustCal.getTime();
    319
    320        Date newDate = null;
    321
    322        //TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution.
    323
    324        //keep getting the next included time until it's farther than one second
    325        // apart. At that point, lastDate is the last valid fire time. We return
    326        // the second immediately following it.
    327        while (difference == 1000)
    328        {
    329            newDate = getTimeAfter(lastDate);
    330            if (newDate == null)
    331                break;
    332
    333            difference = newDate.getTime() - lastDate.getTime();
    334
    335            if (difference == 1000)
    336            {
    337                lastDate = newDate;
    338            }
    339        }
    340
    341        return new Date(lastDate.getTime() + 1000);
    342    }
    343
    344    /**
    345     * Returns the time zone for which this <code>CronExpression</code>
    346     * will be resolved.
    347     */
    348    public TimeZone getTimeZone()
    349    {
    350        if (timeZone == null)
    351        {
    352            timeZone = TimeZone.getDefault();
    353        }
    354
    355        return timeZone;
    356    }
    357
    358    /**
    359     * Sets the time zone for which  this <code>CronExpression</code>
    360     * will be resolved.
    361     */
    362    public void setTimeZone(TimeZone timeZone)
    363    {
    364        this.timeZone = timeZone;
    365    }
    366
    367    /**
    368     * Returns the string representation of the <CODE>CronExpression</CODE>
    369     *
    370     * @return a string representation of the <CODE>CronExpression</CODE>
    371     */
    372    @Override
    373    public String toString()
    374    {
    375        return cronExpression;
    376    }
    377
    378    /**
    379     * Indicates whether the specified cron expression can be parsed into a
    380     * valid cron expression
    381     *
    382     * @param cronExpression
    383     *         the expression to evaluate
    384     * @return a boolean indicating whether the given expression is a valid cron
    385     *         expression
    386     */
    387    public static boolean isValidExpression(String cronExpression)
    388    {
    389
    390        try
    391        {
    392            new CronExpression(cronExpression);
    393        } catch (ParseException pe)
    394        {
    395            return false;
    396        }
    397
    398        return true;
    399    }
    400
    401    public static void validateExpression(String cronExpression) throws ParseException
    402    {
    403
    404        new CronExpression(cronExpression);
    405    }
    406
    407
    408    ////////////////////////////////////////////////////////////////////////////
    409    //
    410    // Expression Parsing Functions
    411    //
    412    ////////////////////////////////////////////////////////////////////////////
    413
    414    protected void buildExpression(String expression) throws ParseException
    415    {
    416        expressionParsed = true;
    417
    418        try
    419        {
    420
    421            if (seconds == null)
    422            {
    423                seconds = new TreeSet<Integer>();
    424            }
    425            if (minutes == null)
    426            {
    427                minutes = new TreeSet<Integer>();
    428            }
    429            if (hours == null)
    430            {
    431                hours = new TreeSet<Integer>();
    432            }
    433            if (daysOfMonth == null)
    434            {
    435                daysOfMonth = new TreeSet<Integer>();
    436            }
    437            if (months == null)
    438            {
    439                months = new TreeSet<Integer>();
    440            }
    441            if (daysOfWeek == null)
    442            {
    443                daysOfWeek = new TreeSet<Integer>();
    444            }
    445            if (years == null)
    446            {
    447                years = new TreeSet<Integer>();
    448            }
    449
    450            int exprOn = SECOND;
    451
    452            StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
    453                    false);
    454
    455            while (exprsTok.hasMoreTokens() && exprOn <= YEAR)
    456            {
    457                String expr = exprsTok.nextToken().trim();
    458
    459                // throw an exception if L is used with other days of the month
    460                if (exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.indexOf(",") >= 0)
    461                {
    462                    throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1);
    463                }
    464                // throw an exception if L is used with other days of the week
    465                if (exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.indexOf(",") >= 0)
    466                {
    467                    throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);
    468                }
    469                if (exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') + 1) != -1)
    470                {
    471                    throw new ParseException("Support for specifying multiple \"nth\" days is not imlemented.", -1);
    472                }
    473
    474                StringTokenizer vTok = new StringTokenizer(expr, ",");
    475                while (vTok.hasMoreTokens())
    476                {
    477                    String v = vTok.nextToken();
    478                    storeExpressionVals(0, v, exprOn);
    479                }
    480
    481                exprOn++;
    482            }
    483
    484            if (exprOn <= DAY_OF_WEEK)
    485            {
    486                throw new ParseException("Unexpected end of expression.",
    487                        expression.length());
    488            }
    489
    490            if (exprOn <= YEAR)
    491            {
    492                storeExpressionVals(0, "*", YEAR);
    493            }
    494
    495            TreeSet dow = getSet(DAY_OF_WEEK);
    496            TreeSet dom = getSet(DAY_OF_MONTH);
    497
    498            // Copying the logic from the UnsupportedOperationException below
    499            boolean dayOfMSpec = !dom.contains(NO_SPEC);
    500            boolean dayOfWSpec = !dow.contains(NO_SPEC);
    501
    502            if (dayOfMSpec && !dayOfWSpec)
    503            {
    504                // skip
    505            } else if (dayOfWSpec && !dayOfMSpec)
    506            {
    507                // skip
    508            } else
    509            {
    510                throw new ParseException(
    511                        "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0);
    512            }
    513        } catch (ParseException pe)
    514        {
    515            throw pe;
    516        } catch (Exception e)
    517        {
    518            throw new ParseException("Illegal cron expression format ("
    519                    + e.toString() + ")", 0);
    520        }
    521    }
    522
    523    protected int storeExpressionVals(int pos, String s, int type)
    524            throws ParseException
    525    {
    526
    527        int incr = 0;
    528        int i = skipWhiteSpace(pos, s);
    529        if (i >= s.length())
    530        {
    531            return i;
    532        }
    533        char c = s.charAt(i);
    534        if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?")))
    535        {
    536            String sub = s.substring(i, i + 3);
    537            int sval = -1;
    538            int eval = -1;
    539            if (type == MONTH)
    540            {
    541                sval = getMonthNumber(sub) + 1;
    542                if (sval <= 0)
    543                {
    544                    throw new ParseException("Invalid Month value: '" + sub + "'", i);
    545                }
    546                if (s.length() > i + 3)
    547                {
    548                    c = s.charAt(i + 3);
    549                    if (c == '-')
    550                    {
    551                        i += 4;
    552                        sub = s.substring(i, i + 3);
    553                        eval = getMonthNumber(sub) + 1;
    554                        if (eval <= 0)
    555                        {
    556                            throw new ParseException("Invalid Month value: '" + sub + "'", i);
    557                        }
    558                    }
    559                }
    560            } else if (type == DAY_OF_WEEK)
    561            {
    562                sval = getDayOfWeekNumber(sub);
    563                if (sval < 0)
    564                {
    565                    throw new ParseException("Invalid Day-of-Week value: '"
    566                            + sub + "'", i);
    567                }
    568                if (s.length() > i + 3)
    569                {
    570                    c = s.charAt(i + 3);
    571                    if (c == '-')
    572                    {
    573                        i += 4;
    574                        sub = s.substring(i, i + 3);
    575                        eval = getDayOfWeekNumber(sub);
    576                        if (eval < 0)
    577                        {
    578                            throw new ParseException(
    579                                    "Invalid Day-of-Week value: '" + sub
    580                                            + "'", i);
    581                        }
    582                    } else if (c == '#')
    583                    {
    584                        try
    585                        {
    586                            i += 4;
    587                            nthdayOfWeek = Integer.parseInt(s.substring(i));
    588                            if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
    589                            {
    590                                throw new Exception();
    591                            }
    592                        } catch (Exception e)
    593                        {
    594                            throw new ParseException(
    595                                    "A numeric value between 1 and 5 must follow the '#' option",
    596                                    i);
    597                        }
    598                    } else if (c == 'L')
    599                    {
    600                        lastdayOfWeek = true;
    601                        i++;
    602                    }
    603                }
    604
    605            } else
    606            {
    607                throw new ParseException(
    608                        "Illegal characters for this position: '" + sub + "'",
    609                        i);
    610            }
    611            if (eval != -1)
    612            {
    613                incr = 1;
    614            }
    615            addToSet(sval, eval, incr, type);
    616            return (i + 3);
    617        }
    618
    619        if (c == '?')
    620        {
    621            i++;
    622            if ((i + 1) < s.length()
    623                    && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t'))
    624            {
    625                throw new ParseException("Illegal character after '?': "
    626                        + s.charAt(i), i);
    627            }
    628            if (type != DAY_OF_WEEK && type != DAY_OF_MONTH)
    629            {
    630                throw new ParseException(
    631                        "'?' can only be specfied for Day-of-Month or Day-of-Week.",
    632                        i);
    633            }
    634            if (type == DAY_OF_WEEK && !lastdayOfMonth)
    635            {
    636                int val = ((Integer) daysOfMonth.last()).intValue();
    637                if (val == NO_SPEC_INT)
    638                {
    639                    throw new ParseException(
    640                            "'?' can only be specfied for Day-of-Month -OR- Day-of-Week.",
    641                            i);
    642                }
    643            }
    644
    645            addToSet(NO_SPEC_INT, -1, 0, type);
    646            return i;
    647        }
    648
    649        if (c == '*' || c == '/')
    650        {
    651            if (c == '*' && (i + 1) >= s.length())
    652            {
    653                addToSet(ALL_SPEC_INT, -1, incr, type);
    654                return i + 1;
    655            } else if (c == '/'
    656                    && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
    657                    .charAt(i + 1) == '\t'))
    658            {
    659                throw new ParseException("'/' must be followed by an integer.", i);
    660            } else if (c == '*')
    661            {
    662                i++;
    663            }
    664            c = s.charAt(i);
    665            if (c == '/')
    666            { // is an increment specified?
    667                i++;
    668                if (i >= s.length())
    669                {
    670                    throw new ParseException("Unexpected end of string.", i);
    671                }
    672
    673                incr = getNumericValue(s, i);
    674
    675                i++;
    676                if (incr > 10)
    677                {
    678                    i++;
    679                }
    680                if (incr > 59 && (type == SECOND || type == MINUTE))
    681                {
    682                    throw new ParseException("Increment > 60 : " + incr, i);
    683                } else if (incr > 23 && (type == HOUR))
    684                {
    685                    throw new ParseException("Increment > 24 : " + incr, i);
    686                } else if (incr > 31 && (type == DAY_OF_MONTH))
    687                {
    688                    throw new ParseException("Increment > 31 : " + incr, i);
    689                } else if (incr > 7 && (type == DAY_OF_WEEK))
    690                {
    691                    throw new ParseException("Increment > 7 : " + incr, i);
    692                } else if (incr > 12 && (type == MONTH))
    693                {
    694                    throw new ParseException("Increment > 12 : " + incr, i);
    695                }
    696            } else
    697            {
    698                incr = 1;
    699            }
    700
    701            addToSet(ALL_SPEC_INT, -1, incr, type);
    702            return i;
    703        } else if (c == 'L')
    704        {
    705            i++;
    706            if (type == DAY_OF_MONTH)
    707            {
    708                lastdayOfMonth = true;
    709            }
    710            if (type == DAY_OF_WEEK)
    711            {
    712                addToSet(7, 7, 0, type);
    713            }
    714            if (type == DAY_OF_MONTH && s.length() > i)
    715            {
    716                c = s.charAt(i);
    717                if (c == '-')
    718                {
    719                    ValueSet vs = getValue(0, s, i + 1);
    720                    lastdayOffset = vs.value;
    721                    if (lastdayOffset > 30)
    722                        throw new ParseException("Offset from last day must be <= 30", i + 1);
    723                    i = vs.pos;
    724                }
    725                if (s.length() > i)
    726                {
    727                    c = s.charAt(i);
    728                    if (c == 'W')
    729                    {
    730                        nearestWeekday = true;
    731                        i++;
    732                    }
    733                }
    734            }
    735            return i;
    736        } else if (c >= '0' && c <= '9')
    737        {
    738            int val = Integer.parseInt(String.valueOf(c));
    739            i++;
    740            if (i >= s.length())
    741            {
    742                addToSet(val, -1, -1, type);
    743            } else
    744            {
    745                c = s.charAt(i);
    746                if (c >= '0' && c <= '9')
    747                {
    748                    ValueSet vs = getValue(val, s, i);
    749                    val = vs.value;
    750                    i = vs.pos;
    751                }
    752                i = checkNext(i, s, val, type);
    753                return i;
    754            }
    755        } else
    756        {
    757            throw new ParseException("Unexpected character: " + c, i);
    758        }
    759
    760        return i;
    761    }
    762
    763    protected int checkNext(int pos, String s, int val, int type)
    764            throws ParseException
    765    {
    766
    767        int end = -1;
    768        int i = pos;
    769
    770        if (i >= s.length())
    771        {
    772            addToSet(val, end, -1, type);
    773            return i;
    774        }
    775
    776        char c = s.charAt(pos);
    777
    778        if (c == 'L')
    779        {
    780            if (type == DAY_OF_WEEK)
    781            {
    782                if (val < 1 || val > 7)
    783                    throw new ParseException("Day-of-Week values must be between 1 and 7", -1);
    784                lastdayOfWeek = true;
    785            } else
    786            {
    787                throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);
    788            }
    789            TreeSet set = getSet(type);
    790            set.add(Integer.valueOf(val));
    791            i++;
    792            return i;
    793        }
    794
    795        if (c == 'W')
    796        {
    797            if (type == DAY_OF_MONTH)
    798            {
    799                nearestWeekday = true;
    800            } else
    801            {
    802                throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i);
    803            }
    804            if (val > 31)
    805                throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i);
    806            TreeSet set = getSet(type);
    807            set.add(Integer.valueOf(val));
    808            i++;
    809            return i;
    810        }
    811
    812        if (c == '#')
    813        {
    814            if (type != DAY_OF_WEEK)
    815            {
    816                throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i);
    817            }
    818            i++;
    819            try
    820            {
    821                nthdayOfWeek = Integer.parseInt(s.substring(i));
    822                if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
    823                {
    824                    throw new Exception();
    825                }
    826            } catch (Exception e)
    827            {
    828                throw new ParseException(
    829                        "A numeric value between 1 and 5 must follow the '#' option",
    830                        i);
    831            }
    832
    833            TreeSet set = getSet(type);
    834            set.add(Integer.valueOf(val));
    835            i++;
    836            return i;
    837        }
    838
    839        if (c == '-')
    840        {
    841            i++;
    842            c = s.charAt(i);
    843            int v = Integer.parseInt(String.valueOf(c));
    844            end = v;
    845            i++;
    846            if (i >= s.length())
    847            {
    848                addToSet(val, end, 1, type);
    849                return i;
    850            }
    851            c = s.charAt(i);
    852            if (c >= '0' && c <= '9')
    853            {
    854                ValueSet vs = getValue(v, s, i);
    855                int v1 = vs.value;
    856                end = v1;
    857                i = vs.pos;
    858            }
    859            if (i < s.length() && ((c = s.charAt(i)) == '/'))
    860            {
    861                i++;
    862                c = s.charAt(i);
    863                int v2 = Integer.parseInt(String.valueOf(c));
    864                i++;
    865                if (i >= s.length())
    866                {
    867                    addToSet(val, end, v2, type);
    868                    return i;
    869                }
    870                c = s.charAt(i);
    871                if (c >= '0' && c <= '9')
    872                {
    873                    ValueSet vs = getValue(v2, s, i);
    874                    int v3 = vs.value;
    875                    addToSet(val, end, v3, type);
    876                    i = vs.pos;
    877                    return i;
    878                } else
    879                {
    880                    addToSet(val, end, v2, type);
    881                    return i;
    882                }
    883            } else
    884            {
    885                addToSet(val, end, 1, type);
    886                return i;
    887            }
    888        }
    889
    890        if (c == '/')
    891        {
    892            i++;
    893            c = s.charAt(i);
    894            int v2 = Integer.parseInt(String.valueOf(c));
    895            i++;
    896            if (i >= s.length())
    897            {
    898                addToSet(val, end, v2, type);
    899                return i;
    900            }
    901            c = s.charAt(i);
    902            if (c >= '0' && c <= '9')
    903            {
    904                ValueSet vs = getValue(v2, s, i);
    905                int v3 = vs.value;
    906                addToSet(val, end, v3, type);
    907                i = vs.pos;
    908                return i;
    909            } else
    910            {
    911                throw new ParseException("Unexpected character '" + c + "' after '/'", i);
    912            }
    913        }
    914
    915        addToSet(val, end, 0, type);
    916        i++;
    917        return i;
    918    }
    919
    920    public String getCronExpression()
    921    {
    922        return cronExpression;
    923    }
    924
    925    protected int skipWhiteSpace(int i, String s)
    926    {
    927        for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++)
    928        {
    929            ;
    930        }
    931
    932        return i;
    933    }
    934
    935    protected int findNextWhiteSpace(int i, String s)
    936    {
    937        for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++)
    938        {
    939            ;
    940        }
    941
    942        return i;
    943    }
    944
    945    protected void addToSet(int val, int end, int incr, int type)
    946            throws ParseException
    947    {
    948
    949        TreeSet<Integer> set = getSet(type);
    950
    951        if (type == SECOND || type == MINUTE)
    952        {
    953            if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT))
    954            {
    955                throw new ParseException(
    956                        "Minute and Second values must be between 0 and 59",
    957                        -1);
    958            }
    959        } else if (type == HOUR)
    960        {
    961            if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT))
    962            {
    963                throw new ParseException(
    964                        "Hour values must be between 0 and 23", -1);
    965            }
    966        } else if (type == DAY_OF_MONTH)
    967        {
    968            if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
    969                    && (val != NO_SPEC_INT))
    970            {
    971                throw new ParseException(
    972                        "Day of month values must be between 1 and 31", -1);
    973            }
    974        } else if (type == MONTH)
    975        {
    976            if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT))
    977            {
    978                throw new ParseException(
    979                        "Month values must be between 1 and 12", -1);
    980            }
    981        } else if (type == DAY_OF_WEEK)
    982        {
    983            if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT)
    984                    && (val != NO_SPEC_INT))
    985            {
    986                throw new ParseException(
    987                        "Day-of-Week values must be between 1 and 7", -1);
    988            }
    989        }
    990
    991        if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT)
    992        {
    993            if (val != -1)
    994            {
    995                set.add(Integer.valueOf(val));
    996            } else
    997            {
    998                set.add(NO_SPEC);
    999            }
    1000
    1001            return;
    1002        }
    1003
    1004        int startAt = val;
    1005        int stopAt = end;
    1006
    1007        if (val == ALL_SPEC_INT && incr <= 0)
    1008        {
    1009            incr = 1;
    1010            set.add(ALL_SPEC); // put in a marker, but also fill values
    1011        }
    1012
    1013        if (type == SECOND || type == MINUTE)
    1014        {
    1015            if (stopAt == -1)
    1016            {
    1017                stopAt = 59;
    1018            }
    1019            if (startAt == -1 || startAt == ALL_SPEC_INT)
    1020            {
    1021                startAt = 0;
    1022            }
    1023        } else if (type == HOUR)
    1024        {
    1025            if (stopAt == -1)
    1026            {
    1027                stopAt = 23;
    1028            }
    1029            if (startAt == -1 || startAt == ALL_SPEC_INT)
    1030            {
    1031                startAt = 0;
    1032            }
    1033        } else if (type == DAY_OF_MONTH)
    1034        {
    1035            if (stopAt == -1)
    1036            {
    1037                stopAt = 31;
    1038            }
    1039            if (startAt == -1 || startAt == ALL_SPEC_INT)
    1040            {
    1041                startAt = 1;
    1042            }
    1043        } else if (type == MONTH)
    1044        {
    1045            if (stopAt == -1)
    1046            {
    1047                stopAt = 12;
    1048            }
    1049            if (startAt == -1 || startAt == ALL_SPEC_INT)
    1050            {
    1051                startAt = 1;
    1052            }
    1053        } else if (type == DAY_OF_WEEK)
    1054        {
    1055            if (stopAt == -1)
    1056            {
    1057                stopAt = 7;
    1058            }
    1059            if (startAt == -1 || startAt == ALL_SPEC_INT)
    1060            {
    1061                startAt = 1;
    1062            }
    1063        } else if (type == YEAR)
    1064        {
    1065            if (stopAt == -1)
    1066            {
    1067                stopAt = MAX_YEAR;
    1068            }
    1069            if (startAt == -1 || startAt == ALL_SPEC_INT)
    1070            {
    1071                startAt = 1970;
    1072            }
    1073        }
    1074
    1075        // if the end of the range is before the start, then we need to overflow into
    1076        // the next day, month etc. This is done by adding the maximum amount for that
    1077        // type, and using modulus max to determine the value being added.
    1078        int max = -1;
    1079        if (stopAt < startAt)
    1080        {
    1081            switch (type)
    1082            {
    1083                case SECOND:
    1084                    max = 60;
    1085                    break;
    1086                case MINUTE:
    1087                    max = 60;
    1088                    break;
    1089                case HOUR:
    1090                    max = 24;
    1091                    break;
    1092                case MONTH:
    1093                    max = 12;
    1094                    break;
    1095                case DAY_OF_WEEK:
    1096                    max = 7;
    1097                    break;
    1098                case DAY_OF_MONTH:
    1099                    max = 31;
    1100                    break;
    1101                case YEAR:
    1102                    throw new IllegalArgumentException("Start year must be less than stop year");
    1103                default:
    1104                    throw new IllegalArgumentException("Unexpected type encountered");
    1105            }
    1106            stopAt += max;
    1107        }
    1108
    1109        for (int i = startAt; i <= stopAt; i += incr)
    1110        {
    1111            if (max == -1)
    1112            {
    1113                // ie: there's no max to overflow over
    1114                set.add(Integer.valueOf(i));
    1115            } else
    1116            {
    1117                // take the modulus to get the real value
    1118                int i2 = i % max;
    1119
    1120                // 1-indexed ranges should not include 0, and should include their max
    1121                if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH))
    1122                {
    1123                    i2 = max;
    1124                }
    1125
    1126                set.add(Integer.valueOf(i2));
    1127            }
    1128        }
    1129    }
    1130
    1131    protected TreeSet<Integer> getSet(int type)
    1132    {
    1133        switch (type)
    1134        {
    1135            case SECOND:
    1136                return seconds;
    1137            case MINUTE:
    1138                return minutes;
    1139            case HOUR:
    1140                return hours;
    1141            case DAY_OF_MONTH:
    1142                return daysOfMonth;
    1143            case MONTH:
    1144                return months;
    1145            case DAY_OF_WEEK:
    1146                return daysOfWeek;
    1147            case YEAR:
    1148                return years;
    1149            default:
    1150                return null;
    1151        }
    1152    }
    1153
    1154    protected ValueSet getValue(int v, String s, int i)
    1155    {
    1156        char c = s.charAt(i);
    1157        StringBuilder s1 = new StringBuilder(String.valueOf(v));
    1158        while (c >= '0' && c <= '9')
    1159        {
    1160            s1.append(c);
    1161            i++;
    1162            if (i >= s.length())
    1163            {
    1164                break;
    1165            }
    1166            c = s.charAt(i);
    1167        }
    1168        ValueSet val = new ValueSet();
    1169
    1170        val.pos = (i < s.length()) ? i : i + 1;
    1171        val.value = Integer.parseInt(s1.toString());
    1172        return val;
    1173    }
    1174
    1175    protected int getNumericValue(String s, int i)
    1176    {
    1177        int endOfVal = findNextWhiteSpace(i, s);
    1178        String val = s.substring(i, endOfVal);
    1179        return Integer.parseInt(val);
    1180    }
    1181
    1182    protected int getMonthNumber(String s)
    1183    {
    1184        Integer integer = (Integer) monthMap.get(s);
    1185
    1186        if (integer == null)
    1187        {
    1188            return -1;
    1189        }
    1190
    1191        return integer.intValue();
    1192    }
    1193
    1194    protected int getDayOfWeekNumber(String s)
    1195    {
    1196        Integer integer = (Integer) dayMap.get(s);
    1197
    1198        if (integer == null)
    1199        {
    1200            return -1;
    1201        }
    1202
    1203        return integer.intValue();
    1204    }
    1205
    1206    ////////////////////////////////////////////////////////////////////////////
    1207    //
    1208    // Computation Functions
    1209    //
    1210    ////////////////////////////////////////////////////////////////////////////
    1211
    1212    public Date getTimeAfter(Date afterTime)
    1213    {
    1214
    1215        // Computation is based on Gregorian year only.
    1216        Calendar cl = new java.util.GregorianCalendar(getTimeZone());
    1217
    1218        // move ahead one second, since we're computing the time *after* the
    1219        // given time
    1220        afterTime = new Date(afterTime.getTime() + 1000);
    1221        // CronTrigger does not deal with milliseconds
    1222        cl.setTime(afterTime);
    1223        cl.set(Calendar.MILLISECOND, 0);
    1224
    1225        boolean gotOne = false;
    1226        // loop until we've computed the next time, or we've past the endTime
    1227        while (!gotOne)
    1228        {
    1229
    1230            //if (endTime != null && cl.getTime().after(endTime)) return null;
    1231            if (cl.get(Calendar.YEAR) > 2999)
    1232            { // prevent endless loop...
    1233                return null;
    1234            }
    1235
    1236            SortedSet st = null;
    1237            int t = 0;
    1238
    1239            int sec = cl.get(Calendar.SECOND);
    1240            int min = cl.get(Calendar.MINUTE);
    1241
    1242            // get second.................................................
    1243            st = seconds.tailSet(Integer.valueOf(sec));
    1244            if (st != null && st.size() != 0)
    1245            {
    1246                sec = ((Integer) st.first()).intValue();
    1247            } else
    1248            {
    1249                sec = ((Integer) seconds.first()).intValue();
    1250                min++;
    1251                cl.set(Calendar.MINUTE, min);
    1252            }
    1253            cl.set(Calendar.SECOND, sec);
    1254
    1255            min = cl.get(Calendar.MINUTE);
    1256            int hr = cl.get(Calendar.HOUR_OF_DAY);
    1257            t = -1;
    1258
    1259            // get minute.................................................
    1260            st = minutes.tailSet(Integer.valueOf(min));
    1261            if (st != null && st.size() != 0)
    1262            {
    1263                t = min;
    1264                min = ((Integer) st.first()).intValue();
    1265            } else
    1266            {
    1267                min = ((Integer) minutes.first()).intValue();
    1268                hr++;
    1269            }
    1270            if (min != t)
    1271            {
    1272                cl.set(Calendar.SECOND, 0);
    1273                cl.set(Calendar.MINUTE, min);
    1274                setCalendarHour(cl, hr);
    1275                continue;
    1276            }
    1277            cl.set(Calendar.MINUTE, min);
    1278
    1279            hr = cl.get(Calendar.HOUR_OF_DAY);
    1280            int day = cl.get(Calendar.DAY_OF_MONTH);
    1281            t = -1;
    1282
    1283            // get hour...................................................
    1284            st = hours.tailSet(Integer.valueOf(hr));
    1285            if (st != null && st.size() != 0)
    1286            {
    1287                t = hr;
    1288                hr = ((Integer) st.first()).intValue();
    1289            } else
    1290            {
    1291                hr = ((Integer) hours.first()).intValue();
    1292                day++;
    1293            }
    1294            if (hr != t)
    1295            {
    1296                cl.set(Calendar.SECOND, 0);
    1297                cl.set(Calendar.MINUTE, 0);
    1298                cl.set(Calendar.DAY_OF_MONTH, day);
    1299                setCalendarHour(cl, hr);
    1300                continue;
    1301            }
    1302            cl.set(Calendar.HOUR_OF_DAY, hr);
    1303
    1304            day = cl.get(Calendar.DAY_OF_MONTH);
    1305            int mon = cl.get(Calendar.MONTH) + 1;
    1306            // '+ 1' because calendar is 0-based for this field, and we are
    1307            // 1-based
    1308            t = -1;
    1309            int tmon = mon;
    1310
    1311            // get day...................................................
    1312            boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
    1313            boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
    1314            if (dayOfMSpec && !dayOfWSpec)
    1315            { // get day by day of month rule
    1316                st = daysOfMonth.tailSet(Integer.valueOf(day));
    1317                if (lastdayOfMonth)
    1318                {
    1319                    if (!nearestWeekday)
    1320                    {
    1321                        t = day;
    1322                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
    1323                        day -= lastdayOffset;
    1324                    } else
    1325                    {
    1326                        t = day;
    1327                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
    1328                        day -= lastdayOffset;
    1329
    1330                        java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
    1331                        tcal.set(Calendar.SECOND, 0);
    1332                        tcal.set(Calendar.MINUTE, 0);
    1333                        tcal.set(Calendar.HOUR_OF_DAY, 0);
    1334                        tcal.set(Calendar.DAY_OF_MONTH, day);
    1335                        tcal.set(Calendar.MONTH, mon - 1);
    1336                        tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
    1337
    1338                        int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
    1339                        int dow = tcal.get(Calendar.DAY_OF_WEEK);
    1340
    1341                        if (dow == Calendar.SATURDAY && day == 1)
    1342                        {
    1343                            day += 2;
    1344                        } else if (dow == Calendar.SATURDAY)
    1345                        {
    1346                            day -= 1;
    1347                        } else if (dow == Calendar.SUNDAY && day == ldom)
    1348                        {
    1349                            day -= 2;
    1350                        } else if (dow == Calendar.SUNDAY)
    1351                        {
    1352                            day += 1;
    1353                        }
    1354
    1355                        tcal.set(Calendar.SECOND, sec);
    1356                        tcal.set(Calendar.MINUTE, min);
    1357                        tcal.set(Calendar.HOUR_OF_DAY, hr);
    1358                        tcal.set(Calendar.DAY_OF_MONTH, day);
    1359                        tcal.set(Calendar.MONTH, mon - 1);
    1360                        Date nTime = tcal.getTime();
    1361                        if (nTime.before(afterTime))
    1362                        {
    1363                            day = 1;
    1364                            mon++;
    1365                        }
    1366                    }
    1367                } else if (nearestWeekday)
    1368                {
    1369                    t = day;
    1370                    day = ((Integer) daysOfMonth.first()).intValue();
    1371
    1372                    java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
    1373                    tcal.set(Calendar.SECOND, 0);
    1374                    tcal.set(Calendar.MINUTE, 0);
    1375                    tcal.set(Calendar.HOUR_OF_DAY, 0);
    1376                    tcal.set(Calendar.DAY_OF_MONTH, day);
    1377                    tcal.set(Calendar.MONTH, mon - 1);
    1378                    tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
    1379
    1380                    int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
    1381                    int dow = tcal.get(Calendar.DAY_OF_WEEK);
    1382
    1383                    if (dow == Calendar.SATURDAY && day == 1)
    1384                    {
    1385                        day += 2;
    1386                    } else if (dow == Calendar.SATURDAY)
    1387                    {
    1388                        day -= 1;
    1389                    } else if (dow == Calendar.SUNDAY && day == ldom)
    1390                    {
    1391                        day -= 2;
    1392                    } else if (dow == Calendar.SUNDAY)
    1393                    {
    1394                        day += 1;
    1395                    }
    1396
    1397
    1398                    tcal.set(Calendar.SECOND, sec);
    1399                    tcal.set(Calendar.MINUTE, min);
    1400                    tcal.set(Calendar.HOUR_OF_DAY, hr);
    1401                    tcal.set(Calendar.DAY_OF_MONTH, day);
    1402                    tcal.set(Calendar.MONTH, mon - 1);
    1403                    Date nTime = tcal.getTime();
    1404                    if (nTime.before(afterTime))
    1405                    {
    1406                        day = ((Integer) daysOfMonth.first()).intValue();
    1407                        mon++;
    1408                    }
    1409                } else if (st != null && st.size() != 0)
    1410                {
    1411                    t = day;
    1412                    day = ((Integer) st.first()).intValue();
    1413                    // make sure we don't over-run a short month, such as february
    1414                    int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
    1415                    if (day > lastDay)
    1416                    {
    1417                        day = ((Integer) daysOfMonth.first()).intValue();
    1418                        mon++;
    1419                    }
    1420                } else
    1421                {
    1422                    day = ((Integer) daysOfMonth.first()).intValue();
    1423                    mon++;
    1424                }
    1425
    1426                if (day != t || mon != tmon)
    1427                {
    1428                    cl.set(Calendar.SECOND, 0);
    1429                    cl.set(Calendar.MINUTE, 0);
    1430                    cl.set(Calendar.HOUR_OF_DAY, 0);
    1431                    cl.set(Calendar.DAY_OF_MONTH, day);
    1432                    cl.set(Calendar.MONTH, mon - 1);
    1433                    // '- 1' because calendar is 0-based for this field, and we
    1434                    // are 1-based
    1435                    continue;
    1436                }
    1437            } else if (dayOfWSpec && !dayOfMSpec)
    1438            { // get day by day of week rule
    1439                if (lastdayOfWeek)
    1440                { // are we looking for the last XXX day of
    1441                    // the month?
    1442                    int dow = ((Integer) daysOfWeek.first()).intValue(); // desired
    1443                    // d-o-w
    1444                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
    1445                    int daysToAdd = 0;
    1446                    if (cDow < dow)
    1447                    {
    1448                        daysToAdd = dow - cDow;
    1449                    }
    1450                    if (cDow > dow)
    1451                    {
    1452                        daysToAdd = dow + (7 - cDow);
    1453                    }
    1454
    1455                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
    1456
    1457                    if (day + daysToAdd > lDay)
    1458                    { // did we already miss the
    1459                        // last one?
    1460                        cl.set(Calendar.SECOND, 0);
    1461                        cl.set(Calendar.MINUTE, 0);
    1462                        cl.set(Calendar.HOUR_OF_DAY, 0);
    1463                        cl.set(Calendar.DAY_OF_MONTH, 1);
    1464                        cl.set(Calendar.MONTH, mon);
    1465                        // no '- 1' here because we are promoting the month
    1466                        continue;
    1467                    }
    1468
    1469                    // find date of last occurrence of this day in this month...
    1470                    while ((day + daysToAdd + 7) <= lDay)
    1471                    {
    1472                        daysToAdd += 7;
    1473                    }
    1474
    1475                    day += daysToAdd;
    1476
    1477                    if (daysToAdd > 0)
    1478                    {
    1479                        cl.set(Calendar.SECOND, 0);
    1480                        cl.set(Calendar.MINUTE, 0);
    1481                        cl.set(Calendar.HOUR_OF_DAY, 0);
    1482                        cl.set(Calendar.DAY_OF_MONTH, day);
    1483                        cl.set(Calendar.MONTH, mon - 1);
    1484                        // '- 1' here because we are not promoting the month
    1485                        continue;
    1486                    }
    1487
    1488                } else if (nthdayOfWeek != 0)
    1489                {
    1490                    // are we looking for the Nth XXX day in the month?
    1491                    int dow = ((Integer) daysOfWeek.first()).intValue(); // desired
    1492                    // d-o-w
    1493                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
    1494                    int daysToAdd = 0;
    1495                    if (cDow < dow)
    1496                    {
    1497                        daysToAdd = dow - cDow;
    1498                    } else if (cDow > dow)
    1499                    {
    1500                        daysToAdd = dow + (7 - cDow);
    1501                    }
    1502
    1503                    boolean dayShifted = false;
    1504                    if (daysToAdd > 0)
    1505                    {
    1506                        dayShifted = true;
    1507                    }
    1508
    1509                    day += daysToAdd;
    1510                    int weekOfMonth = day / 7;
    1511                    if (day % 7 > 0)
    1512                    {
    1513                        weekOfMonth++;
    1514                    }
    1515
    1516                    daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;
    1517                    day += daysToAdd;
    1518                    if (daysToAdd < 0
    1519                            || day > getLastDayOfMonth(mon, cl
    1520                            .get(Calendar.YEAR)))
    1521                    {
    1522                        cl.set(Calendar.SECOND, 0);
    1523                        cl.set(Calendar.MINUTE, 0);
    1524                        cl.set(Calendar.HOUR_OF_DAY, 0);
    1525                        cl.set(Calendar.DAY_OF_MONTH, 1);
    1526                        cl.set(Calendar.MONTH, mon);
    1527                        // no '- 1' here because we are promoting the month
    1528                        continue;
    1529                    } else if (daysToAdd > 0 || dayShifted)
    1530                    {
    1531                        cl.set(Calendar.SECOND, 0);
    1532                        cl.set(Calendar.MINUTE, 0);
    1533                        cl.set(Calendar.HOUR_OF_DAY, 0);
    1534                        cl.set(Calendar.DAY_OF_MONTH, day);
    1535                        cl.set(Calendar.MONTH, mon - 1);
    1536                        // '- 1' here because we are NOT promoting the month
    1537                        continue;
    1538                    }
    1539                } else
    1540                {
    1541                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
    1542                    int dow = ((Integer) daysOfWeek.first()).intValue(); // desired
    1543                    // d-o-w
    1544                    st = daysOfWeek.tailSet(Integer.valueOf(cDow));
    1545                    if (st != null && st.size() > 0)
    1546                    {
    1547                        dow = ((Integer) st.first()).intValue();
    1548                    }
    1549
    1550                    int daysToAdd = 0;
    1551                    if (cDow < dow)
    1552                    {
    1553                        daysToAdd = dow - cDow;
    1554                    }
    1555                    if (cDow > dow)
    1556                    {
    1557                        daysToAdd = dow + (7 - cDow);
    1558                    }
    1559
    1560                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
    1561
    1562                    if (day + daysToAdd > lDay)
    1563                    { // will we pass the end of
    1564                        // the month?
    1565                        cl.set(Calendar.SECOND, 0);
    1566                        cl.set(Calendar.MINUTE, 0);
    1567                        cl.set(Calendar.HOUR_OF_DAY, 0);
    1568                        cl.set(Calendar.DAY_OF_MONTH, 1);
    1569                        cl.set(Calendar.MONTH, mon);
    1570                        // no '- 1' here because we are promoting the month
    1571                        continue;
    1572                    } else if (daysToAdd > 0)
    1573                    { // are we swithing days?
    1574                        cl.set(Calendar.SECOND, 0);
    1575                        cl.set(Calendar.MINUTE, 0);
    1576                        cl.set(Calendar.HOUR_OF_DAY, 0);
    1577                        cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd);
    1578                        cl.set(Calendar.MONTH, mon - 1);
    1579                        // '- 1' because calendar is 0-based for this field,
    1580                        // and we are 1-based
    1581                        continue;
    1582                    }
    1583                }
    1584            } else
    1585            { // dayOfWSpec && !dayOfMSpec
    1586                throw new UnsupportedOperationException(
    1587                        "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
    1588                // TODO:
    1589            }
    1590            cl.set(Calendar.DAY_OF_MONTH, day);
    1591
    1592            mon = cl.get(Calendar.MONTH) + 1;
    1593            // '+ 1' because calendar is 0-based for this field, and we are
    1594            // 1-based
    1595            int year = cl.get(Calendar.YEAR);
    1596            t = -1;
    1597
    1598            // test for expressions that never generate a valid fire date,
    1599            // but keep looping...
    1600            if (year > MAX_YEAR)
    1601            {
    1602                return null;
    1603            }
    1604
    1605            // get month...................................................
    1606            st = months.tailSet(Integer.valueOf(mon));
    1607            if (st != null && st.size() != 0)
    1608            {
    1609                t = mon;
    1610                mon = ((Integer) st.first()).intValue();
    1611            } else
    1612            {
    1613                mon = ((Integer) months.first()).intValue();
    1614                year++;
    1615            }
    1616            if (mon != t)
    1617            {
    1618                cl.set(Calendar.SECOND, 0);
    1619                cl.set(Calendar.MINUTE, 0);
    1620                cl.set(Calendar.HOUR_OF_DAY, 0);
    1621                cl.set(Calendar.DAY_OF_MONTH, 1);
    1622                cl.set(Calendar.MONTH, mon - 1);
    1623                // '- 1' because calendar is 0-based for this field, and we are
    1624                // 1-based
    1625                cl.set(Calendar.YEAR, year);
    1626                continue;
    1627            }
    1628            cl.set(Calendar.MONTH, mon - 1);
    1629            // '- 1' because calendar is 0-based for this field, and we are
    1630            // 1-based
    1631
    1632            year = cl.get(Calendar.YEAR);
    1633            t = -1;
    1634
    1635            // get year...................................................
    1636            st = years.tailSet(Integer.valueOf(year));
    1637            if (st != null && st.size() != 0)
    1638            {
    1639                t = year;
    1640                year = ((Integer) st.first()).intValue();
    1641            } else
    1642            {
    1643                return null; // ran out of years...
    1644            }
    1645
    1646            if (year != t)
    1647            {
    1648                cl.set(Calendar.SECOND, 0);
    1649                cl.set(Calendar.MINUTE, 0);
    1650                cl.set(Calendar.HOUR_OF_DAY, 0);
    1651                cl.set(Calendar.DAY_OF_MONTH, 1);
    1652                cl.set(Calendar.MONTH, 0);
    1653                // '- 1' because calendar is 0-based for this field, and we are
    1654                // 1-based
    1655                cl.set(Calendar.YEAR, year);
    1656                continue;
    1657            }
    1658            cl.set(Calendar.YEAR, year);
    1659
    1660            gotOne = true;
    1661        } // while( !done )
    1662
    1663        return cl.getTime();
    1664    }
    1665
    1666    /**
    1667     * Advance the calendar to the particular hour paying particular attention
    1668     * to daylight saving problems.
    1669     *
    1670     * @param cal
    1671     * @param hour
    1672     */
    1673    protected void setCalendarHour(Calendar cal, int hour)
    1674    {
    1675        cal.set(java.util.Calendar.HOUR_OF_DAY, hour);
    1676        if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24)
    1677        {
    1678            cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1);
    1679        }
    1680    }
    1681
    1682    /**
    1683     * NOT YET IMPLEMENTED: Returns the time before the given time
    1684     * that the <code>CronExpression</code> matches.
    1685     */
    1686    public Date getTimeBefore(Date endTime)
    1687    {
    1688        // TODO: implement QUARTZ-423
    1689        return null;
    1690    }
    1691
    1692    /**
    1693     * NOT YET IMPLEMENTED: Returns the final time that the
    1694     * <code>CronExpression</code> will match.
    1695     */
    1696    public Date getFinalFireTime()
    1697    {
    1698        // TODO: implement QUARTZ-423
    1699        return null;
    1700    }
    1701
    1702    protected boolean isLeapYear(int year)
    1703    {
    1704        return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
    1705    }
    1706
    1707    protected int getLastDayOfMonth(int monthNum, int year)
    1708    {
    1709
    1710        switch (monthNum)
    1711        {
    1712            case 1:
    1713                return 31;
    1714            case 2:
    1715                return (isLeapYear(year)) ? 29 : 28;
    1716            case 3:
    1717                return 31;
    1718            case 4:
    1719                return 30;
    1720            case 5:
    1721                return 31;
    1722            case 6:
    1723                return 30;
    1724            case 7:
    1725                return 31;
    1726            case 8:
    1727                return 31;
    1728            case 9:
    1729                return 30;
    1730            case 10:
    1731                return 31;
    1732            case 11:
    1733                return 30;
    1734            case 12:
    1735                return 31;
    1736            default:
    1737                throw new IllegalArgumentException("Illegal month number: "
    1738                        + monthNum);
    1739        }
    1740    }
    1741}
    1742
    1743class ValueSet
    1744{
    1745    public int value;
    1746
    1747    public int pos;
    1748}