0001: /*
0002: * Copyright (c) 2002-2003 by OpenSymphony
0003: * All rights reserved.
0004: */
0005: package com.opensymphony.oscache.util;
0006:
0007: import java.text.ParseException;
0008:
0009: import java.util.*;
0010: import java.util.Calendar;
0011:
0012: /**
0013: * Parses cron expressions and determines at what time in the past is the
0014: * most recent match for the supplied expression.
0015: *
0016: * @author <a href="mailto:chris@swebtec.com">Chris Miller</a>
0017: * @author $Author: ltorunski $
0018: * @version $Revision: 340 $
0019: */
0020: public class FastCronParser {
0021: private static final int NUMBER_OF_CRON_FIELDS = 5;
0022: private static final int MINUTE = 0;
0023: private static final int HOUR = 1;
0024: private static final int DAY_OF_MONTH = 2;
0025: private static final int MONTH = 3;
0026: private static final int DAY_OF_WEEK = 4;
0027:
0028: // Lookup tables that hold the min/max/size of each of the above field types.
0029: // These tables are precalculated for performance.
0030: private static final int[] MIN_VALUE = { 0, 0, 1, 1, 0 };
0031: private static final int[] MAX_VALUE = { 59, 23, 31, 12, 6 };
0032:
0033: /**
0034: * A lookup table holding the number of days in each month (with the obvious exception
0035: * that February requires special handling).
0036: */
0037: private static final int[] DAYS_IN_MONTH = { 31, 29, 31, 30, 31,
0038: 30, 31, 31, 30, 31, 30, 31 };
0039:
0040: /**
0041: * Holds the raw cron expression that this parser is handling.
0042: */
0043: private String cronExpression = null;
0044:
0045: /**
0046: * This is the main lookup table that holds a parsed cron expression. each long
0047: * represents one of the above field types. Bits in each long value correspond
0048: * to one of the possbile field values - eg, for the minute field, bits 0 -> 59 in
0049: * <code>lookup[MINUTE]</code> map to minutes 0 -> 59 respectively. Bits are set if
0050: * the corresponding value is enabled. So if the minute field in the cron expression
0051: * was <code>"0,2-8,50"</code>, bits 0, 2, 3, 4, 5, 6, 7, 8 and 50 will be set.
0052: * If the cron expression is <code>"*"</code>, the long value is set to
0053: * <code>Long.MAX_VALUE</code>.
0054: */
0055: private long[] lookup = { Long.MAX_VALUE, Long.MAX_VALUE,
0056: Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE };
0057:
0058: /**
0059: * This is based on the contents of the <code>lookup</code> table. It holds the
0060: * <em>highest</em> valid field value for each field type.
0061: */
0062: private int[] lookupMax = { -1, -1, -1, -1, -1 };
0063:
0064: /**
0065: * This is based on the contents of the <code>lookup</code> table. It holds the
0066: * <em>lowest</em> valid field value for each field type.
0067: */
0068: private int[] lookupMin = { Integer.MAX_VALUE, Integer.MAX_VALUE,
0069: Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE };
0070:
0071: /**
0072: * Creates a FastCronParser that uses a default cron expression of <code>"* * * * *"</cron>.
0073: * This will match any time that is supplied.
0074: */
0075: public FastCronParser() {
0076: }
0077:
0078: /**
0079: * Constructs a new FastCronParser based on the supplied expression.
0080: *
0081: * @throws ParseException if the supplied expression is not a valid cron expression.
0082: */
0083: public FastCronParser(String cronExpression) throws ParseException {
0084: setCronExpression(cronExpression);
0085: }
0086:
0087: /**
0088: * Resets the cron expression to the value supplied.
0089: *
0090: * @param cronExpression the new cron expression.
0091: *
0092: * @throws ParseException if the supplied expression is not a valid cron expression.
0093: */
0094: public void setCronExpression(String cronExpression)
0095: throws ParseException {
0096: if (cronExpression == null) {
0097: throw new IllegalArgumentException(
0098: "Cron time expression cannot be null");
0099: }
0100:
0101: this .cronExpression = cronExpression;
0102: parseExpression(cronExpression);
0103: }
0104:
0105: /**
0106: * Retrieves the current cron expression.
0107: *
0108: * @return the current cron expression.
0109: */
0110: public String getCronExpression() {
0111: return this .cronExpression;
0112: }
0113:
0114: /**
0115: * Determines whether this cron expression matches a date/time that is more recent
0116: * than the one supplied.
0117: *
0118: * @param time The time to compare the cron expression against.
0119: *
0120: * @return <code>true</code> if the cron expression matches a time that is closer
0121: * to the current time than the supplied time is, <code>false</code> otherwise.
0122: */
0123: public boolean hasMoreRecentMatch(long time) {
0124: return time < getTimeBefore(System.currentTimeMillis());
0125: }
0126:
0127: /**
0128: * Find the most recent time that matches this cron expression. This time will
0129: * always be in the past, ie a lower value than the supplied time.
0130: *
0131: * @param time The time (in milliseconds) that we're using as our upper bound.
0132: *
0133: * @return The time (in milliseconds) when this cron event last occurred.
0134: */
0135: public long getTimeBefore(long time) {
0136: // It would be nice to get rid of the Calendar class for speed, but it's a lot of work...
0137: // We create this
0138: Calendar cal = new GregorianCalendar();
0139: cal.setTimeInMillis(time);
0140:
0141: int minute = cal.get(Calendar.MINUTE);
0142: int hour = cal.get(Calendar.HOUR_OF_DAY);
0143: int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
0144: int month = cal.get(Calendar.MONTH) + 1; // Calendar is 0-based for this field, and we are 1-based
0145: int year = cal.get(Calendar.YEAR);
0146:
0147: long validMinutes = lookup[MINUTE];
0148: long validHours = lookup[HOUR];
0149: long validDaysOfMonth = lookup[DAY_OF_MONTH];
0150: long validMonths = lookup[MONTH];
0151: long validDaysOfWeek = lookup[DAY_OF_WEEK];
0152:
0153: // Find out if we have a Day of Week or Day of Month field
0154: boolean haveDOM = validDaysOfMonth != Long.MAX_VALUE;
0155: boolean haveDOW = validDaysOfWeek != Long.MAX_VALUE;
0156:
0157: boolean skippedNonLeapYear = false;
0158:
0159: while (true) {
0160: boolean retry = false;
0161:
0162: // Clean up the month if it was wrapped in a previous iteration
0163: if (month < 1) {
0164: month += 12;
0165: year--;
0166: }
0167:
0168: // get month...................................................
0169: boolean found = false;
0170:
0171: if (validMonths != Long.MAX_VALUE) {
0172: for (int i = month + 11; i > (month - 1); i--) {
0173: int testMonth = (i % 12) + 1;
0174:
0175: // Check if the month is valid
0176: if (((1L << (testMonth - 1)) & validMonths) != 0) {
0177: if ((testMonth > month) || skippedNonLeapYear) {
0178: year--;
0179: }
0180:
0181: // Check there are enough days in this month (catches non leap-years trying to match the 29th Feb)
0182: int numDays = numberOfDaysInMonth(testMonth,
0183: year);
0184:
0185: if (!haveDOM
0186: || (numDays >= lookupMin[DAY_OF_MONTH])) {
0187: if ((month != testMonth)
0188: || skippedNonLeapYear) {
0189: // New DOM = min(maxDOM, prevDays); ie, the highest valid value
0190: dayOfMonth = (numDays <= lookupMax[DAY_OF_MONTH]) ? numDays
0191: : lookupMax[DAY_OF_MONTH];
0192: hour = lookupMax[HOUR];
0193: minute = lookupMax[MINUTE];
0194: month = testMonth;
0195: }
0196:
0197: found = true;
0198: break;
0199: }
0200: }
0201: }
0202:
0203: skippedNonLeapYear = false;
0204:
0205: if (!found) {
0206: // The only time we drop out here is when we're searching for the 29th of February and no other date!
0207: skippedNonLeapYear = true;
0208: continue;
0209: }
0210: }
0211:
0212: // Clean up if the dayOfMonth was wrapped. This takes leap years into account.
0213: if (dayOfMonth < 1) {
0214: month--;
0215: dayOfMonth += numberOfDaysInMonth(month, year);
0216: hour = lookupMax[HOUR];
0217: continue;
0218: }
0219:
0220: // get day...................................................
0221: if (haveDOM && !haveDOW) { // get day using just the DAY_OF_MONTH token
0222:
0223: int daysInThisMonth = numberOfDaysInMonth(month, year);
0224: int daysInPreviousMonth = numberOfDaysInMonth(
0225: month - 1, year);
0226:
0227: // Find the highest valid day that is below the current day
0228: for (int i = dayOfMonth + 30; i > (dayOfMonth - 1); i--) {
0229: int testDayOfMonth = (i % 31) + 1;
0230:
0231: // Skip over any days that don't actually exist (eg 31st April)
0232: if ((testDayOfMonth <= dayOfMonth)
0233: && (testDayOfMonth > daysInThisMonth)) {
0234: continue;
0235: }
0236:
0237: if ((testDayOfMonth > dayOfMonth)
0238: && (testDayOfMonth > daysInPreviousMonth)) {
0239: continue;
0240: }
0241:
0242: if (((1L << (testDayOfMonth - 1)) & validDaysOfMonth) != 0) {
0243: if (testDayOfMonth > dayOfMonth) {
0244: // We've found a valid day, but we had to move back a month
0245: month--;
0246: retry = true;
0247: }
0248:
0249: if (dayOfMonth != testDayOfMonth) {
0250: hour = lookupMax[HOUR];
0251: minute = lookupMax[MINUTE];
0252: }
0253:
0254: dayOfMonth = testDayOfMonth;
0255: break;
0256: }
0257: }
0258:
0259: if (retry) {
0260: continue;
0261: }
0262: } else if (haveDOW && !haveDOM) { // get day using just the DAY_OF_WEEK token
0263:
0264: int daysLost = 0;
0265: int currentDOW = dayOfWeek(dayOfMonth, month, year);
0266:
0267: for (int i = currentDOW + 7; i > currentDOW; i--) {
0268: int testDOW = i % 7;
0269:
0270: if (((1L << testDOW) & validDaysOfWeek) != 0) {
0271: dayOfMonth -= daysLost;
0272:
0273: if (dayOfMonth < 1) {
0274: // We've wrapped back a month
0275: month--;
0276: dayOfMonth += numberOfDaysInMonth(month,
0277: year);
0278: retry = true;
0279: }
0280:
0281: if (currentDOW != testDOW) {
0282: hour = lookupMax[HOUR];
0283: minute = lookupMax[MINUTE];
0284: }
0285:
0286: break;
0287: }
0288:
0289: daysLost++;
0290: }
0291:
0292: if (retry) {
0293: continue;
0294: }
0295: }
0296:
0297: // Clean up if the hour has been wrapped
0298: if (hour < 0) {
0299: hour += 24;
0300: dayOfMonth--;
0301: continue;
0302: }
0303:
0304: // get hour...................................................
0305: if (validHours != Long.MAX_VALUE) {
0306: // Find the highest valid hour that is below the current hour
0307: for (int i = hour + 24; i > hour; i--) {
0308: int testHour = i % 24;
0309:
0310: if (((1L << testHour) & validHours) != 0) {
0311: if (testHour > hour) {
0312: // We've found an hour, but we had to move back a day
0313: dayOfMonth--;
0314: retry = true;
0315: }
0316:
0317: if (hour != testHour) {
0318: minute = lookupMax[MINUTE];
0319: }
0320:
0321: hour = testHour;
0322: break;
0323: }
0324: }
0325:
0326: if (retry) {
0327: continue;
0328: }
0329: }
0330:
0331: // get minute.................................................
0332: if (validMinutes != Long.MAX_VALUE) {
0333: // Find the highest valid minute that is below the current minute
0334: for (int i = minute + 60; i > minute; i--) {
0335: int testMinute = i % 60;
0336:
0337: if (((1L << testMinute) & validMinutes) != 0) {
0338: if (testMinute > minute) {
0339: // We've found a minute, but we had to move back an hour
0340: hour--;
0341: retry = true;
0342: }
0343:
0344: minute = testMinute;
0345: break;
0346: }
0347: }
0348:
0349: if (retry) {
0350: continue;
0351: }
0352: }
0353:
0354: break;
0355: }
0356:
0357: // OK, all done. Return the adjusted time value (adjusting this is faster than creating a new Calendar object)
0358: cal.set(Calendar.YEAR, year);
0359: cal.set(Calendar.MONTH, month - 1); // Calendar is 0-based for this field, and we are 1-based
0360: cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
0361: cal.set(Calendar.HOUR_OF_DAY, hour);
0362: cal.set(Calendar.MINUTE, minute);
0363: cal.set(Calendar.SECOND, 0);
0364: cal.set(Calendar.MILLISECOND, 0);
0365:
0366: return cal.getTime().getTime();
0367: }
0368:
0369: /**
0370: * Takes a cron expression as an input parameter, and extracts from it the
0371: * relevant minutes/hours/days/months that the expression matches.
0372: *
0373: * @param expression A valid cron expression.
0374: * @throws ParseException If the supplied expression could not be parsed.
0375: */
0376: private void parseExpression(String expression)
0377: throws ParseException {
0378: try {
0379: // Reset all the lookup data
0380: for (int i = 0; i < lookup.length; lookup[i++] = 0) {
0381: lookupMin[i] = Integer.MAX_VALUE;
0382: lookupMax[i] = -1;
0383: }
0384:
0385: // Create some character arrays to hold the extracted field values
0386: char[][] token = new char[NUMBER_OF_CRON_FIELDS][];
0387:
0388: // Extract the supplied expression into another character array
0389: // for speed
0390: int length = expression.length();
0391: char[] expr = new char[length];
0392: expression.getChars(0, length, expr, 0);
0393:
0394: int field = 0;
0395: int startIndex = 0;
0396: boolean inWhitespace = true;
0397:
0398: // Extract the various cron fields from the expression
0399: for (int i = 0; (i < length)
0400: && (field < NUMBER_OF_CRON_FIELDS); i++) {
0401: boolean haveChar = (expr[i] != ' ')
0402: && (expr[i] != '\t');
0403:
0404: if (haveChar) {
0405: // We have a text character of some sort
0406: if (inWhitespace) {
0407: startIndex = i; // Remember the start of this token
0408: inWhitespace = false;
0409: }
0410: }
0411:
0412: if (i == (length - 1)) { // Adjustment for when we reach the end of the expression
0413: i++;
0414: }
0415:
0416: if (!(haveChar || inWhitespace) || (i == length)) {
0417: // We've reached the end of a token. Copy it into a new char array
0418: token[field] = new char[i - startIndex];
0419: System.arraycopy(expr, startIndex, token[field], 0,
0420: i - startIndex);
0421: inWhitespace = true;
0422: field++;
0423: }
0424: }
0425:
0426: if (field < NUMBER_OF_CRON_FIELDS) {
0427: throw new ParseException(
0428: "Unexpected end of expression while parsing \""
0429: + expression
0430: + "\". Cron expressions require 5 separate fields.",
0431: length);
0432: }
0433:
0434: // OK, we've broken the string up into the 5 cron fields, now lets add
0435: // each field to their lookup table.
0436: for (field = 0; field < NUMBER_OF_CRON_FIELDS; field++) {
0437: startIndex = 0;
0438:
0439: boolean inDelimiter = true;
0440:
0441: // We add each comma-delimited element seperately.
0442: int elementLength = token[field].length;
0443:
0444: for (int i = 0; i < elementLength; i++) {
0445: boolean haveElement = token[field][i] != ',';
0446:
0447: if (haveElement) {
0448: // We have a character from an element in the token
0449: if (inDelimiter) {
0450: startIndex = i;
0451: inDelimiter = false;
0452: }
0453: }
0454:
0455: if (i == (elementLength - 1)) { // Adjustment for when we reach the end of the token
0456: i++;
0457: }
0458:
0459: if (!(haveElement || inDelimiter)
0460: || (i == elementLength)) {
0461: // We've reached the end of an element. Copy it into a new char array
0462: char[] element = new char[i - startIndex];
0463: System.arraycopy(token[field], startIndex,
0464: element, 0, i - startIndex);
0465:
0466: // Add the element to our datastructure.
0467: storeExpressionValues(element, field);
0468:
0469: inDelimiter = true;
0470: }
0471: }
0472:
0473: if (lookup[field] == 0) {
0474: throw new ParseException(
0475: "Token "
0476: + new String(token[field])
0477: + " contains no valid entries for this field.",
0478: 0);
0479: }
0480: }
0481:
0482: // Remove any months that will never be valid
0483: switch (lookupMin[DAY_OF_MONTH]) {
0484: case 31:
0485: lookup[MONTH] &= (0xFFF - 0x528); // Binary 010100101000 - the months that have 30 days
0486: case 30:
0487: lookup[MONTH] &= (0xFFF - 0x2); // Binary 000000000010 - February
0488:
0489: if (lookup[MONTH] == 0) {
0490: throw new ParseException(
0491: "The cron expression \""
0492: + expression
0493: + "\" will never match any months - the day of month field is out of range.",
0494: 0);
0495: }
0496: }
0497:
0498: // Check that we don't have both a day of month and a day of week field.
0499: if ((lookup[DAY_OF_MONTH] != Long.MAX_VALUE)
0500: && (lookup[DAY_OF_WEEK] != Long.MAX_VALUE)) {
0501: throw new ParseException(
0502: "The cron expression \""
0503: + expression
0504: + "\" is invalid. Having both a day-of-month and day-of-week field is not supported.",
0505: 0);
0506: }
0507: } catch (Exception e) {
0508: if (e instanceof ParseException) {
0509: throw (ParseException) e;
0510: } else {
0511: throw new ParseException(
0512: "Illegal cron expression format ("
0513: + e.toString() + ")", 0);
0514: }
0515: }
0516: }
0517:
0518: /**
0519: * Stores the values for the supplied cron element into the specified field.
0520: *
0521: * @param element The cron element to store. A cron element is a single component
0522: * of a cron expression. For example, the complete set of elements for the cron expression
0523: * <code>30 0,6,12,18 * * *</code> would be <code>{"30", "0", "6", "12", "18", "*", "*", "*"}</code>.
0524: * @param field The field that this expression belongs to. Valid values are {@link #MINUTE},
0525: * {@link #HOUR}, {@link #DAY_OF_MONTH}, {@link #MONTH} and {@link #DAY_OF_WEEK}.
0526: *
0527: * @throws ParseException if there was a problem parsing the supplied element.
0528: */
0529: private void storeExpressionValues(char[] element, int field)
0530: throws ParseException {
0531: int i = 0;
0532:
0533: int start = -99;
0534: int end = -99;
0535: int interval = -1;
0536: boolean wantValue = true;
0537: boolean haveInterval = false;
0538:
0539: while ((interval < 0) && (i < element.length)) {
0540: char ch = element[i++];
0541:
0542: // Handle the wildcard character - it can only ever occur at the start of an element
0543: if ((i == 1) && (ch == '*')) {
0544: // Handle the special case where we have '*' and nothing else
0545: if (i >= element.length) {
0546: addToLookup(-1, -1, field, 1);
0547: return;
0548: }
0549:
0550: start = -1;
0551: end = -1;
0552: wantValue = false;
0553: continue;
0554: }
0555:
0556: if (wantValue) {
0557: // Handle any numbers
0558: if ((ch >= '0') && (ch <= '9')) {
0559: ValueSet vs = getValue(ch - '0', element, i);
0560:
0561: if (start == -99) {
0562: start = vs.value;
0563: } else if (!haveInterval) {
0564: end = vs.value;
0565: } else {
0566: if (end == -99) {
0567: end = MAX_VALUE[field];
0568: }
0569:
0570: interval = vs.value;
0571: }
0572:
0573: i = vs.pos;
0574: wantValue = false;
0575: continue;
0576: }
0577:
0578: if (!haveInterval && (end == -99)) {
0579: // Handle any months that have been suplied as words
0580: if (field == MONTH) {
0581: if (start == -99) {
0582: start = getMonthVal(ch, element, i++);
0583: } else {
0584: end = getMonthVal(ch, element, i++);
0585: }
0586:
0587: wantValue = false;
0588:
0589: // Skip past the rest of the month name
0590: while (++i < element.length) {
0591: int c = element[i] | 0x20;
0592:
0593: if ((c < 'a') || (c > 'z')) {
0594: break;
0595: }
0596: }
0597:
0598: continue;
0599: } else if (field == DAY_OF_WEEK) {
0600: if (start == -99) {
0601: start = getDayOfWeekVal(ch, element, i++);
0602: } else {
0603: end = getDayOfWeekVal(ch, element, i++);
0604: }
0605:
0606: wantValue = false;
0607:
0608: // Skip past the rest of the day name
0609: while (++i < element.length) {
0610: int c = element[i] | 0x20;
0611:
0612: if ((c < 'a') || (c > 'z')) {
0613: break;
0614: }
0615: }
0616:
0617: continue;
0618: }
0619: }
0620: } else {
0621: // Handle the range character. A range character is only valid if we have a start but no end value
0622: if ((ch == '-') && (start != -99) && (end == -99)) {
0623: wantValue = true;
0624: continue;
0625: }
0626:
0627: // Handle an interval. An interval is valid as long as we have a start value
0628: if ((ch == '/') && (start != -99)) {
0629: wantValue = true;
0630: haveInterval = true;
0631: continue;
0632: }
0633: }
0634:
0635: throw makeParseException(
0636: "Invalid character encountered while parsing element",
0637: element, i);
0638: }
0639:
0640: if (element.length > i) {
0641: throw makeParseException(
0642: "Extraneous characters found while parsing element",
0643: element, i);
0644: }
0645:
0646: if (end == -99) {
0647: end = start;
0648: }
0649:
0650: if (interval < 0) {
0651: interval = 1;
0652: }
0653:
0654: addToLookup(start, end, field, interval);
0655: }
0656:
0657: /**
0658: * Extracts a numerical value from inside a character array.
0659: *
0660: * @param value The value of the first character
0661: * @param element The character array we're extracting the value from
0662: * @param i The index into the array of the next character to process
0663: *
0664: * @return the new index and the extracted value
0665: */
0666: private ValueSet getValue(int value, char[] element, int i) {
0667: ValueSet result = new ValueSet();
0668: result.value = value;
0669:
0670: if (i >= element.length) {
0671: result.pos = i;
0672: return result;
0673: }
0674:
0675: char ch = element[i];
0676:
0677: while ((ch >= '0') && (ch <= '9')) {
0678: result.value = (result.value * 10) + (ch - '0');
0679:
0680: if (++i >= element.length) {
0681: break;
0682: }
0683:
0684: ch = element[i];
0685: }
0686:
0687: result.pos = i;
0688:
0689: return result;
0690: }
0691:
0692: /**
0693: * Adds a group of valid values to the lookup table for the specified field. This method
0694: * handles ranges that increase in arbitrary step sizes. It is also possible to add a single
0695: * value by specifying a range with the same start and end values.
0696: *
0697: * @param start The starting value for the range. Supplying a value that is less than zero
0698: * will cause the minimum allowable value for the specified field to be used as the start value.
0699: * @param end The maximum value that can be added (ie the upper bound). If the step size is
0700: * greater than one, this maximum value may not necessarily end up being added. Supplying a
0701: * value that is less than zero will cause the maximum allowable value for the specified field
0702: * to be used as the upper bound.
0703: * @param field The field that the values should be added to.
0704: * @param interval Specifies the step size for the range. Any values less than one will be
0705: * treated as a single step interval.
0706: */
0707: private void addToLookup(int start, int end, int field, int interval)
0708: throws ParseException {
0709: // deal with the supplied range
0710: if (start == end) {
0711: if (start < 0) {
0712: // We're setting the entire range of values
0713: start = lookupMin[field] = MIN_VALUE[field];
0714: end = lookupMax[field] = MAX_VALUE[field];
0715:
0716: if (interval <= 1) {
0717: lookup[field] = Long.MAX_VALUE;
0718: return;
0719: }
0720: } else {
0721: // We're only setting a single value - check that it is in range
0722: if (start < MIN_VALUE[field]) {
0723: throw new ParseException(
0724: "Value "
0725: + start
0726: + " in field "
0727: + field
0728: + " is lower than the minimum allowable value for this field (min="
0729: + MIN_VALUE[field] + ")", 0);
0730: } else if (start > MAX_VALUE[field]) {
0731: throw new ParseException(
0732: "Value "
0733: + start
0734: + " in field "
0735: + field
0736: + " is higher than the maximum allowable value for this field (max="
0737: + MAX_VALUE[field] + ")", 0);
0738: }
0739: }
0740: } else {
0741: // For ranges, if the start is bigger than the end value then swap them over
0742: if (start > end) {
0743: end ^= start;
0744: start ^= end;
0745: end ^= start;
0746: }
0747:
0748: if (start < 0) {
0749: start = MIN_VALUE[field];
0750: } else if (start < MIN_VALUE[field]) {
0751: throw new ParseException(
0752: "Value "
0753: + start
0754: + " in field "
0755: + field
0756: + " is lower than the minimum allowable value for this field (min="
0757: + MIN_VALUE[field] + ")", 0);
0758: }
0759:
0760: if (end < 0) {
0761: end = MAX_VALUE[field];
0762: } else if (end > MAX_VALUE[field]) {
0763: throw new ParseException(
0764: "Value "
0765: + end
0766: + " in field "
0767: + field
0768: + " is higher than the maximum allowable value for this field (max="
0769: + MAX_VALUE[field] + ")", 0);
0770: }
0771: }
0772:
0773: if (interval < 1) {
0774: interval = 1;
0775: }
0776:
0777: int i = start - MIN_VALUE[field];
0778:
0779: // Populate the lookup table by setting all the bits corresponding to the valid field values
0780: for (i = start - MIN_VALUE[field]; i <= (end - MIN_VALUE[field]); i += interval) {
0781: lookup[field] |= (1L << i);
0782: }
0783:
0784: // Make sure we remember the minimum value set so far
0785: // Keep track of the highest and lowest values that have been added to this field so far
0786: if (lookupMin[field] > start) {
0787: lookupMin[field] = start;
0788: }
0789:
0790: i += (MIN_VALUE[field] - interval);
0791:
0792: if (lookupMax[field] < i) {
0793: lookupMax[field] = i;
0794: }
0795: }
0796:
0797: /**
0798: * Indicates if a year is a leap year or not.
0799: *
0800: * @param year The year to check
0801: *
0802: * @return <code>true</code> if the year is a leap year, <code>false</code> otherwise.
0803: */
0804: private boolean isLeapYear(int year) {
0805: return (((year % 4) == 0) && ((year % 100) != 0))
0806: || ((year % 400) == 0);
0807: }
0808:
0809: /**
0810: * Calculate the day of the week. Sunday = 0, Monday = 1, ... , Saturday = 6. The formula
0811: * used is an optimized version of Zeller's Congruence.
0812: *
0813: * @param day The day of the month (1-31)
0814: * @param month The month (1 - 12)
0815: * @param year The year
0816: * @return
0817: */
0818: private int dayOfWeek(int day, int month, int year) {
0819: day += ((month < 3) ? year-- : (year - 2));
0820: return ((((23 * month) / 9) + day + 4 + (year / 4))
0821: - (year / 100) + (year / 400)) % 7;
0822: }
0823:
0824: /**
0825: * Retrieves the number of days in the supplied month, taking into account leap years.
0826: * If the month value is outside the range <code>MIN_VALUE[MONTH] - MAX_VALUE[MONTH]</code>
0827: * then the year will be adjusted accordingly and the correct number of days will still
0828: * be returned.
0829: *
0830: * @param month The month of interest.
0831: * @param year The year we are checking.
0832: *
0833: * @return The number of days in the month.
0834: */
0835: private int numberOfDaysInMonth(int month, int year) {
0836: while (month < 1) {
0837: month += 12;
0838: year--;
0839: }
0840:
0841: while (month > 12) {
0842: month -= 12;
0843: year++;
0844: }
0845:
0846: if (month == 2) {
0847: return isLeapYear(year) ? 29 : 28;
0848: } else {
0849: return DAYS_IN_MONTH[month - 1];
0850: }
0851: }
0852:
0853: /**
0854: * Quickly retrieves the day of week value (Sun = 0, ... Sat = 6) that corresponds to the
0855: * day name that is specified in the character array. Only the first 3 characters are taken
0856: * into account; the rest are ignored.
0857: *
0858: * @param element The character array
0859: * @param i The index to start looking at
0860: * @return The day of week value
0861: */
0862: private int getDayOfWeekVal(char ch1, char[] element, int i)
0863: throws ParseException {
0864: if ((i + 1) >= element.length) {
0865: throw makeParseException(
0866: "Unexpected end of element encountered while parsing a day name",
0867: element, i);
0868: }
0869:
0870: int ch2 = element[i] | 0x20;
0871: int ch3 = element[i + 1] | 0x20;
0872:
0873: switch (ch1 | 0x20) {
0874: case 's': // Sunday, Saturday
0875:
0876: if ((ch2 == 'u') && (ch3 == 'n')) {
0877: return 0;
0878: }
0879:
0880: if ((ch2 == 'a') && (ch3 == 't')) {
0881: return 6;
0882: }
0883:
0884: break;
0885: case 'm': // Monday
0886:
0887: if ((ch2 == 'o') && (ch3 == 'n')) {
0888: return 1;
0889: }
0890:
0891: break;
0892: case 't': // Tuesday, Thursday
0893:
0894: if ((ch2 == 'u') && (ch3 == 'e')) {
0895: return 2;
0896: }
0897:
0898: if ((ch2 == 'h') && (ch3 == 'u')) {
0899: return 4;
0900: }
0901:
0902: break;
0903: case 'w': // Wednesday
0904:
0905: if ((ch2 == 'e') && (ch3 == 'd')) {
0906: return 3;
0907: }
0908:
0909: break;
0910: case 'f': // Friday
0911:
0912: if ((ch2 == 'r') && (ch3 == 'i')) {
0913: return 5;
0914: }
0915:
0916: break;
0917: }
0918:
0919: throw makeParseException(
0920: "Unexpected character while parsing a day name",
0921: element, i - 1);
0922: }
0923:
0924: /**
0925: * Quickly retrieves the month value (Jan = 1, ..., Dec = 12) that corresponds to the month
0926: * name that is specified in the character array. Only the first 3 characters are taken
0927: * into account; the rest are ignored.
0928: *
0929: * @param element The character array
0930: * @param i The index to start looking at
0931: * @return The month value
0932: */
0933: private int getMonthVal(char ch1, char[] element, int i)
0934: throws ParseException {
0935: if ((i + 1) >= element.length) {
0936: throw makeParseException(
0937: "Unexpected end of element encountered while parsing a month name",
0938: element, i);
0939: }
0940:
0941: int ch2 = element[i] | 0x20;
0942: int ch3 = element[i + 1] | 0x20;
0943:
0944: switch (ch1 | 0x20) {
0945: case 'j': // January, June, July
0946:
0947: if ((ch2 == 'a') && (ch3 == 'n')) {
0948: return 1;
0949: }
0950:
0951: if (ch2 == 'u') {
0952: if (ch3 == 'n') {
0953: return 6;
0954: }
0955:
0956: if (ch3 == 'l') {
0957: return 7;
0958: }
0959: }
0960:
0961: break;
0962: case 'f': // February
0963:
0964: if ((ch2 == 'e') && (ch3 == 'b')) {
0965: return 2;
0966: }
0967:
0968: break;
0969: case 'm': // March, May
0970:
0971: if (ch2 == 'a') {
0972: if (ch3 == 'r') {
0973: return 3;
0974: }
0975:
0976: if (ch3 == 'y') {
0977: return 5;
0978: }
0979: }
0980:
0981: break;
0982: case 'a': // April, August
0983:
0984: if ((ch2 == 'p') && (ch3 == 'r')) {
0985: return 4;
0986: }
0987:
0988: if ((ch2 == 'u') && (ch3 == 'g')) {
0989: return 8;
0990: }
0991:
0992: break;
0993: case 's': // September
0994:
0995: if ((ch2 == 'e') && (ch3 == 'p')) {
0996: return 9;
0997: }
0998:
0999: break;
1000: case 'o': // October
1001:
1002: if ((ch2 == 'c') && (ch3 == 't')) {
1003: return 10;
1004: }
1005:
1006: break;
1007: case 'n': // November
1008:
1009: if ((ch2 == 'o') && (ch3 == 'v')) {
1010: return 11;
1011: }
1012:
1013: break;
1014: case 'd': // December
1015:
1016: if ((ch2 == 'e') && (ch3 == 'c')) {
1017: return 12;
1018: }
1019:
1020: break;
1021: }
1022:
1023: throw makeParseException(
1024: "Unexpected character while parsing a month name",
1025: element, i - 1);
1026: }
1027:
1028: /**
1029: * Recreates the original human-readable cron expression based on the internal
1030: * datastructure values.
1031: *
1032: * @return A cron expression that corresponds to the current state of the
1033: * internal data structure.
1034: */
1035: public String getExpressionSummary() {
1036: StringBuffer buf = new StringBuffer();
1037:
1038: buf.append(getExpressionSetSummary(MINUTE)).append(' ');
1039: buf.append(getExpressionSetSummary(HOUR)).append(' ');
1040: buf.append(getExpressionSetSummary(DAY_OF_MONTH)).append(' ');
1041: buf.append(getExpressionSetSummary(MONTH)).append(' ');
1042: buf.append(getExpressionSetSummary(DAY_OF_WEEK));
1043:
1044: return buf.toString();
1045: }
1046:
1047: /**
1048: * <p>Converts the internal datastructure that holds a particular cron field into
1049: * a human-readable list of values of the field's contents. For example, if the
1050: * <code>DAY_OF_WEEK</code> field was submitted that had Sunday and Monday specified,
1051: * the string <code>0,1</code> would be returned.</p>
1052: *
1053: * <p>If the field contains all possible values, <code>*</code> will be returned.
1054: *
1055: * @param field The field.
1056: *
1057: * @return A human-readable string representation of the field's contents.
1058: */
1059: private String getExpressionSetSummary(int field) {
1060: if (lookup[field] == Long.MAX_VALUE) {
1061: return "*";
1062: }
1063:
1064: StringBuffer buf = new StringBuffer();
1065:
1066: boolean first = true;
1067:
1068: for (int i = MIN_VALUE[field]; i <= MAX_VALUE[field]; i++) {
1069: if ((lookup[field] & (1L << (i - MIN_VALUE[field]))) != 0) {
1070: if (!first) {
1071: buf.append(",");
1072: } else {
1073: first = false;
1074: }
1075:
1076: buf.append(String.valueOf(i));
1077: }
1078: }
1079:
1080: return buf.toString();
1081: }
1082:
1083: /**
1084: * Makes a <code>ParseException</code>. The exception message is constructed by
1085: * taking the given message parameter and appending the supplied character data
1086: * to the end of it. for example, if <code>msg == "Invalid character
1087: * encountered"</code> and <code>data == {'A','g','u','s','t'}</code>, the resultant
1088: * error message would be <code>"Invalid character encountered [Agust]"</code>.
1089: *
1090: * @param msg The error message
1091: * @param data The data that the message
1092: * @param offset The offset into the data where the error was encountered.
1093: *
1094: * @return a newly created <code>ParseException</code> object.
1095: */
1096: private ParseException makeParseException(String msg, char[] data,
1097: int offset) {
1098: char[] buf = new char[msg.length() + data.length + 3];
1099: int msgLen = msg.length();
1100: System.arraycopy(msg.toCharArray(), 0, buf, 0, msgLen);
1101: buf[msgLen] = ' ';
1102: buf[msgLen + 1] = '[';
1103: System.arraycopy(data, 0, buf, msgLen + 2, data.length);
1104: buf[buf.length - 1] = ']';
1105: return new ParseException(new String(buf), offset);
1106: }
1107: }
1108:
1109: class ValueSet {
1110: public int pos;
1111: public int value;
1112: }
|