PageRenderTime 799ms CodeModel.GetById 80ms app.highlight 511ms RepoModel.GetById 127ms app.codeStats 1ms

/drools-core/src/main/java/org/drools/time/impl/CronExpression.java

https://github.com/mdproctor/drools
Java | 1594 lines | 1112 code | 165 blank | 317 comment | 477 complexity | 229289b4a0eb981b78fff30a7bd6f83f MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/*
   2 * Copyright 2010 JBoss Inc
   3 *
   4 * Licensed under the Apache License, Version 2.0 (the "License");
   5 * you may not use this file except in compliance with the License.
   6 * You may obtain a copy of the License at
   7 *
   8 *      http://www.apache.org/licenses/LICENSE-2.0
   9 *
  10 * Unless required by applicable law or agreed to in writing, software
  11 * distributed under the License is distributed on an "AS IS" BASIS,
  12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 * See the License for the specific language governing permissions and
  14 * limitations under the License.
  15 */
  16
  17package org.drools.time.impl;
  18
  19import java.io.Serializable;
  20import java.text.ParseException;
  21import java.util.Calendar;
  22import java.util.Date;
  23import java.util.HashMap;
  24import java.util.Iterator;
  25import java.util.Locale;
  26import java.util.Map;
  27import java.util.SortedSet;
  28import java.util.StringTokenizer;
  29import java.util.TimeZone;
  30import java.util.TreeSet;
  31
  32/**
  33 * Provides a parser and evaluator for unix-like cron expressions. Cron 
  34 * expressions provide the ability to specify complex time combinations such as
  35 * "At 8:00am every Monday through Friday" or "At 1:30am every 
  36 * last Friday of the month". 
  37 * <P>
  38 * Cron expressions are comprised of 6 required fields and one optional field
  39 * separated by white space. The fields respectively are described as follows:
  40 * 
  41 * <table cellspacing="8">
  42 * <tr>
  43 * <th align="left">Field Name</th>
  44 * <th align="left">&nbsp;</th>
  45 * <th align="left">Allowed Values</th>
  46 * <th align="left">&nbsp;</th>
  47 * <th align="left">Allowed Special Characters</th>
  48 * </tr>
  49 * <tr>
  50 * <td align="left"><code>Seconds</code></td>
  51 * <td align="left">&nbsp;</th>
  52 * <td align="left"><code>0-59</code></td>
  53 * <td align="left">&nbsp;</th>
  54 * <td align="left"><code>, - * /</code></td>
  55 * </tr>
  56 * <tr>
  57 * <td align="left"><code>Minutes</code></td>
  58 * <td align="left">&nbsp;</th>
  59 * <td align="left"><code>0-59</code></td>
  60 * <td align="left">&nbsp;</th>
  61 * <td align="left"><code>, - * /</code></td>
  62 * </tr>
  63 * <tr>
  64 * <td align="left"><code>Hours</code></td>
  65 * <td align="left">&nbsp;</th>
  66 * <td align="left"><code>0-23</code></td>
  67 * <td align="left">&nbsp;</th>
  68 * <td align="left"><code>, - * /</code></td>
  69 * </tr>
  70 * <tr>
  71 * <td align="left"><code>Day-of-month</code></td>
  72 * <td align="left">&nbsp;</th>
  73 * <td align="left"><code>1-31</code></td>
  74 * <td align="left">&nbsp;</th>
  75 * <td align="left"><code>, - * ? / L W</code></td>
  76 * </tr>
  77 * <tr>
  78 * <td align="left"><code>Month</code></td>
  79 * <td align="left">&nbsp;</th>
  80 * <td align="left"><code>1-12 or JAN-DEC</code></td>
  81 * <td align="left">&nbsp;</th>
  82 * <td align="left"><code>, - * /</code></td>
  83 * </tr>
  84 * <tr>
  85 * <td align="left"><code>Day-of-Week</code></td>
  86 * <td align="left">&nbsp;</th>
  87 * <td align="left"><code>1-7 or SUN-SAT</code></td>
  88 * <td align="left">&nbsp;</th>
  89 * <td align="left"><code>, - * ? / L #</code></td>
  90 * </tr>
  91 * <tr>
  92 * <td align="left"><code>Year (Optional)</code></td>
  93 * <td align="left">&nbsp;</th>
  94 * <td align="left"><code>empty, 1970-2199</code></td>
  95 * <td align="left">&nbsp;</th>
  96 * <td align="left"><code>, - * /</code></td>
  97 * </tr>
  98 * </table>
  99 * <P>
 100 * The '*' character is used to specify all values. For example, &quot;*&quot;
 101 * in the minute field means &quot;every minute&quot;.
 102 * <P>
 103 * The '?' character is allowed for the day-of-month and day-of-week fields. It
 104 * is used to specify 'no specific value'. This is useful when you need to
 105 * specify something in one of the two fields, but not the other.
 106 * <P>
 107 * The '-' character is used to specify ranges For example &quot;10-12&quot; in
 108 * the hour field means &quot;the hours 10, 11 and 12&quot;.
 109 * <P>
 110 * The ',' character is used to specify additional values. For example
 111 * &quot;MON,WED,FRI&quot; in the day-of-week field means &quot;the days Monday,
 112 * Wednesday, and Friday&quot;.
 113 * <P>
 114 * The '/' character is used to specify increments. For example &quot;0/15&quot;
 115 * in the seconds field means &quot;the seconds 0, 15, 30, and 45&quot;. And 
 116 * &quot;5/15&quot; in the seconds field means &quot;the seconds 5, 20, 35, and
 117 * 50&quot;.  Specifying '*' before the  '/' is equivalent to specifying 0 is
 118 * the value to start with. Essentially, for each field in the expression, there
 119 * is a set of numbers that can be turned on or off. For seconds and minutes, 
 120 * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
 121 * 31, and for months 1 to 12. The &quot;/&quot; character simply helps you turn
 122 * on every &quot;nth&quot; value in the given set. Thus &quot;7/6&quot; in the
 123 * month field only turns on month &quot;7&quot;, it does NOT mean every 6th 
 124 * month, please note that subtlety.  
 125 * <P>
 126 * The 'L' character is allowed for the day-of-month and day-of-week fields.
 127 * This character is short-hand for &quot;last&quot;, but it has different 
 128 * meaning in each of the two fields. For example, the value &quot;L&quot; in 
 129 * the day-of-month field means &quot;the last day of the month&quot; - day 31 
 130 * for January, day 28 for February on non-leap years. If used in the 
 131 * day-of-week field by itself, it simply means &quot;7&quot; or 
 132 * &quot;SAT&quot;. But if used in the day-of-week field after another value, it
 133 * means &quot;the last xxx day of the month&quot; - for example &quot;6L&quot;
 134 * means &quot;the last friday of the month&quot;. When using the 'L' option, it
 135 * is important not to specify lists, or ranges of values, as you'll get 
 136 * confusing results.
 137 * <P>
 138 * The 'W' character is allowed for the day-of-month field.  This character 
 139 * is used to specify the weekday (Monday-Friday) nearest the given day.  As an 
 140 * example, if you were to specify &quot;15W&quot; as the value for the 
 141 * day-of-month field, the meaning is: &quot;the nearest weekday to the 15th of
 142 * the month&quot;. So if the 15th is a Saturday, the trigger will fire on 
 143 * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
 144 * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. 
 145 * However if you specify &quot;1W&quot; as the value for day-of-month, and the
 146 * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not 
 147 * 'jump' over the boundary of a month's days.  The 'W' character can only be 
 148 * specified when the day-of-month is a single day, not a range or list of days.
 149 * <P>
 150 * The 'L' and 'W' characters can also be combined for the day-of-month 
 151 * expression to yield 'LW', which translates to &quot;last weekday of the 
 152 * month&quot;.
 153 * <P>
 154 * The '#' character is allowed for the day-of-week field. This character is
 155 * used to specify &quot;the nth&quot; XXX day of the month. For example, the 
 156 * value of &quot;6#3&quot; in the day-of-week field means the third Friday of 
 157 * the month (day 6 = Friday and &quot;#3&quot; = the 3rd one in the month). 
 158 * Other examples: &quot;2#1&quot; = the first Monday of the month and 
 159 * &quot;4#5&quot; = the fifth Wednesday of the month. Note that if you specify
 160 * &quot;#5&quot; and there is not 5 of the given day-of-week in the month, then
 161 * no firing will occur that month.  If the '#' character is used, there can
 162 * only be one expression in the day-of-week field (&quot;3#1,6#3&quot; is 
 163 * not valid, since there are two expressions).
 164 * <P>
 165 * <!--The 'C' character is allowed for the day-of-month and day-of-week fields.
 166 * This character is short-hand for "calendar". This means values are
 167 * calculated against the associated calendar, if any. If no calendar is
 168 * associated, then it is equivalent to having an all-inclusive calendar. A
 169 * value of "5C" in the day-of-month field means "the first day included by the
 170 * calendar on or after the 5th". A value of "1C" in the day-of-week field
 171 * means "the first day included by the calendar on or after sunday".-->
 172 * <P>
 173 * The legal characters and the names of months and days of the week are not
 174 * case sensitive.
 175 * 
 176 * <p>
 177 * <b>NOTES:</b>
 178 * <ul>
 179 * <li>Support for specifying both a day-of-week and a day-of-month value is
 180 * not complete (you'll need to use the '?' character in one of these fields).
 181 * </li>
 182 * <li>Overflowing ranges is supported - that is, having a larger number on 
 183 * the left hand side than the right. You might do 22-2 to catch 10 o'clock 
 184 * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is 
 185 * very important to note that overuse of overflowing ranges creates ranges 
 186 * that don't make sense and no effort has been made to determine which 
 187 * interpretation CronExpression chooses. An example would be 
 188 * "0 0 14-6 ? * FRI-MON". </li>
 189 * </ul>
 190 * </p>
 191 */
 192public class CronExpression implements Serializable, Cloneable {
 193
 194    private static final long serialVersionUID = 510l;
 195    
 196    protected static final int SECOND = 0;
 197    protected static final int MINUTE = 1;
 198    protected static final int HOUR = 2;
 199    protected static final int DAY_OF_MONTH = 3;
 200    protected static final int MONTH = 4;
 201    protected static final int DAY_OF_WEEK = 5;
 202    protected static final int YEAR = 6;
 203    protected static final int ALL_SPEC_INT = 99; // '*'
 204    protected static final int NO_SPEC_INT = 98; // '?'
 205    protected static final Integer ALL_SPEC = new Integer(ALL_SPEC_INT);
 206    protected static final Integer NO_SPEC = new Integer(NO_SPEC_INT);
 207    
 208    protected static final Map monthMap = new HashMap(20);
 209    protected static final Map dayMap = new HashMap(60);
 210    static {
 211        monthMap.put("JAN", new Integer(0));
 212        monthMap.put("FEB", new Integer(1));
 213        monthMap.put("MAR", new Integer(2));
 214        monthMap.put("APR", new Integer(3));
 215        monthMap.put("MAY", new Integer(4));
 216        monthMap.put("JUN", new Integer(5));
 217        monthMap.put("JUL", new Integer(6));
 218        monthMap.put("AUG", new Integer(7));
 219        monthMap.put("SEP", new Integer(8));
 220        monthMap.put("OCT", new Integer(9));
 221        monthMap.put("NOV", new Integer(10));
 222        monthMap.put("DEC", new Integer(11));
 223
 224        dayMap.put("SUN", new Integer(1));
 225        dayMap.put("MON", new Integer(2));
 226        dayMap.put("TUE", new Integer(3));
 227        dayMap.put("WED", new Integer(4));
 228        dayMap.put("THU", new Integer(5));
 229        dayMap.put("FRI", new Integer(6));
 230        dayMap.put("SAT", new Integer(7));
 231    }
 232
 233    private String cronExpression = null;
 234    private TimeZone timeZone = null;
 235    protected transient TreeSet seconds;
 236    protected transient TreeSet minutes;
 237    protected transient TreeSet hours;
 238    protected transient TreeSet daysOfMonth;
 239    protected transient TreeSet months;
 240    protected transient TreeSet daysOfWeek;
 241    protected transient TreeSet years;
 242
 243    protected transient boolean lastdayOfWeek = false;
 244    protected transient int nthdayOfWeek = 0;
 245    protected transient boolean lastdayOfMonth = false;
 246    protected transient boolean nearestWeekday = false;
 247    protected transient boolean expressionParsed = false;
 248    
 249    /**
 250     * Constructs a new <CODE>CronExpression</CODE> based on the specified 
 251     * parameter.
 252     * 
 253     * @param cronExpression String representation of the cron expression the
 254     *                       new object should represent
 255     * @throws java.text.ParseException
 256     *         if the string expression cannot be parsed into a valid 
 257     *         <CODE>CronExpression</CODE>
 258     */
 259    public CronExpression(String cronExpression) throws ParseException {
 260        if (cronExpression == null) {
 261            throw new IllegalArgumentException("cronExpression cannot be null");
 262        }
 263        
 264        this.cronExpression = cronExpression.toUpperCase(Locale.US);
 265        
 266        buildExpression(this.cronExpression);
 267    }
 268    
 269    /**
 270     * Indicates whether the given date satisfies the cron expression. Note that
 271     * milliseconds are ignored, so two Dates falling on different milliseconds
 272     * of the same second will always have the same result here.
 273     * 
 274     * @param date the date to evaluate
 275     * @return a boolean indicating whether the given date satisfies the cron
 276     *         expression
 277     */
 278    public boolean isSatisfiedBy(Date date) {
 279        Calendar testDateCal = Calendar.getInstance(getTimeZone());
 280        testDateCal.setTime(date);
 281        testDateCal.set(Calendar.MILLISECOND, 0);
 282        Date originalDate = testDateCal.getTime();
 283        
 284        testDateCal.add(Calendar.SECOND, -1);
 285        
 286        Date timeAfter = getTimeAfter(testDateCal.getTime());
 287
 288        return ((timeAfter != null) && (timeAfter.equals(originalDate)));
 289    }
 290    
 291    /**
 292     * Returns the next date/time <I>after</I> the given date/time which
 293     * satisfies the cron expression.
 294     * 
 295     * @param date the date/time at which to begin the search for the next valid
 296     *             date/time
 297     * @return the next valid date/time
 298     */
 299    public Date getNextValidTimeAfter(Date date) {
 300        return getTimeAfter(date);
 301    }
 302    
 303    /**
 304     * Returns the next date/time <I>after</I> the given date/time which does
 305     * <I>not</I> satisfy the expression
 306     * 
 307     * @param date the date/time at which to begin the search for the next 
 308     *             invalid date/time
 309     * @return the next valid date/time
 310     */
 311    public Date getNextInvalidTimeAfter(Date date) {
 312        long difference = 1000;
 313        
 314        //move back to the nearest second so differences will be accurate
 315        Calendar adjustCal = Calendar.getInstance(getTimeZone());
 316        adjustCal.setTime(date);
 317        adjustCal.set(Calendar.MILLISECOND, 0);
 318        Date lastDate = adjustCal.getTime();
 319        
 320        Date newDate = null;
 321        
 322        //TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution.
 323        
 324        //keep getting the next included time until it's farther than one second
 325        // apart. At that point, lastDate is the last valid fire time. We return
 326        // the second immediately following it.
 327        while (difference == 1000) {
 328            newDate = getTimeAfter(lastDate);
 329            
 330            difference = newDate.getTime() - lastDate.getTime();
 331            
 332            if (difference == 1000) {
 333                lastDate = newDate;
 334            }
 335        }
 336        
 337        return new Date(lastDate.getTime() + 1000);
 338    }
 339    
 340    /**
 341     * Returns the time zone for which this <code>CronExpression</code> 
 342     * will be resolved.
 343     */
 344    public TimeZone getTimeZone() {
 345        if (timeZone == null) {
 346            timeZone = TimeZone.getDefault();
 347        }
 348
 349        return timeZone;
 350    }
 351
 352    /**
 353     * Sets the time zone for which  this <code>CronExpression</code> 
 354     * will be resolved.
 355     */
 356    public void setTimeZone(TimeZone timeZone) {
 357        this.timeZone = timeZone;
 358    }
 359    
 360    /**
 361     * Returns the string representation of the <CODE>CronExpression</CODE>
 362     * 
 363     * @return a string representation of the <CODE>CronExpression</CODE>
 364     */
 365    public String toString() {
 366        return cronExpression;
 367    }
 368
 369    /**
 370     * Indicates whether the specified cron expression can be parsed into a 
 371     * valid cron expression
 372     * 
 373     * @param cronExpression the expression to evaluate
 374     * @return a boolean indicating whether the given expression is a valid cron
 375     *         expression
 376     */
 377    public static boolean isValidExpression(String cronExpression) {
 378        
 379        try {
 380            new CronExpression(cronExpression);
 381        } catch (ParseException pe) {
 382            return false;
 383        }
 384        
 385        return true;
 386    }
 387    
 388    ////////////////////////////////////////////////////////////////////////////
 389    //
 390    // Expression Parsing Functions
 391    //
 392    ////////////////////////////////////////////////////////////////////////////
 393
 394    protected void buildExpression(String expression) throws ParseException {
 395        expressionParsed = true;
 396
 397        try {
 398
 399            if (seconds == null) {
 400                seconds = new TreeSet();
 401            }
 402            if (minutes == null) {
 403                minutes = new TreeSet();
 404            }
 405            if (hours == null) {
 406                hours = new TreeSet();
 407            }
 408            if (daysOfMonth == null) {
 409                daysOfMonth = new TreeSet();
 410            }
 411            if (months == null) {
 412                months = new TreeSet();
 413            }
 414            if (daysOfWeek == null) {
 415                daysOfWeek = new TreeSet();
 416            }
 417            if (years == null) {
 418                years = new TreeSet();
 419            }
 420
 421            int exprOn = SECOND;
 422
 423            StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
 424                    false);
 425
 426            while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
 427                String expr = exprsTok.nextToken().trim();
 428
 429                // throw an exception if L is used with other days of the month
 430                if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.indexOf(",") >= 0) {
 431                    throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1);
 432                }
 433                // throw an exception if L is used with other days of the week
 434                if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1  && expr.indexOf(",") >= 0) {
 435                    throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);
 436                }
 437                
 438                StringTokenizer vTok = new StringTokenizer(expr, ",");
 439                while (vTok.hasMoreTokens()) {
 440                    String v = vTok.nextToken();
 441                    storeExpressionVals(0, v, exprOn);
 442                }
 443
 444                exprOn++;
 445            }
 446
 447            if (exprOn <= DAY_OF_WEEK) {
 448                throw new ParseException("Unexpected end of expression.",
 449                            expression.length());
 450            }
 451
 452            if (exprOn <= YEAR) {
 453                storeExpressionVals(0, "*", YEAR);
 454            }
 455
 456            TreeSet dow = getSet(DAY_OF_WEEK);
 457            TreeSet dom = getSet(DAY_OF_MONTH);
 458
 459            // Copying the logic from the UnsupportedOperationException below
 460            boolean dayOfMSpec = !dom.contains(NO_SPEC);
 461            boolean dayOfWSpec = !dow.contains(NO_SPEC);
 462
 463            if (dayOfMSpec && !dayOfWSpec) {
 464                // skip
 465            } else if (dayOfWSpec && !dayOfMSpec) {
 466                // skip
 467            } else {
 468                throw new ParseException(
 469                        "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0);
 470            }
 471        } catch (ParseException pe) {
 472            throw pe;
 473        } catch (Exception e) {
 474            throw new ParseException("Illegal cron expression format ("
 475                    + e.toString() + ")", 0);
 476        }
 477    }
 478
 479    protected int storeExpressionVals(int pos, String s, int type)
 480        throws ParseException {
 481
 482        int incr = 0;
 483        int i = skipWhiteSpace(pos, s);
 484        if (i >= s.length()) {
 485            return i;
 486        }
 487        char c = s.charAt(i);
 488        if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW"))) {
 489            String sub = s.substring(i, i + 3);
 490            int sval = -1;
 491            int eval = -1;
 492            if (type == MONTH) {
 493                sval = getMonthNumber(sub) + 1;
 494                if (sval <= 0) {
 495                    throw new ParseException("Invalid Month value: '" + sub + "'", i);
 496                }
 497                if (s.length() > i + 3) {
 498                    c = s.charAt(i + 3);
 499                    if (c == '-') {
 500                        i += 4;
 501                        sub = s.substring(i, i + 3);
 502                        eval = getMonthNumber(sub) + 1;
 503                        if (eval <= 0) {
 504                            throw new ParseException("Invalid Month value: '" + sub + "'", i);
 505                        }
 506                    }
 507                }
 508            } else if (type == DAY_OF_WEEK) {
 509                sval = getDayOfWeekNumber(sub);
 510                if (sval < 0) {
 511                    throw new ParseException("Invalid Day-of-Week value: '"
 512                                + sub + "'", i);
 513                }
 514                if (s.length() > i + 3) {
 515                    c = s.charAt(i + 3);
 516                    if (c == '-') {
 517                        i += 4;
 518                        sub = s.substring(i, i + 3);
 519                        eval = getDayOfWeekNumber(sub);
 520                        if (eval < 0) {
 521                            throw new ParseException(
 522                                    "Invalid Day-of-Week value: '" + sub
 523                                        + "'", i);
 524                        }
 525                    } else if (c == '#') {
 526                        try {
 527                            i += 4;
 528                            nthdayOfWeek = Integer.parseInt(s.substring(i));
 529                            if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
 530                                throw new Exception();
 531                            }
 532                        } catch (Exception e) {
 533                            throw new ParseException(
 534                                    "A numeric value between 1 and 5 must follow the '#' option",
 535                                    i);
 536                        }
 537                    } else if (c == 'L') {
 538                        lastdayOfWeek = true;
 539                        i++;
 540                    }
 541                }
 542
 543            } else {
 544                throw new ParseException(
 545                        "Illegal characters for this position: '" + sub + "'",
 546                        i);
 547            }
 548            if (eval != -1) {
 549                incr = 1;
 550            }
 551            addToSet(sval, eval, incr, type);
 552            return (i + 3);
 553        }
 554
 555        if (c == '?') {
 556            i++;
 557            if ((i + 1) < s.length() 
 558                    && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
 559                throw new ParseException("Illegal character after '?': "
 560                            + s.charAt(i), i);
 561            }
 562            if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
 563                throw new ParseException(
 564                            "'?' can only be specfied for Day-of-Month or Day-of-Week.",
 565                            i);
 566            }
 567            if (type == DAY_OF_WEEK && !lastdayOfMonth) {
 568                int val = ((Integer) daysOfMonth.last()).intValue();
 569                if (val == NO_SPEC_INT) {
 570                    throw new ParseException(
 571                                "'?' can only be specfied for Day-of-Month -OR- Day-of-Week.",
 572                                i);
 573                }
 574            }
 575
 576            addToSet(NO_SPEC_INT, -1, 0, type);
 577            return i;
 578        }
 579
 580        if (c == '*' || c == '/') {
 581            if (c == '*' && (i + 1) >= s.length()) {
 582                addToSet(ALL_SPEC_INT, -1, incr, type);
 583                return i + 1;
 584            } else if (c == '/'
 585                    && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
 586                            .charAt(i + 1) == '\t')) {
 587                throw new ParseException("'/' must be followed by an integer.", i);
 588            } else if (c == '*') {
 589                i++;
 590            }
 591            c = s.charAt(i);
 592            if (c == '/') { // is an increment specified?
 593                i++;
 594                if (i >= s.length()) {
 595                    throw new ParseException("Unexpected end of string.", i);
 596                }
 597
 598                incr = getNumericValue(s, i);
 599
 600                i++;
 601                if (incr > 10) {
 602                    i++;
 603                }
 604                if (incr > 59 && (type == SECOND || type == MINUTE)) {
 605                    throw new ParseException("Increment > 60 : " + incr, i);
 606                } else if (incr > 23 && (type == HOUR)) {
 607                    throw new ParseException("Increment > 24 : " + incr, i);
 608                } else if (incr > 31 && (type == DAY_OF_MONTH)) {
 609                    throw new ParseException("Increment > 31 : " + incr, i);
 610                } else if (incr > 7 && (type == DAY_OF_WEEK)) {
 611                    throw new ParseException("Increment > 7 : " + incr, i);
 612                } else if (incr > 12 && (type == MONTH)) {
 613                    throw new ParseException("Increment > 12 : " + incr, i);
 614                }
 615            } else {
 616                incr = 1;
 617            }
 618
 619            addToSet(ALL_SPEC_INT, -1, incr, type);
 620            return i;
 621        } else if (c == 'L') {
 622            i++;
 623            if (type == DAY_OF_MONTH) {
 624                lastdayOfMonth = true;
 625            }
 626            if (type == DAY_OF_WEEK) {
 627                addToSet(7, 7, 0, type);
 628            }
 629            if(type == DAY_OF_MONTH && s.length() > i) {
 630                c = s.charAt(i);
 631                if(c == 'W') {
 632                    nearestWeekday = true;
 633                    i++;
 634                }
 635            }
 636            return i;
 637        } else if (c >= '0' && c <= '9') {
 638            int val = Integer.parseInt(String.valueOf(c));
 639            i++;
 640            if (i >= s.length()) {
 641                addToSet(val, -1, -1, type);
 642            } else {
 643                c = s.charAt(i);
 644                if (c >= '0' && c <= '9') {
 645                    ValueSet vs = getValue(val, s, i);
 646                    val = vs.value;
 647                    i = vs.pos;
 648                }
 649                i = checkNext(i, s, val, type);
 650                return i;
 651            }
 652        } else {
 653            throw new ParseException("Unexpected character: " + c, i);
 654        }
 655
 656        return i;
 657    }
 658
 659    protected int checkNext(int pos, String s, int val, int type)
 660        throws ParseException {
 661        
 662        int end = -1;
 663        int i = pos;
 664
 665        if (i >= s.length()) {
 666            addToSet(val, end, -1, type);
 667            return i;
 668        }
 669
 670        char c = s.charAt(pos);
 671
 672        if (c == 'L') {
 673            if (type == DAY_OF_WEEK) {
 674                lastdayOfWeek = true;
 675            } else {
 676                throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);
 677            }
 678            TreeSet set = getSet(type);
 679            set.add(new Integer(val));
 680            i++;
 681            return i;
 682        }
 683        
 684        if (c == 'W') {
 685            if (type == DAY_OF_MONTH) {
 686                nearestWeekday = true;
 687            } else {
 688                throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i);
 689            }
 690            TreeSet set = getSet(type);
 691            set.add(new Integer(val));
 692            i++;
 693            return i;
 694        }
 695
 696        if (c == '#') {
 697            if (type != DAY_OF_WEEK) {
 698                throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i);
 699            }
 700            i++;
 701            try {
 702                nthdayOfWeek = Integer.parseInt(s.substring(i));
 703                if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
 704                    throw new Exception();
 705                }
 706            } catch (Exception e) {
 707                throw new ParseException(
 708                        "A numeric value between 1 and 5 must follow the '#' option",
 709                        i);
 710            }
 711
 712            TreeSet set = getSet(type);
 713            set.add(new Integer(val));
 714            i++;
 715            return i;
 716        }
 717
 718        if (c == '-') {
 719            i++;
 720            c = s.charAt(i);
 721            int v = Integer.parseInt(String.valueOf(c));
 722            end = v;
 723            i++;
 724            if (i >= s.length()) {
 725                addToSet(val, end, 1, type);
 726                return i;
 727            }
 728            c = s.charAt(i);
 729            if (c >= '0' && c <= '9') {
 730                ValueSet vs = getValue(v, s, i);
 731                int v1 = vs.value;
 732                end = v1;
 733                i = vs.pos;
 734            }
 735            if (i < s.length() && ((c = s.charAt(i)) == '/')) {
 736                i++;
 737                c = s.charAt(i);
 738                int v2 = Integer.parseInt(String.valueOf(c));
 739                i++;
 740                if (i >= s.length()) {
 741                    addToSet(val, end, v2, type);
 742                    return i;
 743                }
 744                c = s.charAt(i);
 745                if (c >= '0' && c <= '9') {
 746                    ValueSet vs = getValue(v2, s, i);
 747                    int v3 = vs.value;
 748                    addToSet(val, end, v3, type);
 749                    i = vs.pos;
 750                    return i;
 751                } else {
 752                    addToSet(val, end, v2, type);
 753                    return i;
 754                }
 755            } else {
 756                addToSet(val, end, 1, type);
 757                return i;
 758            }
 759        }
 760
 761        if (c == '/') {
 762            i++;
 763            c = s.charAt(i);
 764            int v2 = Integer.parseInt(String.valueOf(c));
 765            i++;
 766            if (i >= s.length()) {
 767                addToSet(val, end, v2, type);
 768                return i;
 769            }
 770            c = s.charAt(i);
 771            if (c >= '0' && c <= '9') {
 772                ValueSet vs = getValue(v2, s, i);
 773                int v3 = vs.value;
 774                addToSet(val, end, v3, type);
 775                i = vs.pos;
 776                return i;
 777            } else {
 778                throw new ParseException("Unexpected character '" + c + "' after '/'", i);
 779            }
 780        }
 781
 782        addToSet(val, end, 0, type);
 783        i++;
 784        return i;
 785    }
 786
 787    public String getCronExpression() {
 788        return cronExpression;
 789    }
 790    
 791    public String getExpressionSummary() {
 792        StringBuffer buf = new StringBuffer();
 793
 794        buf.append("seconds: ");
 795        buf.append(getExpressionSetSummary(seconds));
 796        buf.append("\n");
 797        buf.append("minutes: ");
 798        buf.append(getExpressionSetSummary(minutes));
 799        buf.append("\n");
 800        buf.append("hours: ");
 801        buf.append(getExpressionSetSummary(hours));
 802        buf.append("\n");
 803        buf.append("daysOfMonth: ");
 804        buf.append(getExpressionSetSummary(daysOfMonth));
 805        buf.append("\n");
 806        buf.append("months: ");
 807        buf.append(getExpressionSetSummary(months));
 808        buf.append("\n");
 809        buf.append("daysOfWeek: ");
 810        buf.append(getExpressionSetSummary(daysOfWeek));
 811        buf.append("\n");
 812        buf.append("lastdayOfWeek: ");
 813        buf.append(lastdayOfWeek);
 814        buf.append("\n");
 815        buf.append("nearestWeekday: ");
 816        buf.append(nearestWeekday);
 817        buf.append("\n");
 818        buf.append("NthDayOfWeek: ");
 819        buf.append(nthdayOfWeek);
 820        buf.append("\n");
 821        buf.append("lastdayOfMonth: ");
 822        buf.append(lastdayOfMonth);
 823        buf.append("\n");
 824        buf.append("years: ");
 825        buf.append(getExpressionSetSummary(years));
 826        buf.append("\n");
 827
 828        return buf.toString();
 829    }
 830
 831    protected String getExpressionSetSummary(java.util.Set set) {
 832
 833        if (set.contains(NO_SPEC)) {
 834            return "?";
 835        }
 836        if (set.contains(ALL_SPEC)) {
 837            return "*";
 838        }
 839
 840        StringBuffer buf = new StringBuffer();
 841
 842        Iterator itr = set.iterator();
 843        boolean first = true;
 844        while (itr.hasNext()) {
 845            Integer iVal = (Integer) itr.next();
 846            String val = iVal.toString();
 847            if (!first) {
 848                buf.append(",");
 849            }
 850            buf.append(val);
 851            first = false;
 852        }
 853
 854        return buf.toString();
 855    }
 856
 857    protected String getExpressionSetSummary(java.util.ArrayList list) {
 858
 859        if (list.contains(NO_SPEC)) {
 860            return "?";
 861        }
 862        if (list.contains(ALL_SPEC)) {
 863            return "*";
 864        }
 865
 866        StringBuffer buf = new StringBuffer();
 867
 868        Iterator itr = list.iterator();
 869        boolean first = true;
 870        while (itr.hasNext()) {
 871            Integer iVal = (Integer) itr.next();
 872            String val = iVal.toString();
 873            if (!first) {
 874                buf.append(",");
 875            }
 876            buf.append(val);
 877            first = false;
 878        }
 879
 880        return buf.toString();
 881    }
 882
 883    protected int skipWhiteSpace(int i, String s) {
 884        for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) {
 885            ;
 886        }
 887
 888        return i;
 889    }
 890
 891    protected int findNextWhiteSpace(int i, String s) {
 892        for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) {
 893            ;
 894        }
 895
 896        return i;
 897    }
 898
 899    protected void addToSet(int val, int end, int incr, int type)
 900        throws ParseException {
 901        
 902        TreeSet set = getSet(type);
 903
 904        if (type == SECOND || type == MINUTE) {
 905            if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
 906                throw new ParseException(
 907                        "Minute and Second values must be between 0 and 59",
 908                        -1);
 909            }
 910        } else if (type == HOUR) {
 911            if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
 912                throw new ParseException(
 913                        "Hour values must be between 0 and 23", -1);
 914            }
 915        } else if (type == DAY_OF_MONTH) {
 916            if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) 
 917                    && (val != NO_SPEC_INT)) {
 918                throw new ParseException(
 919                        "Day of month values must be between 1 and 31", -1);
 920            }
 921        } else if (type == MONTH) {
 922            if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
 923                throw new ParseException(
 924                        "Month values must be between 1 and 12", -1);
 925            }
 926        } else if (type == DAY_OF_WEEK) {
 927            if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT)
 928                    && (val != NO_SPEC_INT)) {
 929                throw new ParseException(
 930                        "Day-of-Week values must be between 1 and 7", -1);
 931            }
 932        }
 933
 934        if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
 935            if (val != -1) {
 936                set.add(new Integer(val));
 937            } else {
 938                set.add(NO_SPEC);
 939            }
 940            
 941            return;
 942        }
 943
 944        int startAt = val;
 945        int stopAt = end;
 946
 947        if (val == ALL_SPEC_INT && incr <= 0) {
 948            incr = 1;
 949            set.add(ALL_SPEC); // put in a marker, but also fill values
 950        }
 951
 952        if (type == SECOND || type == MINUTE) {
 953            if (stopAt == -1) {
 954                stopAt = 59;
 955            }
 956            if (startAt == -1 || startAt == ALL_SPEC_INT) {
 957                startAt = 0;
 958            }
 959        } else if (type == HOUR) {
 960            if (stopAt == -1) {
 961                stopAt = 23;
 962            }
 963            if (startAt == -1 || startAt == ALL_SPEC_INT) {
 964                startAt = 0;
 965            }
 966        } else if (type == DAY_OF_MONTH) {
 967            if (stopAt == -1) {
 968                stopAt = 31;
 969            }
 970            if (startAt == -1 || startAt == ALL_SPEC_INT) {
 971                startAt = 1;
 972            }
 973        } else if (type == MONTH) {
 974            if (stopAt == -1) {
 975                stopAt = 12;
 976            }
 977            if (startAt == -1 || startAt == ALL_SPEC_INT) {
 978                startAt = 1;
 979            }
 980        } else if (type == DAY_OF_WEEK) {
 981            if (stopAt == -1) {
 982                stopAt = 7;
 983            }
 984            if (startAt == -1 || startAt == ALL_SPEC_INT) {
 985                startAt = 1;
 986            }
 987        } else if (type == YEAR) {
 988            if (stopAt == -1) {
 989                stopAt = CronTrigger.YEAR_TO_GIVEUP_SCHEDULING_AT;
 990            }
 991            if (startAt == -1 || startAt == ALL_SPEC_INT) {
 992                // needs to start at 1969 because timezones can make dates before 1970
 993                startAt = 1969;
 994            }
 995        }
 996
 997        // if the end of the range is before the start, then we need to overflow into 
 998        // the next day, month etc. This is done by adding the maximum amount for that 
 999        // type, and using modulus max to determine the value being added.
1000        int max = -1;
1001        if (stopAt < startAt) {
1002            switch (type) {
1003              case       SECOND : max = 60; break;
1004              case       MINUTE : max = 60; break;
1005              case         HOUR : max = 24; break;
1006              case        MONTH : max = 12; break;
1007              case  DAY_OF_WEEK : max = 7;  break;
1008              case DAY_OF_MONTH : max = 31; break;
1009              case         YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
1010              default           : throw new IllegalArgumentException("Unexpected type encountered");
1011            }
1012            stopAt += max;
1013        }
1014
1015        for (int i = startAt; i <= stopAt; i += incr) {
1016            if (max == -1) {
1017                // ie: there's no max to overflow over
1018                set.add(new Integer(i));
1019            } else {
1020                // take the modulus to get the real value
1021                int i2 = i % max;
1022
1023                // 1-indexed ranges should not include 0, and should include their max
1024                if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) {
1025                    i2 = max;
1026                }
1027
1028                set.add(new Integer(i2));
1029            }
1030        }
1031    }
1032
1033    protected TreeSet getSet(int type) {
1034        switch (type) {
1035            case SECOND:
1036                return seconds;
1037            case MINUTE:
1038                return minutes;
1039            case HOUR:
1040                return hours;
1041            case DAY_OF_MONTH:
1042                return daysOfMonth;
1043            case MONTH:
1044                return months;
1045            case DAY_OF_WEEK:
1046                return daysOfWeek;
1047            case YEAR:
1048                return years;
1049            default:
1050                return null;
1051        }
1052    }
1053
1054    protected ValueSet getValue(int v, String s, int i) {
1055        char c = s.charAt(i);
1056        String s1 = String.valueOf(v);
1057        while (c >= '0' && c <= '9') {
1058            s1 += c;
1059            i++;
1060            if (i >= s.length()) {
1061                break;
1062            }
1063            c = s.charAt(i);
1064        }
1065        ValueSet val = new ValueSet();
1066        
1067        val.pos = (i < s.length()) ? i : i + 1;
1068        val.value = Integer.parseInt(s1);
1069        return val;
1070    }
1071
1072    protected int getNumericValue(String s, int i) {
1073        int endOfVal = findNextWhiteSpace(i, s);
1074        String val = s.substring(i, endOfVal);
1075        return Integer.parseInt(val);
1076    }
1077
1078    protected int getMonthNumber(String s) {
1079        Integer integer = (Integer) monthMap.get(s);
1080
1081        if (integer == null) {
1082            return -1;
1083        }
1084
1085        return integer.intValue();
1086    }
1087
1088    protected int getDayOfWeekNumber(String s) {
1089        Integer integer = (Integer) dayMap.get(s);
1090
1091        if (integer == null) {
1092            return -1;
1093        }
1094
1095        return integer.intValue();
1096    }
1097
1098    ////////////////////////////////////////////////////////////////////////////
1099    //
1100    // Computation Functions
1101    //
1102    ////////////////////////////////////////////////////////////////////////////
1103
1104    protected Date getTimeAfter(Date afterTime) {
1105
1106        Calendar cl = Calendar.getInstance(getTimeZone());
1107
1108        // move ahead one second, since we're computing the time *after* the
1109        // given time
1110        afterTime = new Date(afterTime.getTime() + 1000);
1111        // CronTrigger does not deal with milliseconds
1112        cl.setTime(afterTime);
1113        cl.set(Calendar.MILLISECOND, 0);
1114
1115        boolean gotOne = false;
1116        // loop until we've computed the next time, or we've past the endTime
1117        while (!gotOne) {
1118
1119            //if (endTime != null && cl.getTime().after(endTime)) return null;
1120            if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop...
1121                return null;
1122            }
1123
1124            SortedSet st = null;
1125            int t = 0;
1126
1127            int sec = cl.get(Calendar.SECOND);
1128            int min = cl.get(Calendar.MINUTE);
1129
1130            // get second.................................................
1131            st = seconds.tailSet(new Integer(sec));
1132            if (st != null && st.size() != 0) {
1133                sec = ((Integer) st.first()).intValue();
1134            } else {
1135                sec = ((Integer) seconds.first()).intValue();
1136                min++;
1137                cl.set(Calendar.MINUTE, min);
1138            }
1139            cl.set(Calendar.SECOND, sec);
1140
1141            min = cl.get(Calendar.MINUTE);
1142            int hr = cl.get(Calendar.HOUR_OF_DAY);
1143            t = -1;
1144
1145            // get minute.................................................
1146            st = minutes.tailSet(new Integer(min));
1147            if (st != null && st.size() != 0) {
1148                t = min;
1149                min = ((Integer) st.first()).intValue();
1150            } else {
1151                min = ((Integer) minutes.first()).intValue();
1152                hr++;
1153            }
1154            if (min != t) {
1155                cl.set(Calendar.SECOND, 0);
1156                cl.set(Calendar.MINUTE, min);
1157                setCalendarHour(cl, hr);
1158                continue;
1159            }
1160            cl.set(Calendar.MINUTE, min);
1161
1162            hr = cl.get(Calendar.HOUR_OF_DAY);
1163            int day = cl.get(Calendar.DAY_OF_MONTH);
1164            t = -1;
1165
1166            // get hour...................................................
1167            st = hours.tailSet(new Integer(hr));
1168            if (st != null && st.size() != 0) {
1169                t = hr;
1170                hr = ((Integer) st.first()).intValue();
1171            } else {
1172                hr = ((Integer) hours.first()).intValue();
1173                day++;
1174            }
1175            if (hr != t) {
1176                cl.set(Calendar.SECOND, 0);
1177                cl.set(Calendar.MINUTE, 0);
1178                cl.set(Calendar.DAY_OF_MONTH, day);
1179                setCalendarHour(cl, hr);
1180                continue;
1181            }
1182            cl.set(Calendar.HOUR_OF_DAY, hr);
1183
1184            day = cl.get(Calendar.DAY_OF_MONTH);
1185            int mon = cl.get(Calendar.MONTH) + 1;
1186            // '+ 1' because calendar is 0-based for this field, and we are
1187            // 1-based
1188            t = -1;
1189            int tmon = mon;
1190            
1191            // get day...................................................
1192            boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
1193            boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
1194            if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule
1195                st = daysOfMonth.tailSet(new Integer(day));
1196                if (lastdayOfMonth) {
1197                    if(!nearestWeekday) {
1198                        t = day;
1199                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1200                    } else {
1201                        t = day;
1202                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1203                        
1204                        java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
1205                        tcal.set(Calendar.SECOND, 0);
1206                        tcal.set(Calendar.MINUTE, 0);
1207                        tcal.set(Calendar.HOUR_OF_DAY, 0);
1208                        tcal.set(Calendar.DAY_OF_MONTH, day);
1209                        tcal.set(Calendar.MONTH, mon - 1);
1210                        tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
1211                        
1212                        int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1213                        int dow = tcal.get(Calendar.DAY_OF_WEEK);
1214
1215                        if(dow == Calendar.SATURDAY && day == 1) {
1216                            day += 2;
1217                        } else if(dow == Calendar.SATURDAY) {
1218                            day -= 1;
1219                        } else if(dow == Calendar.SUNDAY && day == ldom) {
1220                            day -= 2;
1221                        } else if(dow == Calendar.SUNDAY) {
1222                            day += 1;
1223                        }
1224                    
1225                        tcal.set(Calendar.SECOND, sec);
1226                        tcal.set(Calendar.MINUTE, min);
1227                        tcal.set(Calendar.HOUR_OF_DAY, hr);
1228                        tcal.set(Calendar.DAY_OF_MONTH, day);
1229                        tcal.set(Calendar.MONTH, mon - 1);
1230                        Date nTime = tcal.getTime();
1231                        if(nTime.before(afterTime)) {
1232                            day = 1;
1233                            mon++;
1234                        }
1235                    }
1236                } else if(nearestWeekday) {
1237                    t = day;
1238                    day = ((Integer) daysOfMonth.first()).intValue();
1239
1240                    java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
1241                    tcal.set(Calendar.SECOND, 0);
1242                    tcal.set(Calendar.MINUTE, 0);
1243                    tcal.set(Calendar.HOUR_OF_DAY, 0);
1244                    tcal.set(Calendar.DAY_OF_MONTH, day);
1245                    tcal.set(Calendar.MONTH, mon - 1);
1246                    tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
1247                    
1248                    int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1249                    int dow = tcal.get(Calendar.DAY_OF_WEEK);
1250
1251                    if(dow == Calendar.SATURDAY && day == 1) {
1252                        day += 2;
1253                    } else if(dow == Calendar.SATURDAY) {
1254                        day -= 1;
1255                    } else if(dow == Calendar.SUNDAY && day == ldom) {
1256                        day -= 2;
1257                    } else if(dow == Calendar.SUNDAY) {
1258                        day += 1;
1259                    }
1260                        
1261                
1262                    tcal.set(Calendar.SECOND, sec);
1263                    tcal.set(Calendar.MINUTE, min);
1264                    tcal.set(Calendar.HOUR_OF_DAY, hr);
1265                    tcal.set(Calendar.DAY_OF_MONTH, day);
1266                    tcal.set(Calendar.MONTH, mon - 1);
1267                    Date nTime = tcal.getTime();
1268                    if(nTime.before(afterTime)) {
1269                        day = ((Integer) daysOfMonth.first()).intValue();
1270                        mon++;
1271                    }
1272                } else if (st != null && st.size() != 0) {
1273                    t = day;
1274                    day = ((Integer) st.first()).intValue();
1275                    // make sure we don't over-run a short month, such as february
1276                    int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1277                    if (day > lastDay) {
1278                        day = ((Integer) daysOfMonth.first()).intValue();
1279                        mon++;
1280                    }
1281                } else {
1282                    day = ((Integer) daysOfMonth.first()).intValue();
1283                    mon++;
1284                }
1285                
1286                if (day != t || mon != tmon) {
1287                    cl.set(Calendar.SECOND, 0);
1288                    cl.set(Calendar.MINUTE, 0);
1289                    cl.set(Calendar.HOUR_OF_DAY, 0);
1290                    cl.set(Calendar.DAY_OF_MONTH, day);
1291                    cl.set(Calendar.MONTH, mon - 1);
1292                    // '- 1' because calendar is 0-based for this field, and we
1293                    // are 1-based
1294                    continue;
1295                }
1296            } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
1297                if (lastdayOfWeek) { // are we looking for the last XXX day of
1298                    // the month?
1299                    int dow = ((Integer) daysOfWeek.first()).intValue(); // desired
1300                    // d-o-w
1301                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
1302                    int daysToAdd = 0;
1303                    if (cDow < dow) {
1304                        daysToAdd = dow - cDow;
1305                    }
1306                    if (cDow > dow) {
1307                        daysToAdd = dow + (7 - cDow);
1308                    }
1309
1310                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1311
1312                    if (day + daysToAdd > lDay) { // did we already miss the
1313                        // last one?
1314                        cl.set(Calendar.SECOND, 0);
1315                        cl.set(Calendar.MINUTE, 0);
1316                        cl.set(Calendar.HOUR_OF_DAY, 0);
1317                        cl.set(Calendar.DAY_OF_MONTH, 1);
1318                        cl.set(Calendar.MONTH, mon);
1319                        // no '- 1' here because we are promoting the month
1320                        continue;
1321                    }
1322
1323                    // find date of last occurance of this day in this month...
1324                    while ((day + daysToAdd + 7) <= lDay) {
1325                        daysToAdd += 7;
1326                    }
1327
1328                    day += daysToAdd;
1329
1330                    if (daysToAdd > 0) {
1331                        cl.set(Calendar.SECOND, 0);
1332                        cl.set(Calendar.MINUTE, 0);
1333                        cl.set(Calendar.HOUR_OF_DAY, 0);
1334                        cl.set(Calendar.DAY_OF_MONTH, day);
1335                        cl.set(Calendar.MONTH, mon - 1);
1336                        // '- 1' here because we are not promoting the month
1337                        continue;
1338                    }
1339
1340                } else if (nthdayOfWeek != 0) {
1341                    // are we looking for the Nth XXX day in the month?
1342                    int dow = ((Integer) daysOfWeek.first()).intValue(); // desired
1343                    // d-o-w
1344                    int 

Large files files are truncated, but you can click here to view the full file