/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.core.scheduler; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.StringTokenizer; import java.util.TimeZone; import org.apache.commons.lang.StringUtils; import org.eclipse.smarthome.core.scheduler.AbstractExpressionPart.BoundedIntegerSet; import org.eclipse.smarthome.core.scheduler.CronExpression.CronExpressionPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>CronExpression</code> is an implementation of {@link Expression} that provides a parser and evaluator for for * unix-like cron expressions that are compatible wit the Quartz (https://quartz-scheduler.org/, see * http://www.quartz-scheduler.org/api/2.2.1/org/quartz/CronExpression.html) framework. Cron * expressions provide the ability to specify complex time combinations such as * "At 8:00am every Monday through Friday" or "At 1:30am every * last Friday of the month". * * @author Karel Goderis - Initial contribution * */ public final class CronExpression extends AbstractExpression<CronExpressionPart> { private final Logger logger = LoggerFactory.getLogger(CronExpression.class); public enum Month { JANUARY("JAN", Calendar.JANUARY, 31), FEBRUARY("FEB", Calendar.FEBRUARY, 28) { @Override public int getNumberOfDays(int year) { if (((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { return 29; } else { return 28; } }; }, MARCH("MAR", Calendar.MARCH, 31), APRIL("APR", Calendar.APRIL, 30), MAY("MAY", Calendar.MAY, 31), JUNE("JUN", Calendar.JUNE, 30), JULY("JUL", Calendar.JULY, 31), AUGUST("AUG", Calendar.AUGUST, 31), SEPTEMBER("SEP", Calendar.SEPTEMBER, 30), OCTOBER("OCT", Calendar.OCTOBER, 31), NOVEMBER("NOV", Calendar.NOVEMBER, 30), DECEMBER("DEC", Calendar.DECEMBER, 31); private final String identifier; private final int calendarMonth; private final int numberOfDays; public static Month getMonth(final int calendar) { return Month.values()[calendar]; } public static Month getMonth(final String id) { for (Month aMonth : Month.values()) { if (aMonth.toString().equals(id)) { return aMonth; } } throw new IllegalArgumentException("invalid calendar value " + id); } private Month(final String code, final int month, final int numberOfDays) { this.identifier = code; this.calendarMonth = month; this.numberOfDays = numberOfDays; } public int getNumberOfDays(int year) { return numberOfDays; } public int getCalendarMonth() { return calendarMonth; } @Override public String toString() { return identifier; } }; public enum WeekDay { SUNDAY("SUN", Calendar.SUNDAY), MONDAY("MON", Calendar.MONDAY), TUESDAY("TUE", Calendar.TUESDAY), WEDNESDAY("WED", Calendar.WEDNESDAY), THURSDAY("THU", Calendar.THURSDAY), FRIDAY("FRI", Calendar.FRIDAY), SATURDAY("SAT", Calendar.SATURDAY); private final String identifier; private final int calendarDay; public static WeekDay getWeekDay(final int calendar) { return WeekDay.values()[calendar]; } public static WeekDay getWeekDay(final String id) { for (WeekDay aDay : WeekDay.values()) { if (aDay.toString().equals(id)) { return aDay; } } throw new IllegalArgumentException("Invalid calendar value " + id); } private WeekDay(final String code, final int day) { this.identifier = code; this.calendarDay = day; } public int getCalendarDay() { return calendarDay; } @Override public String toString() { return identifier; } }; /** * Constructs a new <code>CronExpression</code> based on the specified * parameter. * * @param expression string representation of the cron expression the new object should represent. * @throws ParseException if the string expression cannot be parsed into a valid <code>CronExpression</code>. */ public CronExpression(final String expression) throws ParseException { this(expression, Calendar.getInstance().getTime(), TimeZone.getDefault()); } /** * Constructs a new <code>CronExpression</code> based on the specified * parameter. * * @param expression string representation of the cron expression the new object should represent. * @param startTime the start time to consider for the cron expression. * @throws ParseException if the string expression cannot be parsed into a valid <code>CronExpression</code>. */ public CronExpression(final String expression, final Date startTime) throws ParseException { this(expression, startTime, TimeZone.getDefault()); } /** * Constructs a new <code>CronExpression</code> based on the specified * parameter. * * @param expression string representation of the cron expression the new object should represent * @param startTime the start time to consider for the cron expression. * @param zone the timezone for which this expression will be resolved. * @throws ParseException if the string expression cannot be parsed into a valid <code>CronExpression</code>. */ public CronExpression(final String expression, final Date startTime, final TimeZone zone) throws ParseException { super(expression, " \t", startTime, zone, 0, 2); } @Override public void setStartDate(Date startDate) throws IllegalArgumentException, ParseException { if (startDate == null) { throw new IllegalArgumentException("The start date of the rule can not be null"); } // We set the real start date to the next second; milliseconds are not supported by cron expressions anyways Calendar calendar = Calendar.getInstance(getTimeZone()); calendar.setTime(startDate); if (calendar.get(Calendar.MILLISECOND) != 0) { calendar.add(Calendar.SECOND, 1); calendar.set(Calendar.MILLISECOND, 0); } super.setStartDate(calendar.getTime()); } @Override public boolean isSatisfiedBy(Date date) { Calendar testDateCal = Calendar.getInstance(getTimeZone()); testDateCal.setTime(date); testDateCal.set(Calendar.MILLISECOND, 0); Date originalDate = testDateCal.getTime(); testDateCal.add(Calendar.SECOND, -1); Date timeAfter = getTimeAfter(testDateCal.getTime()); return ((timeAfter != null) && (timeAfter.equals(originalDate))); } /** * Indicates whether the specified expression can be parsed into a * valid expression * * @param expression the expression to evaluate * @return a boolean indicating whether the given expression is a valid cron expression */ public static boolean isValidExpression(String cronExpression) { try { new CronExpression(cronExpression); } catch (ParseException pe) { return false; } return true; } @Override protected void validateExpression() throws IllegalArgumentException { DayOfMonthExpressionPart domPart = (DayOfMonthExpressionPart) this .getExpressionPart(DayOfMonthExpressionPart.class); DayOfWeekExpressionPart dowPart = (DayOfWeekExpressionPart) this .getExpressionPart(DayOfWeekExpressionPart.class); if (domPart.isNotSpecific() && dowPart.isNotSpecific()) { throw new IllegalArgumentException( "The DayOfMonth and DayOfWeek rule parts CAN NOT be not specific at the same time."); } } @Override protected void populateWithSeeds() { YearsExpressionPart thePart = null; for (ExpressionPart part : getExpressionParts()) { if (part instanceof YearsExpressionPart) { thePart = (YearsExpressionPart) part; break; } } YearsExpressionPart yep = null; try { yep = new YearsExpressionPart(""); } catch (ParseException e) { logger.error("An exception occurred while creating an expression part : '{}'", e.getMessage()); return; } if (thePart == null) { BoundedIntegerSet set = yep.getValueSet(); Calendar cal = Calendar.getInstance(getTimeZone()); cal.setTime(getStartDate()); int currentYear = cal.get(Calendar.YEAR); for (int i = 0; i < 10; i++) { set.add(currentYear++); } yep.setValueSet(set); getExpressionParts().add(yep); } else { BoundedIntegerSet set = thePart.getValueSet(); int maxYear = set.last(); for (int i = 0; i < 10; i++) { if (maxYear < YearsExpressionPart.MAX_YEAR) { set.add(maxYear++); } } } } @Override protected CronExpressionPart parseToken(String token, int position) throws ParseException { switch (position) { case 1: return new SecondsExpressionPart(token); case 2: return new MinutesExpressionPart(token); case 3: return new HoursExpressionPart(token); case 4: return new DayOfMonthExpressionPart(token); case 5: return new MonthsExpressionPart(token); case 6: return new DayOfWeekExpressionPart(token); case 7: return new YearsExpressionPart(token); default: return null; } } protected abstract class CronExpressionPart extends AbstractExpressionPart { public CronExpressionPart(String s) throws ParseException { super(s); } @Override public final void parse() throws ParseException { setValueSet(initializeValueSet()); StringTokenizer valueTokenizer = new StringTokenizer(getPart(), ","); while (valueTokenizer.hasMoreTokens()) { String v = valueTokenizer.nextToken(); parseToken(v); } } abstract String getSpecialToken(String token); abstract void parseToken(String v) throws ParseException; } protected class SecondsExpressionPart extends CronExpressionPart { protected static final int MIN_SECOND = 0; protected static final int MAX_SECOND = 59; public SecondsExpressionPart(String s) throws ParseException { super(s); parse(); } @Override public int order() { return 7; } @Override String getSpecialToken(String token) { if (token.equals("*")) { return token; } if (token.contains("-")) { return "-"; } if (token.contains("/")) { return "/"; } return ""; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_SECOND, MAX_SECOND, false, false); } @Override void parseToken(String v) throws ParseException { switch (getSpecialToken(v)) { case "-": { String from = StringUtils.substringBefore(v, "-"); String to = StringUtils.substringAfter(v, "-"); getValueSet().add(Integer.parseInt(from), Integer.parseInt(to), 1); break; } case "/": { String from = StringUtils.substringBefore(v, "/"); String increment = StringUtils.substringAfter(v, "/"); try { if (Integer.parseInt(increment) > MAX_SECOND) { throw new ParseException("Increment is too large", 0); } } catch (Exception e) { throw new ParseException("Increment '" + v + "' is not a valid value", 0); } int fromValue = from.equals("*") ? 0 : Integer.parseInt(from); getValueSet().add(fromValue, MAX_SECOND, Integer.parseInt(increment)); break; } case "*": { getValueSet().add(MIN_SECOND, MAX_SECOND, 1); break; } default: { try { getValueSet().add(Integer.parseInt(v)); } catch (Exception e) { throw new ParseException("'" + v + "' is not a valid token", 0); } break; } } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { final Calendar cal = Calendar.getInstance(getTimeZone()); List<Date> newCandidates = new ArrayList<Date>(); List<Date> oldCandidates = new ArrayList<Date>(); if (candidates.isEmpty()) { candidates.add(startDate); } oldCandidates.addAll(candidates); for (Date date : candidates) { for (Integer element : getValueSet()) { cal.setTime(date); cal.set(Calendar.SECOND, element); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); return candidates; } } protected class MinutesExpressionPart extends CronExpressionPart { protected static final int MIN_MINUTE = 0; protected static final int MAX_MINUTE = 59; public MinutesExpressionPart(String s) throws ParseException { super(s); } @Override public int order() { return 6; } @Override String getSpecialToken(String token) { if (token.equals("*")) { return token; } if (token.contains("-")) { return "-"; } if (token.contains("/")) { return "/"; } return ""; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_MINUTE, MAX_MINUTE, false, false); } @Override void parseToken(String v) throws ParseException { switch (getSpecialToken(v)) { case "-": { String from = StringUtils.substringBefore(v, "-"); String to = StringUtils.substringAfter(v, "-"); getValueSet().add(Integer.parseInt(from), Integer.parseInt(to), 1); break; } case "/": { String from = StringUtils.substringBefore(v, "/"); String increment = StringUtils.substringAfter(v, "/"); try { if (Integer.parseInt(increment) > MAX_MINUTE) { throw new ParseException("Increment is too large", 0); } } catch (Exception e) { throw new ParseException("Increment '" + v + "' is not a valid value", 0); } int fromValue = from.equals("*") ? 0 : Integer.parseInt(from); getValueSet().add(fromValue, MAX_MINUTE, Integer.parseInt(increment)); break; } case "*": { getValueSet().add(MIN_MINUTE, MAX_MINUTE, 1); break; } default: { try { getValueSet().add(Integer.parseInt(v)); } catch (Exception e) { throw new ParseException("'" + v + "' is not a valid token", 0); } break; } } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { final Calendar cal = Calendar.getInstance(getTimeZone()); List<Date> newCandidates = new ArrayList<Date>(); List<Date> oldCandidates = new ArrayList<Date>(); if (candidates.isEmpty()) { candidates.add(startDate); } oldCandidates.addAll(candidates); for (Date date : candidates) { for (Integer element : getValueSet()) { cal.setTime(date); cal.set(Calendar.MINUTE, element); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); return candidates; } } protected class HoursExpressionPart extends CronExpressionPart { protected static final int MIN_HOUR = 0; protected static final int MAX_HOUR = 23; public HoursExpressionPart(String s) throws ParseException { super(s); } @Override public int order() { return 5; } @Override String getSpecialToken(String token) { if (token.equals("*")) { return token; } if (token.contains("-")) { return "-"; } if (token.contains("/")) { return "/"; } return ""; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_HOUR, MAX_HOUR, false, false); } @Override void parseToken(String v) throws ParseException { switch (getSpecialToken(v)) { case "-": { String from = StringUtils.substringBefore(v, "-"); String to = StringUtils.substringAfter(v, "-"); getValueSet().add(Integer.parseInt(from), Integer.parseInt(to), 1); break; } case "/": { String from = StringUtils.substringBefore(v, "/"); String increment = StringUtils.substringAfter(v, "/"); try { if (Integer.parseInt(increment) > MAX_HOUR) { throw new ParseException("Increment is too large", 0); } } catch (Exception e) { throw new ParseException("Increment '" + v + "' is not a valid value", 0); } int fromValue = from.equals("*") ? 0 : Integer.parseInt(from); getValueSet().add(fromValue, MAX_HOUR, Integer.parseInt(increment)); break; } case "*": { getValueSet().add(MIN_HOUR, MAX_HOUR, 1); break; } default: { try { getValueSet().add(Integer.parseInt(v)); } catch (Exception e) { throw new ParseException("'" + v + "' is not a valid token", 0); } break; } } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { final Calendar cal = Calendar.getInstance(getTimeZone()); List<Date> newCandidates = new ArrayList<Date>(); List<Date> oldCandidates = new ArrayList<Date>(); if (candidates.isEmpty()) { candidates.add(startDate); } oldCandidates.addAll(candidates); for (Date date : candidates) { for (Integer element : getValueSet()) { cal.setTime(date); cal.set(Calendar.HOUR_OF_DAY, element); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); return candidates; } } protected class MonthsExpressionPart extends CronExpressionPart { protected static final int MIN_MONTH = 1; protected static final int MAX_MONTH = 12; public MonthsExpressionPart(String s) throws ParseException { super(s); } @Override public int order() { return 2; } @Override String getSpecialToken(String token) { if (token.equals("*")) { return token; } if (token.contains("-")) { return "-"; } if (token.contains("/")) { return "/"; } return ""; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_MONTH, MAX_MONTH, false, true); } protected int monthAsInteger(String monthAsString) throws ParseException { try { return Month.getMonth(monthAsString).getCalendarMonth(); } catch (IllegalArgumentException e) { try { return Integer.parseInt(monthAsString); } catch (Exception f) { throw new ParseException("Invalid Month value: '" + monthAsString + "'", 0); } } } @Override void parseToken(String v) throws ParseException { switch (getSpecialToken(v)) { case "-": { String from = StringUtils.substringBefore(v, "-"); String to = StringUtils.substringAfter(v, "-"); getValueSet().add(monthAsInteger(from), monthAsInteger(to), 1); break; } case "/": { String from = StringUtils.substringBefore(v, "/"); String increment = StringUtils.substringAfter(v, "/"); try { if (monthAsInteger(increment) > MAX_MONTH) { throw new ParseException("Increment is too large", 0); } } catch (Exception e) { throw new ParseException("Increment '" + v + "' is not a valid value", 0); } int fromValue = from.equals("*") ? 0 : monthAsInteger(from); getValueSet().add(fromValue, MAX_MONTH, monthAsInteger(increment)); break; } case "*": { getValueSet().add(MIN_MONTH, MAX_MONTH, 1); break; } default: { try { getValueSet().add(monthAsInteger(v)); } catch (Exception e) { throw new ParseException("'" + v + "' is not a valid token", 0); } break; } } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { final Calendar cal = Calendar.getInstance(getTimeZone()); List<Date> newCandidates = new ArrayList<Date>(); List<Date> oldCandidates = new ArrayList<Date>(); if (candidates.isEmpty()) { candidates.add(startDate); } oldCandidates.addAll(candidates); for (Date date : candidates) { for (Integer element : getValueSet()) { cal.setTime(date); cal.roll(Calendar.MONTH, (element - 1) - cal.get(Calendar.MONTH)); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); return candidates; } } protected class DayOfMonthExpressionPart extends CronExpressionPart { protected static final int MIN_MONTHDAY = 1; protected static final int MAX_MONTHDAY = 31; protected boolean isLastDayOfMonth; protected boolean isLastWeekDayOfMonth; protected boolean isNearestWeekDay; protected boolean isNotSpecific; protected int weekDay; protected int monthOffset; public boolean isLastDayOfMonth() { return isLastDayOfMonth; } public boolean isLastWeekDayOfMonth() { return isLastWeekDayOfMonth; } public boolean isNearestWeekDay() { return isNearestWeekDay; } public boolean isNotSpecific() { return isNotSpecific; } public DayOfMonthExpressionPart(String s) throws ParseException { super(s); } @Override public int order() { return 3; } @Override String getSpecialToken(String token) { if (token.equals("*") || token.equals("?") || token.equals("LW")) { return token; } if (token.contains("-") && !token.contains("L")) { return "-"; } if (token.contains("L") && !token.equals("LW")) { return "L"; } if (token.contains("W") && !token.equals("LW")) { return "W"; } if (token.contains("/")) { return "/"; } return ""; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_MONTHDAY, MAX_MONTHDAY, false, true); } @Override void parseToken(String v) throws ParseException { switch (getSpecialToken(v)) { case "-": { String from = StringUtils.substringBefore(v, "-"); String to = StringUtils.substringAfter(v, "-"); getValueSet().add(Integer.parseInt(from), Integer.parseInt(to), 1); break; } case "/": { String from = StringUtils.substringBefore(v, "/"); String increment = StringUtils.substringAfter(v, "/"); try { if (Integer.parseInt(increment) > MAX_MONTHDAY) { throw new ParseException("Increment is too large", 0); } } catch (Exception e) { throw new ParseException("Increment '" + v + "' is not a valid value", 0); } int fromValue = from.equals("*") ? 0 : Integer.parseInt(from); getValueSet().add(fromValue, MAX_MONTHDAY, Integer.parseInt(increment)); break; } case "*": { getValueSet().add(MIN_MONTHDAY, Calendar.getInstance(getTimeZone()).getActualMaximum(Calendar.DAY_OF_MONTH), 1); break; } case "?": { isNotSpecific = true; break; } case "L": { isLastDayOfMonth = true; monthOffset = StringUtils.substringAfter(v, "L-").equals("") ? 0 : Integer.parseInt(StringUtils.substringAfter(v, "L-")); if (monthOffset > 30) { throw new ParseException("Offset from last day must be <= 30", 0); } break; } case "W": { if (StringUtils.substringBefore(v, "W").equals("")) { throw new ParseException("'W' option need to specify a number", 0); } else { isNearestWeekDay = true; weekDay = Integer.parseInt(StringUtils.substringBefore(v, "W")); if (weekDay > 31) { throw new ParseException("'W' option can not be larger than 31", 0); } } break; } case "LW": { isLastWeekDayOfMonth = true; break; } default: { try { getValueSet().add(Integer.parseInt(v)); } catch (Exception e) { throw new ParseException("'" + v + "' is not a valid token", 0); } break; } } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { if (!isNotSpecific) { final Calendar cal = Calendar.getInstance(getTimeZone()); List<Date> newCandidates = new ArrayList<Date>(); List<Date> oldCandidates = new ArrayList<Date>(); if (candidates.isEmpty()) { candidates.add(startDate); } oldCandidates.addAll(candidates); for (Date date : candidates) { cal.setTime(date); if (isLastDayOfMonth) { cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH)); } else if (isLastWeekDayOfMonth) { cal.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY); cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, -1); } else if (isNearestWeekDay) { cal.set(Calendar.DAY_OF_MONTH, weekDay); if (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) { if (weekDay == 1) { cal.add(Calendar.DATE, 2); } else { cal.add(Calendar.DATE, -1); } } else if (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) { if (weekDay == cal.getActualMaximum(Calendar.DAY_OF_MONTH)) { cal.add(Calendar.DATE, -1); } else { cal.add(Calendar.DATE, 1); } } } else { for (Integer element : getValueSet()) { cal.setTime(date); if (element <= cal.getActualMaximum(Calendar.DAY_OF_MONTH)) { cal.set(Calendar.DAY_OF_MONTH, element); newCandidates.add(cal.getTime()); } } } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } return candidates; } } protected class DayOfWeekExpressionPart extends CronExpressionPart { protected static final int MIN_DAYWEEK = 1; protected static final int MAX_DAYWEEK = 7; protected boolean isLastDayOfMonth; protected boolean isLastDayOfWeek; protected boolean isNotSpecific; protected boolean isInstanceOfWeekday; protected int weekDay; protected int instanceOfMonth; protected int monthOffset; public boolean isLastDayOfMonth() { return isLastDayOfMonth; } public boolean isLastDayOfWeek() { return isLastDayOfWeek; } public boolean isInstanceOfWeekday() { return isInstanceOfWeekday; } public boolean isNotSpecific() { return isNotSpecific; } public DayOfWeekExpressionPart(String s) throws ParseException { super(s); } @Override public int order() { return 4; } @Override String getSpecialToken(String token) { if (token.equals("*") || token.equals("?")) { return token; } if (token.contains("#")) { return "#"; } if (token.contains("-") && !token.contains("L")) { return "-"; } if (token.contains("L")) { return "L"; } if (token.contains("/")) { return "/"; } return ""; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_DAYWEEK, MAX_DAYWEEK, false, true); } protected int dayAsInteger(String dayAsString) throws ParseException { try { return WeekDay.getWeekDay(dayAsString).getCalendarDay(); } catch (IllegalArgumentException e) { try { return Integer.parseInt(dayAsString); } catch (Exception f) { throw new ParseException("Invalid Day of Week value: '" + dayAsString + "'", 0); } } } @Override void parseToken(String v) throws ParseException { switch (getSpecialToken(v)) { case "-": { String from = StringUtils.substringBefore(v, "-"); String to = StringUtils.substringAfter(v, "-"); getValueSet().add(dayAsInteger(from), dayAsInteger(to), 1); break; } case "/": { String from = StringUtils.substringBefore(v, "/"); String increment = StringUtils.substringAfter(v, "/"); try { if (dayAsInteger(increment) > MAX_DAYWEEK) { throw new ParseException("Increment is too large", 0); } } catch (Exception e) { throw new ParseException("Increment '" + v + "' is not a valid value", 0); } int fromValue = from.equals("*") ? 0 : dayAsInteger(from); getValueSet().add(fromValue, MAX_DAYWEEK, dayAsInteger(increment)); break; } case "*": { getValueSet().add(MIN_DAYWEEK, MAX_DAYWEEK, 1); break; } case "?": { isNotSpecific = true; break; } case "L": { monthOffset = StringUtils.substringBefore(v, "L").equals("") ? 0 : Integer.parseInt(StringUtils.substringBefore(v, "L")); if (monthOffset == 0) { isLastDayOfWeek = true; } else { isLastDayOfMonth = true; } break; } case "#": { this.weekDay = dayAsInteger(StringUtils.substringBefore(v, "#")); this.instanceOfMonth = Integer.parseInt(StringUtils.substringAfter(v, "#")); if (instanceOfMonth < 1 || instanceOfMonth > 5) { throw new ParseException("A numeric value between 1 and 5 must follow the '#' option", 0); } else { isInstanceOfWeekday = true; } break; } default: { try { getValueSet().add(dayAsInteger(v)); } catch (Exception e) { throw new ParseException("'" + v + "' is not a valid token", 0); } break; } } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { if (!isNotSpecific) { final Calendar cal = Calendar.getInstance(getTimeZone()); List<Date> oldCandidates = new ArrayList<Date>(); List<Date> newCandidates = new ArrayList<Date>(); if (candidates.isEmpty()) { candidates.add(startDate); } oldCandidates.addAll(candidates); for (Date date : candidates) { cal.setTime(date); if (isLastDayOfMonth) { cal.set(Calendar.DAY_OF_WEEK, monthOffset); cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, -1); newCandidates.add(cal.getTime()); } else if (isLastDayOfWeek) { cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); newCandidates.add(cal.getTime()); } else if (isInstanceOfWeekday) { cal.set(Calendar.DAY_OF_WEEK, weekDay); cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, instanceOfMonth); newCandidates.add(cal.getTime()); } else { Calendar current = Calendar.getInstance(); current.setTime(date); for (int i = 1; i <= 5; i++) { cal.setTime(date); cal.set(Calendar.WEEK_OF_MONTH, i); Date weekInMonth = cal.getTime(); for (Integer element : getValueSet()) { cal.setTime(weekInMonth); cal.set(Calendar.DAY_OF_WEEK, element); if (cal.get(Calendar.MONTH) == current.get(Calendar.MONTH)) { newCandidates.add(cal.getTime()); } } } } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); } return candidates; } } protected class YearsExpressionPart extends CronExpressionPart { protected static final int MIN_YEAR = 1970; protected static final int MAX_YEAR = 2100; public YearsExpressionPart(String s) throws ParseException { super(s); } @Override public int order() { return 1; } @Override String getSpecialToken(String token) { if (token.equals("*")) { return token; } if (token.contains("-")) { return "-"; } if (token.contains("/")) { return "/"; } return ""; } @Override BoundedIntegerSet initializeValueSet() { return new BoundedIntegerSet(MIN_YEAR, MAX_YEAR, false, false); } @Override void parseToken(String v) throws ParseException { switch (getSpecialToken(v)) { case "-": { String from = StringUtils.substringBefore(v, "-"); String to = StringUtils.substringAfter(v, "-"); getValueSet().add(Integer.parseInt(from), Integer.parseInt(to), 1); break; } case "/": { String from = StringUtils.substringBefore(v, "/"); String increment = StringUtils.substringAfter(v, "/"); try { if (Integer.parseInt(increment) > MAX_YEAR) { throw new ParseException("Increment is too large", 0); } } catch (Exception e) { throw new ParseException("Increment '" + v + "' is not a valid value", 0); } int fromValue = from.equals("*") ? 0 : Integer.parseInt(from); getValueSet().add(fromValue, MAX_YEAR, Integer.parseInt(increment)); break; } case "*": { getValueSet().add(MIN_YEAR, MAX_YEAR, 1); break; } default: { try { getValueSet().add(Integer.parseInt(v)); } catch (Exception e) { throw new ParseException("'" + v + "' is not a valid token", 0); } break; } } } @Override public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) { final Calendar cal = Calendar.getInstance(getTimeZone()); List<Date> newCandidates = new ArrayList<Date>(); List<Date> oldCandidates = new ArrayList<Date>(); if (candidates.isEmpty()) { candidates.add(startDate); } oldCandidates.addAll(candidates); for (Date date : candidates) { for (Integer element : getValueSet()) { cal.setTime(date); cal.set(Calendar.YEAR, element); newCandidates.add(cal.getTime()); } } candidates.removeAll(oldCandidates); candidates.addAll(newCandidates); return candidates; } } @Override public boolean hasFloatingStartDate() { return true; } }