PageRenderTime 58ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/src/java/grails/plugin/jesque/CronExpression.java

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