blob: 4b4e9f3bb16219d9ce1a94692ec889040afc031c [file] [log] [blame]
/*
* 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;
private final boolean isExpired;
public EJBCronTrigger(final ScheduleExpression expr) throws ParseException {
final Map<Integer, String> fieldValues = new LinkedHashMap<>();
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());
/*
* @testName: endNeverExpires
*
* @test_Strategy: create a timer with year="currentYear - currentYear+1", and
* end="currentYear-1". The end value is prior to the year values, and this
* timer will never expire. Creating this timer will succeed, but any timer
* method access in a subsequent business method will result in
* NoSuchObjectLocalException.
*
* EJB32 TCK test tries to create an already expired Timer and it's supposed to not fail.
* This may happen whe you restart an application for instance.
* On the other hand, Quartz does not allow endTime to be before StartTime so we need to check first so we don't
* set the endDate but we flag up this timer as being expired.
*
* When the first time is computed we will fail and as a consequence TimerData will be flagged up as being expired.
* So if endDate is not set or endTime after startTime, then we can consider this timer as not expired.
* If endTime is set and it's before startTime, we swallow setEndTime to Quartz and set the expired flag to true
*/
if (expr.getEnd() == null || !isBefore(expr.getEnd(), getStartTime())) {
setEndTime(expr.getEnd());
isExpired = false;
} else {
isExpired = true;
}
// If parsing fails on a field, record the error and move to the next field
final Map<Integer, ParseException> errors = new HashMap<>();
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();
}
private boolean isBefore(final Date end, final Date start) {
return start != null && end != null && start.after(end);
}
/**
* 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 = updateCalendar(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 computeFirstFireTime(final org.apache.openejb.quartz.Calendar calendar) {
// timer may be expired up on creation (see constructor comments)
if (isExpired) {
throw new TimerExpiredException(String.format("Timer %s expired.", this));
}
return super.computeFirstFireTime(calendar);
}
@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 = updateCalendar(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 updateCalendar(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<>();
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<>();
private final List<RangeExpression> weekDayRangeExpressions = new ArrayList<>();
private final List<WeekdayExpression> weekDayExpressions = new ArrayList<>();
private final List<DaysFromLastDayExpression> daysFromLastDayExpressions = new ArrayList<>();
;
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<>(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 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 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);
}
}
}