All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.spikeify.cron.entities.CronJob Maven / Gradle / Ivy

package com.spikeify.cron.entities;

import com.spikeify.annotations.Generation;
import com.spikeify.annotations.Indexed;
import com.spikeify.annotations.UserKey;
import com.spikeify.cron.entities.enums.CronJobResult;
import com.spikeify.cron.entities.enums.RunEvery;
import com.spikeify.cron.utils.Assert;
import com.spikeify.cron.utils.DateTimeUtils;
import com.spikeify.cron.utils.StringUtils;
import com.spikeify.cron.utils.UrlUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Objects;
import java.util.TimeZone;

/**
 * Entity in database holding information about cron task
 */
public class CronJob {

	private static final Logger log = LoggerFactory.getLogger(CronJob.class);

	private static final long ONE_DAY_IN_MILLISECONDS = 24L * 60L * 60L * 1000L;

	// simple measure to ease filtering out enabled and disabled jobs until they are run for the first time
	private static final long RUN_DISABLED = -1L;

	private static final long RUN_ENABLED = 1L;

	/**
	 * Uniquely generated id
	 */
	@UserKey(generate = true)
	protected String id;

	@Generation
	protected int generation;

	/**
	 * description of cron job - should be unique
	 */
	@Indexed
	protected String name;

	/*
	 * time stamp cron job was last modified (schedule)
	 */
	protected long lastModified;

	/**
	 * target URL to call: GET http://some/url
	 */
	protected String target;

	/**
	 * Time stamp when job should run for the first time
	 * null = as soon as possible
	 */
	protected Long firstRun;

	/**
	 * Time stamp of last run
	 * if null no run has been performed - start as soon as possible
	 */
	protected Long lastRun;

	/**
	 * Time job was started ... to prevent other thread starting the same cron job
	 */
	protected Long startTime;

	/**
	 * Last execution result
	 */
	protected CronJobResult lastResult;

	/**
	 * Last run massage if any
	 */
	protected String lastMessage;

	/**
	 * Time job should run next
	 * if nextRun is lower than current time
	 * - is calculated when job is run ...
	 * - to be available on next run
	 * -1 - default / disabled ... until interval is set
	 */
	@Indexed
	protected long nextRun;


	/**
	 * SCHEDULE definition
	 */

	/**
	 * run every, every second, every five (minute, hour, day ...)
	 * 0 = don't run
	 */
	protected int interval;

	/**
	 * run every minute, hour, day
	 */
	protected RunEvery intervalUnit;

	/**
	 * run from hour/minute in current day, null - don't care
	 */
	protected Integer runFromHour;

	protected Integer runFromMinute;

	/**
	 * run to hour/minute in current day, null - don't care
	 * run to can be lower than run from to enable running for instance from: 23:20 until: 1:20
	 */
	protected Integer runToHour;

	protected Integer runToMinute;

	protected CronJob() {
		// Aerospike only
	}

	public CronJob(String jobName) {

		Assert.notNullOrEmptyTrimmed(jobName, "Missing job name!");

		name = jobName.trim();
		nextRun = RUN_ENABLED;
		lastModified = 0;
		startTime = null;
	}

	public void setTarget(String newTarget) {

		Assert.notNullOrEmptyTrimmed(newTarget, "Missing target!");

		try {
			new URI(newTarget); // check if correct
			target = newTarget.trim();

			calculateNextRun();
		}
		catch (URISyntaxException e) {
			log.error("Invalid target URI: " + newTarget, e);
			throw new IllegalArgumentException("Invalid target URI: " + newTarget);
		}
	}

	public boolean run() {

		// not started ... and can be run
		return (canRun() && startTime == null && nextRun <= getTime());
	}


	public boolean canRun() {

		return !(isDisabled() || target == null || intervalUnit == null);

	}

	public String getTarget(String rootUrl) {

		if (StringUtils.isNullOrEmpty(rootUrl)) {
			return target;
		}

		// build url from root url and target
		return UrlUtils.getFullUrl(rootUrl, target);
	}

	public void disable() {

		if (!isDisabled()) {
			disableNextRun();
		}
	}

	public void enable() {

		if (isDisabled()) {
			nextRun = RUN_ENABLED;
			calculateNextRun();
		}
	}

	public boolean isDisabled() {

		return nextRun == RUN_DISABLED;
	}

	public String getId() {

		return id;
	}

	public String getName() {

		return name;
	}

	public Long getFirstRun() {

		return firstRun;
	}

	public Long getLastRun() {

		return lastRun;
	}

	public Long getLastRun(int timezone) {

		if (lastRun == null) {
			return null;
		}

		return DateTimeUtils.getTimezoneTime(lastRun, timezone);
	}

	public CronJobResult getLastResult() {

		return lastResult;
	}

	public String getLastResultMessage() {

		return lastMessage;
	}

	public long getNextRun() {

		return nextRun;
	}

	public long getNextRun(int timeZone) {

		if (isDisabled()) {
			return nextRun;
		}

		return DateTimeUtils.getTimezoneTime(nextRun, timeZone);
	}

	public int getInterval() {

		return interval;
	}

	public RunEvery getIntervalUnit() {

		return intervalUnit;
	}

	public Integer getRunFromHour() {

		return runFromHour;
	}

	public Integer getRunFromHour(int timeZone) {

		return runFromHour != null ? DateTimeUtils.getTimezoneHour(runFromHour, timeZone) : null;
	}

	public Integer getRunFromMinute() {

		return runFromMinute;
	}

	public Integer getRunToHour() {

		return runToHour;
	}

	public Integer getRunToHour(int timeZone) {

		return runToHour != null ? DateTimeUtils.getTimezoneHour(runToHour, timeZone) : null;
	}

	public Integer getRunToMinute() {

		return runToMinute;
	}

	public void setFirstRun(long startTime) {

		Assert.isTrue(startTime >= 0, "Start time must be >= 0!");

		if (startTime < getTime()) {
			firstRun = null;
		}
		else {
			firstRun = startTime;
		}

		// clear last run
		lastRun = null;

		calculateNextRun();
	}

	public void setLastRun(long runTime, CronJobResult result, String message) {

		Assert.isTrue(runTime <= getTime(), "Last run time can't be in the future!");
		Assert.notNull(result, "Missing cron job result!");

		lastRun = runTime;
		lastResult = result;
		lastMessage = message != null ? message.trim() : null;
		startTime = null; // unlock

		calculateNextRun();
	}

	public void setRunInterval(int number, RunEvery unit) {

		Assert.isTrue(number > 0, "Interval must be > 0, but was: " + number + "!");
		Assert.notNull(unit, "Missing interval unit!");

		interval = number;
		intervalUnit = unit;

		calculateNextRun();
	}

	public void runExactlyAt(int hour, int minute) {

		checkHour(hour);
		checkMinutes(minute);

		interval = 1;
		intervalUnit = RunEvery.day;
		runFromHour = hour;
		runFromMinute = minute;

		runToHour = null;
		runToMinute = null;

		calculateNextRun();
	}

	public void runFromTo(int fromHour, int fromMinute, int toHour, int toMinute) {

		checkHour(fromHour);
		checkMinutes(fromMinute);

		checkHour(toHour);
		checkMinutes(toMinute);

		runFromHour = fromHour;
		runFromMinute = fromMinute;

		runToHour = toHour;
		runToMinute = toMinute;

		calculateNextRun();
	}

	public void clearRunFromTo() {

		runFromHour = null;
		runFromMinute = null;
		runToHour = null;
		runToMinute = null;

		calculateNextRun();
	}

	private void checkMinutes(int fromMinute) {

		Assert.isTrue(fromMinute >= 0 && fromMinute < 60, "Expected minute: 0 - 59, but was: " + fromMinute);
	}

	private void checkHour(int fromHour) {

		Assert.isTrue(fromHour >= 0 && fromHour < 24, "Expected hour: 0 - 23, but was: " + fromHour);
	}

	private void disableNextRun() {

		if (nextRun >= RUN_ENABLED) {
			nextRun = RUN_DISABLED;
		}
	}

	public void setStarted(long time) {

		startTime = time;
	}

	public long getStartedTime() {

		if (startTime == null) {
			return 0;
		}
		return startTime;
	}

	public boolean isStarted() {

		return startTime != null;
	}

	@Override
	public String toString() {

		return id + " [" + name + "] " + getDescription(true, 0);
	}

	public String getDescription(boolean withTarget, int timeZone) {

		StringBuilder builder = new StringBuilder();

		if (firstRun != null && firstRun > getTime()) {

			builder.append("first run: ");
			builder.append(formatDateTime(firstRun, timeZone));
		}

		if (intervalUnit == null) {
			if (builder.length() > 0) {
				builder.append(" ");
			}

			builder.append("- no schedule defined (job will not run)!");
			return builder.toString();
		}

		if (builder.length() > 0) {
			builder.append(", ");
		}

		builder.append("runs every ");

		int lastDigit = interval % 10;

		if (lastDigit != 1) {
			builder.append(interval);
		}

		switch (lastDigit) {
			case 1:
				break;

			case 2:
				builder.append("nd ");
				break;

			case 3:
				builder.append("rd ");
				break;

			default:
				builder.append("th ");
				break;
		}

		builder.append(intervalUnit.name());

		if (runFromHour != null) {

			if (runToHour != null) {
				builder.append(", from: ");
			}
			else {
				builder.append(" at: ");
			}
			builder.append(getRunTimeFormatted(runFromHour, runFromMinute, timeZone));

			if (runToHour != null) {
				builder.append(", until: ");
				builder.append(getRunTimeFormatted(runToHour, runToMinute, timeZone));
			}
		}

		if (withTarget) {
			if (target == null || target.trim().length() == 0) {
				builder.append(", missing target (job will not run)");
			}
			else {
				builder.append(", target: ").append(target);
			}
		}

		if (isDisabled()) {
			builder.append(", next run: disabled");
		}
		else if (nextRun >= getTime()) {
			builder.append(", next run: ").append(formatDateTime(nextRun, timeZone));
		}

		return builder.toString();
	}

	private String getRunTimeFormatted(Integer hour, Integer minute, int timezone) {

		if (hour == null || minute == null) {
			return "";
		}

		hour = DateTimeUtils.getTimezoneHour(hour, timezone);

		String out = (hour < 10) ? "0" + hour : "" + hour;
		out = out + ":";
		return (minute < 10) ? out + "0" + minute : out + minute;
	}

	private String formatDateTime(long time, int timezone) {

		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
		format.setCalendar(getCalendar(timezone));
		return DateTimeUtils.format(time, format);
	}

	/**
	 * Calculates when job should run next time
	 */
	protected void calculateNextRun() {

		lastModified = System.currentTimeMillis();

		if (isDisabled()) {
			return; // nothing to calculate
		}

		long currentTime = getTime();

		long start = currentTime;
		if (firstRun != null && firstRun >= currentTime) {
			start = firstRun;
		}

		if (lastRun != null &&
			(firstRun == null || firstRun < lastRun) &&
			lastRun <= currentTime) {
			start = lastRun;
			// calculate next run ...
			start = getNextRunFor(intervalUnit, interval, start);
		}

		// check if from / to limits are met
		Calendar calendar = getCalendar(0);
		calendar.setTimeInMillis(start);
		int startHour = calendar.get(Calendar.HOUR_OF_DAY);
		int startMinute = calendar.get(Calendar.MINUTE);

		if (hasRunFrom() &&
			runFromBeforeRunTo() &&
			isBefore(startHour, startMinute, runFromHour, runFromMinute)) {

			// start at run from hour:minute
			calendar.setTimeInMillis(start);
			calendar.set(Calendar.HOUR_OF_DAY, runFromHour);
			calendar.set(Calendar.MINUTE, runFromMinute);
			start = calendar.getTimeInMillis();

			// recalculate
			startHour = calendar.get(Calendar.HOUR_OF_DAY);
			startMinute = calendar.get(Calendar.MINUTE);
		}

		if (hasRunTo() &&
			runFromBeforeRunTo() &&
			isAfter(startHour, startMinute, runToHour, runToMinute)) {

			// + ONE DAY
			calendar.setTimeInMillis(start + ONE_DAY_IN_MILLISECONDS);
			calendar.set(Calendar.HOUR_OF_DAY, runFromHour);
			calendar.set(Calendar.MINUTE, runFromMinute);
			start = calendar.getTimeInMillis();
		}


		// opposite ... run from is after run to
		if (hasRunTo() &&
			!runFromBeforeRunTo() &&
			isAfter(startHour, startMinute, runToHour, runToMinute) &&
			isBefore(startHour, startMinute, runFromHour, runFromMinute)) {

			// start at run to hour:minute
			calendar.setTimeInMillis(start);
			calendar.set(Calendar.HOUR_OF_DAY, runFromHour);
			calendar.set(Calendar.MINUTE, runFromMinute);
			start = calendar.getTimeInMillis();
		}

		nextRun = start;
	}

	private boolean isBefore(int hour, int minute, int compareHour, int compareMinute) {

		return hour < compareHour || hour == compareHour && minute <= compareMinute;
	}

	private boolean isAfter(int hour, int minute, int compareHour, int compareMinute) {

		return hour > compareHour || hour == compareHour && minute >= compareMinute;
	}

	private boolean hasRunFrom() {

		return runFromMinute != null && runFromHour != null;
	}

	private boolean hasRunTo() {

		return runToMinute != null && runToHour != null;
	}

	private boolean runFromBeforeRunTo() {

		return hasRunFrom() && (!hasRunTo() || isBefore(runFromHour, runFromMinute, runToHour, runToMinute));
	}

	/**
	 * needed for time simulation in unit tests
	 *
	 * @return current system time (for test mocking purposes only)
	 */
	protected long getTime() {

		return System.currentTimeMillis();
	}

	public boolean isOlder(long timeStamp) {

		return timeStamp == 0 ||
			lastModified <= timeStamp;
	}

	private long getNextRunFor(RunEvery interval, long intervalUnits, long start) {

		// calculate next interval
		switch (interval) {
			case minute:
				start = start + (60L * 1000L) * intervalUnits;
				break;

			case hour:
				start = start + (60L * 60L * 1000L) * intervalUnits;
				break;

			case day:
				start = start + ONE_DAY_IN_MILLISECONDS * intervalUnits;
				break;

			case week:
				start = start + (ONE_DAY_IN_MILLISECONDS * 7L * intervalUnits);
				break;
		}

		// new calculated time is in the past ...
		if (start < getTime()) {
			return getTime();
		}

		return start;
	}

	private Calendar getCalendar(int timezone) {

		TimeZone zone = TimeZone.getTimeZone("UTC");
		zone.setRawOffset(60 * 60 * 1000 * timezone);

		return Calendar.getInstance(zone);
	}

	@Override
	public boolean equals(Object o) {

		if (o == this) { return true; }

		if (!(o instanceof CronJob)) { return false; }

		CronJob compare = (CronJob) o;
		return StringUtils.equals(compare.name, name) &&
			StringUtils.equals(compare.target, target) &&
			Objects.equals(compare.firstRun, firstRun) &&
			Objects.equals(compare.runFromHour, runFromHour) &&
			Objects.equals(compare.runFromMinute, runFromMinute) &&
			Objects.equals(compare.runToHour, runToHour) &&
			Objects.equals(compare.runToMinute, runToMinute);
	}

	@Override
	public int hashCode() {

		return getDescription(true, 0).hashCode();
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy