KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > opensymphony > oscache > util > FastCronParser


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 JavaDoc;
8
9 import java.util.*;
10 import java.util.Calendar JavaDoc;
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: 1.2 $
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 JavaDoc 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     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     public FastCronParser(String JavaDoc cronExpression) throws ParseException JavaDoc {
89         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     public void setCronExpression(String JavaDoc cronExpression) throws ParseException JavaDoc {
100         if (cronExpression == null) {
101             throw new IllegalArgumentException JavaDoc("Cron time expression cannot be null");
102         }
103
104         this.cronExpression = cronExpression;
105         parseExpression(cronExpression);
106     }
107
108     /**
109     * Retrieves the current cron expression.
110     *
111     * @return the current cron expression.
112     */

113     public String JavaDoc getCronExpression() {
114         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     public boolean hasMoreRecentMatch(long time) {
127         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     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
Calendar JavaDoc cal = new GregorianCalendar();
142         cal.setTimeInMillis(time);
143
144         int minute = cal.get(Calendar.MINUTE);
145         int hour = cal.get(Calendar.HOUR_OF_DAY);
146         int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
147         int month = cal.get(Calendar.MONTH) + 1; // Calendar is 0-based for this field, and we are 1-based
148
int year = cal.get(Calendar.YEAR);
149
150         long validMinutes = lookup[MINUTE];
151         long validHours = lookup[HOUR];
152         long validDaysOfMonth = lookup[DAY_OF_MONTH];
153         long validMonths = lookup[MONTH];
154         long validDaysOfWeek = lookup[DAY_OF_WEEK];
155
156         // Find out if we have a Day of Week or Day of Month field
157
boolean haveDOM = validDaysOfMonth != Long.MAX_VALUE;
158         boolean haveDOW = validDaysOfWeek != Long.MAX_VALUE;
159
160         boolean skippedNonLeapYear = false;
161
162         while (true) {
163             boolean retry = false;
164
165             // Clean up the month if it was wrapped in a previous iteration
166
if (month < 1) {
167                 month += 12;
168                 year--;
169             }
170
171             // get month...................................................
172
boolean found = false;
173
174             if (validMonths != Long.MAX_VALUE) {
175                 for (int i = month + 11; i > (month - 1); i--) {
176                     int testMonth = (i % 12) + 1;
177
178                     // Check if the month is valid
179
if (((1L << (testMonth - 1)) & validMonths) != 0) {
180                         if ((testMonth > month) || skippedNonLeapYear) {
181                             year--;
182                         }
183
184                         // Check there are enough days in this month (catches non leap-years trying to match the 29th Feb)
185
int numDays = numberOfDaysInMonth(testMonth, year);
186
187                         if (!haveDOM || (numDays >= lookupMin[DAY_OF_MONTH])) {
188                             if ((month != testMonth) || skippedNonLeapYear) {
189                                 // New DOM = min(maxDOM, prevDays); ie, the highest valid value
190
dayOfMonth = (numDays <= lookupMax[DAY_OF_MONTH]) ? numDays : lookupMax[DAY_OF_MONTH];
191                                 hour = lookupMax[HOUR];
192                                 minute = lookupMax[MINUTE];
193                                 month = testMonth;
194                             }
195
196                             found = true;
197                             break;
198                         }
199                     }
200                 }
201
202                 skippedNonLeapYear = false;
203
204                 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
skippedNonLeapYear = true;
207                     continue;
208                 }
209             }
210
211             // Clean up if the dayOfMonth was wrapped. This takes leap years into account.
212
if (dayOfMonth < 1) {
213                 month--;
214                 dayOfMonth += numberOfDaysInMonth(month, year);
215                 hour = lookupMax[HOUR];
216                 continue;
217             }
218
219             // get day...................................................
220
if (haveDOM && !haveDOW) { // get day using just the DAY_OF_MONTH token
221

222                 int daysInThisMonth = numberOfDaysInMonth(month, year);
223                 int daysInPreviousMonth = numberOfDaysInMonth(month - 1, year);
224
225                 // Find the highest valid day that is below the current day
226
for (int i = dayOfMonth + 30; i > (dayOfMonth - 1); i--) {
227                     int testDayOfMonth = (i % 31) + 1;
228
229                     // Skip over any days that don't actually exist (eg 31st April)
230
if ((testDayOfMonth <= dayOfMonth) && (testDayOfMonth > daysInThisMonth)) {
231                         continue;
232                     }
233
234                     if ((testDayOfMonth > dayOfMonth) && (testDayOfMonth > daysInPreviousMonth)) {
235                         continue;
236                     }
237
238                     if (((1L << (testDayOfMonth - 1)) & validDaysOfMonth) != 0) {
239                         if (testDayOfMonth > dayOfMonth) {
240                             // We've found a valid day, but we had to move back a month
241
month--;
242                             retry = true;
243                         }
244
245                         if (dayOfMonth != testDayOfMonth) {
246                             hour = lookupMax[HOUR];
247                             minute = lookupMax[MINUTE];
248                         }
249
250                         dayOfMonth = testDayOfMonth;
251                         break;
252                     }
253                 }
254
255                 if (retry) {
256                     continue;
257                 }
258             } else if (haveDOW && !haveDOM) { // get day using just the DAY_OF_WEEK token
259

260                 int daysLost = 0;
261                 int currentDOW = dayOfWeek(dayOfMonth, month, year);
262
263                 for (int i = currentDOW + 7; i > currentDOW; i--) {
264                     int testDOW = i % 7;
265
266                     if (((1L << testDOW) & validDaysOfWeek) != 0) {
267                         dayOfMonth -= daysLost;
268
269                         if (dayOfMonth < 1) {
270                             // We've wrapped back a month
271
month--;
272                             dayOfMonth += numberOfDaysInMonth(month, year);
273                             retry = true;
274                         }
275
276                         if (currentDOW != testDOW) {
277                             hour = lookupMax[HOUR];
278                             minute = lookupMax[MINUTE];
279                         }
280
281                         break;
282                     }
283
284                     daysLost++;
285                 }
286
287                 if (retry) {
288                     continue;
289                 }
290             }
291
292             // Clean up if the hour has been wrapped
293
if (hour < 0) {
294                 hour += 24;
295                 dayOfMonth--;
296                 continue;
297             }
298
299             // get hour...................................................
300
if (validHours != Long.MAX_VALUE) {
301                 // Find the highest valid hour that is below the current hour
302
for (int i = hour + 24; i > hour; i--) {
303                     int testHour = i % 24;
304
305                     if (((1L << testHour) & validHours) != 0) {
306                         if (testHour > hour) {
307                             // We've found an hour, but we had to move back a day
308
dayOfMonth--;
309                             retry = true;
310                         }
311
312                         if (hour != testHour) {
313                             minute = lookupMax[MINUTE];
314                         }
315
316                         hour = testHour;
317                         break;
318                     }
319                 }
320
321                 if (retry) {
322                     continue;
323                 }
324             }
325
326             // get minute.................................................
327
if (validMinutes != Long.MAX_VALUE) {
328                 // Find the highest valid minute that is below the current minute
329
for (int i = minute + 60; i > minute; i--) {
330                     int testMinute = i % 60;
331
332                     if (((1L << testMinute) & validMinutes) != 0) {
333                         if (testMinute > minute) {
334                             // We've found a minute, but we had to move back an hour
335
hour--;
336                             retry = true;
337                         }
338
339                         minute = testMinute;
340                         break;
341                     }
342                 }
343
344                 if (retry) {
345                     continue;
346                 }
347             }
348
349             break;
350         }
351
352         // OK, all done. Return the adjusted time value (adjusting this is faster than creating a new Calendar object)
353
cal.set(Calendar.YEAR, year);
354         cal.set(Calendar.MONTH, month - 1); // Calendar is 0-based for this field, and we are 1-based
355
cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
356         cal.set(Calendar.HOUR_OF_DAY, hour);
357         cal.set(Calendar.MINUTE, minute);
358         cal.set(Calendar.SECOND, 0);
359         cal.set(Calendar.MILLISECOND, 0);
360
361         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     private void parseExpression(String JavaDoc expression) throws ParseException JavaDoc {
372         try {
373             // Reset all the lookup data
374
for (int i = 0; i < lookup.length; lookup[i++] = 0) {
375                 lookupMin[i] = Integer.MAX_VALUE;
376                 lookupMax[i] = -1;
377             }
378
379             // Create some character arrays to hold the extracted field values
380
char[][] token = new char[NUMBER_OF_CRON_FIELDS][];
381
382             // Extract the supplied expression into another character array
383
// for speed
384
int length = expression.length();
385             char[] expr = new char[length];
386             expression.getChars(0, length, expr, 0);
387
388             int field = 0;
389             int startIndex = 0;
390             boolean inWhitespace = true;
391
392             // Extract the various cron fields from the expression
393
for (int i = 0; (i < length) && (field < NUMBER_OF_CRON_FIELDS);
394                     i++) {
395                 boolean haveChar = (expr[i] != ' ') && (expr[i] != '\t');
396
397                 if (haveChar) {
398                     // We have a text character of some sort
399
if (inWhitespace) {
400                         startIndex = i; // Remember the start of this token
401
inWhitespace = false;
402                     }
403                 }
404
405                 if (i == (length - 1)) { // Adjustment for when we reach the end of the expression
406
i++;
407                 }
408
409                 if (!(haveChar || inWhitespace) || (i == length)) {
410                     // We've reached the end of a token. Copy it into a new char array
411
token[field] = new char[i - startIndex];
412                     System.arraycopy(expr, startIndex, token[field], 0, i - startIndex);
413                     inWhitespace = true;
414                     field++;
415                 }
416             }
417
418             if (field < NUMBER_OF_CRON_FIELDS) {
419                 throw new ParseException JavaDoc("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
for (field = 0; field < NUMBER_OF_CRON_FIELDS; field++) {
425                 startIndex = 0;
426
427                 boolean inDelimiter = true;
428
429                 // We add each comma-delimited element seperately.
430
int elementLength = token[field].length;
431
432                 for (int i = 0; i < elementLength; i++) {
433                     boolean haveElement = token[field][i] != ',';
434
435                     if (haveElement) {
436                         // We have a character from an element in the token
437
if (inDelimiter) {
438                             startIndex = i;
439                             inDelimiter = false;
440                         }
441                     }
442
443                     if (i == (elementLength - 1)) { // Adjustment for when we reach the end of the token
444
i++;
445                     }
446
447                     if (!(haveElement || inDelimiter) || (i == elementLength)) {
448                         // We've reached the end of an element. Copy it into a new char array
449
char[] element = new char[i - startIndex];
450                         System.arraycopy(token[field], startIndex, element, 0, i - startIndex);
451
452                         // Add the element to our datastructure.
453
storeExpressionValues(element, field);
454
455                         inDelimiter = true;
456                     }
457                 }
458
459                 if (lookup[field] == 0) {
460                     throw new ParseException JavaDoc("Token " + new String JavaDoc(token[field]) + " contains no valid entries for this field.", 0);
461                 }
462             }
463
464             // Remove any months that will never be valid
465
switch (lookupMin[DAY_OF_MONTH]) {
466                 case 31:
467                     lookup[MONTH] &= (0xFFF - 0x528); // Binary 010100101000 - the months that have 30 days
468
case 30:
469                     lookup[MONTH] &= (0xFFF - 0x2); // Binary 000000000010 - February
470

471                     if (lookup[MONTH] == 0) {
472                         throw new ParseException JavaDoc("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
if ((lookup[DAY_OF_MONTH] != Long.MAX_VALUE) && (lookup[DAY_OF_WEEK] != Long.MAX_VALUE)) {
478                 throw new ParseException JavaDoc("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 JavaDoc e) {
481             if (e instanceof ParseException JavaDoc) {
482                 throw (ParseException JavaDoc) e;
483             } else {
484                 throw new ParseException JavaDoc("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     private void storeExpressionValues(char[] element, int field) throws ParseException JavaDoc {
501         int i = 0;
502
503         int start = -99;
504         int end = -99;
505         int interval = -1;
506         boolean wantValue = true;
507         boolean haveInterval = false;
508
509         while ((interval < 0) && (i < element.length)) {
510             char ch = element[i++];
511
512             // Handle the wildcard character - it can only ever occur at the start of an element
513
if ((i == 1) && (ch == '*')) {
514                 // Handle the special case where we have '*' and nothing else
515
if (i >= element.length) {
516                     addToLookup(-1, -1, field, 1);
517                     return;
518                 }
519
520                 start = -1;
521                 end = -1;
522                 wantValue = false;
523                 continue;
524             }
525
526             if (wantValue) {
527                 // Handle any numbers
528
if ((ch >= '0') && (ch <= '9')) {
529                     ValueSet vs = getValue(ch - '0', element, i);
530
531                     if (start == -99) {
532                         start = vs.value;
533                     } else if (!haveInterval) {
534                         end = vs.value;
535                     } else {
536                         if (end == -99) {
537                             end = MAX_VALUE[field];
538                         }
539
540                         interval = vs.value;
541                     }
542
543                     i = vs.pos;
544                     wantValue = false;
545                     continue;
546                 }
547
548                 if (!haveInterval && (end == -99)) {
549                     // Handle any months that have been suplied as words
550
if (field == MONTH) {
551                         if (start == -99) {
552                             start = getMonthVal(ch, element, i++);
553                         } else {
554                             end = getMonthVal(ch, element, i++);
555                         }
556
557                         wantValue = false;
558
559                         // Skip past the rest of the month name
560
while (++i < element.length) {
561                             int c = element[i] | 0x20;
562
563                             if ((c < 'a') || (c > 'z')) {
564                                 break;
565                             }
566                         }
567
568                         continue;
569                     } else if (field == DAY_OF_WEEK) {
570                         if (start == -99) {
571                             start = getDayOfWeekVal(ch, element, i++);
572                         } else {
573                             end = getDayOfWeekVal(ch, element, i++);
574                         }
575
576                         wantValue = false;
577
578                         // Skip past the rest of the day name
579
while (++i < element.length) {
580                             int c = element[i] | 0x20;
581
582                             if ((c < 'a') || (c > 'z')) {
583                                 break;
584                             }
585                         }
586
587                         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
if ((ch == '-') && (start != -99) && (end == -99)) {
593                     wantValue = true;
594                     continue;
595                 }
596
597                 // Handle an interval. An interval is valid as long as we have a start value
598
if ((ch == '/') && (start != -99)) {
599                     wantValue = true;
600                     haveInterval = true;
601                     continue;
602                 }
603             }
604
605             throw makeParseException("Invalid character encountered while parsing element", element, i);
606         }
607
608         if (element.length > i) {
609             throw makeParseException("Extraneous characters found while parsing element", element, i);
610         }
611
612         if (end == -99) {
613             end = start;
614         }
615
616         if (interval < 0) {
617             interval = 1;
618         }
619
620         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     private ValueSet getValue(int value, char[] element, int i) {
633         ValueSet result = new ValueSet();
634         result.value = value;
635
636         if (i >= element.length) {
637             result.pos = i;
638             return result;
639         }
640
641         char ch = element[i];
642
643         while ((ch >= '0') && (ch <= '9')) {
644             result.value = (result.value * 10) + (ch - '0');
645
646             if (++i >= element.length) {
647                 break;
648             }
649
650             ch = element[i];
651         }
652
653         result.pos = i;
654
655         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     private void addToLookup(int start, int end, int field, int interval) throws ParseException JavaDoc {
674         // deal with the supplied range
675
if (start == end) {
676             if (start < 0) {
677                 // We're setting the entire range of values
678
start = lookupMin[field] = MIN_VALUE[field];
679                 end = lookupMax[field] = MAX_VALUE[field];
680
681                 if (interval <= 1) {
682                     lookup[field] = Long.MAX_VALUE;
683                     return;
684                 }
685             } else {
686                 // We're only setting a single value - check that it is in range
687
if (start < MIN_VALUE[field]) {
688                     throw new ParseException JavaDoc("Value " + start + " in field " + field + " is lower than the minimum allowable value for this field (min=" + MIN_VALUE[field] + ")", 0);
689                 } else if (start > MAX_VALUE[field]) {
690                     throw new ParseException JavaDoc("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
if (start > end) {
696                 end ^= start;
697                 start ^= end;
698                 end ^= start;
699             }
700
701             if (start < 0) {
702                 start = MIN_VALUE[field];
703             } else if (start < MIN_VALUE[field]) {
704                 throw new ParseException JavaDoc("Value " + start + " in field " + field + " is lower than the minimum allowable value for this field (min=" + MIN_VALUE[field] + ")", 0);
705             }
706
707             if (end < 0) {
708                 end = MAX_VALUE[field];
709             } else if (end > MAX_VALUE[field]) {
710                 throw new ParseException JavaDoc("Value " + end + " in field " + field + " is higher than the maximum allowable value for this field (max=" + MAX_VALUE[field] + ")", 0);
711             }
712         }
713
714         if (interval < 1) {
715             interval = 1;
716         }
717
718         int i = start - MIN_VALUE[field];
719
720         // Populate the lookup table by setting all the bits corresponding to the valid field values
721
for (i = start - MIN_VALUE[field]; i <= (end - MIN_VALUE[field]);
722                 i += interval) {
723             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
if (lookupMin[field] > start) {
729             lookupMin[field] = start;
730         }
731
732         i += (MIN_VALUE[field] - interval);
733
734         if (lookupMax[field] < i) {
735             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     private boolean isLeapYear(int year) {
747         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     private int dayOfWeek(int day, int month, int year) {
760         day += ((month < 3) ? year-- : (year - 2));
761         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     private int numberOfDaysInMonth(int month, int year) {
776         while (month < 1) {
777             month += 12;
778             year--;
779         }
780
781         while (month > 12) {
782             month -= 12;
783             year++;
784         }
785
786         if (month == 2) {
787             return isLeapYear(year) ? 29 : 28;
788         } else {
789             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     private int getDayOfWeekVal(char ch1, char[] element, int i) throws ParseException JavaDoc {
803         if ((i + 1) >= element.length) {
804             throw makeParseException("Unexpected end of element encountered while parsing a day name", element, i);
805         }
806
807         int ch2 = element[i] | 0x20;
808         int ch3 = element[i + 1] | 0x20;
809
810         switch (ch1 | 0x20) {
811             case 's': // Sunday, Saturday
812

813                 if ((ch2 == 'u') && (ch3 == 'n')) {
814                     return 0;
815                 }
816
817                 if ((ch2 == 'a') && (ch3 == 't')) {
818                     return 6;
819                 }
820
821                 break;
822             case 'm': // Monday
823

824                 if ((ch2 == 'o') && (ch3 == 'n')) {
825                     return 1;
826                 }
827
828                 break;
829             case 't': // Tuesday, Thursday
830

831                 if ((ch2 == 'u') && (ch3 == 'e')) {
832                     return 2;
833                 }
834
835                 if ((ch2 == 'h') && (ch3 == 'u')) {
836                     return 4;
837                 }
838
839                 break;
840             case 'w': // Wednesday
841

842                 if ((ch2 == 'e') && (ch3 == 'd')) {
843                     return 3;
844                 }
845
846                 break;
847             case 'f': // Friday
848

849                 if ((ch2 == 'r') && (ch3 == 'i')) {
850                     return 5;
851                 }
852
853                 break;
854         }
855
856         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     private int getMonthVal(char ch1, char[] element, int i) throws ParseException JavaDoc {
869         if ((i + 1) >= element.length) {
870             throw makeParseException("Unexpected end of element encountered while parsing a month name", element, i);
871         }
872
873         int ch2 = element[i] | 0x20;
874         int ch3 = element[i + 1] | 0x20;
875
876         switch (ch1 | 0x20) {
877             case 'j': // January, June, July
878

879                 if ((ch2 == 'a') && (ch3 == 'n')) {
880                     return 1;
881                 }
882
883                 if (ch2 == 'u') {
884                     if (ch3 == 'n') {
885                         return 6;
886                     }
887
888                     if (ch3 == 'l') {
889                         return 7;
890                     }
891                 }
892
893                 break;
894             case 'f': // February
895

896                 if ((ch2 == 'e') && (ch3 == 'b')) {
897                     return 2;
898                 }
899
900                 break;
901             case 'm': // March, May
902

903                 if (ch2 == 'a') {
904                     if (ch3 == 'r') {
905                         return 3;
906                     }
907
908                     if (ch3 == 'y') {
909                         return 5;
910                     }
911                 }
912
913                 break;
914             case 'a': // April, August
915

916                 if ((ch2 == 'p') && (ch3 == 'r')) {
917                     return 4;
918                 }
919
920                 if ((ch2 == 'u') && (ch3 == 'g')) {
921                     return 8;
922                 }
923
924                 break;
925             case 's': // September
926

927                 if ((ch2 == 'e') && (ch3 == 'p')) {
928                     return 9;
929                 }
930
931                 break;
932             case 'o': // October
933

934                 if ((ch2 == 'c') && (ch3 == 't')) {
935                     return 10;
936                 }
937
938                 break;
939             case 'n': // November
940

941                 if ((ch2 == 'o') && (ch3 == 'v')) {
942                     return 11;
943                 }
944
945                 break;
946             case 'd': // December
947

948                 if ((ch2 == 'e') && (ch3 == 'c')) {
949                     return 12;
950                 }
951
952                 break;
953         }
954
955         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     public String JavaDoc getExpressionSummary() {
966         StringBuffer JavaDoc buf = new StringBuffer JavaDoc();
967
968         buf.append(getExpressionSetSummary(MINUTE)).append(' ');
969         buf.append(getExpressionSetSummary(HOUR)).append(' ');
970         buf.append(getExpressionSetSummary(DAY_OF_MONTH)).append(' ');
971         buf.append(getExpressionSetSummary(MONTH)).append(' ');
972         buf.append(getExpressionSetSummary(DAY_OF_WEEK));
973
974         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     private String JavaDoc getExpressionSetSummary(int field) {
990         if (lookup[field] == Long.MAX_VALUE) {
991             return "*";
992         }
993
994         StringBuffer JavaDoc buf = new StringBuffer JavaDoc();
995
996         boolean first = true;
997
998         for (int i = MIN_VALUE[field]; i <= MAX_VALUE[field]; i++) {
999             if ((lookup[field] & (1L << (i - MIN_VALUE[field]))) != 0) {
1000                if (!first) {
1001                    buf.append(",");
1002                } else {
1003                    first = false;
1004                }
1005
1006                buf.append(String.valueOf(i));
1007            }
1008        }
1009
1010        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    private ParseException JavaDoc makeParseException(String JavaDoc msg, char[] data, int offset) {
1027        char[] buf = new char[msg.length() + data.length + 3];
1028        int msgLen = msg.length();
1029        System.arraycopy(msg.toCharArray(), 0, buf, 0, msgLen);
1030        buf[msgLen] = ' ';
1031        buf[msgLen + 1] = '[';
1032        System.arraycopy(data, 0, buf, msgLen + 2, data.length);
1033        buf[buf.length - 1] = ']';
1034        return new ParseException JavaDoc(new String JavaDoc(buf), offset);
1035    }
1036}
1037
1038
1039class ValueSet {
1040    public int pos;
1041    public int value;
1042}
1043
Popular Tags