/* * JBoss, Home of Professional Open Source. * Copyright 2009, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.ejb3.timerservice.schedule.attribute; import org.jboss.as.ejb3.logging.EjbLogger; import org.jboss.as.ejb3.timerservice.schedule.util.CalendarUtil; import org.jboss.as.ejb3.timerservice.schedule.value.RangeValue; import org.jboss.as.ejb3.timerservice.schedule.value.ScheduleExpressionType; import org.jboss.as.ejb3.timerservice.schedule.value.ScheduleValue; import org.jboss.as.ejb3.timerservice.schedule.value.SingleValue; import java.util.Calendar; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Pattern; /** * Represents the value of a day in a month, constructed out of a {@link javax.ejb.ScheduleExpression#getDayOfMonth()} * <p/> * <p> * A {@link DayOfMonth} can hold an {@link Integer} or a {@link String} as its value. * value. The various ways in which a * {@link DayOfMonth} value can be represented are: * <ul> * <li>Wildcard. For example, dayOfMonth = "*"</li> * <li>Range. Examples: * <ul> * <li>dayOfMonth = "1-10"</li> * <li>dayOfMonth = "Sun-Tue"</li> * <li>dayOfMonth = "1st-5th"</li> * </ul> * </li> * <li>List. Examples: * <ul> * <li>dayOfMonth = "1, 12, 20"</li> * <li>dayOfMonth = "Mon, Fri, Sun"</li> * <li>dayOfMonth = "3rd, 1st, Last"</li> * </ul> * </li> * <li>Single value. Examples: * <ul> * <li>dayOfMonth = "Fri"</li> * <li>dayOfMonth = "Last"</li> * <li>dayOfMonth = "10"</li> * </ul> * </li> * </ul> * </p> * * @author Jaikiran Pai * @version $Revision: $ */ public class DayOfMonth extends IntegerBasedExpression { /** * The maximum allowed value for the {@link DayOfMonth} */ public static final Integer MAX_DAY_OF_MONTH = 31; /** * The minimum allowed value for the {@link DayOfMonth} */ public static final Integer MIN_DAY_OF_MONTH = -7; /** * A {@link DayOfMonth} can be represented as a {@link String} too (for example "1st", "Sun" etc...). * Internally, we map all allowed {@link String} values to their {@link Integer} equivalents. * This map holds the {@link String} name to {@link Integer} value mapping. */ private static final Map<String, Integer> DAY_OF_MONTH_ALIAS = new HashMap<String, Integer>(); static { DAY_OF_MONTH_ALIAS.put("sun", Calendar.SUNDAY); DAY_OF_MONTH_ALIAS.put("mon", Calendar.MONDAY); DAY_OF_MONTH_ALIAS.put("tue", Calendar.TUESDAY); DAY_OF_MONTH_ALIAS.put("wed", Calendar.WEDNESDAY); DAY_OF_MONTH_ALIAS.put("thu", Calendar.THURSDAY); DAY_OF_MONTH_ALIAS.put("fri", Calendar.FRIDAY); DAY_OF_MONTH_ALIAS.put("sat", Calendar.SATURDAY); } private static final Set<String> ORDINALS = new HashSet<String>(); private static final Map<String, Integer> ORDINAL_TO_WEEK_NUMBER_MAPPING = new HashMap<String, Integer>(); static { ORDINALS.add("1st"); ORDINALS.add("2nd"); ORDINALS.add("3rd"); ORDINALS.add("4th"); ORDINALS.add("5th"); ORDINALS.add("last"); ORDINAL_TO_WEEK_NUMBER_MAPPING.put("1st", 1); ORDINAL_TO_WEEK_NUMBER_MAPPING.put("2nd", 2); ORDINAL_TO_WEEK_NUMBER_MAPPING.put("3rd", 3); ORDINAL_TO_WEEK_NUMBER_MAPPING.put("4th", 4); ORDINAL_TO_WEEK_NUMBER_MAPPING.put("5th", 5); } /** * Creates a {@link DayOfMonth} by parsing the passed {@link String} <code>value</code> * <p> * Valid values are of type {@link ScheduleExpressionType#WILDCARD}, {@link ScheduleExpressionType#RANGE}, * {@link ScheduleExpressionType#LIST} or {@link ScheduleExpressionType#SINGLE_VALUE} * </p> * * @param value The value to be parsed * @throws IllegalArgumentException If the passed <code>value</code> is neither a {@link ScheduleExpressionType#WILDCARD}, * {@link ScheduleExpressionType#RANGE}, {@link ScheduleExpressionType#LIST}, * nor {@link ScheduleExpressionType#SINGLE_VALUE}. */ public DayOfMonth(String value) { super(value); } /** * Returns the maximum allowed value for a {@link DayOfMonth} * * @see DayOfMonth#MAX_DAY_OF_MONTH */ @Override protected Integer getMaxValue() { return MAX_DAY_OF_MONTH; } /** * Returns the minimum allowed value for a {@link DayOfMonth} * * @see DayOfMonth#MIN_DAY_OF_MONTH */ @Override protected Integer getMinValue() { return MIN_DAY_OF_MONTH; } public Integer getNextMatch(Calendar currentCal) { if (this.scheduleExpressionType == ScheduleExpressionType.WILDCARD) { return currentCal.get(Calendar.DAY_OF_MONTH); } int currentDayOfMonth = currentCal.get(Calendar.DAY_OF_MONTH); SortedSet<Integer> eligibleDaysOfMonth = this.getEligibleDaysOfMonth(currentCal); if (eligibleDaysOfMonth.isEmpty()) { return null; } for (Integer hour : eligibleDaysOfMonth) { if (currentDayOfMonth == hour) { return currentDayOfMonth; } if (hour > currentDayOfMonth) { return hour; } } return eligibleDaysOfMonth.first(); } @Override protected void assertValid(Integer value) throws IllegalArgumentException { if (value != null && value == 0) { throw EjbLogger.EJB3_TIMER_LOGGER.invalidValueDayOfMonth(value); } super.assertValid(value); } private boolean hasRelativeDayOfMonth() { if (this.relativeValues.isEmpty()) { return false; } return true; } private SortedSet<Integer> getEligibleDaysOfMonth(Calendar cal) { if (this.hasRelativeDayOfMonth() == false) { return this.absoluteValues; } SortedSet<Integer> eligibleDaysOfMonth = new TreeSet<Integer>(this.absoluteValues); for (ScheduleValue relativeValue : this.relativeValues) { if (relativeValue instanceof SingleValue) { SingleValue singleValue = (SingleValue) relativeValue; String value = singleValue.getValue(); Integer absoluteDayOfMonth = this.getAbsoluteDayOfMonth(cal, value); eligibleDaysOfMonth.add(absoluteDayOfMonth); } else if (relativeValue instanceof RangeValue) { RangeValue range = (RangeValue) relativeValue; String start = range.getStart(); String end = range.getEnd(); Integer dayOfMonthStart = null; // either start will be relative or end will be relative or both are relative if (this.isRelativeValue(start)) { dayOfMonthStart = this.getAbsoluteDayOfMonth(cal, start); } else { dayOfMonthStart = this.parseInt(start); } Integer dayOfMonthEnd = null; if (this.isRelativeValue(end)) { dayOfMonthEnd = this.getAbsoluteDayOfMonth(cal, end); } else { dayOfMonthEnd = this.parseInt(end); } // validations this.assertValid(dayOfMonthStart); this.assertValid(dayOfMonthEnd); // start and end are both the same. So it's just a single value if (dayOfMonthStart.equals(dayOfMonthEnd)) { eligibleDaysOfMonth.add(dayOfMonthEnd); continue; } if (dayOfMonthStart > dayOfMonthEnd) { // In range "x-y", if x is larger than y, the range is equivalent to // "x-max, min-y", where max is the largest value of the corresponding attribute // and min is the smallest. for (int i = dayOfMonthStart; i <= this.getMaxValue(); i++) { eligibleDaysOfMonth.add(i); } for (int i = this.getMinValue(); i <= dayOfMonthEnd; i++) { eligibleDaysOfMonth.add(i); } } else { // just keep adding from range start to range end (both inclusive). for (int i = dayOfMonthStart; i <= dayOfMonthEnd; i++) { eligibleDaysOfMonth.add(i); } } } } return eligibleDaysOfMonth; } private int getAbsoluteDayOfMonth(Calendar cal, String relativeDayOfMonth) { if (relativeDayOfMonth == null || relativeDayOfMonth.trim().isEmpty()) { throw EjbLogger.EJB3_TIMER_LOGGER.relativeDayOfMonthIsNull(); } String trimmedRelativeDayOfMonth = relativeDayOfMonth.trim(); if (trimmedRelativeDayOfMonth.equalsIgnoreCase("last")) { int lastDayOfCurrentMonth = CalendarUtil.getLastDateOfMonth(cal); return lastDayOfCurrentMonth; } if (this.isValidNegativeDayOfMonth(trimmedRelativeDayOfMonth)) { Integer negativeRelativeDayOfMonth = Integer.parseInt(trimmedRelativeDayOfMonth); int lastDayOfCurrentMonth = CalendarUtil.getLastDateOfMonth(cal); return lastDayOfCurrentMonth + negativeRelativeDayOfMonth; } if (this.isDayOfWeekBased(trimmedRelativeDayOfMonth)) { String[] parts = trimmedRelativeDayOfMonth.split("\\s+"); String ordinal = parts[0]; String day = parts[1]; int dayOfWeek = DAY_OF_MONTH_ALIAS.get(day.toLowerCase(Locale.ENGLISH)); Integer date = null; if (ordinal.equalsIgnoreCase("last")) { date = CalendarUtil.getDateOfLastDayOfWeekInMonth(cal, dayOfWeek); } else { int weekNumber = ORDINAL_TO_WEEK_NUMBER_MAPPING.get(ordinal.toLowerCase(Locale.ENGLISH)); date = CalendarUtil.getNthDayOfMonth(cal, weekNumber, dayOfWeek); } // TODO: Rethink about this. The reason why we have this currently is to handle cases like: // 5th Wed which may not be valid for all months (i.e. all months do not have 5 weeks). In such // cases we set the date to last date of the month. // This needs to be thought about a bit more in detail, to understand it's impact on other scenarios. if (date == null) { date = CalendarUtil.getLastDateOfMonth(cal); } return date; } throw EjbLogger.EJB3_TIMER_LOGGER.invalidRelativeValue(relativeDayOfMonth); } private boolean isValidNegativeDayOfMonth(String dayOfMonth) { try { Integer val = Integer.parseInt(dayOfMonth.trim()); if (val <= -1 && val >= -7) { return true; } return false; } catch (NumberFormatException nfe) { return false; } } private boolean isDayOfWeekBased(String relativeVal) { String trimmedVal = relativeVal.trim(); // one or more spaces (which includes tabs and other forms of space) Pattern p = Pattern.compile("\\s+"); String[] relativeParts = p.split(trimmedVal); if (relativeParts == null) { return false; } if (relativeParts.length != 2) { return false; } String ordinal = relativeParts[0]; String dayOfWeek = relativeParts[1]; if (ordinal == null || dayOfWeek == null) { return false; } String lowerCaseOrdinal = ordinal.toLowerCase(Locale.ENGLISH); if (ORDINALS.contains(lowerCaseOrdinal) == false) { return false; } String lowerCaseDayOfWeek = dayOfWeek.toLowerCase(Locale.ENGLISH); if (DAY_OF_MONTH_ALIAS.containsKey(lowerCaseDayOfWeek) == false) { return false; } return true; } @Override public boolean isRelativeValue(String value) { if (value == null) { throw EjbLogger.EJB3_TIMER_LOGGER.relativeValueIsNull(); } if (value.equalsIgnoreCase("last")) { return true; } if (this.isValidNegativeDayOfMonth(value)) { return true; } if (this.isDayOfWeekBased(value)) { return true; } return false; } @Override protected boolean accepts(ScheduleExpressionType scheduleExprType) { switch (scheduleExprType) { case RANGE: case LIST: case SINGLE_VALUE: case WILDCARD: return true; // day-of-month doesn't support increment case INCREMENT: default: return false; } } public Integer getFirstMatch(Calendar cal) { if (this.scheduleExpressionType == ScheduleExpressionType.WILDCARD) { return Calendar.SUNDAY; } SortedSet<Integer> eligibleDaysOfMonth = this.getEligibleDaysOfMonth(cal); if (eligibleDaysOfMonth.isEmpty()) { return null; } return eligibleDaysOfMonth.first(); } @Override protected Integer parseInt(String alias) { try { return super.parseInt(alias); } catch (NumberFormatException nfe) { if (DAY_OF_MONTH_ALIAS != null) { String lowerCaseAlias = alias.toLowerCase(Locale.ENGLISH); return DAY_OF_MONTH_ALIAS.get(lowerCaseAlias); } } return null; } }