0001: /*
0002: * Copyright 2004-2005 OpenSymphony
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
0005: * use this file except in compliance with the License. You may obtain a copy
0006: * of the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
0012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
0013: * License for the specific language governing permissions and limitations
0014: * under the License.
0015: *
0016: */
0017:
0018: package org.quartz;
0019:
0020: import java.text.NumberFormat;
0021: import java.util.Date;
0022: import java.util.TimeZone;
0023:
0024: /**
0025: * A trigger which fires on the N<SUP>th</SUP> day of every interval type
0026: * ({@link #INTERVAL_TYPE_WEEKLY}, {@link #INTERVAL_TYPE_MONTHLY} or
0027: * {@link #INTERVAL_TYPE_YEARLY}) that is <i>not</i> excluded by the associated
0028: * calendar. When determining what the N<SUP>th</SUP> day of the month or year
0029: * is, <CODE>NthIncludedDayTrigger</CODE> will skip excluded days on the
0030: * associated calendar. This would commonly be used in an N<SUP>th</SUP>
0031: * business day situation, in which the user wishes to fire a particular job on
0032: * the N<SUP>th</SUP> business day (i.e. the 5<SUP>th</SUP> business day of
0033: * every month). Each <CODE>NthIncludedDayTrigger</CODE> also has an associated
0034: * <CODE>fireAtTime</CODE> which indicates at what time of day the trigger is
0035: * to fire.
0036: * <P>
0037: * All <CODE>NthIncludedDayTrigger</CODE>s default to a monthly interval type
0038: * (fires on the N<SUP>th</SUP> day of every month) with N = 1 (first
0039: * non-excluded day) and <CODE>fireAtTime</CODE> set to 12:00 PM (noon). These
0040: * values can be changed using the {@link #setN}, {@link #setIntervalType}, and
0041: * {@link #setFireAtTime} methods. Users may also want to note the
0042: * {@link #setNextFireCutoffInterval} and {@link #getNextFireCutoffInterval}
0043: * methods.
0044: * <P>
0045: * Take, for example, the following calendar:
0046: * <P>
0047: * <PRE>
0048: * July August September
0049: * Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
0050: * 1 W 1 2 3 4 5 W 1 2 W
0051: * W H 5 6 7 8 W W 8 9 10 11 12 W W H 6 7 8 9 W
0052: * W 11 12 13 14 15 W W 15 16 17 18 19 W W 12 13 14 15 16 W
0053: * W 18 19 20 21 22 W W 22 23 24 25 26 W W 19 20 21 22 23 W
0054: * W 25 26 27 28 29 W W 29 30 31 W 26 27 28 29 30
0055: * W
0056: * </PRE>
0057: * <P>
0058: * Where W's represent weekend days, and H's represent holidays, all of which
0059: * are excluded on a calendar associated with an
0060: * <CODE>NthIncludedDayTrigger</CODE> with <CODE>n=5</CODE> and
0061: * <CODE>intervalType=INTERVAL_TYPE_MONTHLY</CODE>. In this case, the trigger
0062: * would fire on the 8<SUP>th</SUP> of July (because of the July 4 holiday),
0063: * the 5<SUP>th</SUP> of August, and the 8<SUP>th</SUP> of September (because
0064: * of Labor Day).
0065: *
0066: * @author Aaron Craven
0067: */
0068: public class NthIncludedDayTrigger extends Trigger {
0069:
0070: static final long serialVersionUID = 6267700049629328293L;
0071:
0072: /**
0073: * Instructs the <CODE>Scheduler</CODE> that upon a mis-fire situation, the
0074: * <CODE>NthIncludedDayTrigger</CODE> wants to be fired now by the
0075: * <CODE>Scheduler</CODE>
0076: */
0077: public static final int MISFIRE_INSTRUCTION_FIRE_ONCE_NOW = 1;
0078:
0079: /**
0080: * Instructs the <CODE>Scheduler</CODE> that upon a mis-fire situation, the
0081: * <CODE>NthIncludedDayTrigger</CODE> wants to have
0082: * <CODE>nextFireTime</CODE> updated to the next time in the schedule after
0083: * the current time, but it does not want to be fired now.
0084: */
0085: public static final int MISFIRE_INSTRUCTION_DO_NOTHING = 2;
0086:
0087: /**
0088: * indicates a monthly trigger type (fires on the N<SUP>th</SUP> included
0089: * day of every month).
0090: */
0091: public static final int INTERVAL_TYPE_MONTHLY = 1;
0092:
0093: /**
0094: * indicates a yearly trigger type (fires on the N<SUP>th</SUP> included
0095: * day of every year).
0096: */
0097: public static final int INTERVAL_TYPE_YEARLY = 2;
0098:
0099: /**
0100: * indicates a weekly trigger type (fires on the N<SUP>th</SUP> included
0101: * day of every week). When using this interval type, care must be taken
0102: * not to think of the value of <CODE>n</CODE> as an analog to
0103: * <CODE>java.util.Calendar.DAY_OF_WEEK</CODE>. Such a comparison can only
0104: * be drawn when there are no calendars associated with the trigger. To
0105: * illustrate, consider an <CODE>NthIncludedDayTrigger</CODE> with
0106: * <CODE>n = 3</CODE> which is associated with a Calendar excluding
0107: * non-weekdays. The trigger would fire on the 3<SUP>rd</SUP>
0108: * <I>included</I> day of the week, which would be 4<SUP>th</SUP>
0109: * <I>actual</I> day of the week.
0110: */
0111: public static final int INTERVAL_TYPE_WEEKLY = 3;
0112:
0113: private Date startTime = new Date();
0114: private Date endTime;
0115: private Date previousFireTime;
0116: private Date nextFireTime;
0117: private Calendar calendar;
0118:
0119: private int n = 1;
0120: private int intervalType = INTERVAL_TYPE_MONTHLY;
0121: private int fireAtHour = 12;
0122: private int fireAtMinute = 0;
0123: private int fireAtSecond = 0;
0124: private int nextFireCutoffInterval = 12;
0125:
0126: private TimeZone timeZone;
0127:
0128: /**
0129: * Create an <CODE>NthIncludedDayTrigger</CODE> with no specified name,
0130: * group, or <CODE>JobDetail</CODE>. This will result initially in a
0131: * default monthly trigger that fires on the first day of every month at
0132: * 12:00 PM (<CODE>n</CODE>=1,
0133: * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>,
0134: * <CODE>fireAtTime="12:00"</CODE>).
0135: * <P>
0136: * Note that <CODE>setName()</CODE>, <CODE>setGroup()</CODE>,
0137: * <CODE>setJobName()</CODE>, and <CODE>setJobGroup()</CODE>, must be
0138: * called before the <CODE>NthIncludedDayTrigger</CODE> can be placed into
0139: * a <CODE>Scheduler</CODE>.
0140: */
0141: public NthIncludedDayTrigger() {
0142: super ();
0143: }
0144:
0145: /**
0146: * Create an <CODE>NthIncludedDayTrigger</CODE> with the given name and
0147: * group but no specified <CODE>JobDetail</CODE>. This will result
0148: * initially in a default monthly trigger that fires on the first day of
0149: * every month at 12:00 PM (<CODE>n</CODE>=1,
0150: * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>,
0151: * <CODE>fireAtTime="12:00"</CODE>).
0152: * <P>
0153: * Note that <CODE>setJobName()</CODE> and <CODE>setJobGroup()</CODE> must
0154: * be called before the <CODE>NthIncludedDayTrigger</CODE> can be placed
0155: * into a <CODE>Scheduler</CODE>.
0156: *
0157: * @param name the name for the <CODE>NthIncludedDayTrigger</CODE>
0158: * @param group the group for the <CODE>NthIncludedDayTrigger</CODE>
0159: */
0160: public NthIncludedDayTrigger(String name, String group) {
0161: super (name, group);
0162: }
0163:
0164: /**
0165: * Create an <CODE>NthIncludedDayTrigger</CODE> with the given name and
0166: * group and the specified <CODE>JobDetail</CODE>. This will result
0167: * initially in a default monthly trigger that fires on the first day of
0168: * every month at 12:00 PM (<CODE>n</CODE>=1,
0169: * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>,
0170: * <CODE>fireAtTime="12:00"</CODE>).
0171: *
0172: * @param name the name for the <CODE>NthIncludedDayTrigger</CODE>
0173: * @param group the group for the <CODE>NthIncludedDayTrigger</CODE>
0174: * @param jobName the name of the job to associate with the
0175: * <CODE>NthIncludedDayTrigger</CODE>
0176: * @param jobGroup the group containing the job to associate with the
0177: * <CODE>NthIncludedDayTrigger</CODE>
0178: */
0179: public NthIncludedDayTrigger(String name, String group,
0180: String jobName, String jobGroup) {
0181: super (name, group, jobName, jobGroup);
0182: }
0183:
0184: /**
0185: * Sets the day of the interval on which the
0186: * <CODE>NthIncludedDayTrigger</CODE> should fire. If the N<SUP>th</SUP>
0187: * day of the interval does not exist (i.e. the 32<SUP>nd</SUP> of a
0188: * month), the trigger simply will never fire. N may not be less than 1.
0189: *
0190: * @param n the day of the interval on which the trigger should fire.
0191: * @throws java.lang.IllegalArgumentException
0192: * the value entered for N was not valid (probably less than or
0193: * equal to zero).
0194: * @see #getN()
0195: */
0196: public void setN(int n) {
0197: if (n > 0) {
0198: this .n = n;
0199: } else {
0200: throw new IllegalArgumentException(
0201: "N must be greater than 0.");
0202: }
0203: }
0204:
0205: /**
0206: * Returns the day of the interval on which the
0207: * <CODE>NthIncludedDayTrigger</CODE> should fire.
0208: *
0209: * @return the value of <CODE>n</CODE>
0210: * @see #setN(int)
0211: */
0212: public int getN() {
0213: return this .n;
0214: }
0215:
0216: /**
0217: * Sets the interval type for the <CODE>NthIncludedDayTrigger</CODE>. If
0218: * {@link #INTERVAL_TYPE_MONTHLY}, the trigger will fire on the
0219: * N<SUP>th</SUP> included day of every month. If
0220: * {@link #INTERVAL_TYPE_YEARLY}, the trigger will fire on the
0221: * N<SUP>th</SUP> included day of every year. If
0222: * {@link #INTERVAL_TYPE_WEEKLY}, the trigger will fire on the
0223: * N<SUP>th</SUP> included day of every week.
0224: *
0225: * @param intervalType the interval type for the trigger
0226: * @throws java.lang.IllegalArgumentException
0227: * the value of <CODE>intervalType</CODE> is not valid. Valid
0228: * values are represented by the INTERVAL_TYPE_WEEKLY,
0229: * INTERVAL_TYPE_MONTHLY and INTERVAL_TYPE_YEARLY constants.
0230: * @see #getIntervalType()
0231: * @see #INTERVAL_TYPE_WEEKLY
0232: * @see #INTERVAL_TYPE_MONTHLY
0233: * @see #INTERVAL_TYPE_YEARLY
0234: */
0235: public void setIntervalType(int intervalType) {
0236: switch (intervalType) {
0237: case INTERVAL_TYPE_WEEKLY:
0238: this .intervalType = intervalType;
0239: break;
0240: case INTERVAL_TYPE_MONTHLY:
0241: this .intervalType = intervalType;
0242: break;
0243: case INTERVAL_TYPE_YEARLY:
0244: this .intervalType = intervalType;
0245: break;
0246: default:
0247: throw new IllegalArgumentException("Invalid Interval Type:"
0248: + intervalType);
0249: }
0250: }
0251:
0252: /**
0253: * Returns the interval type for the <CODE>NthIncludedDayTrigger</CODE>.
0254: *
0255: * @return the trigger's interval type
0256: * @see #setIntervalType(int)
0257: * @see #INTERVAL_TYPE_WEEKLY
0258: * @see #INTERVAL_TYPE_MONTHLY
0259: * @see #INTERVAL_TYPE_YEARLY
0260: */
0261: public int getIntervalType() {
0262: return this .intervalType;
0263: }
0264:
0265: /**
0266: * Sets the fire time for the <CODE>NthIncludedDayTrigger</CODE>, which
0267: * should be represented as a string with the format
0268: * "HH:MM[:SS]", with HH representing the 24-hour clock hour
0269: * of the fire time. Hours can be represented as either a one-digit or
0270: * two-digit number. Seconds are optional.
0271: *
0272: * @param fireAtTime the time at which the trigger should fire
0273: * @throws java.lang.IllegalArgumentException
0274: * the specified value for <CODE>fireAtTime</CODE> could not be
0275: * successfully parsed into a valid time of day.
0276: * @see #getFireAtTime()
0277: */
0278: public void setFireAtTime(String fireAtTime) {
0279: int newFireHour;
0280: int newFireMinute;
0281: int newFireSecond = 0;
0282:
0283: try {
0284: int i = fireAtTime.indexOf(":");
0285: newFireHour = Integer.parseInt(fireAtTime.substring(0, i));
0286: newFireMinute = Integer.parseInt(fireAtTime.substring(
0287: i + 1, i + 3));
0288: i = fireAtTime.indexOf(":", i + 1);
0289: if (i > -1) {
0290: newFireSecond = Integer.parseInt(fireAtTime
0291: .substring(i + 1));
0292: }
0293: } catch (Exception e) {
0294: throw new IllegalArgumentException(
0295: "Could not parse time expression '" + fireAtTime
0296: + "':" + e.getMessage());
0297: }
0298:
0299: // Check ranges
0300: if ((newFireHour < 0) || (newFireHour > 23)) {
0301: throw new IllegalArgumentException(
0302: "Could not parse time expression '" + fireAtTime
0303: + "':"
0304: + "fireAtHour must be between 0 and 23");
0305: } else if ((newFireMinute < 0) || (newFireMinute > 59)) {
0306: throw new IllegalArgumentException(
0307: "Could not parse time expression '" + fireAtTime
0308: + "':"
0309: + "fireAtMinute must be between 0 and 59");
0310: } else if ((newFireSecond < 0) || (newFireSecond > 59)) {
0311: throw new IllegalArgumentException(
0312: "Could not parse time expression '" + fireAtTime
0313: + "':"
0314: + "fireAtMinute must be between 0 and 59");
0315: }
0316:
0317: fireAtHour = newFireHour;
0318: fireAtMinute = newFireMinute;
0319: fireAtSecond = newFireSecond;
0320: }
0321:
0322: /**
0323: * Returns the fire time for the <CODE>NthIncludedDayTrigger</CODE> as a
0324: * string with the format "HH:MM", with HH representing the
0325: * 24-hour clock hour of the fire time.
0326: *
0327: * @return the fire time for the trigger
0328: * @see #setFireAtTime(String)
0329: */
0330: public String getFireAtTime() {
0331: NumberFormat format = NumberFormat.getNumberInstance();
0332: format.setMaximumIntegerDigits(2);
0333: format.setMinimumIntegerDigits(2);
0334: format.setMaximumFractionDigits(0);
0335:
0336: return format.format(this .fireAtHour) + ":"
0337: + format.format(this .fireAtMinute) + ":"
0338: + format.format(this .fireAtSecond);
0339: }
0340:
0341: /**
0342: * Sets the <CODE>nextFireCutoffInterval</CODE> for the
0343: * <CODE>NthIncludedDayTrigger</CODE>.
0344: * <P>
0345: * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
0346: * it is not always possible to decide with certainty that the trigger
0347: * will <I>never</I> fire again. Therefore, it will search for the next
0348: * fire time up to a given cutoff. These cutoffs can be changed by using the
0349: * {@link #setNextFireCutoffInterval(int)} and
0350: * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
0351: * of the intervals specified by <CODE>{@link #getIntervalType()
0352: * intervalType}</CODE>.
0353: * <P>
0354: * In most cases, the default value of this setting (12) is sufficient (it
0355: * is highly unlikely, for example, that you will need to look at more than
0356: * 12 months of dates to ensure that your trigger will never fire again).
0357: * However, this setting is included to allow for the rare exceptions where
0358: * this might not be true.
0359: * <P>
0360: * For example, if your trigger is associated with a calendar that excludes
0361: * a great many dates in the next 12 months, and hardly any following that,
0362: * it is possible (if <CODE>n</CODE> is large enough) that you could run
0363: * into this situation.
0364: *
0365: * @param nextFireCutoffInterval the desired cutoff interval
0366: * @see #getNextFireCutoffInterval()
0367: * @see #getNextFireTime()
0368: */
0369: public void setNextFireCutoffInterval(int nextFireCutoffInterval) {
0370: this .nextFireCutoffInterval = nextFireCutoffInterval;
0371: }
0372:
0373: /**
0374: * Returns the <CODE>nextFireCutoffInterval</CODE> for the
0375: * <CODE>NthIncludedDayTrigger</CODE>.
0376: * <P>
0377: * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
0378: * it is not always possible to decide with certainty that the trigger
0379: * will <I>never</I> fire again. Therefore, it will search for the next
0380: * fire time up to a given cutoff. These cutoffs can be changed by using the
0381: * {@link #setNextFireCutoffInterval(int)} and
0382: * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
0383: * of the intervals specified by <CODE>{@link #getIntervalType()
0384: * intervalType}</CODE>.
0385: *
0386: * @return the chosen cutoff interval
0387: * @see #setNextFireCutoffInterval(int)
0388: * @see #getNextFireTime()
0389: */
0390: public int getNextFireCutoffInterval() {
0391: return this .nextFireCutoffInterval;
0392: }
0393:
0394: /**
0395: * Sets the date/time on which the trigger may begin firing. This defines
0396: * the initial boundary for trigger firings — the trigger will not
0397: * fire prior to this date and time. Defaults to the current date and time
0398: * when the <CODE>NthIncludedDayTrigger</CODE> is created.
0399: *
0400: * @param startTime the initial boundary for trigger firings
0401: * @throws java.lang.IllegalArgumentException
0402: * the specified start time is after the current end time or is
0403: * null
0404: * @see #getStartTime()
0405: */
0406: public void setStartTime(Date startTime) {
0407: if (startTime == null) {
0408: throw new IllegalArgumentException(
0409: "Start time may not be null");
0410: }
0411: if ((this .endTime != null) && endTime.before(startTime)) {
0412: throw new IllegalArgumentException(
0413: "Start time must be before end time.");
0414: }
0415: this .startTime = startTime;
0416: }
0417:
0418: /**
0419: * Returns the date/time on which the trigger may begin firing. This
0420: * defines the initial boundary for trigger firings — the trigger
0421: * will not fire prior to this date and time.
0422: *
0423: * @return the initial date/time on which the trigger may begin firing
0424: * @see #setStartTime(Date)
0425: */
0426: public Date getStartTime() {
0427: return this .startTime;
0428: }
0429:
0430: /**
0431: * Sets the date/time on which the trigger must stop firing. This defines
0432: * the final boundary for trigger firings — the trigger will not
0433: * fire after to this date and time. If this value is null, no end time
0434: * boundary is assumed, and the trigger can continue indefinitely.
0435: *
0436: * @param endTime the final boundary for trigger firings
0437: * @throws java.lang.IllegalArgumentException
0438: * the specified end time is before the current start time
0439: * @see #getEndTime()
0440: */
0441: public void setEndTime(Date endTime) {
0442: if ((endTime != null) && endTime.before(startTime)) {
0443: throw new IllegalArgumentException(
0444: "End time must be after start time.");
0445: }
0446: this .endTime = endTime;
0447: }
0448:
0449: /**
0450: * Returns the date/time on which the trigger must stop firing. This
0451: * defines the final boundary for trigger firings — the trigger will
0452: * not fire after to this date and time. If this value is null, no end time
0453: * boundary is assumed, and the trigger can continue indefinitely.
0454: *
0455: * @return the date/time on which the trigger must stop firing
0456: * @see #setEndTime(Date)
0457: */
0458: public Date getEndTime() {
0459: return this .endTime;
0460: }
0461:
0462: /**
0463: * Sets the time zone in which the <code>fireAtTime</code> will be resolved.
0464: * If no time zone is provided, then the default time zone will be used.
0465: *
0466: * @see TimeZone#getDefault()
0467: * @see #getTimeZone()
0468: * @see #getFireAtTime()
0469: * @see #setFireAtTime(String)
0470: */
0471: public void setTimeZone(TimeZone timeZone) {
0472: this .timeZone = timeZone;
0473: }
0474:
0475: /**
0476: * Gets the time zone in which the <code>fireAtTime</code> will be resolved.
0477: * If no time zone was explicitly set, then the default time zone is used.
0478: *
0479: * @see TimeZone#getDefault()
0480: * @see #getTimeZone()
0481: * @see #getFireAtTime()
0482: * @see #setFireAtTime(String)
0483: */
0484: public TimeZone getTimeZone() {
0485: if (timeZone == null) {
0486: timeZone = TimeZone.getDefault();
0487: }
0488: return timeZone;
0489: }
0490:
0491: /**
0492: * Returns the next time at which the <CODE>NthIncludedDayTrigger</CODE>
0493: * will fire. If the trigger will not fire again, <CODE>null</CODE> will be
0494: * returned.
0495: * <P>
0496: * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
0497: * it is not always possible to decide with certainty that the trigger
0498: * will <I>never</I> fire again. Therefore, it will search for the next
0499: * fire time up to a given cutoff. These cutoffs can be changed by using the
0500: * {@link #setNextFireCutoffInterval(int)} and
0501: * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
0502: * of the intervals specified by <CODE>{@link #getIntervalType()
0503: * intervalType}</CODE>.
0504: * <P>
0505: * The returned value is not guaranteed to be valid until after
0506: * the trigger has been added to the scheduler.
0507: *
0508: * @return the next fire time for the trigger
0509: * @see #getNextFireCutoffInterval()
0510: * @see #setNextFireCutoffInterval(int)
0511: * @see #getFireTimeAfter(Date)
0512: */
0513: public Date getNextFireTime() {
0514: return this .nextFireTime;
0515: }
0516:
0517: /**
0518: * Returns the previous time at which the
0519: * <CODE>NthIncludedDayTrigger</CODE> fired. If the trigger has not yet
0520: * fired, <CODE>null</CODE> will be returned.
0521: *
0522: * @return the previous fire time for the trigger
0523: */
0524: public Date getPreviousFireTime() {
0525: return this .previousFireTime;
0526: }
0527:
0528: /**
0529: * Returns the first time the <CODE>NthIncludedDayTrigger</CODE> will fire
0530: * after the specified date.
0531: * <P>
0532: * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
0533: * it is not always possible to decide with certainty that the trigger
0534: * will <I>never</I> fire again. Therefore, it will search for the next
0535: * fire time up to a given cutoff. These cutoffs can be changed by using the
0536: * {@link #setNextFireCutoffInterval(int)} and
0537: * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
0538: * of the intervals specified by <CODE>{@link #getIntervalType()
0539: * intervalType}</CODE>.
0540: * <P>
0541: * Therefore, for triggers with <CODE>intervalType =
0542: * {@link NthIncludedDayTrigger#INTERVAL_TYPE_WEEKLY
0543: * INTERVAL_TYPE_WEEKLY}</CODE>, if the trigger will not fire within 12
0544: * weeks after the given date/time, <CODE>null</CODE> will be returned. For
0545: * triggers with <CODE>intervalType =
0546: * {@link NthIncludedDayTrigger#INTERVAL_TYPE_MONTHLY
0547: * INTERVAL_TYPE_MONTHLY}</CODE>, if the trigger will not fire within 12
0548: * months after the given date/time, <CODE>null</CODE> will be returned.
0549: * For triggers with <CODE>intervalType =
0550: * {@link NthIncludedDayTrigger#INTERVAL_TYPE_YEARLY
0551: * INTERVAL_TYPE_YEARLY}</CODE>, if the trigger will not fire within 12
0552: * years after the given date/time, <CODE>null</CODE> will be returned. In
0553: * all cases, if the trigger will not fire before <CODE>endTime</CODE>,
0554: * <CODE>null</CODE> will be returned.
0555: *
0556: * @param afterTime The time after which to find the nearest fire time.
0557: * This argument is treated as exclusive — that is,
0558: * if afterTime is a valid fire time for the trigger, it
0559: * will not be returned as the next fire time.
0560: * @return the first time the trigger will fire following the specified
0561: * date
0562: */
0563: public Date getFireTimeAfter(Date afterTime) {
0564: if (afterTime == null) {
0565: afterTime = new Date();
0566: }
0567:
0568: if (afterTime.before(this .startTime)) {
0569: afterTime = new Date(startTime.getTime() - 1000l);
0570: }
0571:
0572: if (this .intervalType == INTERVAL_TYPE_WEEKLY) {
0573: return getWeeklyFireTimeAfter(afterTime);
0574: } else if (this .intervalType == INTERVAL_TYPE_MONTHLY) {
0575: return getMonthlyFireTimeAfter(afterTime);
0576: } else if (this .intervalType == INTERVAL_TYPE_YEARLY) {
0577: return getYearlyFireTimeAfter(afterTime);
0578: } else {
0579: return null;
0580: }
0581: }
0582:
0583: /**
0584: * Returns the last time the <CODE>NthIncludedDayTrigger</CODE> will fire.
0585: * If the trigger will not fire at any point between <CODE>startTime</CODE>
0586: * and <CODE>endTime</CODE>, <CODE>null</CODE> will be returned.
0587: *
0588: * @return the last time the trigger will fire.
0589: */
0590: public Date getFinalFireTime() {
0591: Date finalTime = null;
0592: java.util.Calendar currCal = java.util.Calendar.getInstance();
0593: currCal.setTime(this .endTime);
0594:
0595: while ((finalTime == null)
0596: && (this .startTime.before(currCal.getTime()))) {
0597: currCal.add(java.util.Calendar.DATE, -1);
0598:
0599: finalTime = getFireTimeAfter(currCal.getTime());
0600: }
0601:
0602: return finalTime;
0603: }
0604:
0605: /**
0606: * Called when the <CODE>Scheduler</CODE> has decided to 'fire' the trigger
0607: * (execute the associated <CODE>Job</CODE>), in order to give the
0608: * <CODE>Trigger</CODE> a chance to update itself for its next triggering
0609: * (if any).
0610: */
0611: public void triggered(Calendar calendar) {
0612: this .calendar = calendar;
0613: this .previousFireTime = this .nextFireTime;
0614: this .nextFireTime = getFireTimeAfter(this .nextFireTime);
0615: }
0616:
0617: /**
0618: * Called by the scheduler at the time a <CODE>Trigger</code> is first
0619: * added to the scheduler, in order to have the <CODE>Trigger</CODE>
0620: * compute its first fire time, based on any associated calendar.
0621: * <P>
0622: * After this method has been called, <CODE>getNextFireTime()</CODE>
0623: * should return a valid answer.
0624: * </p>
0625: *
0626: * @return the first time at which the <CODE>Trigger</CODE> will be fired
0627: * by the scheduler, which is also the same value
0628: * {@link #getNextFireTime()} will return (until after the first
0629: * firing of the <CODE>Trigger</CODE>).
0630: */
0631: public Date computeFirstFireTime(Calendar calendar) {
0632: this .calendar = calendar;
0633: this .nextFireTime = getFireTimeAfter(new Date(this .startTime
0634: .getTime() - 1000l));
0635:
0636: return this .nextFireTime;
0637: }
0638:
0639: /**
0640: * Called after the <CODE>Scheduler</CODE> has executed the
0641: * <code>JobDetail</CODE> associated with the <CODE>Trigger</CODE> in order
0642: * to get the final instruction code from the trigger.
0643: *
0644: * @param jobCtx the <CODE>JobExecutionContext</CODE> that was used by the
0645: * <CODE>Job</CODE>'s <CODE>execute()</CODE> method.
0646: * @param result the <CODE>JobExecutionException</CODE> thrown by the
0647: * <CODE>Job</CODE>, if any (may be <CODE>null</CODE>)
0648: * @return one of the Trigger.INSTRUCTION_XXX constants.
0649: */
0650: public int executionComplete(JobExecutionContext jobCtx,
0651: JobExecutionException result) {
0652: if (result != null && result.refireImmediately()) {
0653: return INSTRUCTION_RE_EXECUTE_JOB;
0654: }
0655:
0656: if (result != null && result.unscheduleFiringTrigger()) {
0657: return INSTRUCTION_SET_TRIGGER_COMPLETE;
0658: }
0659:
0660: if (result != null && result.unscheduleAllTriggers()) {
0661: return INSTRUCTION_SET_ALL_JOB_TRIGGERS_COMPLETE;
0662: }
0663:
0664: if (!mayFireAgain()) {
0665: return INSTRUCTION_DELETE_TRIGGER;
0666: }
0667:
0668: return INSTRUCTION_NOOP;
0669: }
0670:
0671: /**
0672: * Used by the <CODE>Scheduler</CODE> to determine whether or not it is
0673: * possible for this <CODE>Trigger</CODE> to fire again.
0674: * <P>
0675: * If the returned value is <CODE>false</CODE> then the
0676: * <CODE>Scheduler</CODE> may remove the <CODE>Trigger</CODE> from the
0677: * <CODE>JobStore</CODE>
0678: *
0679: * @return a boolean indicator of whether the trigger could potentially fire
0680: * again
0681: */
0682: public boolean mayFireAgain() {
0683: return (getNextFireTime() != null);
0684: }
0685:
0686: /**
0687: * Indicates whether <CODE>misfireInstruction</CODE> is a valid misfire
0688: * instruction for this <CODE>Trigger</CODE>.
0689: *
0690: * @return whether <CODE>misfireInstruction</CODE> is valid.
0691: */
0692: protected boolean validateMisfireInstruction(int misfireInstruction) {
0693: if ((misfireInstruction == MISFIRE_INSTRUCTION_SMART_POLICY)
0694: || (misfireInstruction == MISFIRE_INSTRUCTION_DO_NOTHING)
0695: || (misfireInstruction == MISFIRE_INSTRUCTION_FIRE_ONCE_NOW)) {
0696: return true;
0697: } else {
0698: return false;
0699: }
0700: }
0701:
0702: /**
0703: * Updates the <CODE>NthIncludedDayTrigger</CODE>'s state based on the
0704: * MISFIRE_INSTRUCTION_XXX that was selected when the
0705: * <CODE>NthIncludedDayTrigger</CODE> was created
0706: * <P>
0707: * If the misfire instruction is set to MISFIRE_INSTRUCTION_SMART_POLICY,
0708: * then the instruction will be interpreted as
0709: * {@link #MISFIRE_INSTRUCTION_FIRE_ONCE_NOW}.
0710: *
0711: * @param calendar a new or updated calendar to use for the trigger
0712: */
0713: public void updateAfterMisfire(Calendar calendar) {
0714: int instruction = getMisfireInstruction();
0715:
0716: this .calendar = calendar;
0717:
0718: if (instruction == MISFIRE_INSTRUCTION_SMART_POLICY) {
0719: instruction = MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
0720: }
0721:
0722: if (instruction == MISFIRE_INSTRUCTION_DO_NOTHING) {
0723: this .nextFireTime = getFireTimeAfter(new Date());
0724: } else if (instruction == MISFIRE_INSTRUCTION_FIRE_ONCE_NOW) {
0725: this .nextFireTime = new Date();
0726: }
0727: }
0728:
0729: /**
0730: * Updates the <CODE>NthIncludedDayTrigger</CODE>'s state based on the
0731: * given new version of the associated <CODE>Calendar</CODE>.
0732: *
0733: * @param calendar a new or updated calendar to use for the trigger
0734: * @param misfireThreshold the amount of time (in milliseconds) that must
0735: * be between "now" and the time the next
0736: * firing of the trigger is supposed to occur.
0737: */
0738: public void updateWithNewCalendar(Calendar calendar,
0739: long misfireThreshold) {
0740: Date now = new Date();
0741: long diff;
0742:
0743: this .calendar = calendar;
0744: this .nextFireTime = getFireTimeAfter(this .previousFireTime);
0745:
0746: if ((this .nextFireTime != null)
0747: && (this .nextFireTime.before(now))) {
0748: diff = now.getTime() - this .nextFireTime.getTime();
0749: if (diff >= misfireThreshold) {
0750: this .nextFireTime = getFireTimeAfter(this .nextFireTime);
0751: }
0752: }
0753: }
0754:
0755: /**
0756: * Calculates the first time an <CODE>NthIncludedDayTrigger</CODE> with
0757: * <CODE>intervalType = {@link #INTERVAL_TYPE_WEEKLY}</CODE> will fire
0758: * after the specified date. See {@link #getNextFireTime} for more
0759: * information.
0760: *
0761: * @param afterDate The time after which to find the nearest fire time.
0762: * This argument is treated as exclusive — that is,
0763: * if afterTime is a valid fire time for the trigger, it
0764: * will not be returned as the next fire time.
0765: * @return the first time the trigger will fire following the specified
0766: * date
0767: */
0768: private Date getWeeklyFireTimeAfter(Date afterDate) {
0769: int currN = 0;
0770: java.util.Calendar afterCal;
0771: java.util.Calendar currCal;
0772: int currWeek;
0773: int weekCount = 0;
0774: boolean gotOne = false;
0775:
0776: afterCal = java.util.Calendar.getInstance(getTimeZone());
0777: afterCal.setTime(afterDate);
0778:
0779: currCal = java.util.Calendar.getInstance(getTimeZone());
0780: currCal.set(afterCal.get(java.util.Calendar.YEAR), afterCal
0781: .get(java.util.Calendar.MONTH), afterCal
0782: .get(java.util.Calendar.DAY_OF_MONTH));
0783:
0784: //move to the first day of the week (SUNDAY)
0785: currCal.add(java.util.Calendar.DAY_OF_MONTH, (afterCal
0786: .get(java.util.Calendar.DAY_OF_WEEK) - 1)
0787: * -1);
0788:
0789: currCal.set(java.util.Calendar.HOUR_OF_DAY, this .fireAtHour);
0790: currCal.set(java.util.Calendar.MINUTE, this .fireAtMinute);
0791: currCal.set(java.util.Calendar.SECOND, this .fireAtSecond);
0792: currCal.set(java.util.Calendar.MILLISECOND, 0);
0793:
0794: currWeek = currCal.get(java.util.Calendar.WEEK_OF_YEAR);
0795:
0796: while ((!gotOne) && (weekCount < this .nextFireCutoffInterval)) {
0797: while ((currN != this .n) && (weekCount < 12)) {
0798: //if we move into a new week, reset the current "n" counter
0799: if (currCal.get(java.util.Calendar.WEEK_OF_YEAR) != currWeek) {
0800: currN = 0;
0801: weekCount++;
0802: currWeek = currCal
0803: .get(java.util.Calendar.WEEK_OF_YEAR);
0804: }
0805:
0806: //treating a null calendar as an all-inclusive calendar,
0807: // increment currN if the current date being tested is included
0808: // on the calendar
0809: if ((calendar == null)
0810: || (calendar.isTimeIncluded(currCal.getTime()
0811: .getTime()))) {
0812: currN++;
0813: }
0814:
0815: if (currN != this .n) {
0816: currCal.add(java.util.Calendar.DATE, 1);
0817: }
0818:
0819: //if we pass endTime, drop out and return null.
0820: if ((this .endTime != null)
0821: && (currCal.getTime().after(this .endTime))) {
0822: return null;
0823: }
0824: }
0825:
0826: //We found an "n" or we've checked the requisite number of weeks.
0827: // If we've found an "n", is it the right one? -- that is, we could
0828: // be looking at an nth day PRIOR to afterDate
0829: if (currN == this .n) {
0830: if (afterDate.before(currCal.getTime())) {
0831: gotOne = true;
0832: } else { //resume checking on the first day of the next week
0833: currCal.add(java.util.Calendar.DAY_OF_MONTH, -1
0834: * (currN - 1));
0835: currCal.add(java.util.Calendar.DAY_OF_MONTH, 7);
0836: currN = 0;
0837: }
0838: }
0839: }
0840:
0841: if (weekCount < this .nextFireCutoffInterval) {
0842: return currCal.getTime();
0843: } else {
0844: return null;
0845: }
0846: }
0847:
0848: /**
0849: * Calculates the first time an <CODE>NthIncludedDayTrigger</CODE> with
0850: * <CODE>intervalType = {@link #INTERVAL_TYPE_MONTHLY}</CODE> will fire
0851: * after the specified date. See {@link #getNextFireTime} for more
0852: * information.
0853: *
0854: * @param afterDate The time after which to find the nearest fire time.
0855: * This argument is treated as exclusive — that is,
0856: * if afterTime is a valid fire time for the trigger, it
0857: * will not be returned as the next fire time.
0858: * @return the first time the trigger will fire following the specified
0859: * date
0860: */
0861: private Date getMonthlyFireTimeAfter(Date afterDate) {
0862: int currN = 0;
0863: java.util.Calendar afterCal;
0864: java.util.Calendar currCal;
0865: int currMonth;
0866: int monthCount = 0;
0867: boolean gotOne = false;
0868:
0869: afterCal = java.util.Calendar.getInstance(getTimeZone());
0870: afterCal.setTime(afterDate);
0871:
0872: currCal = java.util.Calendar.getInstance(getTimeZone());
0873: currCal.set(afterCal.get(java.util.Calendar.YEAR), afterCal
0874: .get(java.util.Calendar.MONTH), 1);
0875: currCal.set(java.util.Calendar.HOUR_OF_DAY, this .fireAtHour);
0876: currCal.set(java.util.Calendar.MINUTE, this .fireAtMinute);
0877: currCal.set(java.util.Calendar.SECOND, this .fireAtSecond);
0878: currCal.set(java.util.Calendar.MILLISECOND, 0);
0879:
0880: currMonth = currCal.get(java.util.Calendar.MONTH);
0881:
0882: while ((!gotOne) && (monthCount < this .nextFireCutoffInterval)) {
0883: while ((currN != this .n) && (monthCount < 12)) {
0884: //if we move into a new month, reset the current "n" counter
0885: if (currCal.get(java.util.Calendar.MONTH) != currMonth) {
0886: currN = 0;
0887: monthCount++;
0888: currMonth = currCal.get(java.util.Calendar.MONTH);
0889: }
0890:
0891: //treating a null calendar as an all-inclusive calendar,
0892: // increment currN if the current date being tested is included
0893: // on the calendar
0894: if ((calendar == null)
0895: || (calendar.isTimeIncluded(currCal.getTime()
0896: .getTime()))) {
0897: currN++;
0898: }
0899:
0900: if (currN != this .n) {
0901: currCal.add(java.util.Calendar.DATE, 1);
0902: }
0903:
0904: //if we pass endTime, drop out and return null.
0905: if ((this .endTime != null)
0906: && (currCal.getTime().after(this .endTime))) {
0907: return null;
0908: }
0909: }
0910:
0911: //We found an "n" or we've checked the requisite number of months.
0912: // If we've found an "n", is it the right one? -- that is, we could
0913: // be looking at an nth day PRIOR to afterDate
0914: if (currN == this .n) {
0915: if (afterDate.before(currCal.getTime())) {
0916: gotOne = true;
0917: } else { //resume checking on the first day of the next month
0918: currCal.set(java.util.Calendar.DAY_OF_MONTH, 1);
0919: currCal.add(java.util.Calendar.MONTH, 1);
0920: currN = 0;
0921: }
0922: }
0923: }
0924:
0925: if (monthCount < this .nextFireCutoffInterval) {
0926: return currCal.getTime();
0927: } else {
0928: return null;
0929: }
0930: }
0931:
0932: /**
0933: * Calculates the first time an <CODE>NthIncludedDayTrigger</CODE> with
0934: * <CODE>intervalType = {@link #INTERVAL_TYPE_YEARLY}</CODE> will fire
0935: * after the specified date. See {@link #getNextFireTime} for more
0936: * information.
0937: *
0938: * @param afterDate The time after which to find the nearest fire time.
0939: * This argument is treated as exclusive — that is,
0940: * if afterTime is a valid fire time for the trigger, it
0941: * will not be returned as the next fire time.
0942: * @return the first time the trigger will fire following the specified
0943: * date
0944: */
0945: private Date getYearlyFireTimeAfter(Date afterDate) {
0946: int currN = 0;
0947: java.util.Calendar afterCal;
0948: java.util.Calendar currCal;
0949: int currYear;
0950: int yearCount = 0;
0951: boolean gotOne = false;
0952:
0953: afterCal = java.util.Calendar.getInstance(getTimeZone());
0954: afterCal.setTime(afterDate);
0955:
0956: currCal = java.util.Calendar.getInstance(getTimeZone());
0957: currCal.set(afterCal.get(java.util.Calendar.YEAR),
0958: java.util.Calendar.JANUARY, 1);
0959: currCal.set(java.util.Calendar.HOUR_OF_DAY, this .fireAtHour);
0960: currCal.set(java.util.Calendar.MINUTE, this .fireAtMinute);
0961: currCal.set(java.util.Calendar.SECOND, this .fireAtSecond);
0962: currCal.set(java.util.Calendar.MILLISECOND, 0);
0963:
0964: currYear = currCal.get(java.util.Calendar.YEAR);
0965:
0966: while ((!gotOne) && (yearCount < this .nextFireCutoffInterval)) {
0967: while ((currN != this .n) && (yearCount < 5)) {
0968: //if we move into a new year, reset the current "n" counter
0969: if (currCal.get(java.util.Calendar.YEAR) != currYear) {
0970: currN = 0;
0971: yearCount++;
0972: currYear = currCal.get(java.util.Calendar.YEAR);
0973: }
0974:
0975: //treating a null calendar as an all-inclusive calendar,
0976: // increment currN if the current date being tested is included
0977: // on the calendar
0978: if ((calendar == null)
0979: || (calendar.isTimeIncluded(currCal.getTime()
0980: .getTime()))) {
0981: currN++;
0982: }
0983:
0984: if (currN != this .n) {
0985: currCal.add(java.util.Calendar.DATE, 1);
0986: }
0987:
0988: //if we pass endTime, drop out and return null.
0989: if ((this .endTime != null)
0990: && (currCal.getTime().after(this .endTime))) {
0991: return null;
0992: }
0993: }
0994:
0995: //We found an "n" or we've checked the requisite number of years.
0996: // If we've found an "n", is it the right one? -- that is, we
0997: // could be looking at an nth day PRIOR to afterDate
0998: if (currN == this .n) {
0999: if (afterDate.before(currCal.getTime())) {
1000: gotOne = true;
1001: } else { //resume checking on the first day of the next year
1002: currCal.set(java.util.Calendar.DAY_OF_MONTH, 1);
1003: currCal.set(java.util.Calendar.MONTH,
1004: java.util.Calendar.JANUARY);
1005: currCal.add(java.util.Calendar.YEAR, 1);
1006: currN = 0;
1007: }
1008: }
1009: }
1010:
1011: if (yearCount < this.nextFireCutoffInterval) {
1012: return currCal.getTime();
1013: } else {
1014: return null;
1015: }
1016: }
1017: }
|