/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.cron; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; //~--- classes ---------------------------------------------------------------- /** * A CRON entry. * * */ public class CronEntry implements Delayed { private static final Logger logger = LoggerFactory.getLogger(CronService.class.getName()); //~--- fields --------------------------------------------------------- private CronField days = null; private CronField dow = null; private CronField hours = null; private CronField minutes = null; private CronField months = null; private CronField seconds = null; private String name = null; //~--- constructors --------------------------------------------------- private CronEntry(String name) { this.name = name; } //~--- methods -------------------------------------------------------- @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(seconds.toString()); buf.append(" "); buf.append(minutes.toString()); buf.append(" "); buf.append(hours.toString()); buf.append(" "); buf.append(days.toString()); buf.append(" "); buf.append(months.toString()); buf.append(" "); buf.append(dow.toString()); buf.append(" "); return buf.toString(); } // ----- static methods ----- public static CronEntry parse(String task, String expression) { String[] fields = expression.split("[ \\t]+"); if (fields.length == CronService.NUM_FIELDS) { CronEntry cronEntry = new CronEntry(task); String secondsField = fields[0]; String minutesField = fields[1]; String hoursField = fields[2]; String daysField = fields[3]; String monthsField = fields[4]; String weeksField = fields[5]; try { CronField seconds = parseField(secondsField, 0, 59); cronEntry.setSeconds(seconds); } catch (Throwable t) { logger.warn("Invalid cron expression for task {}, field 'seconds': {}", new Object[] { task, t.getMessage() }); } try { CronField minutes = parseField(minutesField, 0, 59); cronEntry.setMinutes(minutes); } catch (Throwable t) { logger.warn("Invalid cron expression for task {}, field 'minutes': {}", new Object[] { task, t.getMessage() }); } try { CronField hours = parseField(hoursField, 0, 23); cronEntry.setHours(hours); } catch (Throwable t) { logger.warn("Invalid cron expression for task {}, field 'hours': {}", new Object[] { task, t.getMessage() }); } try { CronField days = parseField(daysField, 1, 31); cronEntry.setDays(days); } catch (Throwable t) { logger.warn("Invalid cron expression for task {}, field 'days': {}", new Object[] { task, t.getMessage() }); } try { CronField weeks = parseField(weeksField, 0, 6); cronEntry.setWeeks(weeks); } catch (Throwable t) { logger.warn("Invalid cron expression for task {}, field 'weeks': {}", new Object[] { task, t.getMessage() }); } try { CronField months = parseField(monthsField, 1, 12); cronEntry.setMonths(months); } catch (Throwable t) { logger.warn("Invalid cron expression for task {}, field 'months': {}", new Object[] { task, t.getMessage() }); } return cronEntry; } else { logger.warn("Invalid cron expression for task {}: invalid number of fields (must be {}).", new Object[] { task, CronService.NUM_FIELDS }); } return null; } // ----- private methods ----- private static CronField parseField(String field, int minValue, int maxValue) { // asterisk: * if ("*".equals(field)) { return new CronField(minValue, maxValue, 1, true); } // asterisk with step: */3 if (field.startsWith("*/")) { int step = Integer.parseInt(field.substring(2)); if(step > 0 & step <= maxValue) { return new CronField(minValue, maxValue, step); } else { throw new IllegalArgumentException("Illegal step: '" + step + "'"); } } // simple number: 2 if (field.matches("[0-9]{1,2}")) { int value = Integer.parseInt(field); if ((value >= minValue) && (value <= maxValue)) { return new CronField(value, value, 1); } else { throw new IllegalArgumentException("Parameter not within range: '" + field + "'"); } } // range: 4-6 if (field.matches("[0-9]{1,2}-[0-9]{1,2}")) { String[] rangeValues = field.split("[-]+"); if (rangeValues.length == 2) { int start = Integer.parseInt(rangeValues[0]); int end = Integer.parseInt(rangeValues[1]); if ((start >= minValue) && (start <= maxValue) && (end >= minValue) && (end <= maxValue)) { return new CronField(start, end, 1); } else { throw new IllegalArgumentException("Parameters not within range: '" + field + "'"); } } else { throw new IllegalArgumentException("Invalid range: '" + field + "'"); } } // list: 4,6 if (field.contains(",")) { final String[] listValues = field.split("[,]+"); final List<Integer> values = new LinkedList<>(); for (final String value : listValues) { try { values.add(Integer.parseInt(value)); } catch (Throwable t) { throw new IllegalArgumentException("Invalid list value: '" + value + "'"); } } return new CronField(values); } // range with step: 4-6/3 if (field.matches("[0-9]{1,2}-[0-9]{1,2}/[0-9]{1,2}")) { throw new UnsupportedOperationException("Steps are not supported yet."); /* * String[] rangeValues = field.split("[-]{1}"); * if(rangeValues.length == 2) { * * int start = Integer.parseInt(rangeValues[0]); * String[] stepValues = rangeValues[1].split("[/]{1}"); * * if(stepValues.length == 2) { * * int end = Integer.parseInt(stepValues[0]); * int step = Integer.parseInt(stepValues[1]); * * if(step > 0 && step <= maxValue) { * if(start >= minValue && start <= maxValue && end >= minValue && end <= maxValue) { * * * return new CronField(start, end, step); * * } else { * * throw new IllegalArgumentException("Parameters not within range: '" + field + "'"); * } * * } else { * * throw new IllegalArgumentException("Illegal step: '" + step + "'"); * } * * } else { * * throw new IllegalArgumentException("Invalid step: '" + field + "'"); * } * } else { * * throw new IllegalArgumentException("Invalid range: '" + field + "'"); * } */ } throw new IllegalArgumentException("Invalid field: '" + field + "'"); } @Override public int compareTo(Delayed o) { Long myDelay = getDelay(TimeUnit.MILLISECONDS); Long oDelay = o.getDelay(TimeUnit.MILLISECONDS); return myDelay.compareTo(oDelay); } //~--- get methods ---------------------------------------------------- public long getDelayToNextExecutionInMillis() { Calendar now = GregorianCalendar.getInstance(); int nowSeconds = now.get(Calendar.SECOND); int nowMinutes = now.get(Calendar.MINUTE); int nowHours = now.get(Calendar.HOUR_OF_DAY); int nowDays = now.get(Calendar.DAY_OF_MONTH); // DAY_OF_MONTH starts with 1 int nowDow = now.get(Calendar.DAY_OF_WEEK) - 1; // DAY_OF_WEEK starts with 1 (sunday) int nowMonths = now.get(Calendar.MONTH) + 1; // MONTH starts with 0 (why???) boolean modified = true; int maxTries = 10000; int numTries = 0; while(modified && numTries++ < maxTries) { modified = false; if(!modified && !seconds.isInside(nowSeconds)) { now.add(Calendar.SECOND, 1); modified = true; } if(!modified && !minutes.isInside(nowMinutes)) { now.add(Calendar.MINUTE, 1); modified = true; } if(!modified && !hours.isInside(nowHours)) { now.add(Calendar.HOUR_OF_DAY, 1); modified = true; } // exclude day of week and day from each other (both can match) if(!dow.isIsWildcard() && !days.isIsWildcard()) { if(!modified && !(dow.isInside(nowDow) || days.isInside(nowDays))) { now.add(Calendar.DAY_OF_MONTH, 1); modified = true; } } else if(!dow.isIsWildcard()) { if(!modified && !dow.isInside(nowDow)) { now.add(Calendar.DAY_OF_MONTH, 1); modified = true; } } else if(!days.isIsWildcard()) { if(!modified && !days.isInside(nowDays)) { now.add(Calendar.DAY_OF_MONTH, 1); modified = true; } } if(!modified && !months.isInside(nowMonths)) { now.add(Calendar.MONTH, 1); modified = true; } nowSeconds = now.get(Calendar.SECOND); nowMinutes = now.get(Calendar.MINUTE); nowHours = now.get(Calendar.HOUR_OF_DAY); nowDays = now.get(Calendar.DAY_OF_MONTH); // DAY_OF_MONTH starts with 1 nowDow = now.get(Calendar.DAY_OF_WEEK) - 1; // DAY_OF_WEEK starts with 1 (sunday) nowMonths = now.get(Calendar.MONTH) + 1; // MONTH starts with 0 (why???) } if(numTries == maxTries) { throw new IllegalArgumentException("Unable to determine next cron date for task " + name + ", aborting."); } return now.getTimeInMillis() - System.currentTimeMillis(); } public CronField getSeconds() { return seconds; } public CronField getMinutes() { return minutes; } public CronField getHours() { return hours; } public CronField getDays() { return days; } public CronField getWeeks() { return dow; } public CronField getMonths() { return months; } public String getName() { return name; } // ----- interface Delayed ----- @Override public long getDelay(TimeUnit unit) { long next = TimeUnit.MILLISECONDS.convert(getDelayToNextExecutionInMillis(), unit); logger.info("{} ms until start of task {}", new Object[] { next, name }); return next; } //~--- set methods ---------------------------------------------------- public void setSeconds(CronField seconds) { this.seconds = seconds; } public void setMinutes(CronField minutes) { this.minutes = minutes; } public void setHours(CronField hours) { this.hours = hours; } public void setDays(CronField days) { this.days = days; } public void setWeeks(CronField weeks) { this.dow = weeks; } public void setMonths(CronField months) { this.months = months; } public void setName(String name) { this.name = name; } }