~ubuntu-branches/ubuntu/gutsy/oscache/gutsy

« back to all changes in this revision

Viewing changes to src/core/java/com/opensymphony/oscache/util/FastCronParser.java

  • Committer: Bazaar Package Importer
  • Author(s): Kalle Kivimaa
  • Date: 2004-08-13 14:00:00 UTC
  • Revision ID: james.westby@ubuntu.com-20040813140000-lyugvinublk1x8y2
Tags: upstream-2.0.2
ImportĀ upstreamĀ versionĀ 2.0.2

Show diffs side-by-side

added added

removed removed

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