/* * This program 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 2 * of the License, or (at your option) any later version. * * This program 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 this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * For information about the authors of this project Have a look * at the AUTHORS file in the root of this project. */ package net.sourceforge.fullsync.schedule; import java.util.Calendar; import java.util.StringTokenizer; import net.sourceforge.fullsync.DataParseException; import org.w3c.dom.Element; public class CrontabSchedule extends Schedule { public static final String SCHEDULE_TYPE = "crontab"; private static final long serialVersionUID = 2L; private String origPattern; private CrontabPart.Instance minutes; private CrontabPart.Instance hours; private CrontabPart.Instance daysOfMonth; private CrontabPart.Instance months; private CrontabPart.Instance daysOfWeek; private transient long lastExecution; public CrontabSchedule(final Element element) throws DataParseException { // NO_UCD String pattern = "* * * * *"; if (element.hasAttribute("pattern")) { pattern = element.getAttribute("pattern"); } read(pattern); } @Override public Element serialize(final Element element) { element.setAttribute("type", SCHEDULE_TYPE); element.setAttribute("pattern", getPattern()); return element; } public CrontabSchedule() throws DataParseException { read("* * * * *"); } public CrontabSchedule(String pattern) throws DataParseException { read(pattern); if (daysOfWeek.bArray[8]) { daysOfWeek.bArray[1] = true; } } public CrontabSchedule(CrontabPart.Instance minutes, CrontabPart.Instance hours, CrontabPart.Instance daysOfMonth, CrontabPart.Instance months, CrontabPart.Instance daysOfWeek) { this.minutes = minutes; this.hours = hours; this.daysOfMonth = daysOfMonth; this.months = months; this.daysOfWeek = daysOfWeek; if (daysOfWeek.bArray[8]) { daysOfWeek.bArray[1] = true; } StringBuilder buff = new StringBuilder(); buff.append(minutes.pattern).append(' '); buff.append(hours.pattern).append(' '); buff.append(daysOfMonth.pattern).append(' '); buff.append(months.pattern).append(' '); buff.append(daysOfWeek.pattern); origPattern = buff.toString(); } /** * Reads a crontab schedule as specified in the crontab man document: * * The time and date fields are: * field allowed values * ----- -------------- * minute 0-59 * hour 0-23 * day of month 1-31 * month 1-12 (or names, see below) * day of week 0-7 (0 or 7 is Sun, or use names) * A field may be an asterisk (*), which always stands for ``first-last''. * Ranges of numbers are allowed. Ranges are two numbers separated with a * hyphen. The specified range is inclusive. For example, 8-11 for an * 'hours' entry specifies execution at hours 8, 9, 10 and 11. * * Lists are allowed. A list is a set of numbers (or ranges) separated by * commas. Examples: '1,2,5,9', '0-4,8-12'. * * Step values can be used in conjunction with ranges. Following a range * with '/<number>' specifies skips of the number's value through the * range. For example, '0-23/2' can be used in the hours field to spec- * ify command execution every other hour (the alternative in the V7 stan- * dard is '0,2,4,6,8,10,12,14,16,18,20,22'). Steps are also permitted * after an asterisk, so if you want to say 'every two hours', just use * '* /2'. **/ private void read(String pattern) throws DataParseException { origPattern = pattern; StringTokenizer tokenizer = new StringTokenizer(pattern); minutes = CrontabPart.MINUTES.createInstance(tokenizer.nextToken()); hours = CrontabPart.HOURS.createInstance(tokenizer.nextToken()); daysOfMonth = CrontabPart.DAYSOFMONTH.createInstance(tokenizer.nextToken()); months = CrontabPart.MONTHS.createInstance(tokenizer.nextToken()); daysOfWeek = CrontabPart.DAYSOFWEEK.createInstance(tokenizer.nextToken()); } public String getPattern() { // TODO this should be generated return origPattern; } @Override public long getNextOccurrence(long now) { if (now == lastExecution) { now += 1000; } Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(now); // TODO if we have a trigger at minute 0, and hours changes, // min will go to 1 and cycle at least once gotoNextOrStay(months.bArray, cal, Calendar.MONTH); if (months.all && daysOfMonth.all && !daysOfWeek.all) { gotoNextOrStay(daysOfWeek.bArray, cal, Calendar.DAY_OF_WEEK); } else { gotoNextOrStay(daysOfMonth.bArray, cal, Calendar.DAY_OF_MONTH); // TODO currently we miss out the doublecase // !allDaysOfWeek + !allDaysOfMonth } gotoNextOrStay(hours.bArray, cal, Calendar.HOUR_OF_DAY); gotoNextOrStay(minutes.bArray, cal, Calendar.MINUTE); if ((cal.get(Calendar.SECOND) != 0) || (cal.get(Calendar.MILLISECOND) != 0)) { cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); gotoNext(minutes.bArray, cal, Calendar.MINUTE); } return cal.getTimeInMillis(); } private void gotoNextOrStay(boolean[] bArray, Calendar cal, int field) { if (!bArray[cal.get(field)]) { gotoNext(bArray, cal, field); } } private void gotoNext(boolean[] bArray, Calendar cal, int field) { // FIXME we assume that there is a true in the array, // but we should avoid a deadloop anyways. int orig = cal.get(field); int now = orig + 1; int max = cal.getActualMaximum(field); int min = cal.getActualMinimum(field); while ((now > max) || !bArray[now]) { now++; if (now > max) { switch (field) { case Calendar.MONTH: cal.add(Calendar.YEAR, 1); break; case Calendar.DAY_OF_MONTH: gotoNext(months.bArray, cal, Calendar.MONTH); break; case Calendar.DAY_OF_WEEK: cal.add(Calendar.DAY_OF_MONTH, now - orig); // TODO we ignore a formal gotoNext(month) // as dayOfWeek is only available if all // months are allowed orig = now; break; case Calendar.HOUR_OF_DAY: if (months.all && daysOfMonth.all && !daysOfWeek.all) { gotoNext(daysOfWeek.bArray, cal, Calendar.DAY_OF_WEEK); } else if (!daysOfMonth.all) { gotoNext(daysOfMonth.bArray, cal, Calendar.DAY_OF_MONTH); } else { gotoNext(daysOfMonth.bArray, cal, Calendar.DAY_OF_MONTH); } // TODO currently we miss out the doublecase // !allDaysOfWeek + !allDaysOfMonth break; case Calendar.MINUTE: gotoNext(hours.bArray, cal, Calendar.HOUR_OF_DAY); break; } now = min; } } if (now != orig) { switch (field) { case Calendar.MONTH: cal.set(Calendar.DAY_OF_MONTH, 1); case Calendar.DAY_OF_MONTH: case Calendar.DAY_OF_WEEK: cal.set(Calendar.HOUR_OF_DAY, 0); case Calendar.HOUR_OF_DAY: cal.set(Calendar.MINUTE, 0); case Calendar.MINUTE: cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); } } cal.set(field, now); } @Override public void setLastOccurrence(long now) { lastExecution = now; } public CrontabPart.Instance[] getParts() { return new CrontabPart.Instance[] { minutes, hours, daysOfMonth, months, daysOfWeek }; } @Override public String toString() { return "Crontab: " + origPattern; } }