/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.scheduler.limit;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalTime;
import divconq.hub.Hub;
import divconq.util.StringUtil;
import divconq.util.TimeUtil;
import divconq.xml.XElement;
// artificially limit expiration at 10 years, if none present
// server should be restarted at least once every 10 years :)
public class LimitHelper {
protected DayWindow dailyWindow = new DayWindow();
protected DateTime validFrom = null;
protected DateTime validTo = null;
protected DateTimeZone zone = null;
// use only for next/check/blocked/open - do not inherit init/start/end
protected LimitHelper parent = null;
protected List<MonthWindow> monthly = new ArrayList<MonthWindow>();
protected List<WeekdayWindow> weekly = new ArrayList<WeekdayWindow>();
public DayWindow getDailyWindow() {
return this.dailyWindow;
}
/*
* <Limits
* LinkBatch="None,Small,Medium,Large" - include limits defined at server level (batch processing)
* DefaultWindow="T/F" - by default you have 24 hours enabled
ValidFrom="iso-date-time" - schedule only before/after these time
ValidTo="iso-date-time"
TimeZone="name" - zone to apply to the limits
* >
*
* // one or more windows during which it is ok to run the scheduled work
* // defaults to beginning of day to end of day, no matter
* <IncludeWindow From="00:00" To="24:00" />
*
* <ExcludeWindow From="04:15" To="04:17" />
*
* <Weekdays Monday="T/F" Tuesday="n" ... All="T/F" >
* // if exclude is not present, then assume entire day
* <ExcludeWindow From="" To="" />
* </Weekdays>
*
* <Months January="T/F" ... >
* <First Monday="T/F" Tuesday="n" ... All="T/F" >
* <ExcludeWindow From="" To="" />
* </First>
* <Second Monday="T/F" Tuesday="n" ... All="T/F" >
* <ExcludeWindow From="" To="" />
* </Second>
* ... etc, or ...
* <Monthday List="N,N,N,Last">
* <ExcludeWindow From="" To="" />
* </Monthday>
* </Months>
* </Limits>
*
* @param config
*/
public void init(XElement config) {
if (config != null) {
String zone = config.getAttribute("TimeZone");
if (StringUtil.isNotEmpty(zone))
this.zone = DateTimeZone.forID(zone);
String from = config.getAttribute("ValidFrom");
if (!StringUtil.isEmpty(from)) {
this.validFrom = TimeUtil.parseDateTime(from);
// TODO not sure about this - parsing of ISO string should not be circumvented?
if (this.zone != null)
this.validFrom = new DateTime(this.validFrom, this.zone);
}
String to = config.getAttribute("ValidTo");
if (!StringUtil.isEmpty(to)) {
this.validTo = TimeUtil.parseDateTime(to);
// TODO not sure about this - parsing of ISO string should not be circumvented?
if (this.zone != null)
this.validTo = new DateTime(this.validTo, this.zone);
}
// default to 10 years from now
if (this.validTo == null)
this.validTo = new DateTime().plusYears(10);
for (XElement el : config.selectAll("Months")) {
MonthWindow ww = new MonthWindow();
ww.init(this, el);
this.monthly.add(ww);
}
for (XElement el : config.selectAll("Weekdays")) {
WeekdayWindow ww = new WeekdayWindow();
ww.init(this, el);
this.weekly.add(ww);
}
this.dailyWindow.init(config);
String batch = config.getAttribute("LinkBatch");
if (StringUtil.isNotEmpty(batch))
this.parent = Hub.instance.getScheduler().getBatch(batch);
}
else {
this.dailyWindow.init(null);
}
if (this.parent != null) {
// if parent (batch) then we only use our daily overrides,
// not our month or weekly
this.dailyWindow.setParent(this.parent.getDailyWindow());
}
}
// return true if instant can run within a window
// return false if instant is in the past
public boolean checkForRun(DateTime v) {
if (this.zone != null)
v = new DateTime(v, this.zone);
// if this time was ended at mid or before midnight then entire day is blocked
if (this.isEnded(v))
return false;
// if this time was not started by the end of the then entire day is blocked
if (!this.isStarted(v))
return false;
// runs must also be now (recent) or future
if (v.plusMinutes(5).isBeforeNow())
return false;
if (this.parent != null)
return this.parent.checkForRun(v);
CheckInfo ci = new CheckInfo();
ci.setWhen(v);
// if there are any months, those take precedence over other
if (this.monthly.size() > 0) {
for (MonthWindow ww : this.monthly)
if (ww.appliesTo(ci))
return (ww.check(ci) == CheckLimitResult.Pass);
return false;
}
// if there are any weeks, those take precedence over daily
else if (this.weekly.size() > 0) {
for (WeekdayWindow ww : this.weekly)
if (ww.appliesTo(ci))
return (ww.check(ci) == CheckLimitResult.Pass);
return false;
}
return (this.dailyWindow.check(v) == CheckLimitResult.Pass);
}
// return the start of the next available window for running
// from "now"
public DateTime nextAllowedRun() {
return this.nextAllowedRunAfter(new DateTime().minusMinutes(1));
}
// return the start of the next available window for running (must always be after or equal to now)
public DateTime nextAllowedRunAfter(DateTime lin) {
if (this.zone != null)
lin = new DateTime(lin, this.zone);
// cannot run before now - 2 minutes
if (lin.plusMinutes(5).isBeforeNow())
lin = new DateTime().minusMinutes(1); // start back one minute so we can start on time
// cannot run again
if (this.isEnded(lin))
return null;
// must start at least at "from"
if (!this.isStarted(lin))
lin = this.validFrom;
if (this.parent != null)
return this.parent.nextAllowedRunAfter(lin);
CheckInfo ci = new CheckInfo();
ci.setWhen(lin);
LocalTime nt = null;
// move forward 1 day at a time till we find a date that has an opening
while (true) {
// if there are any months, those take precedence over other
if (this.monthly.size() > 0) {
for (MonthWindow ww : this.monthly)
if (ww.appliesTo(ci)) {
nt = ww.nextTimeOn(ci);
break;
}
}
// if there are any weeks, those take precedence over daily
else if (this.weekly.size() > 0) {
for (WeekdayWindow ww : this.weekly)
if (ww.appliesTo(ci)) {
nt = ww.nextTimeOn(ci);
break;
}
}
else
nt = this.dailyWindow.nextTimeOn(ci.getWhen());
if (nt != null)
break;
ci.incrementDay();
// there is no next allowed
if (this.isEnded(ci.getWhen()))
return null;
}
lin = TimeUtil.withTime(ci.getWhen(), nt);
// there is no next allowed
if (this.isEnded(lin))
return null;
return lin;
}
public boolean isDateBlocked(DateTime tlast) {
if (this.zone != null)
tlast = new DateTime(tlast, this.zone);
// if this time was ended at mid or before midnight then entire day is blocked
if (this.isEnded(tlast.withTime(0, 0, 0, 0)))
return true;
// if this time was not started by the end of the then entire day is blocked
if (!this.isStarted(tlast.withTime(23, 59, 59, 0)))
return true;
if (this.parent != null)
return this.parent.isDateBlocked(tlast);
CheckInfo ci = new CheckInfo();
ci.setWhen(tlast);
// if there are any months, those take precedence over other
if (this.monthly.size() > 0) {
for (MonthWindow ww : this.monthly)
if (ww.appliesTo(ci))
return false;
return true;
}
// if there are any weeks, those take precedence over daily
else if (this.weekly.size() > 0) {
// only need to find one window to return false
for (WeekdayWindow ww : this.weekly)
if (ww.appliesTo(ci))
return false;
return true;
}
return this.dailyWindow.excludeAll();
}
// return true if "now" is after valid start date
public boolean isStarted() {
if (this.validFrom != null)
return !this.validFrom.isAfterNow(); // now is equal or greater than from
return true;
}
// return true if param is after valid start date
public boolean isStarted(DateTime scheduleDate) {
if (this.validFrom != null)
return !this.validFrom.isAfter(scheduleDate); // now is equal or greater than from
return true;
}
// return true if "now" is after valid end date
public boolean isEnded() {
if (this.validTo != null)
return !this.validTo.isAfterNow(); // must be before now (not equal)
return false;
}
// return true if param is after valid end date
public boolean isEnded(DateTime scheduleDate) {
if (this.validTo != null)
return !this.validTo.isAfter(scheduleDate);
return false;
}
// return the first valid date for this schedule
public DateTime getFirstDate() {
return this.validFrom;
}
// return the last valid date for this schedule
public DateTime getLastDate() {
return this.validTo;
}
}