001: /* ====================================================================
002: * The Jcorporate Apache Style Software License, Version 1.2 05-07-2002
003: *
004: * Copyright (c) 1995-2002 Jcorporate Ltd. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions
008: * are met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in
015: * the documentation and/or other materials provided with the
016: * distribution.
017: *
018: * 3. The end-user documentation included with the redistribution,
019: * if any, must include the following acknowledgment:
020: * "This product includes software developed by Jcorporate Ltd.
021: * (http://www.jcorporate.com/)."
022: * Alternately, this acknowledgment may appear in the software itself,
023: * if and wherever such third-party acknowledgments normally appear.
024: *
025: * 4. "Jcorporate" and product names such as "Expresso" must
026: * not be used to endorse or promote products derived from this
027: * software without prior written permission. For written permission,
028: * please contact info@jcorporate.com.
029: *
030: * 5. Products derived from this software may not be called "Expresso",
031: * or other Jcorporate product names; nor may "Expresso" or other
032: * Jcorporate product names appear in their name, without prior
033: * written permission of Jcorporate Ltd.
034: *
035: * 6. No product derived from this software may compete in the same
036: * market space, i.e. framework, without prior written permission
037: * of Jcorporate Ltd. For written permission, please contact
038: * partners@jcorporate.com.
039: *
040: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
041: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
042: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
043: * DISCLAIMED. IN NO EVENT SHALL JCORPORATE LTD OR ITS CONTRIBUTORS
044: * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
045: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
046: * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
047: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
048: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
049: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
050: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
051: * SUCH DAMAGE.
052: * ====================================================================
053: *
054: * This software consists of voluntary contributions made by many
055: * individuals on behalf of the Jcorporate Ltd. Contributions back
056: * to the project(s) are encouraged when you make modifications.
057: * Please send them to support@jcorporate.com. For more information
058: * on Jcorporate Ltd. and its products, please see
059: * <http://www.jcorporate.com/>.
060: *
061: * Portions of this software are based upon other open source
062: * products and are subject to their respective licenses.
063: */
064: package com.jcorporate.expresso.services.crontab;
065:
066: import org.apache.log4j.Logger;
067:
068: import java.util.Calendar;
069: import java.util.Date;
070:
071: /**
072: * CrontabEntry represents a repeatable 'action' that can take place at a given
073: * date and time.
074: * <p/>
075: * <p/>
076: * A crontab entry's sort order creates differences between Comparable and
077: * equals(). the objects are compared by the execution time. They are tested
078: * for equality by testing time AND the listeners
079: * </p>
080: *
081: * @author Mike Dubman
082: */
083: public class CrontabEntry implements Comparable, java.io.Serializable {
084: private static final Logger log = Logger
085: .getLogger(CrontabEntry.class);
086:
087: /**
088: * Constant that represents a 'time' value that is unused.
089: */
090: public static final int TIME_UNUSED = -1;
091:
092: /**
093: *
094: */
095: protected static long globalCounter = 0;
096:
097: /**
098: *
099: */
100: protected static Object counterLock = new Object();
101:
102: /**
103: * The listener of the crontab
104: */
105: protected transient CrontabListenerI listener;
106:
107: /**
108: * Label for the crontab
109: */
110: protected String label = "";
111:
112: /**
113: * Is the crontab a 'relative' time?
114: */
115: protected boolean isRelative;
116:
117: /**
118: * Is the crontab a repetitive job
119: */
120: protected boolean isRepetitive;
121:
122: /**
123: * day of month for the crontab
124: */
125: protected int dayOfMonth = TIME_UNUSED;
126:
127: /**
128: * day of week for the crontab
129: */
130: protected int dayOfWeek = TIME_UNUSED;
131:
132: /**
133: * Hours for the crontab
134: */
135: protected int hour = TIME_UNUSED;
136:
137: /**
138: * The minutes for the crontab
139: */
140: protected int minute = TIME_UNUSED;
141:
142: /**
143: * month for the crontab
144: */
145: protected int month = TIME_UNUSED;
146:
147: /**
148: * year for the crontab
149: */
150: protected int year = TIME_UNUSED;
151:
152: /**
153: * When is the next time for the alarm
154: */
155: protected long alarmTime;
156:
157: /**
158: * Unique id for sorting.
159: */
160: protected long counterValue;
161:
162: /**
163: * Job Number for this CrontabEntry's associated JobQueue entry
164: */
165: protected String jobNumber = null;
166:
167: /**
168: * Constructs a Crontab Entry class.
169: *
170: * @param date The date to execute the crontab listener
171: * @param listener The class to execute when the alarm 'rings'
172: * @throws CronException upon error
173: */
174: public CrontabEntry(Date date, CrontabListenerI listener)
175: throws CronException {
176: this .listener = listener;
177:
178: setCounterValue();
179:
180: Calendar alarm = Calendar.getInstance();
181: alarm.setTime(date);
182: minute = alarm.get(Calendar.MINUTE);
183: hour = alarm.get(Calendar.HOUR_OF_DAY);
184: dayOfMonth = alarm.get(Calendar.DAY_OF_MONTH);
185: month = alarm.get(Calendar.MONTH);
186: year = alarm.get(Calendar.YEAR);
187: isRepetitive = false;
188: isRelative = false;
189: alarmTime = date.getTime();
190: checkEntryTime();
191:
192: if (log.isDebugEnabled()) {
193: log.debug("Constructed new crontab entry: "
194: + this .toString());
195: }
196: }
197:
198: /**
199: * Construct a new crontab entry with a given delay
200: *
201: * @param delay the job until the number of minutes in the next hour
202: * @param isRepetitive set if the job should be executed every X amount of
203: * time
204: * @param listener the interface to notify when the cron 'rings'
205: * @throws IllegalArgumentException if the delay is less than 1
206: */
207: public CrontabEntry(int delay, boolean isRepetitive,
208: CrontabListenerI listener) {
209: if (delay < 1) {
210: throw new IllegalArgumentException(
211: "Parameter delay must be >= 1");
212: }
213:
214: setCounterValue();
215:
216: minute = delay;
217: this .listener = listener;
218: this .isRepetitive = isRepetitive;
219:
220: isRelative = true;
221: updateEntryTime();
222: }
223:
224: /**
225: * Construct a Crontab entry.
226: *
227: * @param minute Minute of the hour to execute
228: * @param hour Hour of the day to execute
229: * @param dayOfMonth The day of the month to execute
230: * @param month The month of the year to execute
231: * @param dayOfWeek The day of the week to execute
232: * @param year to execute OR TIME_UNUSED if this is a repetative crontab
233: * entry
234: * @param listener The class that gets called when the crontab 'rings'
235: * @throws CronException upon error
236: */
237: public CrontabEntry(int minute, int hour, int dayOfMonth,
238: int month, int dayOfWeek, int year,
239: CrontabListenerI listener) throws CronException {
240: setCounterValue();
241:
242: this .minute = minute;
243: this .hour = hour;
244: this .dayOfMonth = dayOfMonth;
245: this .month = month;
246: this .dayOfWeek = dayOfWeek;
247: this .year = year;
248: this .listener = listener;
249: this .label = "Unlabelled Crontab Entry";
250: isRepetitive = (year == TIME_UNUSED);
251: isRelative = false;
252: updateEntryTime();
253: checkEntryTime();
254:
255: if (log.isDebugEnabled()) {
256: log.debug("Constructed new crontab entry: "
257: + this .toString());
258: }
259: }
260:
261: /**
262: * Construct a Crontab entry.
263: *
264: * @param minute Minute of the hour to execute
265: * @param hour Hour of the day to execute
266: * @param dayOfMonth The day of the month to execute
267: * @param month The month of the year to execute
268: * @param dayOfWeek The day of the week to execute
269: * @param year to execute OR TIME_UNUSED if this is a repetative crontab
270: * entry
271: * @param jobLabel - The label of the crontab entry.
272: * @param listener The class that gets called when the crontab 'rings'
273: * @throws CronException upon error
274: */
275: public CrontabEntry(int minute, int hour, int dayOfMonth,
276: int month, int dayOfWeek, int year, String jobLabel,
277: CrontabListenerI listener) throws CronException {
278: setCounterValue();
279: this .minute = minute;
280: this .hour = hour;
281: this .dayOfMonth = dayOfMonth;
282: this .month = month;
283: this .dayOfWeek = dayOfWeek;
284: this .year = year;
285: this .listener = listener;
286: this .label = jobLabel;
287: isRepetitive = (year == TIME_UNUSED);
288: isRelative = false;
289: updateEntryTime();
290: checkEntryTime();
291:
292: if (log.isDebugEnabled()) {
293: log.debug("Constructed new crontab entry: "
294: + this .toString());
295: }
296: }
297:
298: /**
299: * Get the alarm time in system time.
300: *
301: * @return long integer.
302: */
303: public long getAlarmTime() {
304: return alarmTime;
305: }
306:
307: /**
308: * Retrieve an incrementing id value unique across crontab instances
309: *
310: * @return long value.
311: */
312: public long getCounter() {
313: return this .counterValue;
314: }
315:
316: /**
317: * Retrieve the day of month setting
318: *
319: * @return integer
320: */
321: public int getDayOfMonth() {
322: return dayOfMonth;
323: }
324:
325: /**
326: * Return day of week setting
327: *
328: * @return integer
329: */
330: public int getDayOfWeek() {
331: return dayOfWeek;
332: }
333:
334: /**
335: * Retrieve hour setting
336: *
337: * @return integer
338: */
339: public int getHour() {
340: return hour;
341: }
342:
343: /**
344: * Return isRelative value
345: *
346: * @return boolean
347: */
348: public boolean isIsRelative() {
349: return isRelative;
350: }
351:
352: /**
353: * Retrieve the isRepetitive value
354: *
355: * @return boolean
356: */
357: public boolean isIsRepetitive() {
358: return isRepetitive;
359: }
360:
361: /**
362: * Retrieve the label of the entry
363: *
364: * @return java.lang.String
365: */
366: public String getLabel() {
367: return label;
368: }
369:
370: /**
371: * Retrieve the listener for the crontab.
372: *
373: * @return
374: */
375: public CrontabListenerI getListener() {
376: return this .listener;
377: }
378:
379: /**
380: * Retrieve the minutes of the entry
381: *
382: * @return integer
383: */
384: public int getMinute() {
385: return minute;
386: }
387:
388: /**
389: * Return the month value of the entry
390: *
391: * @return integer
392: */
393: public int getMonth() {
394: return month;
395: }
396:
397: /**
398: * Return the year value of the entry
399: *
400: * @return integer
401: */
402: public int getYear() {
403: return year;
404: }
405:
406: /**
407: * Standard comparison operator. Checks against the alarm entry time.
408: * <p/>
409: * <p/>
410: * compareTo and equals behave differently. compareTo only sorts by alarm
411: * time. Equals checks alarm times and listeners
412: * </p>
413: *
414: * @param obj The object to compare against.
415: * @return integer as per standard Object.compareTo values
416: * @see java.lang.Comparable#compareTo
417: */
418: public synchronized int compareTo(Object obj) {
419: CrontabEntry entry = (CrontabEntry) obj;
420:
421: if (alarmTime < entry.alarmTime) {
422: return -1;
423: } else if (alarmTime > entry.alarmTime) {
424: return 1;
425: } else {
426: return (new Long(counterValue)).compareTo(new Long(
427: entry.counterValue));
428: }
429: }
430:
431: /**
432: * Returns true if the two alarm times of the comparing crontab entries are
433: * equal.
434: *
435: * @param obj The CrontabEntry to compare against.
436: * @return true if the two objects are equal
437: */
438: public synchronized boolean equals(Object obj) {
439: try {
440: CrontabEntry entry = (CrontabEntry) obj;
441:
442: if ((alarmTime == entry.alarmTime)
443: && (listener == entry.listener)) {
444: return true;
445: }
446: } catch (ClassCastException cce) {
447: log.error("Class cast exception comparing objects", cce);
448: }
449:
450: return false;
451: }
452:
453: /**
454: * Returns the class as a string. Useful for debugging purposes
455: *
456: * @return a String representing this class.
457: */
458: public synchronized String toString() {
459: if (year != TIME_UNUSED) {
460: return "Cron Entry at " + new Date(alarmTime);
461: }
462:
463: StringBuffer sb = new StringBuffer("CronTabEntry params");
464: sb.append(" minute=");
465: sb.append(minute);
466: sb.append(" hour=");
467: sb.append(hour);
468: sb.append(" dayOfMonth=");
469: sb.append(dayOfMonth);
470: sb.append(" month=");
471: sb.append(month);
472: sb.append(" dayOfWeek=");
473: sb.append(dayOfWeek);
474: sb.append(" (next alarm date=" + new Date(alarmTime) + ")");
475:
476: return sb.toString();
477: }
478:
479: /**
480: * Set the new/latest time. For example, if it's a repeatable and the last
481: * alarm time is passed, then we calculate the next one.
482: */
483: public synchronized void updateEntryTime() {
484: if (isRelative) {
485: alarmTime = System.currentTimeMillis() + (minute * 60000);
486:
487: return;
488: }
489:
490: Calendar now = Calendar.getInstance();
491: Calendar alarm = (Calendar) now.clone();
492:
493: if (log.isDebugEnabled()) {
494: log.debug("now: " + now.getTime());
495: }
496:
497: if (year > TIME_UNUSED) {
498: alarm.set(Calendar.YEAR, year);
499: }
500:
501: if (month > TIME_UNUSED) {
502: alarm.set(Calendar.MONTH, month);
503: }
504:
505: if (hour > TIME_UNUSED) {
506: alarm.set(Calendar.HOUR_OF_DAY, hour);
507: }
508:
509: if (minute > TIME_UNUSED) {
510: alarm.set(Calendar.MINUTE, minute);
511: }
512:
513: alarm.set(Calendar.SECOND, 0);
514:
515: //Now roll as necessary to make it the next proper execution time.
516: adjustExecutionTime(now, alarm);
517:
518: if (log.isDebugEnabled()) {
519: log.debug("alarm: " + alarm.getTime());
520: }
521:
522: alarmTime = alarm.getTime().getTime();
523: }
524:
525: /**
526: * Sets the counter so that order is guaranteed
527: */
528: protected void setCounterValue() {
529: //
530: //We use an incremental counter to guarantee execution in the order
531: //of the original queue if times are equal
532: //
533: synchronized (counterLock) {
534: counterValue = globalCounter;
535: globalCounter++;
536: }
537: }
538:
539: /**
540: * Adjusts the execution time so that the next execution time is proper
541: * ie.. all cron rules should be followed and the next cron execution
542: * should be greater than now.
543: *
544: * @param now 'now' calendar instance
545: * @param alarm the next instance that the calendar should execute.
546: */
547: protected synchronized void adjustExecutionTime(Calendar now,
548: Calendar alarm) {
549: // for every minute alarm
550: if (minute == TIME_UNUSED) {
551: // Increments hour if now >= alarm (for every x minutes alarms)
552: if ((hour > TIME_UNUSED && now.get(Calendar.HOUR_OF_DAY) != hour)
553: || (dayOfMonth > TIME_UNUSED && now
554: .get(Calendar.DAY_OF_MONTH) != dayOfMonth)
555: || (dayOfWeek > TIME_UNUSED && now
556: .get(Calendar.DAY_OF_WEEK) != dayOfWeek)) {
557: // if we specify a hour and it's not that hour, or doing a dayOfMonth or dayOfWeek and it's not that day,
558: // we want to set the minute to 0
559: alarm.set(Calendar.MINUTE, 0);
560: } else if ((hour <= TIME_UNUSED)
561: && (now.get(Calendar.MINUTE) >= minute)) {
562: // if we're not looking at the hour, we need to rollover to next hour if it's :59
563: alarm.add(Calendar.MINUTE, 1);
564: } else if ((hour > TIME_UNUSED)
565: && (now.get(Calendar.MINUTE) >= minute)
566: && (now.get(Calendar.MINUTE) == 59)) {
567: // if we have a specific hour and it's the 59th minute, we need to roll the minute and day
568: alarm.roll(Calendar.MINUTE, true);
569: alarm.roll(Calendar.DAY_OF_YEAR, true);
570: } else if (now.get(Calendar.MINUTE) >= minute) {
571: // otherwise, increase minute (without increasing hour)
572: alarm.roll(Calendar.MINUTE, true);
573: }
574: }
575:
576: //For every hour alarm
577: if (minute != TIME_UNUSED && hour == TIME_UNUSED) {
578: // Increments hour if now >= alarm (for every x minutes alarms)
579: if ((dayOfMonth > TIME_UNUSED && now
580: .get(Calendar.DAY_OF_MONTH) != dayOfMonth)
581: || (dayOfWeek > TIME_UNUSED && now
582: .get(Calendar.DAY_OF_WEEK) != dayOfWeek)) {
583: // if we're doing a dayOfMonth or dayOfWeek and it's not that day, we want to set the hour to midnight
584: alarm.set(Calendar.HOUR_OF_DAY, 0);
585: } else if ((dayOfMonth <= TIME_UNUSED)
586: && (dayOfWeek <= TIME_UNUSED)
587: && (now.get(Calendar.MINUTE) >= minute)) {
588: // if we're not looking at the day, we need to rollover to tomorrow if it's 23:00
589: alarm.add(Calendar.HOUR_OF_DAY, 1);
590: } else if (now.get(Calendar.MINUTE) >= minute) {
591: // otherwise, increase hour (without increasing day)
592: alarm.roll(Calendar.HOUR_OF_DAY, true);
593: }
594: }
595:
596: // Increments hour if now >= alarm (for every x hours alarms)
597: // if (hour > TIME_UNUSED && minute > TIME_UNUSED) {
598: // if (now.get(Calendar.MINUTE) >= minute) //hour not increased during job
599: // alarm.add(Calendar.HOUR_OF_DAY, -hour);
600: // else
601: // alarm.add(Calendar.HOUR_OF_DAY, hour - 1);//assume hour increased with one
602: // }
603:
604: // Incrementes dayOfYear if now >= alarm (for every x days alarms)
605: // if ((dayOfMonth <= TIME_UNUSED) && (dayOfWeek <= TIME_UNUSED) &&
606: // (hour > TIME_UNUSED) && (minute > TIME_UNUSED) &&
607: // ((now.get(Calendar.HOUR_OF_DAY) > hour) ||
608: // ((now.get(Calendar.HOUR_OF_DAY) == hour) &&
609: // (now.get(Calendar.MINUTE) >= minute)))) {
610: // alarm.roll(Calendar.DAY_OF_YEAR, true);
611: // }
612:
613: if ((dayOfMonth <= TIME_UNUSED)
614: && (dayOfWeek <= TIME_UNUSED)
615: && (((hour > TIME_UNUSED) && now
616: .get(Calendar.HOUR_OF_DAY) > hour) || ((minute > TIME_UNUSED)
617: && (now.get(Calendar.HOUR_OF_DAY) == hour) && (now
618: .get(Calendar.MINUTE) >= minute)))) {
619: alarm.roll(Calendar.DAY_OF_YEAR, true);
620: }
621:
622: // Incrementes year if now >= alarm (for monthly or yearly alarms)
623: if ((month != TIME_UNUSED)
624: && (year == TIME_UNUSED)
625: && ((now.get(Calendar.MONTH) > month) || ((now
626: .get(Calendar.MONTH) == month) && ((now
627: .get(Calendar.DAY_OF_MONTH) > dayOfMonth) || ((now
628: .get(Calendar.DAY_OF_MONTH) == dayOfMonth) && ((now
629: .get(Calendar.HOUR_OF_DAY) > hour) || ((now
630: .get(Calendar.HOUR_OF_DAY) == hour) && (now
631: .get(Calendar.MINUTE) >= minute)))))))) {
632: alarm.add(Calendar.YEAR, 1);
633: }
634:
635: if (dayOfWeek != TIME_UNUSED) { // Weekly alarms
636:
637: int deltaOfDay = (7 + (dayOfWeek - now
638: .get(Calendar.DAY_OF_WEEK))) % 7;
639:
640: if (log.isDebugEnabled()) {
641: log.debug("deltaOfDay: " + deltaOfDay);
642: }
643:
644: if (deltaOfDay != 0) {
645: alarm.add(Calendar.DAY_OF_YEAR, deltaOfDay);
646: } else if ((now.get(Calendar.HOUR_OF_DAY) > hour)
647: || ((now.get(Calendar.HOUR_OF_DAY) == hour) && (now
648: .get(Calendar.MINUTE) >= minute))) { // Incrementes week if now >= alarm
649: alarm.add(Calendar.WEEK_OF_YEAR, 1);
650: }
651: } else if (dayOfMonth != TIME_UNUSED) { // Monthly alarms
652: alarm.set(Calendar.DAY_OF_MONTH, dayOfMonth);
653:
654: // Incrementes month if now >= alarm (for weekly alarms)
655: if ((month == TIME_UNUSED)
656: && ((now.get(Calendar.DAY_OF_MONTH) > dayOfMonth) || ((now
657: .get(Calendar.DAY_OF_MONTH) == dayOfMonth) && ((now
658: .get(Calendar.HOUR_OF_DAY) > hour) || ((now
659: .get(Calendar.HOUR_OF_DAY) == hour) && (now
660: .get(Calendar.MINUTE) >= minute)))))) {
661: alarm.roll(Calendar.MONTH, true);
662: }
663: }
664: }
665:
666: /**
667: * Checks the entry time.
668: *
669: * @throws CronException if delay is less than 1 second.
670: */
671: void checkEntryTime() throws CronException {
672: long delay = alarmTime - System.currentTimeMillis();
673:
674: if (delay <= 1000) {
675: throw new CronException();
676: }
677: }
678:
679: /**
680: * Return Job Number for this CrontabEntry's associated JobQueue entry
681: *
682: * @return Job Number for this CrontabEntry's associated JobQueue entry
683: */
684: public String getJobNumber() {
685: return jobNumber;
686: }
687:
688: /**
689: * Set Job Number for this CrontabEntry's associated JobQueue entry
690: *
691: * @param s Job Number for this CrontabEntry's associated JobQueue entry
692: */
693: public void setJobNumber(String s) {
694: jobNumber = s;
695: }
696:
697: }
|