2
* Copyright (c) 2002-2003 by OpenSymphony
5
package com.opensymphony.oscache.util;
7
import java.text.ParseException;
10
import java.util.Calendar;
13
* Parses cron expressions and determines at what time in the past is the
14
* most recent match for the supplied expression.
16
* @author <a href="mailto:chris@swebtec.com">Chris Miller</a>
17
* @author $Author: chris_miller $
18
* @version $Revision: 1.3 $
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;
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};
34
* A lookup table holding the number of days in each month (with the obvious exception
35
* that February requires special handling).
37
private static final int[] DAYS_IN_MONTH = {
38
31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
42
* Holds the raw cron expression that this parser is handling.
44
private String cronExpression = null;
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>.
56
private long[] lookup = {
57
Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE,
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.
65
private int[] lookupMax = {-1, -1, -1, -1, -1};
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.
71
private int[] lookupMin = {
72
Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,
73
Integer.MAX_VALUE, Integer.MAX_VALUE
77
* Creates a FastCronParser that uses a default cron expression of <code>"* * * * *"</cron>.
78
* This will match any time that is supplied.
80
public FastCronParser() {
84
* Constructs a new FastCronParser based on the supplied expression.
86
* @throws ParseException if the supplied expression is not a valid cron expression.
88
public FastCronParser(String cronExpression) throws ParseException {
89
setCronExpression(cronExpression);
93
* Resets the cron expression to the value supplied.
95
* @param cronExpression the new cron expression.
97
* @throws ParseException if the supplied expression is not a valid cron expression.
99
public void setCronExpression(String cronExpression) throws ParseException {
100
if (cronExpression == null) {
101
throw new IllegalArgumentException("Cron time expression cannot be null");
104
this.cronExpression = cronExpression;
105
parseExpression(cronExpression);
109
* Retrieves the current cron expression.
111
* @return the current cron expression.
113
public String getCronExpression() {
114
return this.cronExpression;
118
* Determines whether this cron expression matches a date/time that is more recent
119
* than the one supplied.
121
* @param time The time to compare the cron expression against.
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.
126
public boolean hasMoreRecentMatch(long time) {
127
return time < getTimeBefore(System.currentTimeMillis());
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.
134
* @param time The time (in milliseconds) that we're using as our upper bound.
136
* @return The time (in milliseconds) when this cron event last occurred.
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...
141
Calendar cal = new GregorianCalendar();
142
cal.setTime(new Date(time));
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);
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];
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;
161
boolean retry = false;
163
// Clean up the month if it was wrapped in a previous iteration
169
// get month...................................................
170
boolean found = false;
172
if (validMonths != Long.MAX_VALUE) {
173
for (int i = month + 11; i > (month - 1); i--) {
174
int testMonth = (i % 12) + 1;
176
int numDays = numberOfDaysInMonth(testMonth, year);
178
// Check if the month is valid
179
if (((1L << (testMonth - 1)) & validMonths) != 0) {
180
if (testMonth > month) {
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];
190
hour = lookupMax[HOUR];
191
minute = lookupMax[MINUTE];
202
// The only time we drop out here is when we're searching for the 29th of February and no other date!
208
// Clean up if the dayOfMonth was wrapped. This takes leap years into account.
209
if (dayOfMonth < 1) {
211
dayOfMonth += numberOfDaysInMonth(month, year);
212
hour = lookupMax[HOUR];
216
// get day...................................................
217
if (haveDOM && !haveDOW) { // get day using just the DAY_OF_MONTH token
219
int daysInThisMonth = numberOfDaysInMonth(month, year);
220
int daysInPreviousMonth = numberOfDaysInMonth(month - 1, year);
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;
226
// Skip over any days that don't actually exist (eg 31st April)
227
if ((testDayOfMonth <= dayOfMonth) && (testDayOfMonth > daysInThisMonth)) {
231
if ((testDayOfMonth > dayOfMonth) && (testDayOfMonth > daysInPreviousMonth)) {
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
242
if (dayOfMonth != testDayOfMonth) {
243
hour = lookupMax[HOUR];
244
minute = lookupMax[MINUTE];
247
dayOfMonth = testDayOfMonth;
255
} else if (haveDOW && !haveDOM) { // get day using just the DAY_OF_WEEK token
258
int currentDOW = dayOfWeek(dayOfMonth, month, year);
260
for (int i = currentDOW + 7; i > currentDOW; i--) {
263
if (((1L << testDOW) & validDaysOfWeek) != 0) {
264
dayOfMonth -= daysLost;
266
if (dayOfMonth < 1) {
267
// We've wrapped back a month
269
dayOfMonth += numberOfDaysInMonth(month, year);
273
if (currentDOW != testDOW) {
274
hour = lookupMax[HOUR];
275
minute = lookupMax[MINUTE];
289
// Clean up if the hour has been wrapped
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;
302
if (((1L << testHour) & validHours) != 0) {
303
if (testHour > hour) {
304
// We've found an hour, but we had to move back a day
309
if (hour != testHour) {
310
minute = lookupMax[MINUTE];
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;
329
if (((1L << testMinute) & validMinutes) != 0) {
330
if (testMinute > minute) {
331
// We've found a minute, but we had to move back an hour
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);
358
return cal.getTime().getTime();
362
* Takes a cron expression as an input parameter, and extracts from it the
363
* relevant minutes/hours/days/months that the expression matches.
365
* @param expression A valid cron expression.
366
* @throws ParseException If the supplied expression could not be parsed.
368
private void parseExpression(String expression) throws ParseException {
370
// Reset all the lookup data
371
for (int i = 0; i < lookup.length; lookup[i++] = 0) {
372
lookupMin[i] = Integer.MAX_VALUE;
376
// Create some character arrays to hold the extracted field values
377
char[][] token = new char[NUMBER_OF_CRON_FIELDS][];
379
// Extract the supplied expression into another character array
381
int length = expression.length();
382
char[] expr = new char[length];
383
expression.getChars(0, length, expr, 0);
387
boolean inWhitespace = true;
389
// Extract the various cron fields from the expression
390
for (int i = 0; (i < length) && (field < NUMBER_OF_CRON_FIELDS);
392
boolean haveChar = (expr[i] != ' ') && (expr[i] != '\t');
395
// We have a text character of some sort
397
startIndex = i; // Remember the start of this token
398
inWhitespace = false;
402
if (i == (length - 1)) { // Adjustment for when we reach the end of the expression
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);
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);
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++) {
424
boolean inDelimiter = true;
426
// We add each comma-delimited element seperately.
427
int elementLength = token[field].length;
429
for (int i = 0; i < elementLength; i++) {
430
boolean haveElement = token[field][i] != ',';
433
// We have a character from an element in the token
440
if (i == (elementLength - 1)) { // Adjustment for when we reach the end of the token
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);
449
// Add the element to our datastructure.
450
storeExpressionValues(element, field);
456
if (lookup[field] == 0) {
457
throw new ParseException("Token " + new String(token[field]) + " contains no valid entries for this field.", 0);
461
// Remove any months that will never be valid
462
switch (lookupMin[DAY_OF_MONTH]) {
464
lookup[MONTH] &= (0xFFF - 0x528); // Binary 010100101000 - the months that have 30 days
466
lookup[MONTH] &= (0xFFF - 0x2); // Binary 000000000010 - February
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);
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);
477
} catch (Exception e) {
478
if (e instanceof ParseException) {
479
throw (ParseException) e;
481
throw new ParseException("Illegal cron expression format (" + e.toString() + ")", 0);
487
* Stores the values for the supplied cron element into the specified field.
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}.
495
* @throws ParseException if there was a problem parsing the supplied element.
497
private void storeExpressionValues(char[] element, int field) throws ParseException {
503
boolean wantValue = true;
504
boolean haveInterval = false;
506
while ((interval < 0) && (i < element.length)) {
507
char ch = element[i++];
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);
524
// Handle any numbers
525
if ((ch >= '0') && (ch <= '9')) {
526
ValueSet vs = getValue(ch - '0', element, i);
530
} else if (!haveInterval) {
534
end = MAX_VALUE[field];
545
if (!haveInterval && (end == -99)) {
546
// Handle any months that have been suplied as words
547
if (field == MONTH) {
549
start = getMonthVal(ch, element, i++);
551
end = getMonthVal(ch, element, i++);
556
// Skip past the rest of the month name
557
while (++i < element.length) {
558
int c = element[i] | 0x20;
560
if ((c < 'a') || (c > 'z')) {
566
} else if (field == DAY_OF_WEEK) {
568
start = getDayOfWeekVal(ch, element, i++);
570
end = getDayOfWeekVal(ch, element, i++);
575
// Skip past the rest of the day name
576
while (++i < element.length) {
577
int c = element[i] | 0x20;
579
if ((c < 'a') || (c > 'z')) {
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)) {
594
// Handle an interval. An interval is valid as long as we have a start value
595
if ((ch == '/') && (start != -99)) {
602
throw makeParseException("Invalid character encountered while parsing element", element, i);
605
if (element.length > i) {
606
throw makeParseException("Extraneous characters found while parsing element", element, i);
617
addToLookup(start, end, field, interval);
621
* Extracts a numerical value from inside a character array.
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
627
* @return the new index and the extracted value
629
private ValueSet getValue(int value, char[] element, int i) {
630
ValueSet result = new ValueSet();
631
result.value = value;
633
if (i >= element.length) {
638
char ch = element[i];
640
while ((ch >= '0') && (ch <= '9')) {
641
result.value = (result.value * 10) + (ch - '0');
643
if (++i >= element.length) {
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.
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.
670
private void addToLookup(int start, int end, int field, int interval) throws ParseException {
671
// deal with the supplied range
674
// We're setting the entire range of values
675
start = lookupMin[field] = MIN_VALUE[field];
676
end = lookupMax[field] = MAX_VALUE[field];
679
lookup[field] = Long.MAX_VALUE;
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);
691
// For ranges, if the start is bigger than the end value then swap them over
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);
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);
715
int i = start - MIN_VALUE[field];
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]);
720
lookup[field] |= (1L << i);
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;
729
i += (MIN_VALUE[field] - interval);
731
if (lookupMax[field] < i) {
732
lookupMax[field] = i;
737
* Indicates if a year is a leap year or not.
739
* @param year The year to check
741
* @return <code>true</code> if the year is a leap year, <code>false</code> otherwise.
743
private boolean isLeapYear(int year) {
744
return (((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0);
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.
751
* @param day The day of the month (1-31)
752
* @param month The month (1 - 12)
753
* @param year The year
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;
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
767
* @param month The month of interest.
768
* @param year The year we are checking.
770
* @return The number of days in the month.
772
private int numberOfDaysInMonth(int month, int year) {
784
return isLeapYear(year) ? 29 : 28;
786
return DAYS_IN_MONTH[month - 1];
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.
795
* @param element The character array
796
* @param i The index to start looking at
797
* @return The day of week value
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);
804
int ch2 = element[i] | 0x20;
805
int ch3 = element[i + 1] | 0x20;
807
switch (ch1 | 0x20) {
808
case 's': // Sunday, Saturday
810
if ((ch2 == 'u') && (ch3 == 'n')) {
814
if ((ch2 == 'a') && (ch3 == 't')) {
821
if ((ch2 == 'o') && (ch3 == 'n')) {
826
case 't': // Tuesday, Thursday
828
if ((ch2 == 'u') && (ch3 == 'e')) {
832
if ((ch2 == 'h') && (ch3 == 'u')) {
837
case 'w': // Wednesday
839
if ((ch2 == 'e') && (ch3 == 'd')) {
846
if ((ch2 == 'r') && (ch3 == 'i')) {
853
throw makeParseException("Unexpected character while parsing a day name", element, i - 1);
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.
861
* @param element The character array
862
* @param i The index to start looking at
863
* @return The month value
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);
870
int ch2 = element[i] | 0x20;
871
int ch3 = element[i + 1] | 0x20;
873
switch (ch1 | 0x20) {
874
case 'j': // January, June, July
876
if ((ch2 == 'a') && (ch3 == 'n')) {
891
case 'f': // February
893
if ((ch2 == 'e') && (ch3 == 'b')) {
898
case 'm': // March, May
911
case 'a': // April, August
913
if ((ch2 == 'p') && (ch3 == 'r')) {
917
if ((ch2 == 'u') && (ch3 == 'g')) {
922
case 's': // September
924
if ((ch2 == 'e') && (ch3 == 'p')) {
931
if ((ch2 == 'c') && (ch3 == 't')) {
936
case 'n': // November
938
if ((ch2 == 'o') && (ch3 == 'v')) {
943
case 'd': // December
945
if ((ch2 == 'e') && (ch3 == 'c')) {
952
throw makeParseException("Unexpected character while parsing a month name", element, i - 1);
956
* Recreates the original human-readable cron expression based on the internal
957
* datastructure values.
959
* @return A cron expression that corresponds to the current state of the
960
* internal data structure.
962
public String getExpressionSummary() {
963
StringBuffer buf = new StringBuffer();
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));
971
return buf.toString();
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>
980
* <p>If the field contains all possible values, <code>*</code> will be returned.
982
* @param field The field.
984
* @return A human-readable string representation of the field's contents.
986
private String getExpressionSetSummary(int field) {
987
if (lookup[field] == Long.MAX_VALUE) {
991
StringBuffer buf = new StringBuffer();
993
boolean first = true;
995
for (int i = MIN_VALUE[field]; i <= MAX_VALUE[field]; i++) {
996
if ((lookup[field] & (1L << (i - MIN_VALUE[field]))) != 0) {
1003
buf.append(String.valueOf(i));
1007
return buf.toString();
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>.
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.
1021
* @return a newly created <code>ParseException</code> object.
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);
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);