/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.openejb.core.timer; import org.apache.openejb.util.LogCategory; import org.apache.openejb.util.Logger; import org.apache.openejb.quartz.impl.triggers.CronTriggerImpl; import javax.ejb.ScheduleExpression; import java.io.Serializable; import java.text.DateFormatSymbols; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; public class EJBCronTrigger extends CronTriggerImpl { private static final long serialVersionUID = 1L; private static final Logger log = Logger.getInstance(LogCategory.TIMER, EJBCronTrigger.class); private static final Pattern INCREMENTS = Pattern.compile("(\\d+|\\*)/(\\d+)*"); private static final Pattern LIST = Pattern.compile("(([A-Za-z0-9]+)(-[A-Za-z0-9]+)?)?((1ST|2ND|3RD|4TH|5TH|LAST)([A-za-z]+))?(-([0-7]+))?(LAST)?" + "(?:,(([A-Za-z0-9]+)(-[A-Za-z0-9]+)?)?((1ST|2ND|3RD|4TH|5TH|LAST)([A-za-z]+))?(-([0-7]+))?(LAST)?)*"); private static final Pattern WEEKDAY = Pattern.compile("(1ST|2ND|3RD|4TH|5TH|LAST)(SUN|MON|TUE|WED|THU|FRI|SAT)"); private static final Pattern DAYS_TO_LAST = Pattern.compile("-([0-7]+)"); private static final Pattern VALID_YEAR = Pattern.compile("([0-9][0-9][0-9][0-9])|\\*"); private static final Pattern VALID_MONTH = Pattern.compile("(([0]?[1-9])|(1[0-2]))|\\*"); private static final Pattern VALID_DAYS_OF_WEEK = Pattern.compile("[0-7]|\\*"); private static final Pattern VALID_DAYS_OF_MONTH = Pattern.compile("((1ST|2ND|3RD|4TH|5TH|LAST)(SUN|MON|TUE|WED|THU|FRI|SAT))|(([1-9])|(0[1-9])|([12])([0-9]?)|(3[01]?))|(LAST)|-([0-7])|[*]"); private static final Pattern VALID_HOUR = Pattern.compile("(([0-1]?[0-9])|([2][0-3]))|\\*"); private static final Pattern VALID_MINUTE = Pattern.compile("([0-5]?[0-9])|\\*"); private static final Pattern VALID_SECOND = Pattern.compile("([0-5]?[0-9])|\\*"); private static final Pattern RANGE = Pattern.compile("(-?[A-Za-z0-9]+)-(-?[A-Za-z0-9]+)"); public static final String DELIMITER = ";"; private static final String LAST_IDENTIFIER = "LAST"; private static final Map<String, Integer> WEEKDAYS_MAP = new HashMap<String, Integer>(); private static final Map<String, Integer> MONTHS_MAP = new HashMap<String, Integer>(); static { int i = 0; // Jan -> 0 for (final String month : new DateFormatSymbols(Locale.US).getShortMonths()) { MONTHS_MAP.put(month.toUpperCase(Locale.US), i++); } i = 0; // SUN -> 1 for (final String weekday : new DateFormatSymbols(Locale.US).getShortWeekdays()) { WEEKDAYS_MAP.put(weekday.toUpperCase(Locale.US), i++); } } private static final int[] ORDERED_CALENDAR_FIELDS = {Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH, Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND}; private static final Map<Integer, Integer> CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP = new LinkedHashMap<Integer, Integer>(); static { //Initialize a calendar field -> ordered array index map CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.put(Calendar.YEAR, 0); CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.put(Calendar.MONTH, 1); CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.put(Calendar.DAY_OF_MONTH, 2); CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.put(Calendar.DAY_OF_WEEK, 3); CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.put(Calendar.HOUR_OF_DAY, 4); CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.put(Calendar.MINUTE, 5); CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.put(Calendar.SECOND, 6); } private final FieldExpression[] expressions = new FieldExpression[7]; private final TimeZone timezone; private final String rawValue; public EJBCronTrigger(final ScheduleExpression expr) throws ParseException { final Map<Integer, String> fieldValues = new LinkedHashMap<Integer, String>(); fieldValues.put(Calendar.YEAR, expr.getYear()); fieldValues.put(Calendar.MONTH, expr.getMonth()); fieldValues.put(Calendar.DAY_OF_MONTH, expr.getDayOfMonth()); fieldValues.put(Calendar.DAY_OF_WEEK, expr.getDayOfWeek()); fieldValues.put(Calendar.HOUR_OF_DAY, expr.getHour()); fieldValues.put(Calendar.MINUTE, expr.getMinute()); fieldValues.put(Calendar.SECOND, expr.getSecond()); timezone = expr.getTimezone() == null ? TimeZone.getDefault() : TimeZone.getTimeZone(expr.getTimezone()); setStartTime(expr.getStart() == null ? new Date() : expr.getStart()); setEndTime(expr.getEnd()); // If parsing fails on a field, record the error and move to the next field final Map<Integer, ParseException> errors = new HashMap<Integer, ParseException>(); int index = 0; for (final Entry<Integer, String> entry : fieldValues.entrySet()) { final int field = entry.getKey(); final String value = entry.getValue(); try { expressions[index++] = parseExpression(field, value); } catch (final ParseException e) { errors.put(field, e); } } // If there were parsing errors, throw a "master exception" that contains all // exceptions from individual fields if (!errors.isEmpty()) { throw new ParseException(errors); } rawValue = expr.getYear() + DELIMITER + expr.getMonth() + DELIMITER + expr.getDayOfMonth() + DELIMITER + expr.getDayOfWeek() + DELIMITER + expr.getHour() + DELIMITER + expr.getMinute() + DELIMITER + expr.getSecond(); } /** * Computes a set of allowed values for the given field of a calendar based * time expression. * * @param field field type from <code>java.util.Calendar</code> * @param expr a time expression * @throws ParseException when there is a syntax error in the expression, or its values * are out of range */ protected FieldExpression parseExpression(final int field, String expr) throws ParseException { if (expr == null || expr.isEmpty()) { throw new ParseException(field, expr, "expression can't be null"); } // Get rid of whitespace and convert to uppercase expr = expr.replaceAll("\\s+", "").toUpperCase(Locale.ENGLISH); if (expr.length() > 1 && expr.indexOf(",") > 0) { final String[] expressions = expr.split(","); for (final String subExpression : expressions) { validateExpression(field, subExpression); } } else { validateExpression(field, expr); } if (expr.equals("*")) { return new AsteriskExpression(field); } Matcher m = RANGE.matcher(expr); if (m.matches()) { return new RangeExpression(m, field); } switch (field) { case Calendar.HOUR_OF_DAY: case Calendar.MINUTE: case Calendar.SECOND: m = INCREMENTS.matcher(expr); if (m.matches()) { return new IncrementExpression(m, field); } break; case Calendar.DAY_OF_MONTH: if (expr.equals(LAST_IDENTIFIER)) { return new DaysFromLastDayExpression(); } m = DAYS_TO_LAST.matcher(expr); if (m.matches()) { return new DaysFromLastDayExpression(m); } m = WEEKDAY.matcher(expr); if (m.matches()) { return new WeekdayExpression(m); } break; } m = LIST.matcher(expr); if (m.matches()) { return new ListExpression(m, field); } throw new ParseException(field, expr, "Unparseable time expression"); } private void validateExpression(final int field, final String expression) throws ParseException { final Matcher rangeMatcher = RANGE.matcher(expression); final Matcher incrementsMatcher = INCREMENTS.matcher(expression); if (expression.length() > 2 && rangeMatcher.matches()) { validateSingleToken(field, rangeMatcher.group(1)); validateSingleToken(field, rangeMatcher.group(2)); } else if (expression.length() > 2 && incrementsMatcher.matches()) { validateSingleToken(field, incrementsMatcher.group(1)); validateSingleToken(field, incrementsMatcher.group(2)); } else { validateSingleToken(field, expression); } } private void validateSingleToken(final int field, final String token) throws ParseException { if (token == null || token.isEmpty()) { throw new ParseException(field, token, "expression can't be null"); } switch (field) { case Calendar.YEAR: { final Matcher m = VALID_YEAR.matcher(token); if (!m.matches()) { throw new ParseException(field, token, "Valid YEAR is four digit"); } break; } case Calendar.MONTH: { final Matcher m = VALID_MONTH.matcher(token); if (!(m.matches() || MONTHS_MAP.containsKey(token))) { throw new ParseException(field, token, "Valid MONTH is 1-12 or {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', Dec'}"); } break; } case Calendar.DAY_OF_MONTH: { final Matcher m = VALID_DAYS_OF_MONTH.matcher(token); if (!m.matches()) { throw new ParseException(field, token, "Valid DAYS_OF_MONTH is 0-7 or {'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'} "); } break; } case Calendar.DAY_OF_WEEK: { final Matcher m = VALID_DAYS_OF_WEEK.matcher(token); if (!(m.matches() || WEEKDAYS_MAP.containsKey(token))) { throw new ParseException(field, token, "Valid DAYS_OF_WEEK is 1-31 -(1-7) or {'1st', '2nd', '3rd', '4th', '5th', 'Last'} + {'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'} "); } break; } case Calendar.HOUR_OF_DAY: { final Matcher m = VALID_HOUR.matcher(token); if (!m.matches()) { throw new ParseException(field, token, "Valid HOUR_OF_DAY value is 0-23"); } break; } case Calendar.MINUTE: { final Matcher m = VALID_MINUTE.matcher(token); if (!m.matches()) { throw new ParseException(field, token, "Valid MINUTE value is 0-59"); } break; } case Calendar.SECOND: { final Matcher m = VALID_SECOND.matcher(token); if (!m.matches()) { throw new ParseException(field, token, "Valid SECOND value is 0-59"); } break; } } } /** * Works similarly to getFireTimeAfter() but backwards. */ @Override public Date getFinalFireTime() { final Calendar calendar = new GregorianCalendar(timezone); //calendar.setLenient(false); calendar.setFirstDayOfWeek(Calendar.SUNDAY); if (getEndTime() == null) { // If the year field has been left default, there is no end time if (expressions[0] instanceof AsteriskExpression) { return null; } resetFields(calendar, 0, true); calendar.set(Calendar.MILLISECOND, 0); } else { calendar.setTime(getEndTime()); } // Calculate time to give up scheduling final Calendar stopCalendar = new GregorianCalendar(timezone); if (getStartTime() != null) { stopCalendar.setTime(getStartTime()); } else { stopCalendar.setTimeInMillis(0); } int currentFieldIndex = 0; while (currentFieldIndex <= 6 && calendar.after(stopCalendar)) { final FieldExpression expr = expressions[currentFieldIndex]; final Integer value = expr.getPreviousValue(calendar); if (value != null) { final int oldValue = calendar.get(expr.field); if (oldValue != value) { // The value has changed, so update the calendar and reset all // less significant fields calendar.set(expr.field, value); resetFields(calendar, expr.field, true); // If the weekday changed, the day of month changed too if (expr.field == Calendar.DAY_OF_WEEK) { currentFieldIndex--; } else { currentFieldIndex++; } } else { currentFieldIndex++; } } else if (currentFieldIndex >= 1) { // No suitable value was found, so move back to the previous field // and decrease the value final int maxAffectedFieldType = upadteCalendar(calendar, expressions[currentFieldIndex - 1].field, -1); currentFieldIndex = CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.get(maxAffectedFieldType); resetFields(calendar, maxAffectedFieldType, true); } else { return null; // The job will never be run } } return calendar.after(stopCalendar) ? calendar.getTime() : null; } @Override public Date getFireTimeAfter(final Date afterTime) { log.debug("start to getFireTimeAfter:" + afterTime); final Calendar calendar = new GregorianCalendar(timezone); // calendar.setLenient(false); calendar.setFirstDayOfWeek(Calendar.SUNDAY); // Calculate starting time if (getStartTime() != null && getStartTime().after(afterTime)) { calendar.setTime(getStartTime()); } else { calendar.setTime(afterTime); calendar.add(Calendar.SECOND, 1); } // Calculate time to give up scheduling final Calendar stopCalendar = new GregorianCalendar(timezone); if (getEndTime() != null) { stopCalendar.setTime(getEndTime()); } else { final int stopYear = calendar.get(Calendar.YEAR) + 100; stopCalendar.set(Calendar.YEAR, stopYear); } int currentFieldIndex = 0; while (currentFieldIndex <= 6 && calendar.before(stopCalendar)) { final FieldExpression expr = expressions[currentFieldIndex]; Integer value = expr.getNextValue(calendar); /* * 18.2.1.2 Expression Rules * If dayOfMonth has a non-wildcard value and dayOfWeek has a non-wildcard value, then either the * dayOfMonth field or the dayOfWeek field must match the current day (even though the other of the * two fields need not match the current day). */ if (currentFieldIndex == 2 && !(expressions[3] instanceof AsteriskExpression)) { final Calendar clonedCalendarDayOfWeek = (Calendar) calendar.clone(); Integer nextDayOfWeek = expressions[3].getNextValue(clonedCalendarDayOfWeek); while (nextDayOfWeek == null) { clonedCalendarDayOfWeek.add(Calendar.DAY_OF_MONTH, 1); nextDayOfWeek = expressions[3].getNextValue(clonedCalendarDayOfWeek); } if (nextDayOfWeek != null) { clonedCalendarDayOfWeek.set(expressions[3].field, nextDayOfWeek); final int newDayOfMonth = clonedCalendarDayOfWeek.get(expressions[2].field); if (value == null) { value = newDayOfMonth; } else if (clonedCalendarDayOfWeek.get(expressions[1].field) == calendar.get(expressions[1].field)) { value = Math.min(value, newDayOfMonth); } //Next valid DayOfWeek might exist in next month. if (expressions[1].getNextValue(clonedCalendarDayOfWeek) == null) { return null; } else if (value != calendar.get(expressions[2].field) && clonedCalendarDayOfWeek.get(expressions[1].field) > calendar.get(expressions[1].field)) { calendar.set(Calendar.MONTH, clonedCalendarDayOfWeek.get(Calendar.MONTH)); } } } if (currentFieldIndex >= 1 && value == null) { if (currentFieldIndex == 3 && !(expressions[2] instanceof AsteriskExpression)) { /* *18.2.1.2 Expression Rules, the day has been resolved when dayOfMonth expression *is not AsteriskExpression. */ currentFieldIndex++; } else { // No suitable value was found, so move back to the previous field // and increase the value // When current field is HOUR_OF_DAY, its upper field is DAY_OF_MONTH, so we need to -2 due to // DAY_OF_WEEK. final int parentFieldIndex = currentFieldIndex == 4 ? currentFieldIndex - 2 : currentFieldIndex - 1; final int maxAffectedFieldType = upadteCalendar(calendar, expressions[parentFieldIndex].field, 1); currentFieldIndex = CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.get(maxAffectedFieldType); resetFields(calendar, maxAffectedFieldType, false); } } else if (value != null) { final int oldValue = calendar.get(expr.field); if (oldValue != value) { if (currentFieldIndex == 3 && !(expressions[2] instanceof AsteriskExpression)) { /* *18.2.1.2 Expression Rules, the day has been resolved when dayOfMonth expression *is not AsteriskExpression. */ currentFieldIndex++; } else { // The value has changed, so update the calendar and reset all // less significant fields calendar.set(expr.field, value); resetFields(calendar, expr.field, false); currentFieldIndex++; } } else { currentFieldIndex++; } } else { log.debug("end of getFireTimeAfter, result is:" + null); return null; } } log.debug("end of getFireTimeAfter, result is:" + (calendar.before(stopCalendar) ? calendar.getTime() : null)); return calendar.before(stopCalendar) ? calendar.getTime() : null; } /** * Update the value of target field by one, and return the max affected field value * * @param calendar * @param field * @return */ private int upadteCalendar(final Calendar calendar, final int field, final int amount) { final Calendar old = new GregorianCalendar(timezone); old.setTime(calendar.getTime()); calendar.add(field, amount); for (final int fieldType : ORDERED_CALENDAR_FIELDS) { if (calendar.get(fieldType) != old.get(fieldType)) { return fieldType; } } //Should never get here return -1; } public String getRawValue() { return rawValue; } /** * reset those sub field values, we need to configure from the end to begin, as getActualMaximun consider other fields' values * * @param calendar * @param currentField * @param max */ private void resetFields(final Calendar calendar, final int currentField, final boolean max) { for (int index = ORDERED_CALENDAR_FIELDS.length - 1; index >= 0; index--) { final int calendarField = ORDERED_CALENDAR_FIELDS[index]; if (calendarField > currentField) { final int value = max ? calendar.getActualMaximum(calendarField) : calendar.getActualMinimum(calendarField); calendar.set(calendarField, value); } else { break; } } } @Override // we don't want to be a CronTrigger for persistence public boolean hasAdditionalProperties() { return true; } public static class ParseException extends Exception { private final Map<Integer, ParseException> children; private final Integer field; private final String value; private final String error; protected ParseException(final int field, final String value, final String message) { this.children = null; this.field = field; this.value = value; this.error = message; } protected ParseException(final Map<Integer, ParseException> children) { this.children = children; this.field = null; this.value = null; this.error = null; } public Map<Integer, ParseException> getChildren() { return children != null ? Collections.unmodifiableMap(children) : null; } public Integer getField() { return field; } public String getValue() { return value; } public String getError() { return error; } @Override public String toString() { return "ParseException [field=" + field + ", value=" + value + ", error=" + error + "]"; } } private abstract static class FieldExpression implements Serializable { protected static final Calendar CALENDAR = new GregorianCalendar(Locale.US); // For getting min/max field values protected static int convertValue(final String value, final int field) throws ParseException { // If the value begins with a digit, parse it as a number if (Character.isDigit(value.charAt(0))) { int numValue; try { numValue = Integer.parseInt(value); } catch (final NumberFormatException e) { throw new ParseException(field, value, "Unparseable value"); } if (field == Calendar.DAY_OF_WEEK) { numValue++; } else if (field == Calendar.MONTH) { numValue--; // Months are 0-based } return numValue; } // Try converting a textual value to numeric switch (field) { case Calendar.MONTH: return MONTHS_MAP.get(value); case Calendar.DAY_OF_WEEK: return WEEKDAYS_MAP.get(value); } throw new ParseException(field, value, "Unparseable value"); } public final int field; protected FieldExpression(final int field) { this.field = field; } protected int convertValue(final String value) throws ParseException { return convertValue(value, field); } protected boolean isValidResult(final Calendar calendar, final Integer result) { return result != null && result >= calendar.getActualMinimum(field) && result <= calendar.getActualMaximum(field); } /** * Returns the next allowed value in this calendar for the given * field. * * @param calendar a Calendar where all the more significant fields have * been filled out * @return the next value allowed by this expression, or * <code>null</code> if none further allowed values are * found */ public abstract Integer getNextValue(Calendar calendar); /** * Returns the last allowed value in this calendar for the given field. * * @param calendar a Calendar where all the more significant fields have * been filled out * @return the last value allowed by this expression, or * <code>null</code> if none further allowed values are * found */ public abstract Integer getPreviousValue(Calendar calendar); } private static class RangeExpression extends FieldExpression { private int start; private int end; private int start2 = -1; private String startWeekDay; private String endWeekDay; private WeekdayExpression startWeekdayExpr; private WeekdayExpression endWeekdayExpr; private DaysFromLastDayExpression startDaysFromLastDayExpr; private DaysFromLastDayExpression endDaysFromLastDayExpr; //Indicate if the range expression is for "1st mon - 2nd fri" style range of days of month. private boolean isDynamicRangeExpression; public boolean isDynamicRangeExpression() { return isDynamicRangeExpression; } public RangeExpression(final int field, final int start, final int end, final int start2) { super(field); this.start = start; this.end = end; this.start2 = start2; } public RangeExpression(final Matcher m, final int field) throws ParseException { super(field); startWeekDay = m.group(1); endWeekDay = m.group(2); if (field == Calendar.DAY_OF_MONTH) { final Matcher startWeekDayMatcher = WEEKDAY.matcher(m.group(1)); final Matcher endWeekDayMatcher = WEEKDAY.matcher(m.group(2)); final Matcher startDaysFromLastDayMatcher = DAYS_TO_LAST.matcher(m.group(1)); final Matcher endDaysFromLastDayMatcher = DAYS_TO_LAST.matcher(m.group(2)); if (startWeekDayMatcher.matches()) { startWeekdayExpr = new WeekdayExpression(startWeekDayMatcher); } if (endWeekDayMatcher.matches()) { endWeekdayExpr = new WeekdayExpression(endWeekDayMatcher); } if (startDaysFromLastDayMatcher.matches()) { startDaysFromLastDayExpr = new DaysFromLastDayExpression(startDaysFromLastDayMatcher); } if (endDaysFromLastDayMatcher.matches()) { endDaysFromLastDayExpr = new DaysFromLastDayExpression(endDaysFromLastDayMatcher); } if (startWeekdayExpr != null || endWeekdayExpr != null || startDaysFromLastDayExpr != null || endDaysFromLastDayExpr != null || startWeekDay.equals(LAST_IDENTIFIER) || endWeekDay.equals(LAST_IDENTIFIER)) { isDynamicRangeExpression = true; return; } } //not a dynamic range expression, go ahead to init start and end values without a calendar initStartEndValues(null); } private void initStartEndValues(final Calendar calendar) throws ParseException { int beginValue; int endValue; if (isDynamicRangeExpression) { if (startWeekDay.equals(LAST_IDENTIFIER)) { beginValue = calendar.getActualMaximum(field); } else if (startWeekdayExpr != null) { beginValue = startWeekdayExpr.getWeekdayInMonth(calendar); } else if (startDaysFromLastDayExpr != null) { final Integer next = startDaysFromLastDayExpr.getNextValue(calendar); beginValue = next == null ? calendar.get(field) : next; } else { beginValue = convertValue(startWeekDay); } if (endWeekDay.equals(LAST_IDENTIFIER)) { endValue = calendar.getActualMaximum(field); } else if (endWeekdayExpr != null) { endValue = endWeekdayExpr.getWeekdayInMonth(calendar); } else if (endDaysFromLastDayExpr != null) { final Integer next = endDaysFromLastDayExpr.getNextValue(calendar); endValue = next == null ? calendar.get(field) : next; } else { endValue = convertValue(endWeekDay); } } else { beginValue = convertValue(startWeekDay); endValue = convertValue(endWeekDay); } /* * handle 0-7 for day of week range. * * both 0 and 7 represent Sun. We need to remove one from the range. * */ if (field == Calendar.DAY_OF_WEEK) { if (beginValue == 8 && endValue == 1 || endValue == 8 && beginValue == 1) { beginValue = 1; endValue = 7; } else { if (beginValue == 8) { beginValue = 1; } if (endValue == 8) { endValue = 1; } } } // Try converting a textual value to numeric if (endWeekDay.equals(LAST_IDENTIFIER)) { start = -1; end = -1; start2 = beginValue; } else { if (beginValue > endValue) { start = CALENDAR.getMinimum(field); end = endValue; start2 = beginValue; } else { start = beginValue; end = endValue; } } } @Override public Integer getNextValue(final Calendar calendar) { if (isDynamicRangeExpression) { final Integer nextStartWeekday = startWeekdayExpr == null ? start : startWeekdayExpr .getWeekdayInMonth(calendar); final Integer nextendWeekday = endWeekdayExpr == null ? end : endWeekdayExpr. getWeekdayInMonth(calendar); if (nextStartWeekday == null || nextendWeekday == null) { return null; } try { initStartEndValues(calendar); } catch (final ParseException e) { return null; } } final int currValue = calendar.get(field); if (start2 != -1) { if (currValue >= start2) { return isValidResult(calendar, currValue) ? currValue : null; } else if (currValue > end) { return isValidResult(calendar, start2) ? start2 : null; } } if (currValue <= start) { return isValidResult(calendar, start) ? start : null; } else if (currValue <= end) { return isValidResult(calendar, currValue) ? currValue : null; } else { return null; } } @Override public Integer getPreviousValue(final Calendar calendar) { if (isDynamicRangeExpression) { try { initStartEndValues(calendar); } catch (final ParseException e) { return null; } } final int currValue = calendar.get(field); if (start2 != -1) { if (currValue >= start2) { return isValidResult(calendar, currValue) ? currValue : null; } } if (currValue <= start) { return null; } else if (currValue <= end) { return isValidResult(calendar, currValue) ? currValue : null; } else { return isValidResult(calendar, end) ? end : null; } } public List<Integer> getAllValuesInRange(final Calendar calendar) { final List<Integer> values = new ArrayList<Integer>(); if (isDynamicRangeExpression) { try { initStartEndValues(calendar); } catch (final ParseException e) { return values; } } if (start2 == -1) { for (int i = start; i <= end; i++) { values.add(i); } } else { for (int i = start; i <= end; i++) { values.add(i); } for (int i = start2; i <= CALENDAR.getMaximum(field); i++) { values.add(i); } } return values; } } /* * Just find that it is hard to keep those ranges in the list are not overlapped. * The easy way is to list all the values, also we keep a range expression if user defines a LAST expression, e.g. 12-LAST */ private static class ListExpression extends FieldExpression { private final Set<Integer> values = new TreeSet<Integer>(); private final List<RangeExpression> weekDayRangeExpressions = new ArrayList<RangeExpression>(); private final List<WeekdayExpression> weekDayExpressions = new ArrayList<WeekdayExpression>(); private final List<DaysFromLastDayExpression> daysFromLastDayExpressions = new ArrayList<DaysFromLastDayExpression>(); ; public ListExpression(final Matcher m, final int field) throws ParseException { super(field); initialize(m); } private void initialize(final Matcher m) throws ParseException { for (final String value : m.group().split("[,]")) { final Matcher rangeMatcher = RANGE.matcher(value); final Matcher weekDayMatcher = WEEKDAY.matcher(value); final Matcher daysToLastMatcher = DAYS_TO_LAST.matcher(value); if (value.equals(LAST_IDENTIFIER)) { daysFromLastDayExpressions.add(new DaysFromLastDayExpression()); continue; } else if (daysToLastMatcher.matches()) { daysFromLastDayExpressions.add(new DaysFromLastDayExpression(daysToLastMatcher)); continue; } else if (weekDayMatcher.matches()) { weekDayExpressions.add(new WeekdayExpression(weekDayMatcher)); continue; } else if (rangeMatcher.matches()) { final RangeExpression rangeExpression = new RangeExpression(rangeMatcher, field); if (rangeExpression.isDynamicRangeExpression()) { weekDayRangeExpressions.add(new RangeExpression(rangeMatcher, field)); continue; } values.addAll(rangeExpression.getAllValuesInRange(null)); } else { int individualValue = convertValue(value); if (field == Calendar.DAY_OF_WEEK && individualValue == 8) { individualValue = 1; } values.add(individualValue); } } } private TreeSet<Integer> getNewValuesFromDynamicExpressions(final Calendar calendar) { final TreeSet<Integer> newValues = new TreeSet<Integer>(); newValues.addAll(values); for (final RangeExpression weekDayRangeExpression : weekDayRangeExpressions) { newValues.addAll(weekDayRangeExpression.getAllValuesInRange(calendar)); } for (final WeekdayExpression weekdayExpression : weekDayExpressions) { final Integer value = weekdayExpression.getNextValue(calendar); if (value != null) { newValues.add(value); } } for (final DaysFromLastDayExpression daysFromLastDayExpression : daysFromLastDayExpressions) { final Integer value = daysFromLastDayExpression.getNextValue(calendar); if (value != null) { newValues.add(value); } } return newValues; } @Override public Integer getNextValue(final Calendar calendar) { final TreeSet<Integer> newValues = getNewValuesFromDynamicExpressions(calendar); final int currValue = calendar.get(field); final Integer result = newValues.ceiling(currValue); return isValidResult(calendar, result) ? result : null; } @Override public Integer getPreviousValue(final Calendar calendar) { final TreeSet<Integer> newValues = getNewValuesFromDynamicExpressions(calendar); final int currValue = calendar.get(field); final Integer result = newValues.floor(currValue); return isValidResult(calendar, result) ? result : null; } } private static class IncrementExpression extends FieldExpression { private final int start; private final int interval; public IncrementExpression(final Matcher m, final int field) { super(field); final int minValue = CALENDAR.getMinimum(field); start = m.group(1).equals("*") ? minValue : Integer.parseInt(m.group(1)); interval = Integer.parseInt(m.group(2)); } @Override public Integer getNextValue(final Calendar calendar) { final int currValue = calendar.get(field); if (currValue > start) { Integer nextValue = start + interval; while (isValidResult(calendar, nextValue)) { if (nextValue >= currValue) { return nextValue; } nextValue = nextValue + interval; } } else { return new Integer(start); } return null; } @Override public Integer getPreviousValue(final Calendar calendar) { final int currValue = calendar.get(field); if (currValue < start) { Integer previousValue = start - interval; while (isValidResult(calendar, previousValue)) { if (previousValue < currValue) { return previousValue; } previousValue = previousValue - interval; } } else { return new Integer(start); } return null; } } private static class WeekdayExpression extends FieldExpression { private final Integer ordinal; // null means last private final int weekday; public WeekdayExpression(final Matcher m) throws ParseException { super(Calendar.DAY_OF_MONTH); final Character firstChar = m.group(1).charAt(0); ordinal = Character.isDigit(firstChar) ? Integer.valueOf(firstChar.toString()) : null; weekday = convertValue(m.group(2), Calendar.DAY_OF_WEEK); } @Override public Integer getNextValue(final Calendar calendar) { final int currDay = calendar.get(Calendar.DAY_OF_MONTH); final Integer nthDay = getWeekdayInMonth(calendar); final Integer result = nthDay != null && nthDay >= currDay ? nthDay : null; return isValidResult(calendar, result) ? result : null; } public Integer getWeekdayInMonth(final Calendar calendar) { final int currDay = calendar.get(Calendar.DAY_OF_MONTH); final int currWeekday = calendar.get(Calendar.DAY_OF_WEEK); final int maxDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); // Calculate the first day in the month whose weekday is the same as the // one we're looking for int firstWeekday = currDay % 7 - (currWeekday - weekday); firstWeekday = firstWeekday == 0 ? 7 : firstWeekday; // Then calculate how many such weekdays there is in this month final int numWeekdays = firstWeekday >= 0 ? (maxDay - firstWeekday) / 7 + 1 : (maxDay - firstWeekday) / 7; // Then calculate the Nth of those days, or the last one if ordinal is null final int multiplier = ordinal != null ? ordinal : numWeekdays; final int nthDay = firstWeekday >= 0 ? firstWeekday + (multiplier - 1) * 7 : firstWeekday + multiplier * 7; // Return the calculated day, or null if the day is out of range return nthDay <= maxDay ? nthDay : null; } @Override public Integer getPreviousValue(final Calendar calendar) { final int currDay = calendar.get(Calendar.DAY_OF_MONTH); final Integer nthDay = getWeekdayInMonth(calendar); final Integer result = nthDay != null && nthDay <= currDay ? nthDay : null; return isValidResult(calendar, result) ? result : null; } } private static class DaysFromLastDayExpression extends FieldExpression { private final int days; public DaysFromLastDayExpression(final Matcher m) { super(Calendar.DAY_OF_MONTH); days = new Integer(m.group(1)); } public DaysFromLastDayExpression() { super(Calendar.DAY_OF_MONTH); this.days = 0; } @Override public Integer getNextValue(final Calendar calendar) { final int currValue = calendar.get(field); final int maxValue = calendar.getActualMaximum(field); final int value = maxValue - days; final Integer result = currValue <= value ? value : null; return isValidResult(calendar, result) ? result : null; } @Override public Integer getPreviousValue(final Calendar calendar) { final int maxValue = calendar.getActualMaximum(field); final Integer result = maxValue - days; return isValidResult(calendar, result) ? result : null; } } private static class AsteriskExpression extends FieldExpression { public AsteriskExpression(final int field) { super(field); } @Override public Integer getNextValue(final Calendar calendar) { return calendar.get(field); } @Override public Integer getPreviousValue(final Calendar calendar) { return calendar.get(field); } } }