PageRenderTime 249ms CodeModel.GetById 81ms app.highlight 137ms RepoModel.GetById 14ms app.codeStats 0ms

/src/main/java/com/wcs/sys/ejbtimer/util/CronExpression.java

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