com.hfg.util.scheduler.CronSchedule Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com_hfg Show documentation
Show all versions of com_hfg Show documentation
com.hfg xml, html, svg, and bioinformatics utility library
package com.hfg.util.scheduler;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Random;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.collection.OrderedSet;
//------------------------------------------------------------------------------
/**
Scheduler that uses the same specification syntax as the Unix cron utility.
See Wikipedia.
The standard cron specification has five fields and doesn't have second granularity.
This class supports an optional sixth parameter for seconds placed at the start of the specification:
* * * * * * command to execute
| | | | | |
| | | | | |
| | | | | +------ day of week (0 - 6) (0 to 6 are Sunday to Saturday, or use names; 7 is Sunday, the same as 0)
| | | | +----------- month (1 - 12)
| | | +---------------- day of month (1 - 31)
| | +--------------------- hour (0 - 23)
| +-------------------------- min (0 - 59)
+------------------------------- sec (0 - 59) Optional - not a standard cron field. '*' is not an allowed value.
The following special characters are supported:
- Asterisk (*) - a wildcard representing 'all'
- Comma (,) - separator for multiple values in a field
- Hyphen (-) - defines a range of values. 10-12 is interpreted as values of 10, 11, and 12.
- Slash (/) - defines a step value
- 'R' - translated to a random (but consistent) value.
@author J. Alex Taylor, hairyfatguy.com
*/
//------------------------------------------------------------------------------
// com.hfg Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------
// TODO: More to do to fully support cron syntax.
public class CronSchedule implements Schedule
{
private String mSecondSpec; // Not available in the standard cron spec
private String mMinuteSpec;
private String mHourSpec;
private String mDayOfMonthSpec;
private String mMonthSpec;
private String mDayOfWeekSpec;
private OrderedSet mSecondValues;
private OrderedSet mMinuteValues;
private OrderedSet mHourValues;
private OrderedSet mDayOfMonthValues;
private OrderedSet mMonthValues;
private OrderedSet mDayOfWeekValues;
private static Random sRandom;
//###########################################################################
// CONSTRUCTORS
//###########################################################################
//---------------------------------------------------------------------------
public CronSchedule(String inCronString)
{
parse(inCronString);
}
//###########################################################################
// PUBLIC METHODS
//###########################################################################
//---------------------------------------------------------------------------
public long getMillisToNext()
{
// Calculate the interval in milliseconds
return next().getTime() - System.currentTimeMillis();
}
//---------------------------------------------------------------------------
@Override
public Date next()
{
return nextAfter(new Date());
}
//---------------------------------------------------------------------------
@Override
public Date nextAfter(Date inReferenceDate)
{
LocalDateTime localDateTime = inReferenceDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
// We don't go to sub-second granularity
localDateTime = localDateTime.withNano(0);
if (CollectionUtil.hasValues(mSecondValues))
{
localDateTime = localDateTime.plusSeconds(1);
}
else
{
localDateTime = localDateTime.plusMinutes(1)
.withSecond(0);
}
if (CollectionUtil.hasValues(mMonthValues))
{
while (! mMonthValues.contains(localDateTime.getMonthValue()))
{
localDateTime = localDateTime.plusMonths(1);
localDateTime = localDateTime.withDayOfMonth(1);
localDateTime = localDateTime.withHour(0);
localDateTime = localDateTime.withMinute(0);
localDateTime = localDateTime.withSecond(0);
}
}
if (CollectionUtil.hasValues(mDayOfMonthValues))
{
while (! mDayOfMonthValues.contains(localDateTime.getDayOfMonth()))
{
localDateTime = localDateTime.plusDays(1)
.withHour(0)
.withMinute(0)
.withSecond(0);
}
}
if (CollectionUtil.hasValues(mDayOfWeekValues))
{
while (! mDayOfWeekValues.contains(localDateTime.getDayOfWeek().getValue())
|| (mDayOfMonthValues != null && ! mDayOfMonthValues.contains(localDateTime.getDayOfMonth()))
|| (mMonthValues != null && ! mMonthValues.contains(localDateTime.getMonthValue()))
)
{
localDateTime = localDateTime.plusDays(1)
.withHour(0)
.withMinute(0)
.withSecond(0);
}
}
if (CollectionUtil.hasValues(mHourValues))
{
while (! mHourValues.contains(localDateTime.getHour())
|| (mDayOfWeekValues != null && ! mDayOfWeekValues.contains(localDateTime.getDayOfWeek().getValue()))
|| (mDayOfMonthValues != null && ! mDayOfMonthValues.contains(localDateTime.getDayOfMonth()))
|| (mMonthValues != null && ! mMonthValues.contains(localDateTime.getMonthValue()))
)
{
localDateTime = localDateTime.plusHours(1)
.withMinute(0)
.withSecond(0);
}
}
if (CollectionUtil.hasValues(mMinuteValues))
{
while (! mMinuteValues.contains(localDateTime.getMinute())
|| (mHourValues != null && ! mHourValues.contains(localDateTime.getHour()))
|| (mDayOfWeekValues != null && ! mDayOfWeekValues.contains(localDateTime.getDayOfWeek().getValue()))
|| (mDayOfMonthValues != null && ! mDayOfMonthValues.contains(localDateTime.getDayOfMonth()))
|| (mMonthValues != null && ! mMonthValues.contains(localDateTime.getMonthValue()))
)
{
localDateTime = localDateTime.plusMinutes(1)
.withSecond(0);
}
}
if (CollectionUtil.hasValues(mSecondValues))
{
while (! mSecondValues.contains(localDateTime.getSecond())
|| (mMinuteValues != null && ! mMinuteValues.contains(localDateTime.getMinute()))
|| (mHourValues != null && ! mHourValues.contains(localDateTime.getHour()))
|| (mDayOfWeekValues != null && ! mDayOfWeekValues.contains(localDateTime.getDayOfWeek().getValue()))
|| (mDayOfMonthValues != null && ! mDayOfMonthValues.contains(localDateTime.getDayOfMonth()))
|| (mMonthValues != null && ! mMonthValues.contains(localDateTime.getMonthValue()))
)
{
localDateTime = localDateTime.plusSeconds(1);
}
}
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
//---------------------------------------------------------------------------
/**
* Second granularity is not strictly part of the cron specification, but it is a useful addition.
* @param inValue the string specification for seconds
* @return this CronSchedule object to allow method chaining
*/
public CronSchedule setSecondSpec(String inValue)
{
mSecondValues = null;
if (StringUtil.isSet(inValue))
{
String spec = inValue.trim();
if (spec.equals("*"))
{
throw new CronParseException("'*' is not an acceptable value for the seconds field!");
}
else if (spec.equals("R"))
{
mSecondValues = new OrderedSet<>(1);
// Generate a random value in range 0 to 59
mSecondValues.add(getRandom().nextInt(60));
}
else
{
List values = new ArrayList<>(60);
String[] pieces = spec.split(",");
for (String piece : pieces)
{
// Step?
Integer step = null;
int stepIdx = piece.indexOf("/");
if (stepIdx >= 0)
{
String stepString = piece.substring(stepIdx + 1);
if (stepString.equalsIgnoreCase("R"))
{
// Generate a random value in range 1 to 59 (A step of zero would be bad!)
step = getRandom().nextInt(59) + 1;
}
else
{
step = Integer.parseInt(stepString);
if (step <= 0)
{
throw new CronParseException("Invalid second step value: " + stepString + "! Valid value range: 1-59.");
}
}
// Remove the step specification from the piece and continue processing
piece = piece.substring(0, stepIdx);
}
// Range?
int startValue;
int endValue;
int dashIdx = piece.indexOf("-");
if (dashIdx > 0)
{
startValue = Integer.parseInt(piece.substring(0, dashIdx));
if (startValue < 0
|| startValue > 59)
{
throw new CronParseException("Invalid second range start value: " + startValue + "! Valid value range: 0-59.");
}
endValue = Integer.parseInt(piece.substring(dashIdx + 1));
if (endValue < 0
|| endValue > 59)
{
throw new CronParseException("Invalid second range end value: " + endValue + "! Valid value range: 0-59.");
}
else if (endValue < startValue)
{
throw new CronParseException("Invalid second range end value: " + endValue + "! It cannot be less than the range start value.");
}
}
else if (piece.equals("*"))
{
startValue = 0;
endValue = 59;
}
else
{
startValue = endValue = Integer.parseInt(piece);
if (startValue < 0
|| startValue > 59)
{
throw new CronParseException("Invalid second value: " + piece + "! Valid value range: 0-59.");
}
}
for (int i = startValue; i <= endValue; )
{
values.add(i);
i += (step != null ? step : 1);
}
}
// Make sure the values are sorted low to high
Collections.sort(values);
mSecondValues = new OrderedSet<>(values);
}
}
return this;
}
//---------------------------------------------------------------------------
public CronSchedule setMinuteSpec(String inMinuteSpec)
{
mMinuteValues = null;
if (StringUtil.isSet(inMinuteSpec))
{
String spec = inMinuteSpec.trim();
if (spec.equals("R"))
{
Random rand = new Random();
mMinuteValues = new OrderedSet<>(1);
// Generate a random value in range 0 to 59
mMinuteValues.add(rand.nextInt(60));
}
else if (! spec.equals("*"))
{
List values = new ArrayList<>(60);
String[] pieces = spec.split(",");
for (String piece : pieces)
{
// Step?
Integer step = null;
int stepIdx = piece.indexOf("/");
if (stepIdx >= 0)
{
String stepString = piece.substring(stepIdx + 1);
if (stepString.equalsIgnoreCase("R"))
{
// Generate a random value in range 1 to 59 (A step of zero would be bad!)
step = getRandom().nextInt(59) + 1;
}
else
{
step = Integer.parseInt(stepString);
if (step <= 0)
{
throw new CronParseException("Invalid minute step value: " + stepString + "! Valid value range: 1-59.");
}
}
// Remove the step specification from the piece and continue processing
piece = piece.substring(0, stepIdx);
}
// Range?
int startValue;
int endValue;
int dashIdx = piece.indexOf("-");
if (dashIdx > 0)
{
startValue = Integer.parseInt(piece.substring(0, dashIdx));
if (startValue < 0
|| startValue > 59)
{
throw new CronParseException("Invalid minute range start value: " + startValue + "! Valid value range: 0-59.");
}
endValue = Integer.parseInt(piece.substring(dashIdx + 1));
if (endValue < 0
|| endValue > 59)
{
throw new CronParseException("Invalid minute range end value: " + endValue + "! Valid value range: 0-59.");
}
else if (endValue < startValue)
{
throw new CronParseException("Invalid minute range end value: " + endValue + "! It cannot be less than the range start value.");
}
}
else if (piece.equals("*"))
{
startValue = 0;
endValue = 59;
}
else
{
startValue = endValue = Integer.parseInt(piece);
if (startValue < 0
|| startValue > 59)
{
throw new CronParseException("Invalid minute value: " + piece + "! Valid value range: 0-59.");
}
}
for (int i = startValue; i <= endValue; )
{
values.add(i);
i += (step != null ? step : 1);
}
}
// Make sure the values are sorted low to high
Collections.sort(values);
mMinuteValues = new OrderedSet<>(values);
}
}
return this;
}
//---------------------------------------------------------------------------
public CronSchedule setHourSpec(String inHourSpec)
{
mHourValues = null;
if (StringUtil.isSet(inHourSpec))
{
String spec = inHourSpec.trim();
if (spec.equals("R"))
{
Random rand = new Random();
mHourValues = new OrderedSet<>(1);
// Generate a random value in range 0 to 23
mHourValues.add(rand.nextInt(24));
}
else if (! spec.equals("*"))
{
List values = new ArrayList<>(24);
String[] pieces = spec.split(",");
for (String piece : pieces)
{
int startValue;
int endValue;
int dashIdx = piece.indexOf("-");
if (dashIdx > 0)
{
startValue = Integer.parseInt(piece.substring(0, dashIdx));
endValue = Integer.parseInt(piece.substring(dashIdx + 1));
}
else
{
startValue = endValue = Integer.parseInt(piece);
}
for (int i = startValue; i <= endValue; i++)
{
if (i < 0
|| i > 23)
{
throw new CronParseException("Invalid hour value: " + StringUtil.singleQuote(i) + "! Valid value range: 0-23.");
}
values.add(i);
}
}
// Make sure the values are sorted low to high
Collections.sort(values);
mHourValues = new OrderedSet<>(values);
}
}
return this;
}
//---------------------------------------------------------------------------
public CronSchedule setDayOfMonthSpec(String inDayOfMonthSpec)
{
mDayOfMonthValues = null;
if (StringUtil.isSet(inDayOfMonthSpec))
{
String spec = inDayOfMonthSpec.trim();
if (spec.equals("R"))
{
Random rand = new Random();
mDayOfMonthValues = new OrderedSet<>(1);
// Generate a random value in range 1 to 28 (Since we don't know which month it might be used for)
mDayOfMonthValues.add(rand.nextInt(28) + 1);
}
else if (! spec.equals("*"))
{
List values = new ArrayList<>(32);
String[] pieces = spec.split(",");
for (String piece : pieces)
{
int startValue;
int endValue;
int dashIdx = piece.indexOf("-");
if (dashIdx > 0)
{
startValue = Integer.parseInt(piece.substring(0, dashIdx));
endValue = Integer.parseInt(piece.substring(dashIdx + 1));
}
else
{
startValue = endValue = Integer.parseInt(piece);
}
for (int i = startValue; i <= endValue; i++)
{
if (i < 1
|| i > 31)
{
throw new CronParseException("Invalid day of month value: " + StringUtil.singleQuote(i) + "! Valid value range: 1-31.");
}
values.add(i);
}
}
// Make sure the values are sorted low to high
Collections.sort(values);
mDayOfMonthValues = new OrderedSet<>(values);
}
}
return this;
}
//---------------------------------------------------------------------------
public CronSchedule setMonthSpec(String inMonthSpec)
{
mMonthValues = null;
if (StringUtil.isSet(inMonthSpec))
{
String spec = inMonthSpec.trim();
if (spec.equals("R"))
{
Random rand = new Random();
mMonthValues = new OrderedSet<>(1);
// Generate a random value in range 1 to 12
mMonthValues.add(rand.nextInt(12) + 1);
}
else if (! spec.equals("*"))
{
List values = new ArrayList<>(13);
String[] pieces = spec.split(",");
for (String piece : pieces)
{
int startValue;
int endValue;
int dashIdx = piece.indexOf("-");
if (dashIdx > 0)
{
startValue = Integer.parseInt(piece.substring(0, dashIdx));
endValue = Integer.parseInt(piece.substring(dashIdx + 1));
}
else
{
startValue = endValue = Integer.parseInt(piece);
}
for (int i = startValue; i <= endValue; i++)
{
if (i < 1
|| i > 12)
{
throw new CronParseException("Invalid month value: " + StringUtil.singleQuote(i) + "! Valid value range: 1-12.");
}
values.add(i);
}
}
// Make sure the values are sorted low to high
Collections.sort(values);
mMonthValues = new OrderedSet<>(values);
}
}
return this;
}
//---------------------------------------------------------------------------
public CronSchedule setDayOfWeekSpec(String inDayOfWeekSpec)
{
mDayOfWeekValues = null;
if (StringUtil.isSet(inDayOfWeekSpec))
{
String spec = inDayOfWeekSpec.trim();
if (spec.equals("R"))
{
Random rand = new Random();
mDayOfWeekValues = new OrderedSet<>(1);
// Generate a random value in range 1 to 7
mDayOfWeekValues.add(rand.nextInt(7) + 1);
}
else if (! spec.equals("*"))
{
List values = new ArrayList<>(13);
String[] pieces = spec.split(",");
for (String piece : pieces)
{
int startValue;
int endValue;
int dashIdx = piece.indexOf("-");
if (dashIdx > 0)
{
String startString = piece.substring(0, dashIdx);
if (StringUtil.isNumber(startString))
{
startValue = Integer.parseInt(startString);
endValue = Integer.parseInt(piece.substring(dashIdx + 1));
}
else
{
startValue = dayOfWeekStringToInt(startString);
endValue = dayOfWeekStringToInt(piece.substring(dashIdx + 1));
}
}
else
{
if (StringUtil.isNumber(piece))
{
startValue = endValue = Integer.parseInt(piece);
}
else
{
startValue = endValue = dayOfWeekStringToInt(piece);
}
}
int value = startValue;
do
{
if (value < 0
|| value > 7)
{
throw new CronParseException("Invalid day of week value: " + StringUtil.singleQuote(value) + "! Valid value range: 0-7 where 0 = 7 = Sunday.");
}
values.add(0 == value ? 7 : value); // LocalDateTime class SUN = 7
if (value == endValue)
{
break;
}
else if (7 == value)
{
value = 1; // Loop back around
}
else
{
value++;
}
}
while (true);
}
// Make sure the values are sorted low to high
Collections.sort(values);
mDayOfWeekValues = new OrderedSet<>(values);
}
}
return this;
}
//###########################################################################
// PRIVATE METHODS
//###########################################################################
//---------------------------------------------------------------------------
private void parse(String inCronString)
{
if (! StringUtil.isSet(inCronString))
{
throw new CronParseException("No cron string specified!");
}
try
{
String[] pieces = inCronString.trim().split("\\s+");
if (pieces.length == 6)
{
// An optional seconds field has been specified
mSecondSpec = pieces[0];
setSecondSpec(mSecondSpec);
// Remove the seconds field from the pieces array
pieces = Arrays.copyOfRange(pieces, 1, pieces.length);
}
else if (pieces.length != 5)
{
throw new CronParseException("The cron string " + StringUtil.singleQuote(inCronString) + " did not have the expected 5 cron fields!");
}
mMinuteSpec = pieces[0];
mHourSpec = pieces[1];
mDayOfMonthSpec = pieces[2];
mMonthSpec = pieces[3];
mDayOfWeekSpec = pieces[4];
setMinuteSpec(mMinuteSpec);
setHourSpec(mHourSpec);
setDayOfMonthSpec(mDayOfMonthSpec);
setMonthSpec(mMonthSpec);
setDayOfWeekSpec(mDayOfWeekSpec);
}
catch (Exception e)
{
throw new CronParseException("Problem parsing cron string " + StringUtil.singleQuote(inCronString) + "!", e);
}
}
//---------------------------------------------------------------------------
private static Random getRandom()
{
if (null == sRandom)
{
sRandom = new Random();
}
return sRandom;
}
//---------------------------------------------------------------------------
private int dayOfWeekStringToInt(String inValue)
{
String ucValue = inValue.toUpperCase();
int result = -1;
if (ucValue.startsWith("MON"))
{
result = 1;
}
else if (ucValue.startsWith("TUE"))
{
result = 2;
}
else if (ucValue.startsWith("WED"))
{
result = 3;
}
else if (ucValue.startsWith("THU"))
{
result = 4;
}
else if (ucValue.startsWith("FRI"))
{
result = 5;
}
else if (ucValue.startsWith("SAT"))
{
result = 6;
}
else if (ucValue.startsWith("SUN"))
{
result = 7;
}
else
{
throw new CronParseException(StringUtil.singleQuote(inValue) + " is not a recognized Day of Week value!");
}
return result;
}
}