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