001: /*******************************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: *******************************************************************************/package org.ofbiz.service.calendar;
019:
020: import java.util.Arrays;
021: import java.util.Calendar;
022: import java.util.Date;
023: import java.util.Iterator;
024: import java.util.List;
025:
026: import org.ofbiz.base.util.Debug;
027: import org.ofbiz.base.util.StringUtil;
028: import org.ofbiz.base.util.UtilMisc;
029: import org.ofbiz.entity.GenericDelegator;
030: import org.ofbiz.entity.GenericEntityException;
031: import org.ofbiz.entity.GenericValue;
032:
033: /**
034: * Recurrence Rule Object
035: */
036: public class RecurrenceRule {
037:
038: public static final String module = RecurrenceRule.class.getName();
039:
040: // **********************
041: // * byXXX constants
042: // **********************
043: public static final int MIN_SEC = 0;
044: public static final int MAX_SEC = 59;
045: public static final int MIN_MIN = 0;
046: public static final int MAX_MIN = 59;
047: public static final int MIN_HR = 0;
048: public static final int MAX_HR = 23;
049: public static final int MIN_MTH_DAY = -31;
050: public static final int MAX_MTH_DAY = 31;
051: public static final int MIN_YEAR_DAY = -366;
052: public static final int MAX_YEAR_DAY = 366;
053: public static final int MIN_WEEK_NO = -53;
054: public static final int MAX_WEEK_NO = 53;
055: public static final int MIN_MTH = 1;
056: public static final int MAX_MTH = 12;
057:
058: // **********************
059: // * Frequency constants
060: // **********************
061: /** Frequency SECONDLY */
062: public static final int SECONDLY = 1;
063:
064: /** Frequency MINUTELY */
065: public static final int MINUTELY = 2;
066:
067: /** Frequency HOURLY */
068: public static final int HOURLY = 3;
069:
070: /** Frequency DAILY */
071: public static final int DAILY = 4;
072:
073: /** Frequency WEEKLY */
074: public static final int WEEKLY = 5;
075:
076: /** Frequency MONTHLY */
077: public static final int MONTHLY = 6;
078:
079: /** Frequency YEARLY */
080: public static final int YEARLY = 7;
081:
082: // **********************
083: // * GenericValue object
084: // **********************
085: protected GenericValue rule;
086:
087: // **********************
088: // * Parsed byXXX lists
089: // **********************
090: protected List bySecondList;
091: protected List byMinuteList;
092: protected List byHourList;
093: protected List byDayList;
094: protected List byMonthDayList;
095: protected List byYearDayList;
096: protected List byWeekNoList;
097: protected List byMonthList;
098: protected List bySetPosList;
099:
100: /**
101: * Creates a new RecurrenceRule object from a RecurrenceInfo entity.
102: *@param rule GenericValue object defining this rule.
103: */
104: public RecurrenceRule(GenericValue rule)
105: throws RecurrenceRuleException {
106: this .rule = rule;
107: if (!rule.getEntityName().equals("RecurrenceRule"))
108: throw new RecurrenceRuleException(
109: "Invalid RecurrenceRule Value object.");
110: init();
111: }
112:
113: /**
114: * Initializes the rules for this RecurrenceInfo object.
115: *@throws RecurrenceRuleException
116: */
117: public void init() throws RecurrenceRuleException {
118: // Check the validity of the rule
119: String freq = rule.getString("frequency");
120:
121: if (!checkFreq(freq))
122: throw new RecurrenceRuleException(
123: "Recurrence FREQUENCY is a required parameter.");
124: if (rule.getLong("intervalNumber").longValue() < 1)
125: throw new RecurrenceRuleException(
126: "Recurrence INTERVAL must be a positive integer.");
127:
128: // Initialize the byXXX lists
129: bySecondList = StringUtil.split(rule.getString("bySecondList"),
130: ",");
131: byMinuteList = StringUtil.split(rule.getString("byMinuteList"),
132: ",");
133: byHourList = StringUtil
134: .split(rule.getString("byHourList"), ",");
135: byDayList = StringUtil.split(rule.getString("byDayList"), ",");
136: byMonthDayList = StringUtil.split(rule
137: .getString("byMonthDayList"), ",");
138: byYearDayList = StringUtil.split(rule
139: .getString("byYearDayList"), ",");
140: byWeekNoList = StringUtil.split(rule.getString("byWeekNoList"),
141: ",");
142: byMonthList = StringUtil.split(rule.getString("byMonthList"),
143: ",");
144: bySetPosList = StringUtil.split(rule.getString("bySetPosList"),
145: ",");
146: }
147:
148: // Checks for a valid frequency property.
149: private boolean checkFreq(String freq) {
150: if (freq == null)
151: return false;
152: if (freq.equalsIgnoreCase("SECONDLY"))
153: return true;
154: if (freq.equalsIgnoreCase("MINUTELY"))
155: return true;
156: if (freq.equalsIgnoreCase("HOURLY"))
157: return true;
158: if (freq.equalsIgnoreCase("DAILY"))
159: return true;
160: if (freq.equalsIgnoreCase("WEEKLY"))
161: return true;
162: if (freq.equalsIgnoreCase("MONTHLY"))
163: return true;
164: if (freq.equalsIgnoreCase("YEARLY"))
165: return true;
166: return false;
167: }
168:
169: /**
170: * Gets the end time of the recurrence rule or 0 if none.
171: *@return long The timestamp of the end time for this rule or 0 for none.
172: */
173: public long getEndTime() {
174: if (rule == null) {
175: Debug.logVerbose("Rule is null.", module);
176: return -1;
177: }
178: long time = 0;
179: java.sql.Timestamp stamp = null;
180:
181: stamp = rule.getTimestamp("untilDateTime");
182: Debug.logVerbose("Stamp value: " + stamp, module);
183:
184: if (stamp != null) {
185: long nanos = (long) stamp.getNanos();
186: time = stamp.getTime();
187: time += (nanos / 1000000);
188: }
189: Debug.logVerbose("Returning time: " + time, module);
190: return time;
191: }
192:
193: /**
194: * Get the number of times this recurrence will run (-1 until end time).
195: *@return long The number of time this recurrence will run.
196: */
197: public long getCount() {
198: if (rule.get("countNumber") != null)
199: return rule.getLong("countNumber").longValue();
200: return 0;
201: }
202:
203: /**
204: * Returns the frequency name of the recurrence.
205: *@return String The name of this frequency.
206: */
207: public String getFrequencyName() {
208: return rule.getString("frequency").toUpperCase();
209: }
210:
211: /**
212: * Returns the frequency of this recurrence.
213: *@return int The reference value for the frequency
214: */
215: public int getFrequency() {
216: String freq = rule.getString("frequency");
217:
218: if (freq == null)
219: return 0;
220: if (freq.equalsIgnoreCase("SECONDLY"))
221: return SECONDLY;
222: if (freq.equalsIgnoreCase("MINUTELY"))
223: return MINUTELY;
224: if (freq.equalsIgnoreCase("HOURLY"))
225: return HOURLY;
226: if (freq.equalsIgnoreCase("DAILY"))
227: return DAILY;
228: if (freq.equalsIgnoreCase("WEEKLY"))
229: return WEEKLY;
230: if (freq.equalsIgnoreCase("MONTHLY"))
231: return MONTHLY;
232: if (freq.equalsIgnoreCase("YEARLY"))
233: return YEARLY;
234: return 0;
235: }
236:
237: /**
238: * Returns the interval of the frequency.
239: *@return long Interval value
240: */
241: public long getInterval() {
242: if (rule.get("intervalNumber") == null)
243: return 1;
244: return rule.getLong("intervalNumber").longValue();
245: }
246:
247: /**
248: * Returns the interval of the frequency as an int.
249: *@return The interval of this frequency as an integer.
250: */
251: public int getIntervalInt() {
252: // if (Debug.verboseOn()) Debug.logVerbose("[RecurrenceInfo.getInterval] : " + getInterval(), module);
253: return (int) getInterval();
254: }
255:
256: /**
257: * Returns the next recurrence of this rule.
258: *@param startTime The time this recurrence first began.
259: *@param fromTime The time to base the next recurrence on.
260: *@param currentCount The total number of times the recurrence has run.
261: *@return long The next recurrence as a long.
262: */
263: public long next(long startTime, long fromTime, long currentCount) {
264: // Set up the values
265: if (startTime == 0)
266: startTime = RecurrenceUtil.now();
267: if (fromTime == 0)
268: fromTime = startTime;
269:
270: // Test the end time of the recurrence.
271: if (getEndTime() != 0 && getEndTime() <= RecurrenceUtil.now())
272: return 0;
273: Debug.logVerbose("Rule NOT expired by end time.", module);
274:
275: // Test the recurrence limit.
276: if (getCount() != -1 && currentCount >= getCount())
277: return 0;
278: Debug.logVerbose("Rule NOT expired by max count.", module);
279:
280: boolean isSeeking = true;
281: long nextRuntime = 0;
282: long seekTime = fromTime;
283: int loopProtection = 0;
284: int maxLoop = (10 * 10 * 10 * 10 * 10);
285:
286: while (isSeeking && loopProtection < maxLoop) {
287: Date nextRun = getNextFreq(startTime, seekTime);
288: seekTime = nextRun.getTime();
289: if (validByRule(nextRun)) {
290: isSeeking = false;
291: nextRuntime = nextRun.getTime();
292: }
293: loopProtection++;
294: }
295: return nextRuntime;
296: }
297:
298: /**
299: * Tests the date to see if it falls within the rules
300: *@param startDate date object to test
301: *@return True if the date is within the rules
302: */
303: public boolean isValid(Date startDate, Date date) {
304: return isValid(startDate.getTime(), date.getTime());
305: }
306:
307: /**
308: * Tests the date to see if it falls within the rules
309: *@param startTime date object to test
310: *@return True if the date is within the rules
311: */
312: public boolean isValid(long startTime, long dateTime) {
313: long testTime = startTime;
314:
315: if (testTime == dateTime)
316: return true;
317: while (testTime < dateTime) {
318: testTime = next(startTime, testTime, 1);
319: if (testTime == dateTime)
320: return true;
321: }
322: return false;
323: }
324:
325: /**
326: * Removes this rule from the persistant store.
327: *@throws RecurrenceRuleException
328: */
329: public void remove() throws RecurrenceRuleException {
330: try {
331: rule.remove();
332: } catch (GenericEntityException e) {
333: throw new RecurrenceRuleException(e.getMessage(), e);
334: }
335: }
336:
337: // Gets the next frequency/interval recurrence from specified time
338: private Date getNextFreq(long startTime, long fromTime) {
339: // Build a Calendar object
340: Calendar cal = Calendar.getInstance();
341:
342: cal.setTime(new Date(startTime));
343:
344: long nextStartTime = startTime;
345:
346: while (nextStartTime < fromTime) {
347: // if (Debug.verboseOn()) Debug.logVerbose("[RecurrenceInfo.getNextFreq] : Updating time - " + getFrequency(), module);
348: switch (getFrequency()) {
349: case SECONDLY:
350: cal.add(Calendar.SECOND, getIntervalInt());
351: break;
352:
353: case MINUTELY:
354: cal.add(Calendar.MINUTE, getIntervalInt());
355: break;
356:
357: case HOURLY:
358: cal.add(Calendar.HOUR, getIntervalInt());
359: break;
360:
361: case DAILY:
362: cal.add(Calendar.DAY_OF_MONTH, getIntervalInt());
363: break;
364:
365: case WEEKLY:
366: cal.add(Calendar.WEEK_OF_YEAR, getIntervalInt());
367: break;
368:
369: case MONTHLY:
370: cal.add(Calendar.MONTH, getIntervalInt());
371: break;
372:
373: case YEARLY:
374: cal.add(Calendar.YEAR, getIntervalInt());
375: break;
376:
377: default:
378: return null; // should never happen
379: }
380: nextStartTime = cal.getTime().getTime();
381: }
382: return new Date(nextStartTime);
383: }
384:
385: // Checks to see if a date is valid by the byXXX rules
386: private boolean validByRule(Date date) {
387: // Build a Calendar object
388: Calendar cal = Calendar.getInstance();
389:
390: cal.setTime(date);
391:
392: // Test each byXXX rule.
393: if (bySecondList != null && bySecondList.size() > 0) {
394: if (!bySecondList.contains(new Integer(cal
395: .get(Calendar.SECOND))))
396: return false;
397: }
398: if (byMinuteList != null && byMinuteList.size() > 0) {
399: if (!byMinuteList.contains(new Integer(cal
400: .get(Calendar.MINUTE))))
401: return false;
402: }
403: if (byHourList != null && byHourList.size() > 0) {
404: if (!byHourList
405: .contains(new Integer(cal.get(Calendar.HOUR))))
406: return false;
407: }
408: if (byDayList != null && byDayList.size() > 0) {
409: Iterator iter = byDayList.iterator();
410: boolean foundDay = false;
411:
412: while (iter.hasNext() && !foundDay) {
413: String dayRule = (String) iter.next();
414: String dayString = getDailyString(dayRule);
415:
416: if (Calendar.DAY_OF_WEEK == getCalendarDay(dayString)) {
417: if ((hasNumber(dayRule))
418: && (getFrequency() == MONTHLY || getFrequency() == YEARLY)) {
419: int modifier = getDailyNumber(dayRule);
420:
421: if (modifier == 0)
422: foundDay = true;
423:
424: if (getFrequency() == MONTHLY) {
425: // figure if we are the nth xDAY if this month
426: int currentPos = cal
427: .get(Calendar.WEEK_OF_MONTH);
428: int dayPosCalc = cal
429: .get(Calendar.DAY_OF_MONTH)
430: - ((currentPos - 1) * 7);
431:
432: if (dayPosCalc < 1)
433: currentPos--;
434: if (modifier > 0) {
435: if (currentPos == modifier) {
436: foundDay = true;
437: }
438: } else if (modifier < 0) {
439: int maxDay = cal
440: .getActualMaximum(Calendar.DAY_OF_MONTH);
441: int firstDay = dayPosCalc > 0 ? dayPosCalc
442: : dayPosCalc + 7;
443: int totalDay = ((maxDay - firstDay) / 7) + 1;
444: int this Diff = (currentPos - totalDay) - 1;
445:
446: if (this Diff == modifier) {
447: foundDay = true;
448: }
449: }
450: } else if (getFrequency() == YEARLY) {
451: // figure if we are the nth xDAY if this year
452: int currentPos = cal
453: .get(Calendar.WEEK_OF_YEAR);
454: int dayPosCalc = cal
455: .get(Calendar.DAY_OF_YEAR)
456: - ((currentPos - 1) * 7);
457:
458: if (dayPosCalc < 1) {
459: currentPos--;
460: }
461: if (modifier > 0) {
462: if (currentPos == modifier) {
463: foundDay = true;
464: }
465: } else if (modifier < 0) {
466: int maxDay = cal
467: .getActualMaximum(Calendar.DAY_OF_YEAR);
468: int firstDay = dayPosCalc > 0 ? dayPosCalc
469: : dayPosCalc + 7;
470: int totalDay = ((maxDay - firstDay) / 7) + 1;
471: int this Diff = (currentPos - totalDay) - 1;
472:
473: if (this Diff == modifier) {
474: foundDay = true;
475: }
476: }
477: }
478: } else {
479: // we are a DOW only rule
480: foundDay = true;
481: }
482: }
483: }
484: if (!foundDay) {
485: return false;
486: }
487: }
488: if (byMonthDayList != null && byMonthDayList.size() > 0) {
489: Iterator iter = byMonthDayList.iterator();
490: boolean foundDay = false;
491:
492: while (iter.hasNext() && !foundDay) {
493: int day = 0;
494: String dayStr = (String) iter.next();
495:
496: try {
497: day = Integer.parseInt(dayStr);
498: } catch (NumberFormatException nfe) {
499: Debug.logError(nfe, "Error parsing day string "
500: + dayStr + ": " + nfe.toString(), module);
501: }
502: int maxDay = cal
503: .getActualMaximum(Calendar.DAY_OF_MONTH);
504: int currentDay = cal.get(Calendar.DAY_OF_MONTH);
505:
506: if (day > 0 && day == currentDay) {
507: foundDay = true;
508: }
509: if (day < 0 && day == ((currentDay - maxDay) - 1)) {
510: foundDay = true;
511: }
512: }
513: if (!foundDay) {
514: return false;
515: }
516: }
517: if (byYearDayList != null && byYearDayList.size() > 0) {
518: Iterator iter = byYearDayList.iterator();
519: boolean foundDay = false;
520:
521: while (iter.hasNext() && !foundDay) {
522: int day = 0;
523: String dayStr = (String) iter.next();
524:
525: try {
526: day = Integer.parseInt(dayStr);
527: } catch (NumberFormatException nfe) {
528: Debug.logError(nfe, "Error parsing day string "
529: + dayStr + ": " + nfe.toString(), module);
530: }
531: int maxDay = cal.getActualMaximum(Calendar.DAY_OF_YEAR);
532: int currentDay = cal.get(Calendar.DAY_OF_YEAR);
533:
534: if (day > 0 && day == currentDay)
535: foundDay = true;
536: if (day < 0 && day == ((currentDay - maxDay) - 1))
537: foundDay = true;
538: }
539: if (!foundDay)
540: return false;
541: }
542: if (byWeekNoList != null && byWeekNoList.size() > 0) {
543: Iterator iter = byWeekNoList.iterator();
544: boolean foundWeek = false;
545:
546: while (iter.hasNext() && !foundWeek) {
547: int week = 0;
548: String weekStr = (String) iter.next();
549:
550: try {
551: week = Integer.parseInt(weekStr);
552: } catch (NumberFormatException nfe) {
553: Debug.logError(nfe, "Error parsing week string "
554: + weekStr + ": " + nfe.toString(), module);
555: }
556: int maxWeek = cal
557: .getActualMaximum(Calendar.WEEK_OF_YEAR);
558: int currentWeek = cal.get(Calendar.WEEK_OF_YEAR);
559:
560: if (week > 0 && week == currentWeek)
561: foundWeek = true;
562: if (week < 0 && week == ((currentWeek - maxWeek) - 1))
563: foundWeek = true;
564: }
565: if (!foundWeek)
566: return false;
567: }
568: if (byMonthList != null && byMonthList.size() > 0) {
569: Iterator iter = byMonthList.iterator();
570: boolean foundMonth = false;
571:
572: while (iter.hasNext() && !foundMonth) {
573: int month = 0;
574: String monthStr = (String) iter.next();
575:
576: try {
577: month = Integer.parseInt(monthStr);
578: } catch (NumberFormatException nfe) {
579: Debug.logError(nfe, "Error parsing month string "
580: + monthStr + ": " + nfe.toString(), module);
581: }
582: if (month == cal.get(Calendar.MONTH)) {
583: foundMonth = true;
584: }
585: }
586: if (!foundMonth)
587: return false;
588: }
589:
590: return true;
591: }
592:
593: // Tests a string for the contents of a number at the beginning
594: private boolean hasNumber(String str) {
595: String list[] = { "+", "-", "1", "2", "3", "4", "5", "6", "7",
596: "8", "9", "0" };
597: List numberList = Arrays.asList(list);
598: String firstChar = str.substring(0, 0);
599:
600: if (numberList.contains(firstChar))
601: return true;
602: return false;
603: }
604:
605: // Gets the numeric value of the number at the beginning of the string
606: private int getDailyNumber(String str) {
607: int number = 0;
608: StringBuffer numberBuf = new StringBuffer();
609:
610: for (int i = 0; i < str.length(); i++) {
611: String this Char = str.substring(i, i);
612:
613: if (hasNumber(this Char))
614: numberBuf.append(this Char);
615: }
616: String numberStr = numberBuf.toString();
617:
618: if (numberStr.length() > 0
619: && (numberStr.length() > 1 || (numberStr.charAt(0) != '+' && numberStr
620: .charAt(0) != '-'))) {
621: try {
622: number = Integer.parseInt(numberStr);
623: } catch (NumberFormatException nfe) {
624: Debug.logError(nfe,
625: "Error parsing daily number string "
626: + numberStr + ": " + nfe.toString(),
627: module);
628: }
629: }
630: return number;
631: }
632:
633: // Gets the string part of the combined number+string
634: private String getDailyString(String str) {
635: StringBuffer sBuf = new StringBuffer();
636:
637: for (int i = 0; i < str.length(); i++) {
638: String this Char = str.substring(i, i);
639:
640: if (!hasNumber(this Char)) {
641: sBuf.append(this Char);
642: }
643: }
644: return sBuf.toString();
645: }
646:
647: // Returns the Calendar day of the rule day string
648: private int getCalendarDay(String day) {
649: if (day.equalsIgnoreCase("MO"))
650: return Calendar.MONDAY;
651: if (day.equalsIgnoreCase("TU"))
652: return Calendar.TUESDAY;
653: if (day.equalsIgnoreCase("WE"))
654: return Calendar.WEDNESDAY;
655: if (day.equalsIgnoreCase("TH"))
656: return Calendar.THURSDAY;
657: if (day.equalsIgnoreCase("FR"))
658: return Calendar.FRIDAY;
659: if (day.equalsIgnoreCase("SA"))
660: return Calendar.SATURDAY;
661: if (day.equalsIgnoreCase("SU"))
662: return Calendar.SUNDAY;
663: return 0;
664: }
665:
666: public String primaryKey() {
667: return rule.getString("recurrenceRuleId");
668: }
669:
670: public static RecurrenceRule makeRule(GenericDelegator delegator,
671: int frequency, int interval, int count)
672: throws RecurrenceRuleException {
673: return makeRule(delegator, frequency, interval, count, 0);
674: }
675:
676: public static RecurrenceRule makeRule(GenericDelegator delegator,
677: int frequency, int interval, long endTime)
678: throws RecurrenceRuleException {
679: return makeRule(delegator, frequency, interval, -1, endTime);
680: }
681:
682: public static RecurrenceRule makeRule(GenericDelegator delegator,
683: int frequency, int interval, int count, long endTime)
684: throws RecurrenceRuleException {
685: String freq[] = { "", "SECONDLY", "MINUTELY", "HOURLY",
686: "DAILY", "WEEKLY", "MONTHLY", "YEARLY" };
687:
688: if (frequency < 1 || frequency > 7)
689: throw new RecurrenceRuleException("Invalid frequency");
690: if (interval < 0)
691: throw new RecurrenceRuleException("Invalid interval");
692:
693: String freqStr = freq[frequency];
694:
695: try {
696: String ruleId = delegator.getNextSeqId("RecurrenceRule");
697: GenericValue value = delegator.makeValue("RecurrenceRule",
698: UtilMisc.toMap("recurrenceRuleId", ruleId));
699:
700: value.set("frequency", freqStr);
701: value.set("intervalNumber", new Long(interval));
702: value.set("countNumber", new Long(count));
703: if (endTime > 0) {
704: value.set("untilDateTime", new java.sql.Timestamp(
705: endTime));
706: }
707: delegator.create(value);
708: RecurrenceRule newRule = new RecurrenceRule(value);
709:
710: return newRule;
711: } catch (GenericEntityException ee) {
712: throw new RecurrenceRuleException(ee.getMessage(), ee);
713: } catch (RecurrenceRuleException re) {
714: throw re;
715: }
716: }
717: }
|