Clover coverage report -
Coverage timestamp: Sa Jul 7 2007 09:11:40 CEST
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%
coverage coverage
 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    }