| /* |
| * 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<>(); |
| 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<>(); |
| 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<>(); |
| |
| 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); |
| } |
| } |
| } |