Clover coverage report - Coverage timestamp: Sa Jul 7 2007 09:11:40 CEST FRAMES NO FRAMES |
file stats: | LOC: | 1.042 |  | Methods: | 18
| NCLOC: | 611 |  | Classes: | 2
Source file | Conditionals | Statements | Methods | TOTAL
FastCronParser.java | 94,8% | 97,6% | 94,4% | 96,5% |

1 |  | /*
2 |  | * Copyright (c) 2002-2003 by OpenSymphony
3 |  | * All rights reserved.
4 |  | */
5 |  | package com.opensymphony.oscache.util;
6 |  |
7 |  | import java.text.ParseException;
8 |  |
9 |  | import java.util.*;
10 |  | import java.util.Calendar;
11 |  |
12 |  | /**
13 |  | * Parses cron expressions and determines at what time in the past is the
14 |  | * most recent match for the supplied expression.
15 |  | *
16 |  | * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
17 |  | * @author $Author: ltorunski $
18 |  | * @version $Revision: 340 $
19 |  | */
20 |  | public class FastCronParser {
21 |  | private static final int NUMBER_OF_CRON_FIELDS = 5;
22 |  | private static final int MINUTE = 0;
23 |  | private static final int HOUR = 1;
24 |  | private static final int DAY_OF_MONTH = 2;
25 |  | private static final int MONTH = 3;
26 |  | private static final int DAY_OF_WEEK = 4;
27 |  |
28 |  | // Lookup tables that hold the min/max/size of each of the above field types.
29 |  | // These tables are precalculated for performance.
30 |  | private static final int[] MIN_VALUE = {0, 0, 1, 1, 0};
31 |  | private static final int[] MAX_VALUE = {59, 23, 31, 12, 6};
32 |  |
33 |  | /**
34 |  | * A lookup table holding the number of days in each month (with the obvious exception
35 |  | * that February requires special handling).
36 |  | */
37 |  | private static final int[] DAYS_IN_MONTH = {
38 |  | 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
39 |  | };
40 |  |
41 |  | /**
42 |  | * Holds the raw cron expression that this parser is handling.
43 |  | */
44 |  | private String cronExpression = null;
45 |  |
46 |  | /**
47 |  | * This is the main lookup table that holds a parsed cron expression. each long
48 |  | * represents one of the above field types. Bits in each long value correspond
49 |  | * to one of the possbile field values - eg, for the minute field, bits 0 -> 59 in
50 |  | * <code>lookup[MINUTE]</code> map to minutes 0 -> 59 respectively. Bits are set if
51 |  | * the corresponding value is enabled. So if the minute field in the cron expression
52 |  | * was <code>"0,2-8,50"</code>, bits 0, 2, 3, 4, 5, 6, 7, 8 and 50 will be set.
53 |  | * If the cron expression is <code>"*"</code>, the long value is set to
54 |  | * <code>Long.MAX_VALUE</code>.
55 |  | */
56 |  | private long[] lookup = {
57 |  | Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE,
58 |  | Long.MAX_VALUE
59 |  | };
60 |  |
61 |  | /**
62 |  | * This is based on the contents of the <code>lookup</code> table. It holds the
63 |  | * <em>highest</em> valid field value for each field type.
64 |  | */
65 |  | private int[] lookupMax = {-1, -1, -1, -1, -1};
66 |  |
67 |  | /**
68 |  | * This is based on the contents of the <code>lookup</code> table. It holds the
69 |  | * <em>lowest</em> valid field value for each field type.
70 |  | */
71 |  | private int[] lookupMin = {
72 |  | Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,
73 |  | Integer.MAX_VALUE, Integer.MAX_VALUE
74 |  | };
75 |  |
76 |  | /**
77 |  | * Creates a FastCronParser that uses a default cron expression of <code>"* * * * *"</cron>.
78 |  | * This will match any time that is supplied.
79 |  | */
80 | 25 | public FastCronParser() {
81 |  | }
82 |  |
83 |  | /**
84 |  | * Constructs a new FastCronParser based on the supplied expression.
85 |  | *
86 |  | * @throws ParseException if the supplied expression is not a valid cron expression.
87 |  | */
88 | 395 | public FastCronParser(String cronExpression) throws ParseException {
89 | 395 | setCronExpression(cronExpression);
90 |  | }
91 |  |
92 |  | /**
93 |  | * Resets the cron expression to the value supplied.
94 |  | *
95 |  | * @param cronExpression the new cron expression.
96 |  | *
97 |  | * @throws ParseException if the supplied expression is not a valid cron expression.
98 |  | */
99 | 10050415 | public void setCronExpression(String cronExpression) throws ParseException {
100 | 10050415 | if (cronExpression == null) {
101 | 5 | throw new IllegalArgumentException("Cron time expression cannot be null");
102 |  | }
103 |  |
104 | 10050410 | this.cronExpression = cronExpression;
105 | 10050410 | parseExpression(cronExpression);
106 |  | }
107 |  |
108 |  | /**
109 |  | * Retrieves the current cron expression.
110 |  | *
111 |  | * @return the current cron expression.
112 |  | */
113 | 15 | public String getCronExpression() {
114 | 15 | return this.cronExpression;
115 |  | }
116 |  |
117 |  | /**
118 |  | * Determines whether this cron expression matches a date/time that is more recent
119 |  | * than the one supplied.
120 |  | *
121 |  | * @param time The time to compare the cron expression against.
122 |  | *
123 |  | * @return <code>true</code> if the cron expression matches a time that is closer
124 |  | * to the current time than the supplied time is, <code>false</code> otherwise.
125 |  | */
126 | 0 | public boolean hasMoreRecentMatch(long time) {
127 | 0 | return time < getTimeBefore(System.currentTimeMillis());
128 |  | }
129 |  |
130 |  | /**
131 |  | * Find the most recent time that matches this cron expression. This time will
132 |  | * always be in the past, ie a lower value than the supplied time.
133 |  | *
134 |  | * @param time The time (in milliseconds) that we're using as our upper bound.
135 |  | *
136 |  | * @return The time (in milliseconds) when this cron event last occurred.
137 |  | */
138 | 20050210 | public long getTimeBefore(long time) {
139 |  | // It would be nice to get rid of the Calendar class for speed, but it's a lot of work...
140 |  | // We create this
141 | 20050210 | Calendar cal = new GregorianCalendar();
142 | 20050210 | cal.setTimeInMillis(time);
143 |  |
144 | 20050210 | int minute = cal.get(Calendar.MINUTE);
145 | 20050210 | int hour = cal.get(Calendar.HOUR_OF_DAY);
146 | 20050210 | int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
147 | 20050210 | int month = cal.get(Calendar.MONTH) + 1; // Calendar is 0-based for this field, and we are 1-based
148 | 20050210 | int year = cal.get(Calendar.YEAR);
149 |  |
150 | 20050210 | long validMinutes = lookup[MINUTE];
151 | 20050210 | long validHours = lookup[HOUR];
152 | 20050210 | long validDaysOfMonth = lookup[DAY_OF_MONTH];
153 | 20050210 | long validMonths = lookup[MONTH];
154 | 20050210 | long validDaysOfWeek = lookup[DAY_OF_WEEK];
155 |  |
156 |  | // Find out if we have a Day of Week or Day of Month field
157 | 20050210 | boolean haveDOM = validDaysOfMonth != Long.MAX_VALUE;
158 | 20050210 | boolean haveDOW = validDaysOfWeek != Long.MAX_VALUE;
159 |  |
160 | 20050210 | boolean skippedNonLeapYear = false;
161 |  |
162 | 20050210 | while (true) {
163 | 20050395 | boolean retry = false;
164 |  |
165 |  | // Clean up the month if it was wrapped in a previous iteration
166 | 20050395 | if (month < 1) {
167 | 35 | month += 12;
168 | 35 | year--;
169 |  | }
170 |  |
171 |  | // get month...................................................
172 | 20050395 | boolean found = false;
173 |  |
174 | 20050395 | if (validMonths != Long.MAX_VALUE) {
175 | 10050135 | for (int i = month + 11; i > (month - 1); i--) {
176 | 110600945 | int testMonth = (i % 12) + 1;
177 |  |
178 |  | // Check if the month is valid
179 | 110600945 | if (((1L << (testMonth - 1)) & validMonths) != 0) {
180 | 10050135 | if ((testMonth > month) || skippedNonLeapYear) {
181 | 10050075 | year--;
182 |  | }
183 |  |
184 |  | // Check there are enough days in this month (catches non leap-years trying to match the 29th Feb)
185 | 10050135 | int numDays = numberOfDaysInMonth(testMonth, year);
186 |  |
187 | 10050135 | if (!haveDOM || (numDays >= lookupMin[DAY_OF_MONTH])) {
188 | 10050090 | if ((month != testMonth) || skippedNonLeapYear) {
189 |  | // New DOM = min(maxDOM, prevDays); ie, the highest valid value
190 | 10050070 | dayOfMonth = (numDays <= lookupMax[DAY_OF_MONTH]) ? numDays : lookupMax[DAY_OF_MONTH];
191 | 10050070 | hour = lookupMax[HOUR];
192 | 10050070 | minute = lookupMax[MINUTE];
193 | 10050070 | month = testMonth;
194 |  | }
195 |  |
196 | 10050090 | found = true;
197 | 10050090 | break;
198 |  | }
199 |  | }
200 |  | }
201 |  |
202 | 10050135 | skippedNonLeapYear = false;
203 |  |
204 | 10050135 | if (!found) {
205 |  | // The only time we drop out here is when we're searching for the 29th of February and no other date!
206 | 45 | skippedNonLeapYear = true;
207 | 45 | continue;
208 |  | }
209 |  | }
210 |  |
211 |  | // Clean up if the dayOfMonth was wrapped. This takes leap years into account.
212 | 20050350 | if (dayOfMonth < 1) {
213 | 25 | month--;
214 | 25 | dayOfMonth += numberOfDaysInMonth(month, year);
215 | 25 | hour = lookupMax[HOUR];
216 | 25 | continue;
217 |  | }
218 |  |
219 |  | // get day...................................................
220 | 20050325 | if (haveDOM && !haveDOW) { // get day using just the DAY_OF_MONTH token
221 |  |
222 | 10050090 | int daysInThisMonth = numberOfDaysInMonth(month, year);
223 | 10050090 | int daysInPreviousMonth = numberOfDaysInMonth(month - 1, year);
224 |  |
225 |  | // Find the highest valid day that is below the current day
226 | 10050415 | for (int i = dayOfMonth + 30; i > (dayOfMonth - 1); i--) {
227 | 10050415 | int testDayOfMonth = (i % 31) + 1;
228 |  |
229 |  | // Skip over any days that don't actually exist (eg 31st April)
230 | 10050415 | if ((testDayOfMonth <= dayOfMonth) && (testDayOfMonth > daysInThisMonth)) {
231 | 0 | continue;
232 |  | }
233 |  |
234 | 10050415 | if ((testDayOfMonth > dayOfMonth) && (testDayOfMonth > daysInPreviousMonth)) {
235 | 5 | continue;
236 |  | }
237 |  |
238 | 10050410 | if (((1L << (testDayOfMonth - 1)) & validDaysOfMonth) != 0) {
239 | 10050090 | if (testDayOfMonth > dayOfMonth) {
240 |  | // We've found a valid day, but we had to move back a month
241 | 30 | month--;
242 | 30 | retry = true;
243 |  | }
244 |  |
245 | 10050090 | if (dayOfMonth != testDayOfMonth) {
246 | 35 | hour = lookupMax[HOUR];
247 | 35 | minute = lookupMax[MINUTE];
248 |  | }
249 |  |
250 | 10050090 | dayOfMonth = testDayOfMonth;
251 | 10050090 | break;
252 |  | }
253 |  | }
254 |  |
255 | 10050090 | if (retry) {
256 | 30 | continue;
257 |  | }
258 | 10000235 | } else if (haveDOW && !haveDOM) { // get day using just the DAY_OF_WEEK token
259 |  |
260 | 75 | int daysLost = 0;
261 | 75 | int currentDOW = dayOfWeek(dayOfMonth, month, year);
262 |  |
263 | 260 | for (int i = currentDOW + 7; i > currentDOW; i--) {
264 | 260 | int testDOW = i % 7;
265 |  |
266 | 260 | if (((1L << testDOW) & validDaysOfWeek) != 0) {
267 | 75 | dayOfMonth -= daysLost;
268 |  |
269 | 75 | if (dayOfMonth < 1) {
270 |  | // We've wrapped back a month
271 | 20 | month--;
272 | 20 | dayOfMonth += numberOfDaysInMonth(month, year);
273 | 20 | retry = true;
274 |  | }
275 |  |
276 | 75 | if (currentDOW != testDOW) {
277 | 50 | hour = lookupMax[HOUR];
278 | 50 | minute = lookupMax[MINUTE];
279 |  | }
280 |  |
281 | 75 | break;
282 |  | }
283 |  |
284 | 185 | daysLost++;
285 |  | }
286 |  |
287 | 75 | if (retry) {
288 | 20 | continue;
289 |  | }
290 |  | }
291 |  |
292 |  | // Clean up if the hour has been wrapped
293 | 20050275 | if (hour < 0) {
294 | 15 | hour += 24;
295 | 15 | dayOfMonth--;
296 | 15 | continue;
297 |  | }
298 |  |
299 |  | // get hour...................................................
300 | 20050260 | if (validHours != Long.MAX_VALUE) {
301 |  | // Find the highest valid hour that is below the current hour
302 | 10050620 | for (int i = hour + 24; i > hour; i--) {
303 | 10050620 | int testHour = i % 24;
304 |  |
305 | 10050620 | if (((1L << testHour) & validHours) != 0) {
306 | 10050150 | if (testHour > hour) {
307 |  | // We've found an hour, but we had to move back a day
308 | 20 | dayOfMonth--;
309 | 20 | retry = true;
310 |  | }
311 |  |
312 | 10050150 | if (hour != testHour) {
313 | 40 | minute = lookupMax[MINUTE];
314 |  | }
315 |  |
316 | 10050150 | hour = testHour;
317 | 10050150 | break;
318 |  | }
319 |  | }
320 |  |
321 | 10050150 | if (retry) {
322 | 20 | continue;
323 |  | }
324 |  | }
325 |  |
326 |  | // get minute.................................................
327 | 20050240 | if (validMinutes != Long.MAX_VALUE) {
328 |  | // Find the highest valid minute that is below the current minute
329 | 10051055 | for (int i = minute + 60; i > minute; i--) {
330 | 10051055 | int testMinute = i % 60;
331 |  |
332 | 10051055 | if (((1L << testMinute) & validMinutes) != 0) {
333 | 10050150 | if (testMinute > minute) {
334 |  | // We've found a minute, but we had to move back an hour
335 | 30 | hour--;
336 | 30 | retry = true;
337 |  | }
338 |  |
339 | 10050150 | minute = testMinute;
340 | 10050150 | break;
341 |  | }
342 |  | }
343 |  |
344 | 10050150 | if (retry) {
345 | 30 | continue;
346 |  | }
347 |  | }
348 |  |
349 | 20050210 | break;
350 |  | }
351 |  |
352 |  | // OK, all done. Return the adjusted time value (adjusting this is faster than creating a new Calendar object)
353 | 20050210 | cal.set(Calendar.YEAR, year);
354 | 20050210 | cal.set(Calendar.MONTH, month - 1); // Calendar is 0-based for this field, and we are 1-based
355 | 20050210 | cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
356 | 20050210 | cal.set(Calendar.HOUR_OF_DAY, hour);
357 | 20050210 | cal.set(Calendar.MINUTE, minute);
358 | 20050210 | cal.set(Calendar.SECOND, 0);
359 | 20050210 | cal.set(Calendar.MILLISECOND, 0);
360 |  |
361 | 20050210 | return cal.getTime().getTime();
362 |  | }
363 |  |
364 |  | /**
365 |  | * Takes a cron expression as an input parameter, and extracts from it the
366 |  | * relevant minutes/hours/days/months that the expression matches.
367 |  | *
368 |  | * @param expression A valid cron expression.
369 |  | * @throws ParseException If the supplied expression could not be parsed.
370 |  | */
371 | 10050410 | private void parseExpression(String expression) throws ParseException {
372 | 10050410 | try {
373 |  | // Reset all the lookup data
374 | 10050410 | for (int i = 0; i < lookup.length; lookup[i++] = 0) {
375 | 50252050 | lookupMin[i] = Integer.MAX_VALUE;
376 | 50252050 | lookupMax[i] = -1;
377 |  | }
378 |  |
379 |  | // Create some character arrays to hold the extracted field values
380 | 10050410 | char[][] token = new char[NUMBER_OF_CRON_FIELDS][];
381 |  |
382 |  | // Extract the supplied expression into another character array
383 |  | // for speed
384 | 10050410 | int length = expression.length();
385 | 10050410 | char[] expr = new char[length];
386 | 10050410 | expression.getChars(0, length, expr, 0);
387 |  |
388 | 10050410 | int field = 0;
389 | 10050410 | int startIndex = 0;
390 | 10050410 | boolean inWhitespace = true;
391 |  |
392 |  | // Extract the various cron fields from the expression
393 | 10050410 | for (int i = 0; (i < length) && (field < NUMBER_OF_CRON_FIELDS);
394 |  | i++) {
395 | 250655680 | boolean haveChar = (expr[i] != ' ') && (expr[i] != '\t');
396 |  |
397 | 250655680 | if (haveChar) {
398 |  | // We have a text character of some sort
399 | 210454025 | if (inWhitespace) {
400 | 50252020 | startIndex = i; // Remember the start of this token
401 | 50252020 | inWhitespace = false;
402 |  | }
403 |  | }
404 |  |
405 | 250655680 | if (i == (length - 1)) { // Adjustment for when we reach the end of the expression
406 | 10050405 | i++;
407 |  | }
408 |  |
409 | 250655680 | if (!(haveChar || inWhitespace) || (i == length)) {
410 |  | // We've reached the end of a token. Copy it into a new char array
411 | 50252020 | token[field] = new char[i - startIndex];
412 | 50252020 | System.arraycopy(expr, startIndex, token[field], 0, i - startIndex);
413 | 50252020 | inWhitespace = true;
414 | 50252020 | field++;
415 |  | }
416 |  | }
417 |  |
418 | 10050410 | if (field < NUMBER_OF_CRON_FIELDS) {
419 | 10 | throw new ParseException("Unexpected end of expression while parsing \"" + expression + "\". Cron expressions require 5 separate fields.", length);
420 |  | }
421 |  |
422 |  | // OK, we've broken the string up into the 5 cron fields, now lets add
423 |  | // each field to their lookup table.
424 | 10050400 | for (field = 0; field < NUMBER_OF_CRON_FIELDS; field++) {
425 | 50251820 | startIndex = 0;
426 |  |
427 | 50251820 | boolean inDelimiter = true;
428 |  |
429 |  | // We add each comma-delimited element seperately.
430 | 50251820 | int elementLength = token[field].length;
431 |  |
432 | 50251820 | for (int i = 0; i < elementLength; i++) {
433 | 210453815 | boolean haveElement = token[field][i] != ',';
434 |  |
435 | 210453815 | if (haveElement) {
436 |  | // We have a character from an element in the token
437 | 170453620 | if (inDelimiter) {
438 | 90252015 | startIndex = i;
439 | 90252015 | inDelimiter = false;
440 |  | }
441 |  | }
442 |  |
443 | 210453815 | if (i == (elementLength - 1)) { // Adjustment for when we reach the end of the token
444 | 50251820 | i++;
445 |  | }
446 |  |
447 | 210453815 | if (!(haveElement || inDelimiter) || (i == elementLength)) {
448 |  | // We've reached the end of an element. Copy it into a new char array
449 | 90252015 | char[] element = new char[i - startIndex];
450 | 90252015 | System.arraycopy(token[field], startIndex, element, 0, i - startIndex);
451 |  |
452 |  | // Add the element to our datastructure.
453 | 90252015 | storeExpressionValues(element, field);
454 |  |
455 | 90251870 | inDelimiter = true;
456 |  | }
457 |  | }
458 |  |
459 | 50251675 | if (lookup[field] == 0) {
460 | 0 | throw new ParseException("Token " + new String(token[field]) + " contains no valid entries for this field.", 0);
461 |  | }
462 |  | }
463 |  |
464 |  | // Remove any months that will never be valid
465 | 10050255 | switch (lookupMin[DAY_OF_MONTH]) {
466 | 5 | case 31:
467 | 5 | lookup[MONTH] &= (0xFFF - 0x528); // Binary 010100101000 - the months that have 30 days
468 | 10 | case 30:
469 | 15 | lookup[MONTH] &= (0xFFF - 0x2); // Binary 000000000010 - February
470 |  |
471 | 15 | if (lookup[MONTH] == 0) {
472 | 10 | throw new ParseException("The cron expression \"" + expression + "\" will never match any months - the day of month field is out of range.", 0);
473 |  | }
474 |  | }
475 |  |
476 |  | // Check that we don't have both a day of month and a day of week field.
477 | 10050245 | if ((lookup[DAY_OF_MONTH] != Long.MAX_VALUE) && (lookup[DAY_OF_WEEK] != Long.MAX_VALUE)) {
478 | 5 | throw new ParseException("The cron expression \"" + expression + "\" is invalid. Having both a day-of-month and day-of-week field is not supported.", 0);
479 |  | }
480 |  | } catch (Exception e) {
481 | 170 | if (e instanceof ParseException) {
482 | 170 | throw (ParseException) e;
483 |  | } else {
484 | 0 | throw new ParseException("Illegal cron expression format (" + e.toString() + ")", 0);
485 |  | }
486 |  | }
487 |  | }
488 |  |
489 |  | /**
490 |  | * Stores the values for the supplied cron element into the specified field.
491 |  | *
492 |  | * @param element The cron element to store. A cron element is a single component
493 |  | * of a cron expression. For example, the complete set of elements for the cron expression
494 |  | * <code>30 0,6,12,18 * * *</code> would be <code>{"30", "0", "6", "12", "18", "*", "*", "*"}</code>.
495 |  | * @param field The field that this expression belongs to. Valid values are {@link #MINUTE},
496 |  | * {@link #HOUR}, {@link #DAY_OF_MONTH}, {@link #MONTH} and {@link #DAY_OF_WEEK}.
497 |  | *
498 |  | * @throws ParseException if there was a problem parsing the supplied element.
499 |  | */
500 | 90252015 | private void storeExpressionValues(char[] element, int field) throws ParseException {
501 | 90252015 | int i = 0;
502 |  |
503 | 90252015 | int start = -99;
504 | 90252015 | int end = -99;
505 | 90252015 | int interval = -1;
506 | 90252015 | boolean wantValue = true;
507 | 90252015 | boolean haveInterval = false;
508 |  |
509 | 90252015 | while ((interval < 0) && (i < element.length)) {
510 | 130252415 | char ch = element[i++];
511 |  |
512 |  | // Handle the wildcard character - it can only ever occur at the start of an element
513 | 130252415 | if ((i == 1) && (ch == '*')) {
514 |  | // Handle the special case where we have '*' and nothing else
515 | 30051165 | if (i >= element.length) {
516 | 30051160 | addToLookup(-1, -1, field, 1);
517 | 30051160 | return;
518 |  | }
519 |  |
520 | 5 | start = -1;
521 | 5 | end = -1;
522 | 5 | wantValue = false;
523 | 5 | continue;
524 |  | }
525 |  |
526 | 100201250 | if (wantValue) {
527 |  | // Handle any numbers
528 | 80201045 | if ((ch >= '0') && (ch <= '9')) {
529 | 80200805 | ValueSet vs = getValue(ch - '0', element, i);
530 |  |
531 | 80200805 | if (start == -99) {
532 | 60200650 | start = vs.value;
533 | 20000155 | } else if (!haveInterval) {
534 | 20000100 | end = vs.value;
535 |  | } else {
536 | 55 | if (end == -99) {
537 | 30 | end = MAX_VALUE[field];
538 |  | }
539 |  |
540 | 55 | interval = vs.value;
541 |  | }
542 |  |
543 | 80200805 | i = vs.pos;
544 | 80200805 | wantValue = false;
545 | 80200805 | continue;
546 |  | }
547 |  |
548 | 240 | if (!haveInterval && (end == -99)) {
549 |  | // Handle any months that have been suplied as words
550 | 240 | if (field == MONTH) {
551 | 135 | if (start == -99) {
552 | 120 | start = getMonthVal(ch, element, i++);
553 |  | } else {
554 | 15 | end = getMonthVal(ch, element, i++);
555 |  | }
556 |  |
557 | 85 | wantValue = false;
558 |  |
559 |  | // Skip past the rest of the month name
560 | 85 | while (++i < element.length) {
561 | 135 | int c = element[i] | 0x20;
562 |  |
563 | 135 | if ((c < 'a') || (c > 'z')) {
564 | 20 | break;
565 |  | }
566 |  | }
567 |  |
568 | 85 | continue;
569 | 105 | } else if (field == DAY_OF_WEEK) {
570 | 90 | if (start == -99) {
571 | 70 | start = getDayOfWeekVal(ch, element, i++);
572 |  | } else {
573 | 20 | end = getDayOfWeekVal(ch, element, i++);
574 |  | }
575 |  |
576 | 60 | wantValue = false;
577 |  |
578 |  | // Skip past the rest of the day name
579 | 60 | while (++i < element.length) {
580 | 120 | int c = element[i] | 0x20;
581 |  |
582 | 120 | if ((c < 'a') || (c > 'z')) {
583 | 20 | break;
584 |  | }
585 |  | }
586 |  |
587 | 60 | continue;
588 |  | }
589 |  | }
590 |  | } else {
591 |  | // Handle the range character. A range character is only valid if we have a start but no end value
592 | 20000205 | if ((ch == '-') && (start != -99) && (end == -99)) {
593 | 20000140 | wantValue = true;
594 | 20000140 | continue;
595 |  | }
596 |  |
597 |  | // Handle an interval. An interval is valid as long as we have a start value
598 | 65 | if ((ch == '/') && (start != -99)) {
599 | 55 | wantValue = true;
600 | 55 | haveInterval = true;
601 | 55 | continue;
602 |  | }
603 |  | }
604 |  |
605 | 25 | throw makeParseException("Invalid character encountered while parsing element", element, i);
606 |  | }
607 |  |
608 | 60200750 | if (element.length > i) {
609 | 5 | throw makeParseException("Extraneous characters found while parsing element", element, i);
610 |  | }
611 |  |
612 | 60200745 | if (end == -99) {
613 | 40200590 | end = start;
614 |  | }
615 |  |
616 | 60200745 | if (interval < 0) {
617 | 60200695 | interval = 1;
618 |  | }
619 |  |
620 | 60200745 | addToLookup(start, end, field, interval);
621 |  | }
622 |  |
623 |  | /**
624 |  | * Extracts a numerical value from inside a character array.
625 |  | *
626 |  | * @param value The value of the first character
627 |  | * @param element The character array we're extracting the value from
628 |  | * @param i The index into the array of the next character to process
629 |  | *
630 |  | * @return the new index and the extracted value
631 |  | */
632 | 80200805 | private ValueSet getValue(int value, char[] element, int i) {
633 | 80200805 | ValueSet result = new ValueSet();
634 | 80200805 | result.value = value;
635 |  |
636 | 80200805 | if (i >= element.length) {
637 | 30000365 | result.pos = i;
638 | 30000365 | return result;
639 |  | }
640 |  |
641 | 50200440 | char ch = element[i];
642 |  |
643 | 50200440 | while ((ch >= '0') && (ch <= '9')) {
644 | 40200330 | result.value = (result.value * 10) + (ch - '0');
645 |  |
646 | 40200330 | if (++i >= element.length) {
647 | 30200275 | break;
648 |  | }
649 |  |
650 | 10000055 | ch = element[i];
651 |  | }
652 |  |
653 | 50200440 | result.pos = i;
654 |  |
655 | 50200440 | return result;
656 |  | }
657 |  |
658 |  | /**
659 |  | * Adds a group of valid values to the lookup table for the specified field. This method
660 |  | * handles ranges that increase in arbitrary step sizes. It is also possible to add a single
661 |  | * value by specifying a range with the same start and end values.
662 |  | *
663 |  | * @param start The starting value for the range. Supplying a value that is less than zero
664 |  | * will cause the minimum allowable value for the specified field to be used as the start value.
665 |  | * @param end The maximum value that can be added (ie the upper bound). If the step size is
666 |  | * greater than one, this maximum value may not necessarily end up being added. Supplying a
667 |  | * value that is less than zero will cause the maximum allowable value for the specified field
668 |  | * to be used as the upper bound.
669 |  | * @param field The field that the values should be added to.
670 |  | * @param interval Specifies the step size for the range. Any values less than one will be
671 |  | * treated as a single step interval.
672 |  | */
673 | 90251905 | private void addToLookup(int start, int end, int field, int interval) throws ParseException {
674 |  | // deal with the supplied range
675 | 90251905 | if (start == end) {
676 | 70251755 | if (start < 0) {
677 |  | // We're setting the entire range of values
678 | 30051165 | start = lookupMin[field] = MIN_VALUE[field];
679 | 30051165 | end = lookupMax[field] = MAX_VALUE[field];
680 |  |
681 | 30051165 | if (interval <= 1) {
682 | 30051160 | lookup[field] = Long.MAX_VALUE;
683 | 30051160 | return;
684 |  | }
685 |  | } else {
686 |  | // We're only setting a single value - check that it is in range
687 | 40200590 | if (start < MIN_VALUE[field]) {
688 | 5 | throw new ParseException("Value " + start + " in field " + field + " is lower than the minimum allowable value for this field (min=" + MIN_VALUE[field] + ")", 0);
689 | 40200585 | } else if (start > MAX_VALUE[field]) {
690 | 10 | throw new ParseException("Value " + start + " in field " + field + " is higher than the maximum allowable value for this field (max=" + MAX_VALUE[field] + ")", 0);
691 |  | }
692 |  | }
693 |  | } else {
694 |  | // For ranges, if the start is bigger than the end value then swap them over
695 | 20000150 | if (start > end) {
696 | 10 | end ^= start;
697 | 10 | start ^= end;
698 | 10 | end ^= start;
699 |  | }
700 |  |
701 | 20000150 | if (start < 0) {
702 | 0 | start = MIN_VALUE[field];
703 | 20000150 | } else if (start < MIN_VALUE[field]) {
704 | 10 | throw new ParseException("Value " + start + " in field " + field + " is lower than the minimum allowable value for this field (min=" + MIN_VALUE[field] + ")", 0);
705 |  | }
706 |  |
707 | 20000140 | if (end < 0) {
708 | 0 | end = MAX_VALUE[field];
709 | 20000140 | } else if (end > MAX_VALUE[field]) {
710 | 10 | throw new ParseException("Value " + end + " in field " + field + " is higher than the maximum allowable value for this field (max=" + MAX_VALUE[field] + ")", 0);
711 |  | }
712 |  | }
713 |  |
714 | 60200710 | if (interval < 1) {
715 | 0 | interval = 1;
716 |  | }
717 |  |
718 | 60200710 | int i = start - MIN_VALUE[field];
719 |  |
720 |  | // Populate the lookup table by setting all the bits corresponding to the valid field values
721 | 60200710 | for (i = start - MIN_VALUE[field]; i <= (end - MIN_VALUE[field]);
722 |  | i += interval) {
723 | 435201595 | lookup[field] |= (1L << i);
724 |  | }
725 |  |
726 |  | // Make sure we remember the minimum value set so far
727 |  | // Keep track of the highest and lowest values that have been added to this field so far
728 | 60200710 | if (lookupMin[field] > start) {
729 | 20200520 | lookupMin[field] = start;
730 |  | }
731 |  |
732 | 60200710 | i += (MIN_VALUE[field] - interval);
733 |  |
734 | 60200710 | if (lookupMax[field] < i) {
735 | 25200655 | lookupMax[field] = i;
736 |  | }
737 |  | }
738 |  |
739 |  | /**
740 |  | * Indicates if a year is a leap year or not.
741 |  | *
742 |  | * @param year The year to check
743 |  | *
744 |  | * @return <code>true</code> if the year is a leap year, <code>false</code> otherwise.
745 |  | */
746 | 10100115 | private boolean isLeapYear(int year) {
747 | 10100115 | return (((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0);
748 |  | }
749 |  |
750 |  | /**
751 |  | * Calculate the day of the week. Sunday = 0, Monday = 1, ... , Saturday = 6. The formula
752 |  | * used is an optimized version of Zeller's Congruence.
753 |  | *
754 |  | * @param day The day of the month (1-31)
755 |  | * @param month The month (1 - 12)
756 |  | * @param year The year
757 |  | * @return
758 |  | */
759 | 75 | private int dayOfWeek(int day, int month, int year) {
760 | 75 | day += ((month < 3) ? year-- : (year - 2));
761 | 75 | return ((((23 * month) / 9) + day + 4 + (year / 4)) - (year / 100) + (year / 400)) % 7;
762 |  | }
763 |  |
764 |  | /**
765 |  | * Retrieves the number of days in the supplied month, taking into account leap years.
766 |  | * If the month value is outside the range <code>MIN_VALUE[MONTH] - MAX_VALUE[MONTH]</code>
767 |  | * then the year will be adjusted accordingly and the correct number of days will still
768 |  | * be returned.
769 |  | *
770 |  | * @param month The month of interest.
771 |  | * @param year The year we are checking.
772 |  | *
773 |  | * @return The number of days in the month.
774 |  | */
775 | 30150360 | private int numberOfDaysInMonth(int month, int year) {
776 | 30150360 | while (month < 1) {
777 | 40 | month += 12;
778 | 40 | year--;
779 |  | }
780 |  |
781 | 30150360 | while (month > 12) {
782 | 0 | month -= 12;
783 | 0 | year++;
784 |  | }
785 |  |
786 | 30150360 | if (month == 2) {
787 | 10100115 | return isLeapYear(year) ? 29 : 28;
788 |  | } else {
789 | 20050245 | return DAYS_IN_MONTH[month - 1];
790 |  | }
791 |  | }
792 |  |
793 |  | /**
794 |  | * Quickly retrieves the day of week value (Sun = 0, ... Sat = 6) that corresponds to the
795 |  | * day name that is specified in the character array. Only the first 3 characters are taken
796 |  | * into account; the rest are ignored.
797 |  | *
798 |  | * @param element The character array
799 |  | * @param i The index to start looking at
800 |  | * @return The day of week value
801 |  | */
802 | 90 | private int getDayOfWeekVal(char ch1, char[] element, int i) throws ParseException {
803 | 90 | if ((i + 1) >= element.length) {
804 | 5 | throw makeParseException("Unexpected end of element encountered while parsing a day name", element, i);
805 |  | }
806 |  |
807 | 85 | int ch2 = element[i] | 0x20;
808 | 85 | int ch3 = element[i + 1] | 0x20;
809 |  |
810 | 85 | switch (ch1 | 0x20) {
811 | 30 | case 's': // Sunday, Saturday
812 |  |
813 | 30 | if ((ch2 == 'u') && (ch3 == 'n')) {
814 | 15 | return 0;
815 |  | }
816 |  |
817 | 15 | if ((ch2 == 'a') && (ch3 == 't')) {
818 | 10 | return 6;
819 |  | }
820 |  |
821 | 5 | break;
822 | 10 | case 'm': // Monday
823 |  |
824 | 10 | if ((ch2 == 'o') && (ch3 == 'n')) {
825 | 5 | return 1;
826 |  | }
827 |  |
828 | 5 | break;
829 | 25 | case 't': // Tuesday, Thursday
830 |  |
831 | 25 | if ((ch2 == 'u') && (ch3 == 'e')) {
832 | 10 | return 2;
833 |  | }
834 |  |
835 | 15 | if ((ch2 == 'h') && (ch3 == 'u')) {
836 | 10 | return 4;
837 |  | }
838 |  |
839 | 5 | break;
840 | 10 | case 'w': // Wednesday
841 |  |
842 | 10 | if ((ch2 == 'e') && (ch3 == 'd')) {
843 | 5 | return 3;
844 |  | }
845 |  |
846 | 5 | break;
847 | 10 | case 'f': // Friday
848 |  |
849 | 10 | if ((ch2 == 'r') && (ch3 == 'i')) {
850 | 5 | return 5;
851 |  | }
852 |  |
853 | 5 | break;
854 |  | }
855 |  |
856 | 25 | throw makeParseException("Unexpected character while parsing a day name", element, i - 1);
857 |  | }
858 |  |
859 |  | /**
860 |  | * Quickly retrieves the month value (Jan = 1, ..., Dec = 12) that corresponds to the month
861 |  | * name that is specified in the character array. Only the first 3 characters are taken
862 |  | * into account; the rest are ignored.
863 |  | *
864 |  | * @param element The character array
865 |  | * @param i The index to start looking at
866 |  | * @return The month value
867 |  | */
868 | 135 | private int getMonthVal(char ch1, char[] element, int i) throws ParseException {
869 | 135 | if ((i + 1) >= element.length) {
870 | 0 | throw makeParseException("Unexpected end of element encountered while parsing a month name", element, i);
871 |  | }
872 |  |
873 | 135 | int ch2 = element[i] | 0x20;
874 | 135 | int ch3 = element[i + 1] | 0x20;
875 |  |
876 | 135 | switch (ch1 | 0x20) {
877 | 35 | case 'j': // January, June, July
878 |  |
879 | 35 | if ((ch2 == 'a') && (ch3 == 'n')) {
880 | 10 | return 1;
881 |  | }
882 |  |
883 | 25 | if (ch2 == 'u') {
884 | 20 | if (ch3 == 'n') {
885 | 10 | return 6;
886 |  | }
887 |  |
888 | 10 | if (ch3 == 'l') {
889 | 5 | return 7;
890 |  | }
891 |  | }
892 |  |
893 | 10 | break;
894 | 15 | case 'f': // February
895 |  |
896 | 15 | if ((ch2 == 'e') && (ch3 == 'b')) {
897 | 10 | return 2;
898 |  | }
899 |  |
900 | 5 | break;
901 | 20 | case 'm': // March, May
902 |  |
903 | 20 | if (ch2 == 'a') {
904 | 15 | if (ch3 == 'r') {
905 | 5 | return 3;
906 |  | }
907 |  |
908 | 10 | if (ch3 == 'y') {
909 | 5 | return 5;
910 |  | }
911 |  | }
912 |  |
913 | 10 | break;
914 | 25 | case 'a': // April, August
915 |  |
916 | 25 | if ((ch2 == 'p') && (ch3 == 'r')) {
917 | 10 | return 4;
918 |  | }
919 |  |
920 | 15 | if ((ch2 == 'u') && (ch3 == 'g')) {
921 | 10 | return 8;
922 |  | }
923 |  |
924 | 5 | break;
925 | 10 | case 's': // September
926 |  |
927 | 10 | if ((ch2 == 'e') && (ch3 == 'p')) {
928 | 5 | return 9;
929 |  | }
930 |  |
931 | 5 | break;
932 | 10 | case 'o': // October
933 |  |
934 | 10 | if ((ch2 == 'c') && (ch3 == 't')) {
935 | 5 | return 10;
936 |  | }
937 |  |
938 | 5 | break;
939 | 10 | case 'n': // November
940 |  |
941 | 10 | if ((ch2 == 'o') && (ch3 == 'v')) {
942 | 5 | return 11;
943 |  | }
944 |  |
945 | 5 | break;
946 | 10 | case 'd': // December
947 |  |
948 | 10 | if ((ch2 == 'e') && (ch3 == 'c')) {
949 | 5 | return 12;
950 |  | }
951 |  |
952 | 5 | break;
953 |  | }
954 |  |
955 | 50 | throw makeParseException("Unexpected character while parsing a month name", element, i - 1);
956 |  | }
957 |  |
958 |  | /**
959 |  | * Recreates the original human-readable cron expression based on the internal
960 |  | * datastructure values.
961 |  | *
962 |  | * @return A cron expression that corresponds to the current state of the
963 |  | * internal data structure.
964 |  | */
965 | 20 | public String getExpressionSummary() {
966 | 20 | StringBuffer buf = new StringBuffer();
967 |  |
968 | 20 | buf.append(getExpressionSetSummary(MINUTE)).append(' ');
969 | 20 | buf.append(getExpressionSetSummary(HOUR)).append(' ');
970 | 20 | buf.append(getExpressionSetSummary(DAY_OF_MONTH)).append(' ');
971 | 20 | buf.append(getExpressionSetSummary(MONTH)).append(' ');
972 | 20 | buf.append(getExpressionSetSummary(DAY_OF_WEEK));
973 |  |
974 | 20 | return buf.toString();
975 |  | }
976 |  |
977 |  | /**
978 |  | * <p>Converts the internal datastructure that holds a particular cron field into
979 |  | * a human-readable list of values of the field's contents. For example, if the
980 |  | * <code>DAY_OF_WEEK</code> field was submitted that had Sunday and Monday specified,
981 |  | * the string <code>0,1</code> would be returned.</p>
982 |  | *
983 |  | * <p>If the field contains all possible values, <code>*</code> will be returned.
984 |  | *
985 |  | * @param field The field.
986 |  | *
987 |  | * @return A human-readable string representation of the field's contents.
988 |  | */
989 | 100 | private String getExpressionSetSummary(int field) {
990 | 100 | if (lookup[field] == Long.MAX_VALUE) {
991 | 65 | return "*";
992 |  | }
993 |  |
994 | 35 | StringBuffer buf = new StringBuffer();
995 |  |
996 | 35 | boolean first = true;
997 |  |
998 | 35 | for (int i = MIN_VALUE[field]; i <= MAX_VALUE[field]; i++) {
999 | 1235 | if ((lookup[field] & (1L << (i - MIN_VALUE[field]))) != 0) {
1000 | 170 | if (!first) {
1001 | 135 | buf.append(",");
1002 |  | } else {
1003 | 35 | first = false;
1004 |  | }
1005 |  |
1006 | 170 | buf.append(String.valueOf(i));
1007 |  | }
1008 |  | }
1009 |  |
1010 | 35 | return buf.toString();
1011 |  | }
1012 |  |
1013 |  | /**
1014 |  | * Makes a <code>ParseException</code>. The exception message is constructed by
1015 |  | * taking the given message parameter and appending the supplied character data
1016 |  | * to the end of it. for example, if <code>msg == "Invalid character
1017 |  | * encountered"</code> and <code>data == {'A','g','u','s','t'}</code>, the resultant
1018 |  | * error message would be <code>"Invalid character encountered [Agust]"</code>.
1019 |  | *
1020 |  | * @param msg The error message
1021 |  | * @param data The data that the message
1022 |  | * @param offset The offset into the data where the error was encountered.
1023 |  | *
1024 |  | * @return a newly created <code>ParseException</code> object.
1025 |  | */
1026 | 110 | private ParseException makeParseException(String msg, char[] data, int offset) {
1027 | 110 | char[] buf = new char[msg.length() + data.length + 3];
1028 | 110 | int msgLen = msg.length();
1029 | 110 | System.arraycopy(msg.toCharArray(), 0, buf, 0, msgLen);
1030 | 110 | buf[msgLen] = ' ';
1031 | 110 | buf[msgLen + 1] = '[';
1032 | 110 | System.arraycopy(data, 0, buf, msgLen + 2, data.length);
1033 | 110 | buf[buf.length - 1] = ']';
1034 | 110 | return new ParseException(new String(buf), offset);
1035 |  | }
1036 |  | }
1037 |  |
1038 |  |
1039 |  | class ValueSet {
1040 |  | public int pos;
1041 |  | public int value;
1042 |  | }