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     * "At 8:00am every Monday through Friday" or "At 1:30am every
033     * last Friday of the month".
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    }