Commits

Anonymous committed 1df1689

Issue number: QUARTZ-264
Obtained from:
Submitted by:
Reviewed by:
CVS: ----------------------------------------------------------------------
CVS: Issue number:
CVS: If this change addresses one or more issues,
CVS: then enter the issue number(s) here.
CVS: Obtained from:
CVS: If this change has been taken from another system,
CVS: then name the system in this line, otherwise delete it.
CVS: Submitted by:
CVS: If this code has been contributed to the project by someone else; i.e.,
CVS: they sent us a patch or a set of diffs, then include their name/email
CVS: address here. If this is your work then delete this line.
CVS: Reviewed by:
CVS: If we are doing pre-commit code reviews and someone else has
CVS: reviewed your changes, include their name(s) here.
CVS: If you have not had it reviewed then delete this line.

git-svn-id: http://svn.opensymphony.com/svn/quartz/trunk@26769f7d36a-ea1c-0410-88ea-9fd03e4c9665

Comments (0)

Files changed (1)

src/java/org/quartz/NthIncludedDayTrigger.java

-
 /* 
  * Copyright 2004-2005 OpenSymphony 
  * 
 
 /**
  * A trigger which fires on the N<SUP>th</SUP> day of every interval type 
- * ({@link #INTERVAL_TYPE_MONTHLY} or {@link #INTERVAL_TYPE_YEARLY}) that is
- * <i>not</i> excluded by the associated calendar. When determining what the 
- * N<SUP>th</SUP> day of the month or year is, 
- * <CODE>NthIncludedDayTrigger</CODE> will skip excluded days on the associated
- * calendar. This would commonly be used in an N<SUP>th</SUP> business day 
- * situation, in which the user wishes to fire a particular job on the 
- * N<SUP>th</SUP> business day (i.e. the 5<SUP>th</SUP> business day of every 
- * month). Each <CODE>NthIncludedDayTrigger</CODE> also has an associated
+ * ({@link #INTERVAL_TYPE_WEEKLY}, {@link #INTERVAL_TYPE_MONTHLY} or 
+ * {@link #INTERVAL_TYPE_YEARLY}) that is <i>not</i> excluded by the associated
+ * calendar. When determining what the N<SUP>th</SUP> day of the month or year
+ * is, <CODE>NthIncludedDayTrigger</CODE> will skip excluded days on the 
+ * associated calendar. This would commonly be used in an N<SUP>th</SUP> 
+ * business day situation, in which the user wishes to fire a particular job on
+ * the N<SUP>th</SUP> business day (i.e. the 5<SUP>th</SUP> business day of
+ * every month). Each <CODE>NthIncludedDayTrigger</CODE> also has an associated
  * <CODE>fireAtTime</CODE> which indicates at what time of day the trigger is
  * to fire.
  * <P>
      */
     public static final int MISFIRE_INSTRUCTION_DO_NOTHING = 2;
 
-	/**
-	 * indicates a monthly trigger type (fires on the N<SUP>th</SUP> included
-	 * day of every month).
-	 */
-	public static final int INTERVAL_TYPE_MONTHLY = 1;
-	
-	/**
-	 * indicates a yearly trigger type (fires on the N<SUP>th</SUP> included 
-	 * day of every year).
-	 */
-	public static final int INTERVAL_TYPE_YEARLY = 2;
-	
-	private Date startTime = new Date();
-	private Date endTime;
-	private Date previousFireTime;
-	private Date nextFireTime;
-	private Calendar calendar;
-	
-	private int n = 1;
-	private int intervalType = INTERVAL_TYPE_MONTHLY;
-	private int fireAtHour = 12;
-	private int fireAtMinute = 0;
-	private int nextFireCutoffInterval = 12;
-	
-	/**
-	 * Create an <CODE>NthIncludedDayTrigger</CODE> with no specified name,
-	 * group, or <CODE>JobDetail</CODE>. This will result initially in a
-	 * default monthly trigger that fires on the first day of every month at
-	 * 12:00 PM (<CODE>n</CODE>=1, 
-	 * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>, 
-	 * <CODE>fireAtTime="12:00"</CODE>).
-	 * <P>
-	 * Note that <CODE>setName()</CODE>, <CODE>setGroup()</CODE>, 
-	 * <CODE>setJobName()</CODE>, and <CODE>setJobGroup()</CODE>, must be 
-	 * called before the <CODE>NthIncludedDayTrigger</CODE> can be placed into
-	 * a <CODE>Scheduler</CODE>.
-	 */
-	public NthIncludedDayTrigger() {
-		super();
-	}
-
-	/**
-	 * Create an <CODE>NthIncludedDayTrigger</CODE> with the given name and
-	 * group but no specified <CODE>JobDetail</CODE>. This will result 
-	 * initially in a default monthly trigger that fires on the first day of 
-	 * every month at 12:00 PM (<CODE>n</CODE>=1, 
-	 * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>, 
-	 * <CODE>fireAtTime="12:00"</CODE>).
-	 * <P>
-	 * Note that <CODE>setJobName()</CODE> and <CODE>setJobGroup()</CODE> must
-	 * be called before the <CODE>NthIncludedDayTrigger</CODE> can be placed 
-	 * into a <CODE>Scheduler</CODE>.
-	 *  
-	 * @param name  the name for the <CODE>NthIncludedDayTrigger</CODE>
-	 * @param group the group for the <CODE>NthIncludedDayTrigger</CODE>
-	 */
-	public NthIncludedDayTrigger(String name, String group) {
-		super(name, group);
-	}
-
-	/**
-	 * Create an <CODE>NthIncludedDayTrigger</CODE> with the given name and
-	 * group and the specified <CODE>JobDetail</CODE>. This will result 
-	 * initially in a default monthly trigger that fires on the first day of
-	 * every month at 12:00 PM (<CODE>n</CODE>=1, 
-	 * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>, 
-	 * <CODE>fireAtTime="12:00"</CODE>).
-	 * 
-	 * @param name     the name for the <CODE>NthIncludedDayTrigger</CODE>
-	 * @param group    the group for the <CODE>NthIncludedDayTrigger</CODE>
-	 * @param jobName  the name of the job to associate with the 
-	 *                 <CODE>NthIncludedDayTrigger</CODE>
-	 * @param jobGroup the group containing the job to associate with the 
-	 *                 <CODE>NthIncludedDayTrigger</CODE>
-	 */
-	public NthIncludedDayTrigger(String name, String group, String jobName,
-			String jobGroup) {
-		super(name, group, jobName, jobGroup);
-	}
-
-	/**
-	 * Sets the day of the interval on which the 
-	 * <CODE>NthIncludedDayTrigger</CODE> should fire. If the N<SUP>th</SUP>
-	 * day of the interval does not exist (i.e. the 32<SUP>nd</SUP> of a 
-	 * month), the trigger simply will never fire. N may not be less than 1.
-	 * 
-	 * @param  n the day of the interval on which the trigger should fire.
-	 * @throws java.lang.IllegalArgumentException
-	 *         the value entered for N was not valid (probably less than or 
-	 *         equal to zero).
-	 * @see #getN()
-	 */
-	public void setN(int n) {
-		if (n > 0) {
-			this.n = n;
-		} else {
-			throw new IllegalArgumentException("N must be greater than 0.");
-		}
-	}
-	
-	/**
-	 * Returns the day of the interval on which the 
-	 * <CODE>NthIncludedDayTrigger</CODE> should fire.
-	 * 
-	 * @return the value of <CODE>n</CODE>
-	 * @see #setN(int)
-	 */
-	public int getN() {
-		return this.n;
-	}
-	
-	/**
-	 * Sets the interval type for the <CODE>NthIncludedDayTrigger</CODE>. If
-	 * {@link #INTERVAL_TYPE_MONTHLY}, the trigger will fire on the 
-	 * N<SUP>th</SUP> included day of every month. If 
-	 * {@link #INTERVAL_TYPE_YEARLY}, the trigger will fire on the 
-	 * N<SUP>th</SUP> included day of every year.
-	 * 
-	 * @param  intervalType the interval type for the trigger
-	 * @throws java.lang.IllegalArgumentException
-	 *         the value of <CODE>intervalType</CODE> is not valid. Valid
-	 *         values are represented by the INTERVAL_TYPE_MONTHLY and 
-	 *         INTERVAL_TYPE_YEARLY constants.
-	 * @see #getIntervalType()
-	 * @see #INTERVAL_TYPE_MONTHLY
-	 * @see #INTERVAL_TYPE_YEARLY
-	 */
-	public void setIntervalType(int intervalType) {
-		switch (intervalType) {
-		case INTERVAL_TYPE_MONTHLY:
-			this.intervalType = intervalType;
-			break;
-		case INTERVAL_TYPE_YEARLY:
-			this.intervalType = intervalType;
-			break;
-		default:
-			throw new IllegalArgumentException("Invalid Interval Type:" 
-					                           + intervalType);
-		}
-	}
-	
-	/**
-	 * Returns the interval type for the <CODE>NthIncludedDayTrigger</CODE>.
-	 * 
-	 * @return the trigger's interval type
-	 * @see #setIntervalType(int)
-	 * @see #INTERVAL_TYPE_MONTHLY
-	 * @see #INTERVAL_TYPE_YEARLY
-	 */
-	public int getIntervalType() {
-		return this.intervalType;		
-	}
-	
-	/**
-	 * Sets the fire time for the <CODE>NthIncludedDayTrigger</CODE>, which
-	 * should be represented as a string with the format &quot;HH:MM&quot;, 
-	 * with HH representing the 24-hour clock hour of the fire time. Hours can
-	 * be represented as either a one-digit or two-digit number.
-	 * 
-	 * @param  fireAtTime the time at which the trigger should fire
-	 * @throws java.lang.IllegalArgumentException
-	 *         the specified value for <CODE>fireAtTime</CODE> could not be 
-	 *         successfully parsed into a valid time of day.
-	 * @see #getFireAtTime()
-	 */
-	public void setFireAtTime(String fireAtTime) {
-		int fireHour = 12;
-		int fireMinute = 0;
-		String[] components;
-		
-		try {
-			int i = fireAtTime.indexOf(":");
-			fireHour = Integer.parseInt(fireAtTime.substring(0, i));
-			fireMinute = Integer.parseInt(fireAtTime.substring(i+1));
-		} catch (Exception e) {
-			fireHour = 12;
-			fireMinute = 0;
-			throw new 
-				IllegalArgumentException("Could not parse time expression: " 
-						                 + e.getMessage());
-		} finally {
-			this.fireAtHour = fireHour;
-			this.fireAtMinute = fireMinute;
-		}
-	}
-	
-	/**
-	 * Returns the fire time for the <CODE>NthIncludedDayTrigger</CODE> as a
-	 * string with the format &quot;HH:MM&quot;, with HH representing the 
-	 * 24-hour clock hour of the fire time.
-	 * 
-	 * @return the fire time for the trigger
-	 * @see #setFireAtTime(String)
-	 */
-	public String getFireAtTime() {
-		return this.fireAtHour + ":" + this.fireAtMinute;		
-	}
-
-	/**
-	 * Sets the <CODE>nextFireCutoffInterval</CODE> for the 
-	 * <CODE>NthIncludedDayTrigger</CODE>.
-	 * <P>
-	 * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
-	 * it is not always possible to decide with certainty that the trigger
-	 * will <I>never</I> fire again. Therefore, it will search for the next 
-	 * fire time up to a given cutoff. These cutoffs can be changed by using the
-	 * {@link #setNextFireCutoffInterval(int)} and 
-	 * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
-	 * of the intervals specified by <CODE>{@link #getIntervalType()
-	 * intervalType}</CODE>.
-	 * <P>
-	 * In most cases, the default value of this setting (12) is sufficient (it
-	 * is highly unlikely, for example, that you will need to look at more than
-	 * 12 months of dates to ensure that your trigger will never fire again).  
-	 * However, this setting is included to allow for the rare exceptions where
-	 * this might not be true.
-	 * <P>
-	 * For example, if your trigger is associated with a calendar that excludes
-	 * a great many dates in the next 12 months, and hardly any following that,
-	 * it is possible (if <CODE>n</CODE> is large enough) that you could run 
-	 * into this situation.  
-	 * 
-	 * @param nextFireCutoffInterval the desired cutoff interval
-	 * @see #getNextFireCutoffInterval()
-	 * @see #getNextFireTime()
-	 */
-	public void setNextFireCutoffInterval(int nextFireCutoffInterval) {
-		this.nextFireCutoffInterval = nextFireCutoffInterval;
-	}
-	
-	/**
-	 * Returns the <CODE>nextFireCutoffInterval</CODE> for the 
-	 * <CODE>NthIncludedDayTrigger</CODE>.
-	 * <P>
-	 * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
-	 * it is not always possible to decide with certainty that the trigger
-	 * will <I>never</I> fire again. Therefore, it will search for the next 
-	 * fire time up to a given cutoff. These cutoffs can be changed by using the
-	 * {@link #setNextFireCutoffInterval(int)} and 
-	 * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
-	 * of the intervals specified by <CODE>{@link #getIntervalType()
-	 * intervalType}</CODE>.
-	 * 
-	 * @return the chosen cutoff interval
-	 * @see #setNextFireCutoffInterval(int)
-	 * @see #getNextFireTime()
-	 */
-	public int getNextFireCutoffInterval() {
-		return this.nextFireCutoffInterval;
-	}
-	
-	/** 
-	 * Sets the date/time on which the trigger may begin firing. This defines
-	 * the initial boundary for trigger firings &mdash; the trigger will not
-	 * fire prior to this date and time. Defaults to the current date and time
-	 * when the <CODE>NthIncludedDayTrigger</CODE> is created.
-	 * 
-	 * @param  startTime the initial boundary for trigger firings
-	 * @throws java.lang.IllegalArgumentException
-	 *         the specified start time is after the current end time or is 
-	 *         null
-	 * @see #getStartTime()
-	 */
-	public void setStartTime(Date startTime) {
-		if (startTime == null) {
-			throw new IllegalArgumentException("Start time may not be null");
-		}
-		if ((this.endTime != null) && endTime.before(startTime)) {
-			throw new IllegalArgumentException("Start time must be before end time.");
-		}
-		this.startTime = startTime;
-	}
-
-	/**
-	 * Returns the date/time on which the trigger may begin firing. This 
-	 * defines the initial boundary for trigger firings &mdash; the trigger
-	 * will not fire prior to this date and time.
-	 * 
-	 * @return the initial date/time on which the trigger may begin firing
-	 * @see #setStartTime(Date)
-	 */
-	public Date getStartTime() {
-		return this.startTime;
-	}
-
-	/** 
-	 * Sets the date/time on which the trigger must stop firing. This defines
-	 * the final boundary for trigger firings &mdash; the trigger will not
-	 * fire after to this date and time. If this value is null, no end time
-	 * boundary is assumed, and the trigger can continue indefinitely.
-	 * 
-	 * @param  endTime the final boundary for trigger firings
-	 * @throws java.lang.IllegalArgumentException
-	 *         the specified end time is before the current start time
-	 * @see #getEndTime()
-	 */
-	public void setEndTime(Date endTime) {
-		if ((endTime != null) && endTime.before(startTime)) {
-			throw new IllegalArgumentException("End time must be after start time.");
-		}
-		this.endTime = endTime;
-	}
-
-	/** 
-	 * Returns the date/time on which the trigger must stop firing. This 
-	 * defines the final boundary for trigger firings &mdash; the trigger will
-	 * not fire after to this date and time. If this value is null, no end time
-	 * boundary is assumed, and the trigger can continue indefinitely.
-	 * 
-	 * @return the date/time on which the trigger must stop firing
-	 * @see #setEndTime(Date)
-	 */
-	public Date getEndTime() {
-		return this.endTime;
-	}
-
-	/**
-	 * Returns the next time at which the <CODE>NthIncludedDayTrigger</CODE>
-	 * will fire. If the trigger will not fire again, <CODE>null</CODE> will be
-	 * returned. 
-	 * <P>
-	 * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
-	 * it is not always possible to decide with certainty that the trigger
-	 * will <I>never</I> fire again. Therefore, it will search for the next 
-	 * fire time up to a given cutoff. These cutoffs can be changed by using the
-	 * {@link #setNextFireCutoffInterval(int)} and 
-	 * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
-	 * of the intervals specified by <CODE>{@link #getIntervalType()
-	 * intervalType}</CODE>.
-	 * <P>
-	 * The returned value is not guaranteed to be valid until after
-	 * the trigger has been added to the scheduler.
-	 * 
-	 * @return the next fire time for the trigger
-	 * @see #getNextFireCutoffInterval()
-	 * @see #setNextFireCutoffInterval(int)
-	 * @see #getFireTimeAfter(Date)
-	 */
-	public Date getNextFireTime() {
-		return this.nextFireTime;
-	}
-
-	/**
-	 * Returns the previous time at which the 
-	 * <CODE>NthIncludedDayTrigger</CODE> fired. If the trigger has not yet 
-	 * fired, <CODE>null</CODE> will be returned.
-	 * 
-	 * @return the previous fire time for the trigger
-	 */
-	public Date getPreviousFireTime() {
-		return this.previousFireTime;
-	}
-
-	/**
-	 * Returns the first time the <CODE>NthIncludedDayTrigger</CODE> will fire
-	 * after the specified date. 
-	 * <P> 
-	 * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
-	 * it is not always possible to decide with certainty that the trigger
-	 * will <I>never</I> fire again. Therefore, it will search for the next 
-	 * fire time up to a given cutoff. These cutoffs can be changed by using the
-	 * {@link #setNextFireCutoffInterval(int)} and 
-	 * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
-	 * of the intervals specified by <CODE>{@link #getIntervalType()
-	 * intervalType}</CODE>.
-	 * <P>
-	 * Therefore, for triggers with <CODE>intervalType = 
-	 * {@link NthIncludedDayTrigger#INTERVAL_TYPE_MONTHLY
-	 * INTERVAL_TYPE_MONTHLY}</CODE>, if the trigger will not fire within 12 
-	 * months after the given date/time, <CODE>null</CODE> will be returned. 
-	 * For triggers with <CODE>intervalType = 
-	 * {@link NthIncludedDayTrigger#INTERVAL_TYPE_YEARLY 
-	 * INTERVAL_TYPE_YEARLY}</CODE>, if the trigger will not fire within 12
-	 * years after the given date/time, <CODE>null</CODE> will be returned. In 
-	 * all cases, if the trigger will not fire before <CODE>endTime</CODE>, 
-	 * <CODE>null</CODE> will be returned.
-	 * 
-	 * @param  afterTime The time after which to find the nearest fire time.
-	 *                   This argument is treated as exclusive &mdash; that is,
-	 *                   if afterTime is a valid fire time for the trigger, it
-	 *                   will not be returned as the next fire time.
-	 * @return the first time the trigger will fire following the specified
-	 *         date
-	 */
-	public Date getFireTimeAfter(Date afterTime) {
-		if (afterTime == null) {
-			afterTime = new Date();
-		}
-		
-		if (afterTime.before(this.startTime)) {
-			afterTime = new Date(startTime.getTime() - 1000l);
-		}
-		
-		if (this.intervalType == INTERVAL_TYPE_MONTHLY) {
-			return getMonthlyFireTimeAfter(afterTime);
-		} else if (this.intervalType == INTERVAL_TYPE_YEARLY) {
-			return getYearlyFireTimeAfter(afterTime);
-		} else {
-			return null;
-		}
-	}
-
-	/**
-	 * Returns the last time the <CODE>NthIncludedDayTrigger</CODE> will fire.
-	 * If the trigger will not fire at any point between <CODE>startTime</CODE>
-	 * and <CODE>endTime</CODE>, <CODE>null</CODE> will be returned.
-	 * 
-	 * @return the last time the trigger will fire.
-	 */
-	public Date getFinalFireTime() {
-		Date finalTime = null;
-		java.util.Calendar currCal = java.util.Calendar.getInstance();
-		currCal.setTime(this.endTime);
-		
-		while ((finalTime == null) 
-				&& (this.startTime.before(currCal.getTime()))) {
-			currCal.add(java.util.Calendar.DATE, -1);
+    /**
+     * indicates a monthly trigger type (fires on the N<SUP>th</SUP> included
+     * day of every month).
+     */
+    public static final int INTERVAL_TYPE_MONTHLY = 1;
+    
+    /**
+     * indicates a yearly trigger type (fires on the N<SUP>th</SUP> included 
+     * day of every year).
+     */
+    public static final int INTERVAL_TYPE_YEARLY = 2;
+    
+    /**
+     * indicates a weekly trigger type (fires on the N<SUP>th</SUP> included
+     * day of every week). When using this interval type, care must be taken
+     * not to think of the value of <CODE>n</CODE> as an analog to 
+     * <CODE>java.util.Calendar.DAY_OF_WEEK</CODE>. Such a comparison can only
+     * be drawn when there are no calendars associated with the trigger. To 
+     * illustrate, consider an <CODE>NthIncludedDayTrigger</CODE> with 
+     * <CODE>n = 3</CODE> which is associated with a Calendar excluding
+     * non-weekdays. The trigger would fire on the 3<SUP>rd</SUP> 
+     * <I>included</I> day of the week, which would be 4<SUP>th</SUP> 
+     * <I>actual</I> day of the week.
+     */
+    public static final int INTERVAL_TYPE_WEEKLY = 3;
+    
+    private Date startTime = new Date();
+    private Date endTime;
+    private Date previousFireTime;
+    private Date nextFireTime;
+    private Calendar calendar;
+    
+    private int n = 1;
+    private int intervalType = INTERVAL_TYPE_MONTHLY;
+    private int fireAtHour = 12;
+    private int fireAtMinute = 0;
+    private int nextFireCutoffInterval = 12;
+    
+    /**
+     * Create an <CODE>NthIncludedDayTrigger</CODE> with no specified name,
+     * group, or <CODE>JobDetail</CODE>. This will result initially in a
+     * default monthly trigger that fires on the first day of every month at
+     * 12:00 PM (<CODE>n</CODE>=1, 
+     * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>, 
+     * <CODE>fireAtTime="12:00"</CODE>).
+     * <P>
+     * Note that <CODE>setName()</CODE>, <CODE>setGroup()</CODE>, 
+     * <CODE>setJobName()</CODE>, and <CODE>setJobGroup()</CODE>, must be 
+     * called before the <CODE>NthIncludedDayTrigger</CODE> can be placed into
+     * a <CODE>Scheduler</CODE>.
+     */
+    public NthIncludedDayTrigger() {
+        super();
+    }
+
+    /**
+     * Create an <CODE>NthIncludedDayTrigger</CODE> with the given name and
+     * group but no specified <CODE>JobDetail</CODE>. This will result 
+     * initially in a default monthly trigger that fires on the first day of 
+     * every month at 12:00 PM (<CODE>n</CODE>=1, 
+     * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>, 
+     * <CODE>fireAtTime="12:00"</CODE>).
+     * <P>
+     * Note that <CODE>setJobName()</CODE> and <CODE>setJobGroup()</CODE> must
+     * be called before the <CODE>NthIncludedDayTrigger</CODE> can be placed 
+     * into a <CODE>Scheduler</CODE>.
+     *  
+     * @param name  the name for the <CODE>NthIncludedDayTrigger</CODE>
+     * @param group the group for the <CODE>NthIncludedDayTrigger</CODE>
+     */
+    public NthIncludedDayTrigger(String name, String group) {
+        super(name, group);
+    }
+
+    /**
+     * Create an <CODE>NthIncludedDayTrigger</CODE> with the given name and
+     * group and the specified <CODE>JobDetail</CODE>. This will result 
+     * initially in a default monthly trigger that fires on the first day of
+     * every month at 12:00 PM (<CODE>n</CODE>=1, 
+     * <CODE>intervalType={@link #INTERVAL_TYPE_MONTHLY}</CODE>, 
+     * <CODE>fireAtTime="12:00"</CODE>).
+     * 
+     * @param name     the name for the <CODE>NthIncludedDayTrigger</CODE>
+     * @param group    the group for the <CODE>NthIncludedDayTrigger</CODE>
+     * @param jobName  the name of the job to associate with the 
+     *                 <CODE>NthIncludedDayTrigger</CODE>
+     * @param jobGroup the group containing the job to associate with the 
+     *                 <CODE>NthIncludedDayTrigger</CODE>
+     */
+    public NthIncludedDayTrigger(String name, String group, String jobName,
+            String jobGroup) {
+        super(name, group, jobName, jobGroup);
+    }
+
+    /**
+     * Sets the day of the interval on which the 
+     * <CODE>NthIncludedDayTrigger</CODE> should fire. If the N<SUP>th</SUP>
+     * day of the interval does not exist (i.e. the 32<SUP>nd</SUP> of a 
+     * month), the trigger simply will never fire. N may not be less than 1.
+     * 
+     * @param  n the day of the interval on which the trigger should fire.
+     * @throws java.lang.IllegalArgumentException
+     *         the value entered for N was not valid (probably less than or 
+     *         equal to zero).
+     * @see #getN()
+     */
+    public void setN(int n) {
+        if (n > 0) {
+            this.n = n;
+        } else {
+            throw new IllegalArgumentException("N must be greater than 0.");
+        }
+    }
+    
+    /**
+     * Returns the day of the interval on which the 
+     * <CODE>NthIncludedDayTrigger</CODE> should fire.
+     * 
+     * @return the value of <CODE>n</CODE>
+     * @see #setN(int)
+     */
+    public int getN() {
+        return this.n;
+    }
+    
+    /**
+     * Sets the interval type for the <CODE>NthIncludedDayTrigger</CODE>. If
+     * {@link #INTERVAL_TYPE_MONTHLY}, the trigger will fire on the 
+     * N<SUP>th</SUP> included day of every month. If 
+     * {@link #INTERVAL_TYPE_YEARLY}, the trigger will fire on the 
+     * N<SUP>th</SUP> included day of every year. If 
+     * {@link #INTERVAL_TYPE_WEEKLY}, the trigger will fire on the 
+     * N<SUP>th</SUP> included day of every month. 
+     * 
+     * @param  intervalType the interval type for the trigger
+     * @throws java.lang.IllegalArgumentException
+     *         the value of <CODE>intervalType</CODE> is not valid. Valid
+     *         values are represented by the INTERVAL_TYPE_WEEKLY, 
+     *         INTERVAL_TYPE_MONTHLY and INTERVAL_TYPE_YEARLY constants.
+     * @see #getIntervalType()
+     * @see #INTERVAL_TYPE_WEEKLY
+     * @see #INTERVAL_TYPE_MONTHLY
+     * @see #INTERVAL_TYPE_YEARLY
+     */
+    public void setIntervalType(int intervalType) {
+        switch (intervalType) {
+        case INTERVAL_TYPE_WEEKLY:
+            this.intervalType = intervalType;
+            break;
+        case INTERVAL_TYPE_MONTHLY:
+            this.intervalType = intervalType;
+            break;
+        case INTERVAL_TYPE_YEARLY:
+            this.intervalType = intervalType;
+            break;
+        default:
+            throw new IllegalArgumentException("Invalid Interval Type:" 
+                                               + intervalType);
+        }
+    }
+    
+    /**
+     * Returns the interval type for the <CODE>NthIncludedDayTrigger</CODE>.
+     * 
+     * @return the trigger's interval type
+     * @see #setIntervalType(int)
+     * @see #INTERVAL_TYPE_WEEKLY
+     * @see #INTERVAL_TYPE_MONTHLY
+     * @see #INTERVAL_TYPE_YEARLY
+     */
+    public int getIntervalType() {
+        return this.intervalType;       
+    }
+    
+    /**
+     * Sets the fire time for the <CODE>NthIncludedDayTrigger</CODE>, which
+     * should be represented as a string with the format &quot;HH:MM&quot;, 
+     * with HH representing the 24-hour clock hour of the fire time. Hours can
+     * be represented as either a one-digit or two-digit number.
+     * 
+     * @param  fireAtTime the time at which the trigger should fire
+     * @throws java.lang.IllegalArgumentException
+     *         the specified value for <CODE>fireAtTime</CODE> could not be 
+     *         successfully parsed into a valid time of day.
+     * @see #getFireAtTime()
+     */
+    public void setFireAtTime(String fireAtTime) {
+        int fireHour = 12;
+        int fireMinute = 0;
+        String[] components;
+        
+        try {
+            int i = fireAtTime.indexOf(":");
+            fireHour = Integer.parseInt(fireAtTime.substring(0, i));
+            fireMinute = Integer.parseInt(fireAtTime.substring(i+1));
+        } catch (Exception e) {
+            fireHour = 12;
+            fireMinute = 0;
+            throw new 
+                IllegalArgumentException("Could not parse time expression: " 
+                                         + e.getMessage());
+        } finally {
+            this.fireAtHour = fireHour;
+            this.fireAtMinute = fireMinute;
+        }
+    }
+    
+    /**
+     * Returns the fire time for the <CODE>NthIncludedDayTrigger</CODE> as a
+     * string with the format &quot;HH:MM&quot;, with HH representing the 
+     * 24-hour clock hour of the fire time.
+     * 
+     * @return the fire time for the trigger
+     * @see #setFireAtTime(String)
+     */
+    public String getFireAtTime() {
+        return this.fireAtHour + ":" + this.fireAtMinute;       
+    }
+
+    /**
+     * Sets the <CODE>nextFireCutoffInterval</CODE> for the 
+     * <CODE>NthIncludedDayTrigger</CODE>.
+     * <P>
+     * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
+     * it is not always possible to decide with certainty that the trigger
+     * will <I>never</I> fire again. Therefore, it will search for the next 
+     * fire time up to a given cutoff. These cutoffs can be changed by using the
+     * {@link #setNextFireCutoffInterval(int)} and 
+     * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
+     * of the intervals specified by <CODE>{@link #getIntervalType()
+     * intervalType}</CODE>.
+     * <P>
+     * In most cases, the default value of this setting (12) is sufficient (it
+     * is highly unlikely, for example, that you will need to look at more than
+     * 12 months of dates to ensure that your trigger will never fire again).  
+     * However, this setting is included to allow for the rare exceptions where
+     * this might not be true.
+     * <P>
+     * For example, if your trigger is associated with a calendar that excludes
+     * a great many dates in the next 12 months, and hardly any following that,
+     * it is possible (if <CODE>n</CODE> is large enough) that you could run 
+     * into this situation.  
+     * 
+     * @param nextFireCutoffInterval the desired cutoff interval
+     * @see #getNextFireCutoffInterval()
+     * @see #getNextFireTime()
+     */
+    public void setNextFireCutoffInterval(int nextFireCutoffInterval) {
+        this.nextFireCutoffInterval = nextFireCutoffInterval;
+    }
+    
+    /**
+     * Returns the <CODE>nextFireCutoffInterval</CODE> for the 
+     * <CODE>NthIncludedDayTrigger</CODE>.
+     * <P>
+     * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
+     * it is not always possible to decide with certainty that the trigger
+     * will <I>never</I> fire again. Therefore, it will search for the next 
+     * fire time up to a given cutoff. These cutoffs can be changed by using the
+     * {@link #setNextFireCutoffInterval(int)} and 
+     * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
+     * of the intervals specified by <CODE>{@link #getIntervalType()
+     * intervalType}</CODE>.
+     * 
+     * @return the chosen cutoff interval
+     * @see #setNextFireCutoffInterval(int)
+     * @see #getNextFireTime()
+     */
+    public int getNextFireCutoffInterval() {
+        return this.nextFireCutoffInterval;
+    }
+    
+    /** 
+     * Sets the date/time on which the trigger may begin firing. This defines
+     * the initial boundary for trigger firings &mdash; the trigger will not
+     * fire prior to this date and time. Defaults to the current date and time
+     * when the <CODE>NthIncludedDayTrigger</CODE> is created.
+     * 
+     * @param  startTime the initial boundary for trigger firings
+     * @throws java.lang.IllegalArgumentException
+     *         the specified start time is after the current end time or is 
+     *         null
+     * @see #getStartTime()
+     */
+    public void setStartTime(Date startTime) {
+        if (startTime == null) {
+            throw new IllegalArgumentException("Start time may not be null");
+        }
+        if ((this.endTime != null) && endTime.before(startTime)) {
+            throw new IllegalArgumentException("Start time must be before end time.");
+        }
+        this.startTime = startTime;
+    }
+
+    /**
+     * Returns the date/time on which the trigger may begin firing. This 
+     * defines the initial boundary for trigger firings &mdash; the trigger
+     * will not fire prior to this date and time.
+     * 
+     * @return the initial date/time on which the trigger may begin firing
+     * @see #setStartTime(Date)
+     */
+    public Date getStartTime() {
+        return this.startTime;
+    }
+
+    /** 
+     * Sets the date/time on which the trigger must stop firing. This defines
+     * the final boundary for trigger firings &mdash; the trigger will not
+     * fire after to this date and time. If this value is null, no end time
+     * boundary is assumed, and the trigger can continue indefinitely.
+     * 
+     * @param  endTime the final boundary for trigger firings
+     * @throws java.lang.IllegalArgumentException
+     *         the specified end time is before the current start time
+     * @see #getEndTime()
+     */
+    public void setEndTime(Date endTime) {
+        if ((endTime != null) && endTime.before(startTime)) {
+            throw new IllegalArgumentException("End time must be after start time.");
+        }
+        this.endTime = endTime;
+    }
+
+    /** 
+     * Returns the date/time on which the trigger must stop firing. This 
+     * defines the final boundary for trigger firings &mdash; the trigger will
+     * not fire after to this date and time. If this value is null, no end time
+     * boundary is assumed, and the trigger can continue indefinitely.
+     * 
+     * @return the date/time on which the trigger must stop firing
+     * @see #setEndTime(Date)
+     */
+    public Date getEndTime() {
+        return this.endTime;
+    }
+
+    /**
+     * Returns the next time at which the <CODE>NthIncludedDayTrigger</CODE>
+     * will fire. If the trigger will not fire again, <CODE>null</CODE> will be
+     * returned. 
+     * <P>
+     * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
+     * it is not always possible to decide with certainty that the trigger
+     * will <I>never</I> fire again. Therefore, it will search for the next 
+     * fire time up to a given cutoff. These cutoffs can be changed by using the
+     * {@link #setNextFireCutoffInterval(int)} and 
+     * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
+     * of the intervals specified by <CODE>{@link #getIntervalType()
+     * intervalType}</CODE>.
+     * <P>
+     * The returned value is not guaranteed to be valid until after
+     * the trigger has been added to the scheduler.
+     * 
+     * @return the next fire time for the trigger
+     * @see #getNextFireCutoffInterval()
+     * @see #setNextFireCutoffInterval(int)
+     * @see #getFireTimeAfter(Date)
+     */
+    public Date getNextFireTime() {
+        return this.nextFireTime;
+    }
+
+    /**
+     * Returns the previous time at which the 
+     * <CODE>NthIncludedDayTrigger</CODE> fired. If the trigger has not yet 
+     * fired, <CODE>null</CODE> will be returned.
+     * 
+     * @return the previous fire time for the trigger
+     */
+    public Date getPreviousFireTime() {
+        return this.previousFireTime;
+    }
+
+    /**
+     * Returns the first time the <CODE>NthIncludedDayTrigger</CODE> will fire
+     * after the specified date. 
+     * <P> 
+     * Because of the conceptual design of <CODE>NthIncludedDayTrigger</CODE>,
+     * it is not always possible to decide with certainty that the trigger
+     * will <I>never</I> fire again. Therefore, it will search for the next 
+     * fire time up to a given cutoff. These cutoffs can be changed by using the
+     * {@link #setNextFireCutoffInterval(int)} and 
+     * {@link #getNextFireCutoffInterval()} methods. The default cutoff is 12
+     * of the intervals specified by <CODE>{@link #getIntervalType()
+     * intervalType}</CODE>.
+     * <P>
+     * Therefore, for triggers with <CODE>intervalType = 
+     * {@link NthIncludedDayTrigger#INTERVAL_TYPE_WEEKLY 
+     * INTERVAL_TYPE_WEEKLY}</CODE>, if the trigger will not fire within 12
+     * weeks after the given date/time, <CODE>null</CODE> will be returned. For
+     * triggers with <CODE>intervalType = 
+     * {@link NthIncludedDayTrigger#INTERVAL_TYPE_MONTHLY
+     * INTERVAL_TYPE_MONTHLY}</CODE>, if the trigger will not fire within 12 
+     * months after the given date/time, <CODE>null</CODE> will be returned. 
+     * For triggers with <CODE>intervalType = 
+     * {@link NthIncludedDayTrigger#INTERVAL_TYPE_YEARLY 
+     * INTERVAL_TYPE_YEARLY}</CODE>, if the trigger will not fire within 12
+     * years after the given date/time, <CODE>null</CODE> will be returned.  In 
+     * all cases, if the trigger will not fire before <CODE>endTime</CODE>, 
+     * <CODE>null</CODE> will be returned.
+     * 
+     * @param  afterTime The time after which to find the nearest fire time.
+     *                   This argument is treated as exclusive &mdash; that is,
+     *                   if afterTime is a valid fire time for the trigger, it
+     *                   will not be returned as the next fire time.
+     * @return the first time the trigger will fire following the specified
+     *         date
+     */
+    public Date getFireTimeAfter(Date afterTime) {
+        if (afterTime == null) {
+            afterTime = new Date();
+        }
+        
+        if (afterTime.before(this.startTime)) {
+            afterTime = new Date(startTime.getTime() - 1000l);
+        }
+        
+        if (this.intervalType == INTERVAL_TYPE_WEEKLY) {
+            return getWeeklyFireTimeAfter(afterTime);
+        } else if (this.intervalType == INTERVAL_TYPE_MONTHLY) {
+            return getMonthlyFireTimeAfter(afterTime);
+        } else if (this.intervalType == INTERVAL_TYPE_YEARLY) {
+            return getYearlyFireTimeAfter(afterTime);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the last time the <CODE>NthIncludedDayTrigger</CODE> will fire.
+     * If the trigger will not fire at any point between <CODE>startTime</CODE>
+     * and <CODE>endTime</CODE>, <CODE>null</CODE> will be returned.
+     * 
+     * @return the last time the trigger will fire.
+     */
+    public Date getFinalFireTime() {
+        Date finalTime = null;
+        java.util.Calendar currCal = java.util.Calendar.getInstance();
+        currCal.setTime(this.endTime);
+        
+        while ((finalTime == null) 
+                && (this.startTime.before(currCal.getTime()))) {
+            currCal.add(java.util.Calendar.DATE, -1);
+            
             finalTime = getFireTimeAfter(currCal.getTime());
-		}
-		
-		return finalTime;
-	}
+        }
+        
+        return finalTime;
+    }
 
-	/**
+    /**
      * Called when the <CODE>Scheduler</CODE> has decided to 'fire' the trigger
      * (execute the associated <CODE>Job</CODE>), in order to give the 
      * <CODE>Trigger</CODE> a chance to update itself for its next triggering 
      * (if any).
-	 */
-	public void triggered(Calendar calendar) {
-		this.calendar = calendar;
+     */
+    public void triggered(Calendar calendar) {
+        this.calendar = calendar;
         this.previousFireTime = this.nextFireTime;
         this.nextFireTime = getFireTimeAfter(this.nextFireTime);
-	}
-	
-	/**
+    }
+    
+    /**
      * Called by the scheduler at the time a <CODE>Trigger</code> is first
      * added to the scheduler, in order to have the <CODE>Trigger</CODE>
      * compute its first fire time, based on any associated calendar.
      *         by the scheduler, which is also the same value 
      *         {@link #getNextFireTime()} will return (until after the first 
      *         firing of the <CODE>Trigger</CODE>).
-	 */
-	public Date computeFirstFireTime(Calendar calendar) {		
-		this.calendar = calendar;
-		this.nextFireTime = 
-			getFireTimeAfter(new Date(this.startTime.getTime() - 1000l));
-		
-		return this.nextFireTime;
-	}
-
-	/**
+     */
+    public Date computeFirstFireTime(Calendar calendar) {       
+        this.calendar = calendar;
+        this.nextFireTime = 
+            getFireTimeAfter(new Date(this.startTime.getTime() - 1000l));
+        
+        return this.nextFireTime;
+    }
+
+    /**
     * Called after the <CODE>Scheduler</CODE> has executed the 
     * <code>JobDetail</CODE> associated with the <CODE>Trigger</CODE> in order
     * to get the final instruction code from the trigger.
     *               <CODE>Job</CODE>, if any (may be <CODE>null</CODE>)
     * @return one of the Trigger.INSTRUCTION_XXX constants.
     */
-	public int executionComplete(JobExecutionContext jobCtx,
-			JobExecutionException result) {
-		if (result != null && result.refireImmediately())
-			return INSTRUCTION_RE_EXECUTE_JOB;
-
-		if (result != null && result.unscheduleFiringTrigger())
-	            return INSTRUCTION_SET_TRIGGER_COMPLETE;
-	
-	    if (result != null && result.unscheduleAllTriggers())
-	            return INSTRUCTION_SET_ALL_JOB_TRIGGERS_COMPLETE;
-	
-	    if (!mayFireAgain()) return INSTRUCTION_DELETE_TRIGGER;
-	
-	    return INSTRUCTION_NOOP;
-	}
-
-	/**
+    public int executionComplete(JobExecutionContext jobCtx,
+            JobExecutionException result) {
+        if (result != null && result.refireImmediately())
+            return INSTRUCTION_RE_EXECUTE_JOB;
+
+        if (result != null && result.unscheduleFiringTrigger())
+                return INSTRUCTION_SET_TRIGGER_COMPLETE;
+    
+        if (result != null && result.unscheduleAllTriggers())
+                return INSTRUCTION_SET_ALL_JOB_TRIGGERS_COMPLETE;
+    
+        if (!mayFireAgain()) return INSTRUCTION_DELETE_TRIGGER;
+    
+        return INSTRUCTION_NOOP;
+    }
+
+    /**
     * Used by the <CODE>Scheduler</CODE> to determine whether or not it is
     * possible for this <CODE>Trigger</CODE> to fire again.
     * <P>
     * @return a boolean indicator of whether the trigger could potentially fire
     *         again
     */
-	public boolean mayFireAgain() {
-		return (getNextFireTime() == null);
-	}
-
-	/**
-	 * Indicates whether <CODE>misfireInstruction</CODE> is a valid misfire
-	 * instruction for this <CODE>Trigger</CODE>.
-	 * 
-	 * @return whether <CODE>misfireInstruction</CODE> is valid.
-	 */
-	protected boolean validateMisfireInstruction(int misfireInstruction) {
-		if ((misfireInstruction == MISFIRE_INSTRUCTION_SMART_POLICY) ||
-				(misfireInstruction == MISFIRE_INSTRUCTION_DO_NOTHING) ||
-				(misfireInstruction == MISFIRE_INSTRUCTION_FIRE_ONCE_NOW)) {
-			return true;
-		} else {
-			return false;
-		}
-	}
+    public boolean mayFireAgain() {
+        return (getNextFireTime() == null);
+    }
+
+    /**
+     * Indicates whether <CODE>misfireInstruction</CODE> is a valid misfire
+     * instruction for this <CODE>Trigger</CODE>.
+     * 
+     * @return whether <CODE>misfireInstruction</CODE> is valid.
+     */
+    protected boolean validateMisfireInstruction(int misfireInstruction) {
+        if ((misfireInstruction == MISFIRE_INSTRUCTION_SMART_POLICY) ||
+                (misfireInstruction == MISFIRE_INSTRUCTION_DO_NOTHING) ||
+                (misfireInstruction == MISFIRE_INSTRUCTION_FIRE_ONCE_NOW)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
 
     /**
      * Updates the <CODE>NthIncludedDayTrigger</CODE>'s state based on the
      * 
      * @param calendar a new or updated calendar to use for the trigger
      */
-	public void updateAfterMisfire(Calendar calendar) {
-		int instruction = getMisfireInstruction();
-		
-		this.calendar = calendar;
-		
-		if (instruction == MISFIRE_INSTRUCTION_SMART_POLICY) {
-			instruction = MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
-		}
-		
-		if (instruction == MISFIRE_INSTRUCTION_DO_NOTHING) {
-			this.nextFireTime = getFireTimeAfter(new Date());
-		} else if (instruction == MISFIRE_INSTRUCTION_FIRE_ONCE_NOW) {
-			this.nextFireTime = new Date();
-		}
-	}
-
-	/**
+    public void updateAfterMisfire(Calendar calendar) {
+        int instruction = getMisfireInstruction();
+        
+        this.calendar = calendar;
+        
+        if (instruction == MISFIRE_INSTRUCTION_SMART_POLICY) {
+            instruction = MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
+        }
+        
+        if (instruction == MISFIRE_INSTRUCTION_DO_NOTHING) {
+            this.nextFireTime = getFireTimeAfter(new Date());
+        } else if (instruction == MISFIRE_INSTRUCTION_FIRE_ONCE_NOW) {
+            this.nextFireTime = new Date();
+        }
+    }
+
+    /**
      * Updates the <CODE>NthIncludedDayTrigger</CODE>'s state based on the 
      * given new version of the associated <CODE>Calendar</CODE>. 
      * 
      *                         be between &quot;now&quot; and the time the next
      *                         firing of the trigger is supposed to occur.
      */
-	public void updateWithNewCalendar(Calendar calendar, 
-			long misfireThreshold) {
-		Date now = new Date();
-		long diff;
-		
-		this.calendar = calendar;
-		this.nextFireTime = getFireTimeAfter(this.previousFireTime);
-		
-		if ((this.nextFireTime != null) && (this.nextFireTime.before(now))) {
-			diff = now.getTime() - this.nextFireTime.getTime();
-			if (diff >= misfireThreshold) {
-				this.nextFireTime = getFireTimeAfter(this.nextFireTime);
-			}
-		}
-	}
-
-	/**
-	 * Calculates the first time an <CODE>NthIncludedDayTrigger</CODE> with 
-	 * <CODE>intervalType = {@link #INTERVAL_TYPE_MONTHLY}</CODE> will fire 
-	 * after the specified date. See {@link #getNextFireTime} for more 
-	 * information.
-	 * 
-	 * @param afterDate The time after which to find the nearest fire time.
-	 *                  This argument is treated as exclusive &mdash; that is,
-	 *                  if afterTime is a valid fire time for the trigger, it
-	 *                  will not be returned as the next fire time.
-	 * @return the first time the trigger will fire following the specified
-	 *         date
-	 */
-	private Date getMonthlyFireTimeAfter(Date afterDate) {
-		int currN = 0;
-		java.util.Calendar afterCal;
-		java.util.Calendar currCal;
-		int currMonth;
-		int monthCount = 0;
-		boolean gotOne = false;
-		
-		afterCal = java.util.Calendar.getInstance();
-		afterCal.setTime(afterDate);
-		
-		currCal = java.util.Calendar.getInstance();
-		currCal.set(afterCal.get(java.util.Calendar.YEAR),
-				    afterCal.get(java.util.Calendar.MONTH), 1);
-		currCal.set(java.util.Calendar.HOUR_OF_DAY, this.fireAtHour);
-		currCal.set(java.util.Calendar.MINUTE, this.fireAtMinute);
-		currCal.set(java.util.Calendar.SECOND, 0);
-		currCal.set(java.util.Calendar.MILLISECOND, 0);
-		
-		currMonth = currCal.get(java.util.Calendar.MONTH);
-		
-		while ((!gotOne) && (monthCount < this.nextFireCutoffInterval)) {
-			while ((currN != this.n) && (monthCount < 12)) {
-				//if we move into a new month, reset the current "n" counter
-				if (currCal.get(java.util.Calendar.MONTH) != currMonth) {
-					currN = 0;
-					monthCount++;
-					currMonth = currCal.get(java.util.Calendar.MONTH);
-				}
-				
-				//treating a null calendar as an all-inclusive calendar,
-				// increment currN if the current date being tested is included
-				// on the calendar
-				if ((calendar == null) 
-						|| (calendar.isTimeIncluded(currCal.getTime().getTime()))) {
-					currN++;
-				}
-
-				if (currN != this.n) {
-					currCal.add(java.util.Calendar.DATE, 1);
-				}
-				
-				//if we pass endTime, drop out and return null.
-				if ((this.endTime != null) 
-						&& (currCal.getTime().after(this.endTime))) {
-					return null;
-				}
-			} 
-			
-			//We found an "n" or we've checked the requisite number of months.
-			// If we've found an "n", is it the right one? -- that is, we could
-			// be looking at an nth day PRIOR to afterDate
-			if (currN == this.n) {
-				if (afterDate.before(currCal.getTime())) {
-					gotOne = true;
-				} else { //resume checking on the first day of the next month
-					currCal.set(java.util.Calendar.DAY_OF_MONTH, 1);
-					currCal.add(java.util.Calendar.MONTH, 1);
-					currN = 0;
-				}
-			}
-		}
-		
-		if (monthCount < this.nextFireCutoffInterval) {
-			return currCal.getTime();			
-		} else {
-			return null;
-		}
-	}
-
-	/**
-	 * Calculates the first time an <CODE>NthIncludedDayTrigger</CODE> with 
-	 * <CODE>intervalType = {@link #INTERVAL_TYPE_YEARLY}</CODE> will fire 
-	 * after the specified date. See {@link #getNextFireTime} for more 
-	 * information.
-	 * 
-	 * @param afterDate The time after which to find the nearest fire time.
-	 *                  This argument is treated as exclusive &mdash; that is,
-	 *                  if afterTime is a valid fire time for the trigger, it
-	 *                  will not be returned as the next fire time.
-	 * @return the first time the trigger will fire following the specified
-	 *         date
-	 */
-	private Date getYearlyFireTimeAfter(Date afterDate) {
-		int currN = 0;
-		java.util.Calendar afterCal;
-		java.util.Calendar currCal;
-		int currYear;
-		int yearCount = 0;
-		boolean gotOne = false;
-		
-		afterCal = java.util.Calendar.getInstance();
-		afterCal.setTime(afterDate);
-		
-		currCal = java.util.Calendar.getInstance();
-		currCal.set(afterCal.get(java.util.Calendar.YEAR),
-				    java.util.Calendar.JANUARY, 1);
-		currCal.set(java.util.Calendar.HOUR_OF_DAY, this.fireAtHour);
-		currCal.set(java.util.Calendar.MINUTE, this.fireAtMinute);
-		currCal.set(java.util.Calendar.SECOND, 0);
-		currCal.set(java.util.Calendar.MILLISECOND, 0);
-		
-		currYear = currCal.get(java.util.Calendar.YEAR);
-		
-		while ((!gotOne) && (yearCount < this.nextFireCutoffInterval)) {
-			while ((currN != this.n) && (yearCount < 5)) {
-				//if we move into a new year, reset the current "n" counter
-				if (currCal.get(java.util.Calendar.YEAR) != currYear) {
-					currN = 0;
-					yearCount++;
-					currYear = currCal.get(java.util.Calendar.YEAR);
-				}
-				
-				//treating a null calendar as an all-inclusive calendar,
-				// increment currN if the current date being tested is included
-				// on the calendar
-				if ((calendar == null) 
-						|| (calendar.isTimeIncluded(currCal.getTime().getTime()))) {
-					currN++;
-				}
-
-				if (currN != this.n) {
-					currCal.add(java.util.Calendar.DATE, 1);
-				}
-				
-				//if we pass endTime, drop out and return null.
-				if ((this.endTime != null) 
-						&& (currCal.getTime().after(this.endTime))) {
-					return null;
-				}
-			} 
-			
-			//We found an "n" or we've checked the requisite number of years.
-			// If we've found an "n", is it the right one? -- that is, we 
-			// could be looking at an nth day PRIOR to afterDate
-			if (currN == this.n) {
-				if (afterDate.before(currCal.getTime())) {
-					gotOne = true;
-				} else { //resume checking on the first day of the next year
-					currCal.set(java.util.Calendar.DAY_OF_MONTH, 1);
-					currCal.set(java.util.Calendar.MONTH, 
-							    java.util.Calendar.JANUARY);
-					currCal.add(java.util.Calendar.YEAR, 1);
-					currN = 0;
-				}
-			}
-		}
-		
-		if (yearCount < this.nextFireCutoffInterval) {
-			return currCal.getTime();			
-		} else {
-			return null;
-		}
-	}
+    public void updateWithNewCalendar(Calendar calendar, 
+            long misfireThreshold) {
+        Date now = new Date();
+        long diff;
+        
+        this.calendar = calendar;
+        this.nextFireTime = getFireTimeAfter(this.previousFireTime);
+        
+        if ((this.nextFireTime != null) && (this.nextFireTime.before(now))) {
+            diff = now.getTime() - this.nextFireTime.getTime();
+            if (diff >= misfireThreshold) {
+                this.nextFireTime = getFireTimeAfter(this.nextFireTime);
+            }
+        }
+    }
+
+    /**
+     * Calculates the first time an <CODE>NthIncludedDayTrigger</CODE> with 
+     * <CODE>intervalType = {@link #INTERVAL_TYPE_WEEKLY}</CODE> will fire 
+     * after the specified date. See {@link #getNextFireTime} for more 
+     * information.
+     * 
+     * @param afterDate The time after which to find the nearest fire time.
+     *                  This argument is treated as exclusive &mdash; that is,
+     *                  if afterTime is a valid fire time for the trigger, it
+     *                  will not be returned as the next fire time.
+     * @return the first time the trigger will fire following the specified
+     *         date
+     */
+    private Date getWeeklyFireTimeAfter(Date afterDate) {
+        int currN = 0;
+        java.util.Calendar afterCal;
+        java.util.Calendar currCal;
+        int currWeek;
+        int weekCount = 0;
+        boolean gotOne = false;
+        
+        afterCal = java.util.Calendar.getInstance();
+        afterCal.setTime(afterDate);
+        
+        currCal = java.util.Calendar.getInstance();
+        currCal.set(afterCal.get(java.util.Calendar.YEAR),
+                    afterCal.get(java.util.Calendar.MONTH), 
+                    afterCal.get(java.util.Calendar.DAY_OF_MONTH));
+
+        //move to the first day of the week (SUNDAY)
+        currCal.add(java.util.Calendar.DAY_OF_MONTH, 
+                (afterCal.get(java.util.Calendar.DAY_OF_WEEK) - 1) * -1);
+
+        currCal.set(java.util.Calendar.HOUR_OF_DAY, this.fireAtHour);
+        currCal.set(java.util.Calendar.MINUTE, this.fireAtMinute);
+        currCal.set(java.util.Calendar.SECOND, 0);
+        currCal.set(java.util.Calendar.MILLISECOND, 0);
+        
+        currWeek = currCal.get(java.util.Calendar.WEEK_OF_YEAR);
+        
+        while ((!gotOne) && (weekCount < this.nextFireCutoffInterval)) {
+            while ((currN != this.n) && (weekCount < 12)) {
+                //if we move into a new month, reset the current "n" counter
+                if (currCal.get(java.util.Calendar.WEEK_OF_YEAR) != currWeek) {
+                    currN = 0;
+                    weekCount++;
+                    currWeek = currCal.get(java.util.Calendar.WEEK_OF_YEAR);
+                }
+                
+                //treating a null calendar as an all-inclusive calendar,
+                // increment currN if the current date being tested is included
+                // on the calendar
+                if ((calendar == null) 
+                        || (calendar.isTimeIncluded(currCal.getTime().getTime()))) {
+                    currN++;
+                }
+
+                if (currN != this.n) {
+                    currCal.add(java.util.Calendar.DATE, 1);
+                }
+                
+                //if we pass endTime, drop out and return null.
+                if ((this.endTime != null) 
+                        && (currCal.getTime().after(this.endTime))) {
+                    return null;
+                }
+            } 
+            
+            //We found an "n" or we've checked the requisite number of weeks.
+            // If we've found an "n", is it the right one? -- that is, we could
+            // be looking at an nth day PRIOR to afterDate
+            if (currN == this.n) {
+                if (afterDate.before(currCal.getTime())) {
+                    gotOne = true;
+                } else { //resume checking on the first day of the next week
+                    currCal.add(java.util.Calendar.DAY_OF_MONTH, -1 * (currN - 1));
+                    currCal.add(java.util.Calendar.DAY_OF_MONTH, 7);
+                    currN = 0;
+                }
+            }
+        }
+        
+        if (weekCount < this.nextFireCutoffInterval) {
+            return currCal.getTime();           
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Calculates the first time an <CODE>NthIncludedDayTrigger</CODE> with 
+     * <CODE>intervalType = {@link #INTERVAL_TYPE_MONTHLY}</CODE> will fire 
+     * after the specified date. See {@link #getNextFireTime} for more 
+     * information.
+     * 
+     * @param afterDate The time after which to find the nearest fire time.
+     *                  This argument is treated as exclusive &mdash; that is,
+     *                  if afterTime is a valid fire time for the trigger, it
+     *                  will not be returned as the next fire time.
+     * @return the first time the trigger will fire following the specified
+     *         date
+     */
+    private Date getMonthlyFireTimeAfter(Date afterDate) {
+        int currN = 0;
+        java.util.Calendar afterCal;
+        java.util.Calendar currCal;
+        int currMonth;
+        int monthCount = 0;
+        boolean gotOne = false;
+        
+        afterCal = java.util.Calendar.getInstance();
+        afterCal.setTime(afterDate);
+        
+        currCal = java.util.Calendar.getInstance();
+        currCal.set(afterCal.get(java.util.Calendar.YEAR),
+                    afterCal.get(java.util.Calendar.MONTH), 1);
+        currCal.set(java.util.Calendar.HOUR_OF_DAY, this.fireAtHour);
+        currCal.set(java.util.Calendar.MINUTE, this.fireAtMinute);
+        currCal.set(java.util.Calendar.SECOND, 0);
+        currCal.set(java.util.Calendar.MILLISECOND, 0);
+        
+        currMonth = currCal.get(java.util.Calendar.MONTH);
+        
+        while ((!gotOne) && (monthCount < this.nextFireCutoffInterval)) {
+            while ((currN != this.n) && (monthCount < 12)) {
+                //if we move into a new month, reset the current "n" counter
+                if (currCal.get(java.util.Calendar.MONTH) != currMonth) {
+                    currN = 0;
+                    monthCount++;
+                    currMonth = currCal.get(java.util.Calendar.MONTH);
+                }
+                
+                //treating a null calendar as an all-inclusive calendar,
+                // increment currN if the current date being tested is included
+                // on the calendar
+                if ((calendar == null) 
+                        || (calendar.isTimeIncluded(currCal.getTime().getTime()))) {
+                    currN++;
+                }
+
+                if (currN != this.n) {
+                    currCal.add(java.util.Calendar.DATE, 1);
+                }
+                
+                //if we pass endTime, drop out and return null.
+                if ((this.endTime != null) 
+                        && (currCal.getTime().after(this.endTime))) {
+                    return null;
+                }
+            } 
+            
+            //We found an "n" or we've checked the requisite number of months.
+            // If we've found an "n", is it the right one? -- that is, we could
+            // be looking at an nth day PRIOR to afterDate
+            if (currN == this.n) {
+                if (afterDate.before(currCal.getTime())) {
+                    gotOne = true;
+                } else { //resume checking on the first day of the next month
+                    currCal.set(java.util.Calendar.DAY_OF_MONTH, 1);
+                    currCal.add(java.util.Calendar.MONTH, 1);
+                    currN = 0;
+                }
+            }
+        }
+        
+        if (monthCount < this.nextFireCutoffInterval) {
+            return currCal.getTime();           
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Calculates the first time an <CODE>NthIncludedDayTrigger</CODE> with 
+     * <CODE>intervalType = {@link #INTERVAL_TYPE_YEARLY}</CODE> will fire 
+     * after the specified date. See {@link #getNextFireTime} for more 
+     * information.
+     * 
+     * @param afterDate The time after which to find the nearest fire time.
+     *                  This argument is treated as exclusive &mdash; that is,
+     *                  if afterTime is a valid fire time for the trigger, it
+     *                  will not be returned as the next fire time.
+     * @return the first time the trigger will fire following the specified
+     *         date
+     */
+    private Date getYearlyFireTimeAfter(Date afterDate) {
+        int currN = 0;
+        java.util.Calendar afterCal;
+        java.util.Calendar currCal;
+        int currYear;
+        int yearCount = 0;
+        boolean gotOne = false;
+        
+        afterCal = java.util.Calendar.getInstance();
+        afterCal.setTime(afterDate);
+        
+        currCal = java.util.Calendar.getInstance();
+        currCal.set(afterCal.get(java.util.Calendar.YEAR),
+                    java.util.Calendar.JANUARY, 1);
+        currCal.set(java.util.Calendar.HOUR_OF_DAY, this.fireAtHour);
+        currCal.set(java.util.Calendar.MINUTE, this.fireAtMinute);
+        currCal.set(java.util.Calendar.SECOND, 0);
+        currCal.set(java.util.Calendar.MILLISECOND, 0);
+        
+        currYear = currCal.get(java.util.Calendar.YEAR);
+        
+        while ((!gotOne) && (yearCount < this.nextFireCutoffInterval)) {
+            while ((currN != this.n) && (yearCount < 5)) {
+                //if we move into a new year, reset the current "n" counter
+                if (currCal.get(java.util.Calendar.YEAR) != currYear) {
+                    currN = 0;
+                    yearCount++;
+                    currYear = currCal.get(java.util.Calendar.YEAR);
+                }
+                
+                //treating a null calendar as an all-inclusive calendar,
+                // increment currN if the current date being tested is included
+                // on the calendar
+                if ((calendar == null) 
+                        || (calendar.isTimeIncluded(currCal.getTime().getTime()))) {
+                    currN++;
+                }
+
+                if (currN != this.n) {
+                    currCal.add(java.util.Calendar.DATE, 1);
+                }
+                
+                //if we pass endTime, drop out and return null.
+                if ((this.endTime != null) 
+                        && (currCal.getTime().after(this.endTime))) {
+                    return null;
+                }
+            } 
+            
+            //We found an "n" or we've checked the requisite number of years.
+            // If we've found an "n", is it the right one? -- that is, we 
+            // could be looking at an nth day PRIOR to afterDate
+            if (currN == this.n) {
+                if (afterDate.before(currCal.getTime())) {
+                    gotOne = true;
+                } else { //resume checking on the first day of the next year
+                    currCal.set(java.util.Calendar.DAY_OF_MONTH, 1);
+                    currCal.set(java.util.Calendar.MONTH, 
+                                java.util.Calendar.JANUARY);
+                    currCal.add(java.util.Calendar.YEAR, 1);
+                    currN = 0;
+                }
+            }
+        }
+        
+        if (yearCount < this.nextFireCutoffInterval) {
+            return currCal.getTime();           
+        } else {
+            return null;
+        }
+    }
 }