package uws.service.file; /* * This file is part of UWSLibrary. * * UWSLibrary 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 3 of the License, or * (at your option) any later version. * * UWSLibrary 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 UWSLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2014-2015 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ import java.text.DateFormat; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Scanner; import java.util.regex.MatchResult; /** * <p>Let interpret and computing a frequency.</p> * * <h3>Frequency syntax</h3> * * <p>The frequency is expressed as a string at initialization of this object. This string must respect the following syntax:</p> * <ul> * <li>'D' hh mm : daily schedule at hh:mm</li> * <li>'W' dd hh mm : weekly schedule at the given day of the week (1:sunday, 2:monday, ..., 7:saturday) at hh:mm</li> * <li>'M' dd hh mm : monthly schedule at the given day of the month at hh:mm</li> * <li>'h' mm : hourly schedule at the given minute</li> * <li>'m' : scheduled every minute (for completness :-))</li> * </ul> * <p><i>Where: hh = integer between 0 and 23, mm = integer between 0 and 59, dd (for 'W') = integer between 1 and 7 (1:sunday, 2:monday, ..., 7:saturday), * dd (for 'M') = integer between 1 and 31.</i></p> * * <p><i><b>Warning:</b> * The frequency type is case sensitive! Then you should particularly pay attention at the case * when using the frequency types 'M' (monthly) and 'm' (every minute). * </i></p> * * <p> * Parsing errors are not thrown but "resolved" silently. The "solution" depends of the error. * 2 cases of errors are considered: * </p> * <ul> * <li><b>Frequency type mismatch:</b> It happens when the first character is not one of the expected (D, W, M, h, m). * That means: bad case (i.e. 'd' rather than 'D'), another character. * In this case, the frequency will be: <b>daily at 00:00</b>.</li> * * <li><b>Parameter(s) missing or incorrect:</b> With the "daily" frequency ('D'), at least 2 parameters must be provided ; * 3 for "weekly" ('W') and "monthly" ('M') ; only 1 for "hourly" ('h') ; none for "every minute" ('m'). * This number of parameters is a minimum: only the n first parameters will be considered while * the others will be ignored. * If this minimum number of parameters is not respected or if a parameter value is incorrect, * <b>all parameters will be set to their default value</b> * (which is 0 for all parameter except dd for which it is 1).</li> * </ul> * * <p>Examples:</p> * <ul> * <li><i>"" or NULL</i> = every day at 00:00</li> * <li><i>"D 06 30" or "D 6 30"</i> = every day at 06:30</li> * <li><i>"D 24 30"</i> = every day at 00:00, because hh must respect the rule: 0 ≤ hh ≤ 23</li> * <li><i>"d 06 30" or "T 06 30"</i> = every day at 00:00, because the frequency type "d" (lower case of "D") or "T" do not exist</li> * <li><i>"W 2 6 30"</i> = every week on Tuesday at 06:30</li> * <li><i>"W 8 06 30"</i> = every week on Sunday at 00:00, because with 'W' dd must respect the rule: 1 ≤ dd ≤ 7</li> * <li><i>"M 2 6 30"</i> = every month on the 2nd at 06:30</li> * <li><i>"M 32 6 30"</i> = every month on the 1st at 00:00, because with 'M' dd must respect the rule: 1 ≤ dd ≤ 31</li> * <li><i>"M 5 6 30 12"</i> = every month on the 5th at 06:30, because at least 3 parameters are expected and so considered: "12" and eventual other parameters are ignored</li> * </ul> * * <h3>Computing next event date</h3> * * <p> * When this class is initialized with a frequency, it is able to compute the date of the event following a given date. * The functions {@link #nextEvent()} and {@link #nextEvent(Date)} will compute this next event date * from, respectively, now (current date/time) and the given date (the date of the last event). Both are computing the date of the next * event by "adding" the frequency to the given date. And finally, the computed date is stored and returned. * </p> * * <p>Then, you have 2 possibilities to trigger the desired event:</p> * <ul> * <li>By calling {@link #isTimeElapsed()}, you can test whether at the current moment the date of the next event has been reached or not. * In function of the value returned by this function you will be then able to process the desired action or not.</li> * <li>By creating a Timer with the next date event. Thus, the desired action will be automatically triggered at the exact moment.</li> * </p> * * * @author Marc Wenger (CDS) * @author Grégory Mantelet (ARI) * @version 4.1 (02/2015) * @since 4.1 */ public final class EventFrequency { /** String format of a hour or a minute number. */ private static final NumberFormat NN = new DecimalFormat("00"); /** Date-Time format to use in order to identify a frequent event. */ private static final DateFormat EVENT_ID_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmm"); /** Ordered list of all week days (there, the first week day is Sunday). */ private static final String[] WEEK_DAYS = {"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}; /** Ordinal day number suffix (1<b>st</b>, 2<b>nd</b>, 3<b>rd</b> and <b>th</b> for the others). */ private static final String[] DAY_SUFFIX = {"st","nd","rd","th"}; /** Frequency type (D, W, M, h, m). Default value: 'D' */ private char dwm = 'D'; /** "day" (dd) parameter of the frequency. */ private int day = 0; /** "hour" (hh) parameter of the frequency. */ private int hour = 0; /** "minute" (mm) parameter of the frequency. */ private int min = 0; /** ID of the next event. By default, it is built using the date of the last event with the format {@link #EVENT_ID_FORMAT}. */ private String eventID = ""; /** Date (in millisecond) of the next event. */ private long nextEvent = -1; /** * <p>Create a new event frequency.</p> * * <p>The frequency string must respect the following syntax:</p> * <ul> * <li>'D' hh mm : daily schedule at hh:mm</li> * <li>'W' dd hh mm : weekly schedule at the given day of the week (1:sunday, 2:monday, ..., 7:saturday) at hh:mm</li> * <li>'M' dd hh mm : monthly schedule at the given day of the month at hh:mm</li> * <li>'h' mm : hourly schedule at the given minute</li> * <li>'m' : scheduled every minute (for completness :-))</li> * </ul> * <p><i>Where: hh = integer between 0 and 23, mm = integer between 0 and 59, dd (for 'W') = integer between 1 and 7 (1:sunday, 2:monday, ..., 7:saturday), * dd (for 'M') = integer between 1 and 31.</i></p> * * <p><i><b>Warning:</b> * The frequency type is case sensitive! Then you should particularly pay attention at the case * when using the frequency types 'M' (monthly) and 'm' (every minute). * </i></p> * * <p> * Parsing errors are not thrown but "resolved" silently. The "solution" depends of the error. * 2 cases of errors are considered: * </p> * <ul> * <li><b>Frequency type mismatch:</b> It happens when the first character is not one of the expected (D, W, M, h, m). * That means: bad case (i.e. 'd' rather than 'D'), another character. * In this case, the frequency will be: <b>daily at 00:00</b>.</li> * * <li><b>Parameter(s) missing or incorrect:</b> With the "daily" frequency ('D'), at least 2 parameters must be provided ; * 3 for "weekly" ('W') and "monthly" ('M') ; only 1 for "hourly" ('h') ; none for "every minute" ('m'). * This number of parameters is a minimum: only the n first parameters will be considered while * the others will be ignored. * If this minimum number of parameters is not respected or if a parameter value is incorrect, * <b>all parameters will be set to their default value</b> * (which is 0 for all parameter except dd for which it is 1).</li> * </ul> * * <p>Examples:</p> * <ul> * <li><i>"" or NULL</i> = every day at 00:00</li> * <li><i>"D 06 30" or "D 6 30"</i> = every day at 06:30</li> * <li><i>"D 24 30"</i> = every day at 00:00, because hh must respect the rule: 0 ≤ hh ≤ 23</li> * <li><i>"d 06 30" or "T 06 30"</i> = every day at 00:00, because the frequency type "d" (lower case of "D") or "T" do not exist</li> * <li><i>"W 2 6 30"</i> = every week on Tuesday at 06:30</li> * <li><i>"W 8 06 30"</i> = every week on Sunday at 00:00, because with 'W' dd must respect the rule: 1 ≤ dd ≤ 7</li> * <li><i>"M 2 6 30"</i> = every month on the 2nd at 06:30</li> * <li><i>"M 32 6 30"</i> = every month on the 1st at 00:00, because with 'M' dd must respect the rule: 1 ≤ dd ≤ 31</li> * <li><i>"M 5 6 30 12"</i> = every month on the 5th at 06:30, because at least 3 parameters are expected and so considered: "12" and eventual other parameters are ignored</li> * </ul> * * @param interval A string defining the event frequency (see above for the string format). */ public EventFrequency(String interval){ String str; // Determine the separation between the frequency type character (D, W, M, h, m) and the parameters // and normalize the given interval: int p = -1; if (interval == null) interval = ""; else{ interval = interval.replaceAll("[ \t]+", " ").trim(); p = interval.indexOf(' '); } // Parse the given interval ONLY IF a frequency type is provided (even if there is no parameter): if (p == 1 || interval.length() == 1){ MatchResult result; Scanner scan = null; // Extract and identify the frequency type: dwm = interval.charAt(0); str = interval.substring(p + 1); scan = new Scanner(str); // Extract the parameters in function of the frequency type: switch(dwm){ // CASE: DAILY case 'D': scan.findInLine("(\\d{1,2}) (\\d{1,2})"); try{ result = scan.match(); hour = parseHour(result.group(1)); min = parseMinute(result.group(2)); }catch(IllegalStateException ise){ day = hour = min = 0; } break; // CASE: WEEKLY AND MONTHLY case 'W': case 'M': scan.findInLine("(\\d{1,2}) (\\d{1,2}) (\\d{1,2})"); try{ result = scan.match(); day = (dwm == 'W') ? parseDayOfWeek(result.group(1)) : parseDayOfMonth(result.group(1)); hour = parseHour(result.group(2)); min = parseMinute(result.group(3)); }catch(IllegalStateException ise){ day = (dwm == 'W') ? 0 : 1; hour = min = 0; } break; // CASE: HOURLY case 'h': scan.findInLine("(\\d{1,2})"); try{ result = scan.match(); min = parseMinute(result.group(1)); }catch(IllegalStateException ise){ min = 0; } break; // CASE: EVERY MINUTE case 'm': // no other data needed break; // CASE: UNKNOWN FREQUENCY TYPE default: dwm = 'D'; day = hour = min = 0; } if (scan != null) scan.close(); } } /** * Parse a string representing the day of the week (as a number). * * @param dayNbStr String containing an integer representing a week day. * * @return The identified week day. (integer between 0 and 6 (included)) * * @throws IllegalStateException If the given string does not contain an integer or is not between 1 and 7 (included). */ private int parseDayOfWeek(final String dayNbStr) throws IllegalStateException{ try{ int d = Integer.parseInt(dayNbStr); if (d >= 1 && d <= WEEK_DAYS.length) return d - 1; }catch(Exception e){} throw new IllegalStateException("Incorrect day of week (" + dayNbStr + ") ; it should be between 1 and 7 (both included)!"); } /** * Parse a string representing the day of the month. * * @param dayStr String containing an integer representing a month day. * * @return The identified month day. (integer between 1 and 31 (included)) * * @throws IllegalStateException If the given string does not contain an integer or is not between 1 and 31 (included). */ private int parseDayOfMonth(final String dayStr) throws IllegalStateException{ try{ int d = Integer.parseInt(dayStr); if (d >= 1 && d <= 31) return d; }catch(Exception e){} throw new IllegalStateException("Incorrect day of month (" + dayStr + ") ; it should be between 1 and 31 (both included)!"); } /** * Parse a string representing the hour part of a time (<b>hh</b>:mm). * * @param hourStr String containing an integer representing an hour. * * @return The identified hour. (integer between 0 and 23 (included)) * * @throws IllegalStateException If the given string does not contain an integer or is not between 0 and 23 (included). */ private int parseHour(final String hourStr) throws IllegalStateException{ try{ int h = Integer.parseInt(hourStr); if (h >= 0 && h <= 23) return h; }catch(Exception e){} throw new IllegalStateException("Incorrect hour number(" + hourStr + ") ; it should be between 0 and 23 (both included)!"); } /** * Parse a string representing the minute part of a time (hh:<b>mm</b>). * * @param minStr String containing an integer representing a minute. * * @return The identified minute. (integer between 0 and 59 (included)) * * @throws IllegalStateException If the given string does not contain an integer or is not between 0 and 59 (included). */ private int parseMinute(final String minStr) throws IllegalStateException{ try{ int m = Integer.parseInt(minStr); if (m >= 0 && m <= 59) return m; }catch(Exception e){} throw new IllegalStateException("Incorrect minute number (" + minStr + ") ; it should be between 0 and 59 (both included)!"); } /** * Tell whether the interval between the last event and now is greater or equals to the frequency represented by this object. * * @return <i>true</i> if the next event date has been reached, <i>false</i> otherwise. */ public boolean isTimeElapsed(){ return (nextEvent <= 0) || (System.currentTimeMillis() >= nextEvent); } /** * Get the date of the next event. * * @return Date of the next event, or NULL if no date has yet been computed. */ public Date getNextEvent(){ return (nextEvent <= 0) ? null : new Date(nextEvent); } /** * <p>Get a string which identity the period between the last event and the next one (whose the date has been computed by this object).</p> * * <p>This ID is built by formatting in string the given date of the last event.</p> * * @return ID of the period before the next event. */ public String getEventID(){ return eventID; } /** * <p>Compute the date of the event, by adding the interval represented by this object to the current date/time.</p> * * <p> * The role of this function is to compute the next event date, not to get it. After computation, you can get this date * thanks to {@link #getNextEvent()}. Furthermore, using {@link #isTimeElapsed()} after having called this function will * let you test whether the next event should (have) occur(red). * </p> * * <p><i>Note: * This function computes the next event date by taking the current date as the date of the last event. However, * if the last event occurred at a different date, you should use {@link #nextEvent(Date)}. * </i></p> * * @return Date at which the next event should occur. (basically, it is: NOW + frequency) */ public Date nextEvent(){ return nextEvent(new Date()); } /** * <p>Compute the date of the event, by adding the interval represented by this object to the given date/time.</p> * * <p> * The role of this function is to compute the next event date, not to get it. After computation, you can get this date * thanks to {@link #getNextEvent()}. Furthermore, using {@link #isTimeElapsed()} after having called this function will * let you test whether the next event should (have) occur(red). * </p> * * @return Date at which the next event should occur. (basically, it is lastEventDate + frequency) */ public Date nextEvent(final Date lastEventDate){ // Set the calendar to the given date: GregorianCalendar date = new GregorianCalendar(); date.setTime(lastEventDate); // Compute the date of the next event: switch(dwm){ // CASE: DAILY case 'D': date.add(Calendar.DAY_OF_YEAR, 1); date.set(Calendar.HOUR_OF_DAY, hour); date.set(Calendar.MINUTE, min); date.set(Calendar.SECOND, 0); break; // CASE: WEEKLY case 'W': // find the next right day to trigger the rotation int weekday = date.get(Calendar.DAY_OF_WEEK); // sunday=1, ... saturday=7 if (weekday == day){ date.add(Calendar.WEEK_OF_YEAR, 1); }else{ // for the first scheduling which can happen any day int delta = day - weekday; if (delta <= 0) delta += 7; date.add(Calendar.DAY_OF_YEAR, delta); } date.set(Calendar.HOUR_OF_DAY, hour); date.set(Calendar.MINUTE, min); date.set(Calendar.SECOND, 0); break; // CASE: MONTHLY case 'M': date.add(Calendar.MONTH, 1); date.set(Calendar.DAY_OF_MONTH, day); date.set(Calendar.HOUR_OF_DAY, hour); date.set(Calendar.MINUTE, min); date.set(Calendar.SECOND, 0); break; // CASE: HOURLY case 'h': date.add(Calendar.HOUR_OF_DAY, 1); date.set(Calendar.MINUTE, min); date.set(Calendar.SECOND, 0); break; // CASE: EVERY MINUTE case 'm': date.add(Calendar.MINUTE, 1); date.set(Calendar.SECOND, 0); break; /* OTHERWISE, the next event date is the given date! */ } // Save it in millisecond for afterward comparison with the current time (so that telling whether the time is elapsed or not): nextEvent = date.getTimeInMillis(); // Build the ID of this waiting period (the period between the last event and the next one): eventID = EVENT_ID_FORMAT.format(new Date()); // Return the date of the next event: return date.getTime(); } /** * Display in a human readable way the frequency represented by this object. * * @return a string, i.e. weekly on Sunday at HH:MM */ @Override public String toString(){ StringBuilder str = new StringBuilder(); switch(dwm){ case 'D': str.append("daily"); str.append(" at ").append(NN.format(hour)).append(':').append(NN.format(min)); break; case 'W': str.append("weekly on ").append(WEEK_DAYS[day % 7]); str.append(" at ").append(NN.format(hour)).append(':').append(NN.format(min)); break; case 'M': str.append("monthly on the ").append(day).append(DAY_SUFFIX[Math.min(day - 1, 3)]); str.append(" at ").append(NN.format(hour)).append(':').append(NN.format(min)); break; case 'h': str.append("hourly at ").append(NN.format(min)); break; case 'm': str.append("every minute"); break; } return str.toString(); } }