001: /*
002: * Copyright 2001-2007 Geert Bevin <gbevin[remove] at uwyn dot com>
003: * Distributed under the terms of either:
004: * - the common development and distribution license (CDDL), v1.0; or
005: * - the GNU Lesser General Public License, v2.1 or later
006: * $Id: Frequency.java 3714 2007-04-08 02:57:38Z gbevin $
007: */
008: package com.uwyn.rife.scheduler;
009:
010: import com.uwyn.rife.config.RifeConfig;
011: import com.uwyn.rife.scheduler.exceptions.FrequencyException;
012: import com.uwyn.rife.tools.Localization;
013: import com.uwyn.rife.tools.StringUtils;
014: import java.util.ArrayList;
015: import java.util.Arrays;
016: import java.util.Calendar;
017:
018: class Frequency {
019: final static private int MAX_YEAR = 2050;
020:
021: final static private byte[] ALL_MINUTES = new byte[] { 0, 1, 2, 3,
022: 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
023: 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
024: 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
025: 50, 51, 52, 53, 54, 55, 56, 57, 58, 59 };
026: final static private byte[] ALL_HOURS = new byte[] { 0, 1, 2, 3, 4,
027: 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
028: 21, 22, 23 };
029: final static private byte[] ALL_DATES = new byte[] { 1, 2, 3, 4, 5,
030: 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
031: 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 };
032: final static private byte[] ALL_MONTHS = new byte[] { 1, 2, 3, 4,
033: 5, 6, 7, 8, 9, 10, 11, 12 };
034: final static private byte[] ALL_WEEKDAYS = new byte[] { 1, 2, 3, 4,
035: 5, 6, 7 };
036: final static private byte[] EMPTY_DATE_OVERFLOW = new byte[] { -1,
037: -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
038: -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
039:
040: private String mFrequency = null;
041:
042: private byte[] mMinutes = null;
043: private byte[] mHours = null;
044: private byte[] mDates = null;
045: private byte[] mDatesUnderflow = null;
046: private byte[] mDatesOverflow = null;
047: private byte[] mMonths = null;
048: private byte[] mWeekdays = null;
049:
050: private boolean mParsed = false;
051:
052: Frequency(String frequency) throws FrequencyException {
053: parse(frequency);
054: }
055:
056: long getNextDate(long start) throws FrequencyException {
057: if (start < 0)
058: throw new IllegalArgumentException(
059: "start should be positive");
060:
061: Calendar calendar = Calendar.getInstance(RifeConfig.Tools
062: .getDefaultTimeZone(), Localization.getLocale());
063: calendar.setTimeInMillis(start);
064:
065: int minute = calendar.get(Calendar.MINUTE);
066: int hour = calendar.get(Calendar.HOUR_OF_DAY);
067: int date = calendar.get(Calendar.DATE);
068: int month = calendar.get(Calendar.MONTH) + 1;
069: int year = calendar.get(Calendar.YEAR);
070:
071: // got to next valid time
072: minute++;
073: if (-1 == (minute = getNextValidMinute(minute))
074: || -1 == mHours[hour] || -1 == mMonths[month - 1]
075: || -1 == getDates(month, year)[date - 1]) {
076: hour++;
077: if (-1 == (hour = getNextValidHour(hour))
078: || -1 == mMonths[month - 1]
079: || -1 == getDates(month, year)[date - 1]) {
080: date++;
081: hour = getFirstValidHour();
082: }
083: minute = getFirstValidMinute();
084: }
085:
086: // got to next valid date
087: while (year < MAX_YEAR) {
088: if (-1 == (date = getNextValidDate(date, month, year))
089: || -1 == mMonths[month - 1]) {
090: month++;
091: if (-1 == (month = getNextValidMonth(month))) {
092: year++;
093: month = getFirstValidMonth();
094: }
095: date = getFirstValidDate(month, year);
096: if (-1 == date) {
097: date = 1;
098: continue;
099: }
100: }
101:
102: calendar.set(year, month - 1, date, hour, minute);
103:
104: if (year == calendar.get(Calendar.YEAR)
105: && month == calendar.get(Calendar.MONTH) + 1) {
106: int weekday = calendar.get(Calendar.DAY_OF_WEEK) - 2;
107: if (-1 == weekday) {
108: weekday = 6;
109: }
110:
111: if (mWeekdays[weekday] != -1) {
112: return calendar.getTimeInMillis();
113: }
114: }
115:
116: date++;
117: }
118:
119: throw new FrequencyException("no valid next date available");
120: }
121:
122: private int getFirstValidMinute() {
123: return getNextValidMinute(0);
124: }
125:
126: private int getNextValidMinute(int minute) {
127: assert minute >= 0;
128:
129: for (int i = minute; i < mMinutes.length; i++) {
130: if (mMinutes[i] != -1) {
131: return mMinutes[i];
132: }
133: }
134:
135: return -1;
136: }
137:
138: private int getFirstValidHour() {
139: return getNextValidHour(0);
140: }
141:
142: private int getNextValidHour(int hour) {
143: assert hour >= 0;
144:
145: for (int i = hour; i < mHours.length; i++) {
146: if (mHours[i] != -1) {
147: return mHours[i];
148: }
149: }
150:
151: return -1;
152: }
153:
154: private byte[] getDates(int month, int year) {
155: assert month >= 1;
156: assert year >= 0;
157:
158: Calendar calendar = Calendar.getInstance(RifeConfig.Tools
159: .getDefaultTimeZone(), Localization.getLocale());
160: calendar.set(year, month - 1, 1);
161: byte maximum_date = (byte) calendar
162: .getActualMaximum(Calendar.DATE);
163: byte[] dates = null;
164:
165: // only retain the dates that are valid for this month
166: dates = new byte[ALL_DATES.length];
167: Arrays.fill(dates, (byte) -1);
168: System.arraycopy(mDates, 0, dates, 0, maximum_date);
169:
170: if (mDatesUnderflow != null && mDatesOverflow != null) {
171: // get the maximum date of the previous month
172: calendar.roll(Calendar.MONTH, -1);
173: byte maximum_date_previous = (byte) calendar
174: .getActualMaximum(Calendar.DATE);
175:
176: // integrate overflowed dates
177: byte end_value = ALL_DATES[ALL_DATES.length - 1];
178: byte difference = (byte) (end_value - maximum_date_previous);
179:
180: int start_position = ALL_DATES.length - 1;
181: int target_position = 0;
182: for (int i = start_position; i >= 0; i--) {
183: if (mDatesUnderflow[i] != 0) {
184: // handle the possibility where due to the difference,
185: // the underflow turns into an overflow
186: if (i > maximum_date_previous - 1) {
187: target_position = i - maximum_date_previous;
188: if (target_position < mDatesUnderflow[i]
189: && target_position < maximum_date) {
190: dates[target_position] = ALL_DATES[target_position];
191: }
192: }
193: }
194:
195: if (mDatesOverflow[i] != 0) {
196: // handle the overflow of the end of the previous month
197: target_position = i + difference;
198: if (target_position < mDatesOverflow[i]
199: && target_position < maximum_date) {
200: dates[target_position] = ALL_DATES[target_position];
201: }
202: }
203: }
204: }
205:
206: return dates;
207: }
208:
209: private int getFirstValidDate(int month, int year) {
210: return getNextValidDate(1, month, year);
211: }
212:
213: private int getNextValidDate(int date, int month, int year) {
214: assert date >= 1;
215: assert month >= 1;
216: assert year >= 0;
217:
218: byte[] dates = getDates(month, year);
219:
220: for (int i = date - 1; i < dates.length; i++) {
221: if (dates[i] != -1) {
222: return dates[i];
223: }
224: }
225:
226: return -1;
227: }
228:
229: private int getFirstValidMonth() {
230: return getNextValidMonth(1);
231: }
232:
233: private int getNextValidMonth(int month) {
234: assert month >= 1;
235:
236: for (int i = month - 1; i < mMonths.length; i++) {
237: if (mMonths[i] != -1) {
238: return mMonths[i];
239: }
240: }
241:
242: return -1;
243: }
244:
245: boolean isParsed() {
246: return mParsed;
247: }
248:
249: String getFrequency() {
250: return mFrequency;
251: }
252:
253: byte[] getMinutes() {
254: return mMinutes;
255: }
256:
257: byte[] getHours() {
258: return mHours;
259: }
260:
261: byte[] getDates() {
262: return mDates;
263: }
264:
265: byte[] getDatesUnderflow() {
266: return mDatesUnderflow;
267: }
268:
269: byte[] getDatesOverflow() {
270: return mDatesOverflow;
271: }
272:
273: byte[] getMonths() {
274: return mMonths;
275: }
276:
277: byte[] getWeekdays() {
278: return mWeekdays;
279: }
280:
281: void parse(String frequency) throws FrequencyException {
282: if (null == frequency)
283: throw new IllegalArgumentException(
284: "frequency can't be null");
285: if (0 == frequency.length())
286: throw new IllegalArgumentException(
287: "frequency can't be empty");
288:
289: mFrequency = frequency;
290: mParsed = false;
291:
292: mMinutes = null;
293: mHours = null;
294: mDates = null;
295: mDatesUnderflow = new byte[ALL_DATES.length];
296: mDatesOverflow = new byte[ALL_DATES.length];
297: mMonths = null;
298: mWeekdays = null;
299:
300: ArrayList<String> frequency_parts = StringUtils.split(
301: frequency, " ");
302: if (frequency_parts.size() != 5) {
303: throw new FrequencyException(
304: "invalid frequency, should be 5 fields seperated by a space");
305: }
306:
307: String minutes = frequency_parts.get(0);
308: String hours = frequency_parts.get(1);
309: String dates = frequency_parts.get(2);
310: String months = frequency_parts.get(3);
311: String weekdays = frequency_parts.get(4);
312:
313: mMinutes = processParts(StringUtils.split(minutes, ","),
314: ALL_MINUTES, false, null, null);
315: mHours = processParts(StringUtils.split(hours, ","), ALL_HOURS,
316: false, null, null);
317: mDates = processParts(StringUtils.split(dates, ","), ALL_DATES,
318: true, mDatesUnderflow, mDatesOverflow);
319: if (Arrays.equals(mDatesUnderflow, EMPTY_DATE_OVERFLOW)) {
320: mDatesUnderflow = null;
321: }
322: if (Arrays.equals(mDatesOverflow, EMPTY_DATE_OVERFLOW)) {
323: mDatesOverflow = null;
324: }
325: mMonths = processParts(StringUtils.split(months, ","),
326: ALL_MONTHS, false, null, null);
327: mWeekdays = processParts(StringUtils.split(weekdays, ","),
328: ALL_WEEKDAYS, false, null, null);
329:
330: mParsed = true;
331: }
332:
333: private byte[] processParts(ArrayList<String> parts,
334: byte[] allValues, boolean deferOverflowProcessing,
335: byte[] underflowStorage, byte[] overflowStorage)
336: throws FrequencyException {
337: assert parts != null;
338: assert parts.size() > 0;
339: assert allValues != null;
340: assert allValues.length > 0;
341: assert !deferOverflowProcessing
342: || (deferOverflowProcessing && underflowStorage != null && overflowStorage != null);
343:
344: String part = null;
345: byte[] result_values = null;
346:
347: // initialize the values to -1, the frequency syntax
348: // will enable the specified array positions by copying them
349: // from the reference array
350: result_values = new byte[allValues.length];
351: Arrays.fill(result_values, (byte) -1);
352: if (underflowStorage != null) {
353: Arrays.fill(underflowStorage, (byte) -1);
354: }
355: if (overflowStorage != null) {
356: Arrays.fill(overflowStorage, (byte) -1);
357: }
358:
359: byte begin = allValues[0];
360: byte end = allValues[allValues.length - 1];
361:
362: for (String current_part : parts) {
363: part = current_part;
364:
365: // plain wildcard
366: if (current_part.equals("*")) {
367: result_values = allValues;
368: return result_values;
369: }
370:
371: try {
372: int seperator = -1;
373: byte divider = -1;
374:
375: // divider
376: if ((seperator = current_part.indexOf("/")) != -1) {
377: divider = Byte.parseByte(current_part
378: .substring(seperator + 1));
379: current_part = current_part.substring(0, seperator);
380: }
381:
382: // wildcard
383: if (current_part.equals("*")) {
384: if (-1 == divider) {
385: throw new FrequencyException(
386: "invalid frequency part '" + part + "'");
387: }
388:
389: for (byte i = 0; i < allValues.length; i += divider) {
390: result_values[i] = allValues[i];
391: }
392: continue;
393: }
394: // range
395: else if ((seperator = current_part.indexOf("-")) != -1) {
396: byte left = Byte.parseByte(current_part.substring(
397: 0, seperator));
398: byte right = Byte.parseByte(current_part
399: .substring(seperator + 1));
400:
401: if (left < begin || left > end) {
402: throw new FrequencyException(
403: "value out of range '" + left + "'");
404: }
405: if (right < begin || right > end) {
406: throw new FrequencyException(
407: "value out of range '" + right + "'");
408: }
409:
410: if (left == right) {
411: if (divider != -1) {
412: throw new FrequencyException(
413: "invalid frequency part '" + part
414: + "'");
415: }
416: result_values[left - begin] = allValues[left
417: - begin];
418: continue;
419: }
420:
421: if (-1 == divider) {
422: divider = 1;
423: }
424:
425: if (right < left) {
426: if (deferOverflowProcessing) {
427: // the overflow processing should be done later
428:
429: // store the underflow both in the regular fashion and
430: // preserve it seperately for later underflow processing
431: // since it might bleed into overflow
432: while (left <= end) {
433: result_values[left - begin] = allValues[left
434: - begin];
435: // don't store the actual values after the overflow breakpoint
436: // but store the value of the rightmost
437: // limit of the corresponding range
438: if (underflowStorage[left - begin] < right) {
439: underflowStorage[left - begin] = right;
440: }
441: left += divider;
442: }
443:
444: left = (byte) (begin + (left - end) - 1);
445:
446: // store the positions at which entries are located
447: // the positions contain the value of the rightmost
448: // limit of the corresponding range
449: // this is needed to be able to calculate the correct
450: // transformations later
451: while (left <= right) {
452: // preserve a later right limit
453: if (overflowStorage[left - begin] < right) {
454: overflowStorage[left - begin] = right;
455: }
456: left += divider;
457: }
458: continue;
459: } else {
460: while (left <= end) {
461: result_values[left - begin] = allValues[left
462: - begin];
463: left += divider;
464: }
465:
466: left = (byte) (begin + (left - end) - 1);
467: }
468: }
469:
470: while (left <= right) {
471: result_values[left - begin] = allValues[left
472: - begin];
473: left += divider;
474: }
475: continue;
476: }
477: // one number
478: else {
479: if (divider != -1) {
480: throw new FrequencyException(
481: "invalid frequency part '" + part + "'");
482: }
483:
484: byte minute = Byte.parseByte(current_part);
485: if (minute < begin || minute > end) {
486: throw new FrequencyException(
487: "value out of range '" + minute + "'");
488: }
489: result_values[minute - begin] = allValues[minute
490: - begin];
491: }
492: } catch (NumberFormatException e) {
493: throw new FrequencyException("invalid frequency part '"
494: + part + "'", e);
495: }
496: }
497:
498: return result_values;
499: }
500: }
|