定时器CronExpression配置说明详解
项目中经常要使用到定时器,其中CronExpression配置非常重要。现在就配置说明详细解说一下:
CronExpression表达式是由6个必需字段(秒、分、时、日、月、周)和一个可选字段(年)通过空格组成。
序号 | 字段名 | 允许值 | 允许特殊字符 |
1 | 秒 | 0-59 | , - * / |
2 | 分 | 0-59 | , - * / |
3 | 时 | 0-23 | , - * / |
4 | 日 | 1-31 | , - * ? / L W |
5 | 月 | 1-12 or JAN-DEC | , - * / |
6 | 周 | 1-7 or SUN-SAT | , - * ? / L # |
7 | 年(可选字段) | empty, 1970-2199 | , - * / |
下面对特殊字符表达意思进行解说:
* 代表所有值,比如一分钟里代表每一个分钟。
? 只允许使用在日和周的表达式中,代表不定值。使用的场景为不需要关心当前设置这个字段的值,例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
- 代表在一个范围里,比如10-12 代表10、11、12
,代表指定多个值,比如 在周字段配置 1,3,5 代表周一、周三、周五
/ 代表递增触发,比如在秒字段 0/15 代表0,15,30,45 每15秒执行一次。 5/15代表 5,20,35,50
L 只允许使用在日和周的表达式中,L是last的简称,配置在日中代表一个月的最后一天,比如1月则为31日,2月非闰年则为28日,闰年为29日那天。
配置在周字段里,代表7或者星期六。6L代表这个月的最后一个星期五。
W 只允许使用在日字段里,代表最近的工作日(周一到周五),比如15w, 这个月的15号是星期六,则14号(星期五)触发,如果是星期日,则16号(星期一)触发,如果是星期二,则星期二触发。
LW也可以联合使用,如果配置LW,则代表这个月最后的工作日。
# 值允许使用在周字段里,比如6#3 代表这个月的第三个星期五 (6代表星期五,#3代表这个月的第三个)
注意:月和周字段是不区分大小写(JAN Jan MON mon)
配置指定日和周字段里,不完整,需要在其中一个用?代替配置。
溢出范围是支持的,但是溢出范围过大可能会出问题。比如配置22-2 晚上10点到凌晨2点, NOV-FEB 代表11月-2月。这个是支持的,但14-6,这个就溢出太对,可能会出问题。
下面就例子进行说明:
0 0 12 * * ? 每天中午十二点触发
0 15 10 ? * * 每天早上10:15触发
0 15 10 * * ? 每天早上10:15触发
0 15 10 * * ? * 每天早上10:15触发
0 15 10 * * ? 2005 2005年的每天早上10:15触发
0 * 14 * * ? 每天从下午2点开始到2点59分每分钟一次触发
0 0/5 14 * * ? 每天从下午2点开始到2:55分结束每5分钟一次触发
0 0/5 14,18 * * ? 每天的下午2点至2:55和6点至6点55分两个时间段内每5分钟一次触发
0 0-5 14 * * ? 每天14:00至14:05每分钟一次触发
0 10,44 14 ? 3 WED 三月的每周三的14:10和14:44触发
0 15 10 ? * MON-FRI 每个周一、周二、周三、周四、周五的10:15触发
0 15 10 15 * ? 每月15号上午10点15分触发
0 15 10 L * ? 每月最后一天的10点15分触发
0 15 10 ? * 6L 每月最后一周的星期五的10点15分触发
0 15 10 ? * 6L 2002-2005 从2002年到2005年每月最后一周的星期五的10点15分触发
0 15 10 ? * 6#3 每月的第三周的星期五10点15分触发开始触发
具体可参照quartz源码
CronTrigger类
1 /* 2 * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy 6 * of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations 14 * under the License. 15 * 16 */ 17 18 package org.quartz; 19 20 import java.util.Calendar; 21 import java.util.TimeZone; 22 23 /** 24 * The public interface for inspecting settings specific to a CronTrigger, . 25 * which is used to fire a <code>{@link org.quartz.Job}</code> 26 * at given moments in time, defined with Unix 'cron-like' schedule definitions. 27 * 28 * <p> 29 * For those unfamiliar with "cron", this means being able to create a firing 30 * schedule such as: "At 8:00am every Monday through Friday" or "At 1:30am 31 * every last Friday of the month". 32 * </p> 33 * 34 * <p> 35 * The format of a "Cron-Expression" string is documented on the 36 * {@link org.quartz.CronExpression} class. 37 * </p> 38 * 39 * <p> 40 * Here are some full examples: <br><table cellspacing="8"> 41 * <tr> 42 * <th align="left">Expression</th> 43 * <th align="left"> </th> 44 * <th align="left">Meaning</th> 45 * </tr> 46 * <tr> 47 * <td align="left"><code>"0 0 12 * * ?"</code></td> 48 * <td align="left"> </th> 49 * <td align="left"><code>Fire at 12pm (noon) every day</code></td> 50 * </tr> 51 * <tr> 52 * <td align="left"><code>"0 15 10 ? * *"</code></td> 53 * <td align="left"> </th> 54 * <td align="left"><code>Fire at 10:15am every day</code></td> 55 * </tr> 56 * <tr> 57 * <td align="left"><code>"0 15 10 * * ?"</code></td> 58 * <td align="left"> </th> 59 * <td align="left"><code>Fire at 10:15am every day</code></td> 60 * </tr> 61 * <tr> 62 * <td align="left"><code>"0 15 10 * * ? *"</code></td> 63 * <td align="left"> </th> 64 * <td align="left"><code>Fire at 10:15am every day</code></td> 65 * </tr> 66 * <tr> 67 * <td align="left"><code>"0 15 10 * * ? 2005"</code></td> 68 * <td align="left"> </th> 69 * <td align="left"><code>Fire at 10:15am every day during the year 2005</code> 70 * </td> 71 * </tr> 72 * <tr> 73 * <td align="left"><code>"0 * 14 * * ?"</code></td> 74 * <td align="left"> </th> 75 * <td align="left"><code>Fire every minute starting at 2pm and ending at 2:59pm, every day</code> 76 * </td> 77 * </tr> 78 * <tr> 79 * <td align="left"><code>"0 0/5 14 * * ?"</code></td> 80 * <td align="left"> </th> 81 * <td align="left"><code>Fire every 5 minutes starting at 2pm and ending at 2:55pm, every day</code> 82 * </td> 83 * </tr> 84 * <tr> 85 * <td align="left"><code>"0 0/5 14,18 * * ?"</code></td> 86 * <td align="left"> </th> 87 * <td align="left"><code>Fire every 5 minutes starting at 2pm and ending at 2:55pm, AND fire every 5 minutes starting at 6pm and ending at 6:55pm, every day</code> 88 * </td> 89 * </tr> 90 * <tr> 91 * <td align="left"><code>"0 0-5 14 * * ?"</code></td> 92 * <td align="left"> </th> 93 * <td align="left"><code>Fire every minute starting at 2pm and ending at 2:05pm, every day</code> 94 * </td> 95 * </tr> 96 * <tr> 97 * <td align="left"><code>"0 10,44 14 ? 3 WED"</code></td> 98 * <td align="left"> </th> 99 * <td align="left"><code>Fire at 2:10pm and at 2:44pm every Wednesday in the month of March.</code> 100 * </td> 101 * </tr> 102 * <tr> 103 * <td align="left"><code>"0 15 10 ? * MON-FRI"</code></td> 104 * <td align="left"> </th> 105 * <td align="left"><code>Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday</code> 106 * </td> 107 * </tr> 108 * <tr> 109 * <td align="left"><code>"0 15 10 15 * ?"</code></td> 110 * <td align="left"> </th> 111 * <td align="left"><code>Fire at 10:15am on the 15th day of every month</code> 112 * </td> 113 * </tr> 114 * <tr> 115 * <td align="left"><code>"0 15 10 L * ?"</code></td> 116 * <td align="left"> </th> 117 * <td align="left"><code>Fire at 10:15am on the last day of every month</code> 118 * </td> 119 * </tr> 120 * <tr> 121 * <td align="left"><code>"0 15 10 ? * 6L"</code></td> 122 * <td align="left"> </th> 123 * <td align="left"><code>Fire at 10:15am on the last Friday of every month</code> 124 * </td> 125 * </tr> 126 * <tr> 127 * <td align="left"><code>"0 15 10 ? * 6L"</code></td> 128 * <td align="left"> </th> 129 * <td align="left"><code>Fire at 10:15am on the last Friday of every month</code> 130 * </td> 131 * </tr> 132 * <tr> 133 * <td align="left"><code>"0 15 10 ? * 6L 2002-2005"</code></td> 134 * <td align="left"> </th> 135 * <td align="left"><code>Fire at 10:15am on every last Friday of every month during the years 2002, 2003, 2004 and 2005</code> 136 * </td> 137 * </tr> 138 * <tr> 139 * <td align="left"><code>"0 15 10 ? * 6#3"</code></td> 140 * <td align="left"> </th> 141 * <td align="left"><code>Fire at 10:15am on the third Friday of every month</code> 142 * </td> 143 * </tr> 144 * </table> 145 * </p> 146 * 147 * <p> 148 * Pay attention to the effects of '?' and '*' in the day-of-week and 149 * day-of-month fields! 150 * </p> 151 * 152 * <p> 153 * <b>NOTES:</b> 154 * <ul> 155 * <li>Support for specifying both a day-of-week and a day-of-month value is 156 * not complete (you'll need to use the '?' character in on of these fields). 157 * </li> 158 * <li>Be careful when setting fire times between mid-night and 1:00 AM - 159 * "daylight savings" can cause a skip or a repeat depending on whether the 160 * time moves back or jumps forward.</li> 161 * </ul> 162 * </p> 163 * 164 * @see CronScheduleBuilder 165 * @see TriggerBuilder 166 * 167 * @author jhouse 168 * @author Contributions from Mads Henderson 169 */ 170 public interface CronTrigger extends Trigger { 171 172 public static final long serialVersionUID = -8644953146451592766L; 173 174 /** 175 * <p> 176 * Instructs the <code>{@link Scheduler}</code> that upon a mis-fire 177 * situation, the <code>{@link CronTrigger}</code> wants to be fired now 178 * by <code>Scheduler</code>. 179 * </p> 180 */ 181 public static final int MISFIRE_INSTRUCTION_FIRE_ONCE_NOW = 1; 182 183 /** 184 * <p> 185 * Instructs the <code>{@link Scheduler}</code> that upon a mis-fire 186 * situation, the <code>{@link CronTrigger}</code> wants to have it's 187 * next-fire-time updated to the next time in the schedule after the 188 * current time (taking into account any associated <code>{@link Calendar}</code>, 189 * but it does not want to be fired now. 190 * </p> 191 */ 192 public static final int MISFIRE_INSTRUCTION_DO_NOTHING = 2; 193 194 public String getCronExpression(); 195 196 /** 197 * <p> 198 * Returns the time zone for which the <code>cronExpression</code> of 199 * this <code>CronTrigger</code> will be resolved. 200 * </p> 201 */ 202 public TimeZone getTimeZone(); 203 204 public String getExpressionSummary(); 205 206 TriggerBuilder<CronTrigger> getTriggerBuilder(); 207 }
CronExpression 类
1 /* 2 * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy 6 * of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations 14 * under the License. 15 * 16 */ 17 18 package org.quartz; 19 20 import java.io.Serializable; 21 import java.text.ParseException; 22 import java.util.Calendar; 23 import java.util.Date; 24 import java.util.HashMap; 25 import java.util.Iterator; 26 import java.util.Locale; 27 import java.util.Map; 28 import java.util.SortedSet; 29 import java.util.StringTokenizer; 30 import java.util.TimeZone; 31 import java.util.TreeSet; 32 33 /** 34 * Provides a parser and evaluator for unix-like cron expressions. Cron 35 * expressions provide the ability to specify complex time combinations such as 36 * "At 8:00am every Monday through Friday" or "At 1:30am every 37 * last Friday of the month". 38 * <P> 39 * Cron expressions are comprised of 6 required fields and one optional field 40 * separated by white space. The fields respectively are described as follows: 41 * 42 * <table cellspacing="8"> 43 * <tr> 44 * <th align="left">Field Name</th> 45 * <th align="left"> </th> 46 * <th align="left">Allowed Values</th> 47 * <th align="left"> </th> 48 * <th align="left">Allowed Special Characters</th> 49 * </tr> 50 * <tr> 51 * <td align="left"><code>Seconds</code></td> 52 * <td align="left"> </th> 53 * <td align="left"><code>0-59</code></td> 54 * <td align="left"> </th> 55 * <td align="left"><code>, - * /</code></td> 56 * </tr> 57 * <tr> 58 * <td align="left"><code>Minutes</code></td> 59 * <td align="left"> </th> 60 * <td align="left"><code>0-59</code></td> 61 * <td align="left"> </th> 62 * <td align="left"><code>, - * /</code></td> 63 * </tr> 64 * <tr> 65 * <td align="left"><code>Hours</code></td> 66 * <td align="left"> </th> 67 * <td align="left"><code>0-23</code></td> 68 * <td align="left"> </th> 69 * <td align="left"><code>, - * /</code></td> 70 * </tr> 71 * <tr> 72 * <td align="left"><code>Day-of-month</code></td> 73 * <td align="left"> </th> 74 * <td align="left"><code>1-31</code></td> 75 * <td align="left"> </th> 76 * <td align="left"><code>, - * ? / L W</code></td> 77 * </tr> 78 * <tr> 79 * <td align="left"><code>Month</code></td> 80 * <td align="left"> </th> 81 * <td align="left"><code>1-12 or JAN-DEC</code></td> 82 * <td align="left"> </th> 83 * <td align="left"><code>, - * /</code></td> 84 * </tr> 85 * <tr> 86 * <td align="left"><code>Day-of-Week</code></td> 87 * <td align="left"> </th> 88 * <td align="left"><code>1-7 or SUN-SAT</code></td> 89 * <td align="left"> </th> 90 * <td align="left"><code>, - * ? / L #</code></td> 91 * </tr> 92 * <tr> 93 * <td align="left"><code>Year (Optional)</code></td> 94 * <td align="left"> </th> 95 * <td align="left"><code>empty, 1970-2199</code></td> 96 * <td align="left"> </th> 97 * <td align="left"><code>, - * /</code></td> 98 * </tr> 99 * </table> 100 * <P> 101 * The '*' character is used to specify all values. For example, "*" 102 * in the minute field means "every minute". 103 * <P> 104 * The '?' character is allowed for the day-of-month and day-of-week fields. It 105 * is used to specify 'no specific value'. This is useful when you need to 106 * specify something in one of the two fields, but not the other. 107 * <P> 108 * The '-' character is used to specify ranges For example "10-12" in 109 * the hour field means "the hours 10, 11 and 12". 110 * <P> 111 * The ',' character is used to specify additional values. For example 112 * "MON,WED,FRI" in the day-of-week field means "the days Monday, 113 * Wednesday, and Friday". 114 * <P> 115 * The '/' character is used to specify increments. For example "0/15" 116 * in the seconds field means "the seconds 0, 15, 30, and 45". And 117 * "5/15" in the seconds field means "the seconds 5, 20, 35, and 118 * 50". Specifying '*' before the '/' is equivalent to specifying 0 is 119 * the value to start with. Essentially, for each field in the expression, there 120 * is a set of numbers that can be turned on or off. For seconds and minutes, 121 * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to 122 * 31, and for months 1 to 12. The "/" character simply helps you turn 123 * on every "nth" value in the given set. Thus "7/6" in the 124 * month field only turns on month "7", it does NOT mean every 6th 125 * month, please note that subtlety. 126 * <P> 127 * The 'L' character is allowed for the day-of-month and day-of-week fields. 128 * This character is short-hand for "last", but it has different 129 * meaning in each of the two fields. For example, the value "L" in 130 * the day-of-month field means "the last day of the month" - day 31 131 * for January, day 28 for February on non-leap years. If used in the 132 * day-of-week field by itself, it simply means "7" or 133 * "SAT". But if used in the day-of-week field after another value, it 134 * means "the last xxx day of the month" - for example "6L" 135 * means "the last friday of the month". You can also specify an offset 136 * from the last day of the month, such as "L-3" which would mean the third-to-last 137 * day of the calendar month. <i>When using the 'L' option, it is important not to 138 * specify lists, or ranges of values, as you'll get confusing/unexpected results.</i> 139 * <P> 140 * The 'W' character is allowed for the day-of-month field. This character 141 * is used to specify the weekday (Monday-Friday) nearest the given day. As an 142 * example, if you were to specify "15W" as the value for the 143 * day-of-month field, the meaning is: "the nearest weekday to the 15th of 144 * the month". So if the 15th is a Saturday, the trigger will fire on 145 * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the 146 * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. 147 * However if you specify "1W" as the value for day-of-month, and the 148 * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not 149 * 'jump' over the boundary of a month's days. The 'W' character can only be 150 * specified when the day-of-month is a single day, not a range or list of days. 151 * <P> 152 * The 'L' and 'W' characters can also be combined for the day-of-month 153 * expression to yield 'LW', which translates to "last weekday of the 154 * month". 155 * <P> 156 * The '#' character is allowed for the day-of-week field. This character is 157 * used to specify "the nth" XXX day of the month. For example, the 158 * value of "6#3" in the day-of-week field means the third Friday of 159 * the month (day 6 = Friday and "#3" = the 3rd one in the month). 160 * Other examples: "2#1" = the first Monday of the month and 161 * "4#5" = the fifth Wednesday of the month. Note that if you specify 162 * "#5" and there is not 5 of the given day-of-week in the month, then 163 * no firing will occur that month. If the '#' character is used, there can 164 * only be one expression in the day-of-week field ("3#1,6#3" is 165 * not valid, since there are two expressions). 166 * <P> 167 * <!--The 'C' character is allowed for the day-of-month and day-of-week fields. 168 * This character is short-hand for "calendar". This means values are 169 * calculated against the associated calendar, if any. If no calendar is 170 * associated, then it is equivalent to having an all-inclusive calendar. A 171 * value of "5C" in the day-of-month field means "the first day included by the 172 * calendar on or after the 5th". A value of "1C" in the day-of-week field 173 * means "the first day included by the calendar on or after Sunday".--> 174 * <P> 175 * The legal characters and the names of months and days of the week are not 176 * case sensitive. 177 * 178 * <p> 179 * <b>NOTES:</b> 180 * <ul> 181 * <li>Support for specifying both a day-of-week and a day-of-month value is 182 * not complete (you'll need to use the '?' character in one of these fields). 183 * </li> 184 * <li>Overflowing ranges is supported - that is, having a larger number on 185 * the left hand side than the right. You might do 22-2 to catch 10 o'clock 186 * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is 187 * very important to note that overuse of overflowing ranges creates ranges 188 * that don't make sense and no effort has been made to determine which 189 * interpretation CronExpression chooses. An example would be 190 * "0 0 14-6 ? * FRI-MON". </li> 191 * </ul> 192 * </p> 193 * 194 * 195 * @author Sharada Jambula, James House 196 * @author Contributions from Mads Henderson 197 * @author Refactoring from CronTrigger to CronExpression by Aaron Craven 198 */ 199 public final class CronExpression implements Serializable, Cloneable { 200 201 private static final long serialVersionUID = 12423409423L; 202 203 protected static final int SECOND = 0; 204 protected static final int MINUTE = 1; 205 protected static final int HOUR = 2; 206 protected static final int DAY_OF_MONTH = 3; 207 protected static final int MONTH = 4; 208 protected static final int DAY_OF_WEEK = 5; 209 protected static final int YEAR = 6; 210 protected static final int ALL_SPEC_INT = 99; // '*' 211 protected static final int NO_SPEC_INT = 98; // '?' 212 protected static final Integer ALL_SPEC = ALL_SPEC_INT; 213 protected static final Integer NO_SPEC = NO_SPEC_INT; 214 215 protected static final Map<String, Integer> monthMap = new HashMap<String, Integer>(20); 216 protected static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60); 217 static { 218 monthMap.put("JAN", 0); 219 monthMap.put("FEB", 1); 220 monthMap.put("MAR", 2); 221 monthMap.put("APR", 3); 222 monthMap.put("MAY", 4); 223 monthMap.put("JUN", 5); 224 monthMap.put("JUL", 6); 225 monthMap.put("AUG", 7); 226 monthMap.put("SEP", 8); 227 monthMap.put("OCT", 9); 228 monthMap.put("NOV", 10); 229 monthMap.put("DEC", 11); 230 231 dayMap.put("SUN", 1); 232 dayMap.put("MON", 2); 233 dayMap.put("TUE", 3); 234 dayMap.put("WED", 4); 235 dayMap.put("THU", 5); 236 dayMap.put("FRI", 6); 237 dayMap.put("SAT", 7); 238 } 239 240 private final String cronExpression; 241 private TimeZone timeZone = null; 242 protected transient TreeSet<Integer> seconds; 243 protected transient TreeSet<Integer> minutes; 244 protected transient TreeSet<Integer> hours; 245 protected transient TreeSet<Integer> daysOfMonth; 246 protected transient TreeSet<Integer> months; 247 protected transient TreeSet<Integer> daysOfWeek; 248 protected transient TreeSet<Integer> years; 249 250 protected transient boolean lastdayOfWeek = false; 251 protected transient int nthdayOfWeek = 0; 252 protected transient boolean lastdayOfMonth = false; 253 protected transient boolean nearestWeekday = false; 254 protected transient int lastdayOffset = 0; 255 protected transient boolean expressionParsed = false; 256 257 public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; 258 259 /** 260 * Constructs a new <CODE>CronExpression</CODE> based on the specified 261 * parameter. 262 * 263 * @param cronExpression String representation of the cron expression the 264 * new object should represent 265 * @throws java.text.ParseException 266 * if the string expression cannot be parsed into a valid 267 * <CODE>CronExpression</CODE> 268 */ 269 public CronExpression(String cronExpression) throws ParseException { 270 if (cronExpression == null) { 271 throw new IllegalArgumentException("cronExpression cannot be null"); 272 } 273 274 this.cronExpression = cronExpression.toUpperCase(Locale.US); 275 276 buildExpression(this.cronExpression); 277 } 278 279 /** 280 * Constructs a new {@code CronExpression} as a copy of an existing 281 * instance. 282 * 283 * @param expression 284 * The existing cron expression to be copied 285 */ 286 public CronExpression(CronExpression expression) { 287 /* 288 * We don't call the other constructor here since we need to swallow the 289 * ParseException. We also elide some of the sanity checking as it is 290 * not logically trippable. 291 */ 292 this.cronExpression = expression.getCronExpression(); 293 try { 294 buildExpression(cronExpression); 295 } catch (ParseException ex) { 296 throw new AssertionError(); 297 } 298 if (expression.getTimeZone() != null) { 299 setTimeZone((TimeZone) expression.getTimeZone().clone()); 300 } 301 } 302 303 /** 304 * Indicates whether the given date satisfies the cron expression. Note that 305 * milliseconds are ignored, so two Dates falling on different milliseconds 306 * of the same second will always have the same result here. 307 * 308 * @param date the date to evaluate 309 * @return a boolean indicating whether the given date satisfies the cron 310 * expression 311 */ 312 public boolean isSatisfiedBy(Date date) { 313 Calendar testDateCal = Calendar.getInstance(getTimeZone()); 314 testDateCal.setTime(date); 315 testDateCal.set(Calendar.MILLISECOND, 0); 316 Date originalDate = testDateCal.getTime(); 317 318 testDateCal.add(Calendar.SECOND, -1); 319 320 Date timeAfter = getTimeAfter(testDateCal.getTime()); 321 322 return ((timeAfter != null) && (timeAfter.equals(originalDate))); 323 } 324 325 /** 326 * Returns the next date/time <I>after</I> the given date/time which 327 * satisfies the cron expression. 328 * 329 * @param date the date/time at which to begin the search for the next valid 330 * date/time 331 * @return the next valid date/time 332 */ 333 public Date getNextValidTimeAfter(Date date) { 334 return getTimeAfter(date); 335 } 336 337 /** 338 * Returns the next date/time <I>after</I> the given date/time which does 339 * <I>not</I> satisfy the expression 340 * 341 * @param date the date/time at which to begin the search for the next 342 * invalid date/time 343 * @return the next valid date/time 344 */ 345 public Date getNextInvalidTimeAfter(Date date) { 346 long difference = 1000; 347 348 //move back to the nearest second so differences will be accurate 349 Calendar adjustCal = Calendar.getInstance(getTimeZone()); 350 adjustCal.setTime(date); 351 adjustCal.set(Calendar.MILLISECOND, 0); 352 Date lastDate = adjustCal.getTime(); 353 354 Date newDate; 355 356 //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. 357 358 //keep getting the next included time until it's farther than one second 359 // apart. At that point, lastDate is the last valid fire time. We return 360 // the second immediately following it. 361 while (difference == 1000) { 362 newDate = getTimeAfter(lastDate); 363 if(newDate == null) 364 break; 365 366 difference = newDate.getTime() - lastDate.getTime(); 367 368 if (difference == 1000) { 369 lastDate = newDate; 370 } 371 } 372 373 return new Date(lastDate.getTime() + 1000); 374 } 375 376 /** 377 * Returns the time zone for which this <code>CronExpression</code> 378 * will be resolved. 379 */ 380 public TimeZone getTimeZone() { 381 if (timeZone == null) { 382 timeZone = TimeZone.getDefault(); 383 } 384 385 return timeZone; 386 } 387 388 /** 389 * Sets the time zone for which this <code>CronExpression</code> 390 * will be resolved. 391 */ 392 public void setTimeZone(TimeZone timeZone) { 393 this.timeZone = timeZone; 394 } 395 396 /** 397 * Returns the string representation of the <CODE>CronExpression</CODE> 398 * 399 * @return a string representation of the <CODE>CronExpression</CODE> 400 */ 401 @Override 402 public String toString() { 403 return cronExpression; 404 } 405 406 /** 407 * Indicates whether the specified cron expression can be parsed into a 408 * valid cron expression 409 * 410 * @param cronExpression the expression to evaluate 411 * @return a boolean indicating whether the given expression is a valid cron 412 * expression 413 */ 414 public static boolean isValidExpression(String cronExpression) { 415 416 try { 417 new CronExpression(cronExpression); 418 } catch (ParseException pe) { 419 return false; 420 } 421 422 return true; 423 } 424 425 public static void validateExpression(String cronExpression) throws ParseException { 426 427 new CronExpression(cronExpression); 428 } 429 430 431 //////////////////////////////////////////////////////////////////////////// 432 // 433 // Expression Parsing Functions 434 // 435 //////////////////////////////////////////////////////////////////////////// 436 437 protected void buildExpression(String expression) throws ParseException { 438 expressionParsed = true; 439 440 try { 441 442 if (seconds == null) { 443 seconds = new TreeSet<Integer>(); 444 } 445 if (minutes == null) { 446 minutes = new TreeSet<Integer>(); 447 } 448 if (hours == null) { 449 hours = new TreeSet<Integer>(); 450 } 451 if (daysOfMonth == null) { 452 daysOfMonth = new TreeSet<Integer>(); 453 } 454 if (months == null) { 455 months = new TreeSet<Integer>(); 456 } 457 if (daysOfWeek == null) { 458 daysOfWeek = new TreeSet<Integer>(); 459 } 460 if (years == null) { 461 years = new TreeSet<Integer>(); 462 } 463 464 int exprOn = SECOND; 465 466 StringTokenizer exprsTok = new StringTokenizer(expression, " \t", 467 false); 468 469 while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { 470 String expr = exprsTok.nextToken().trim(); 471 472 // throw an exception if L is used with other days of the month 473 if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { 474 throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); 475 } 476 // throw an exception if L is used with other days of the week 477 if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { 478 throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); 479 } 480 if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { 481 throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); 482 } 483 484 StringTokenizer vTok = new StringTokenizer(expr, ","); 485 while (vTok.hasMoreTokens()) { 486 String v = vTok.nextToken(); 487 storeExpressionVals(0, v, exprOn); 488 } 489 490 exprOn++; 491 } 492 493 if (exprOn <= DAY_OF_WEEK) { 494 throw new ParseException("Unexpected end of expression.", 495 expression.length()); 496 } 497 498 if (exprOn <= YEAR) { 499 storeExpressionVals(0, "*", YEAR); 500 } 501 502 TreeSet<Integer> dow = getSet(DAY_OF_WEEK); 503 TreeSet<Integer> dom = getSet(DAY_OF_MONTH); 504 505 // Copying the logic from the UnsupportedOperationException below 506 boolean dayOfMSpec = !dom.contains(NO_SPEC); 507 boolean dayOfWSpec = !dow.contains(NO_SPEC); 508 509 if (!dayOfMSpec || dayOfWSpec) { 510 if (!dayOfWSpec || dayOfMSpec) { 511 throw new ParseException( 512 "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); 513 } 514 } 515 } catch (ParseException pe) { 516 throw pe; 517 } catch (Exception e) { 518 throw new ParseException("Illegal cron expression format (" 519 + e.toString() + ")", 0); 520 } 521 } 522 523 protected int storeExpressionVals(int pos, String s, int type) 524 throws ParseException { 525 526 int incr = 0; 527 int i = skipWhiteSpace(pos, s); 528 if (i >= s.length()) { 529 return i; 530 } 531 char c = s.charAt(i); 532 if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { 533 String sub = s.substring(i, i + 3); 534 int sval = -1; 535 int eval = -1; 536 if (type == MONTH) { 537 sval = getMonthNumber(sub) + 1; 538 if (sval <= 0) { 539 throw new ParseException("Invalid Month value: '" + sub + "'", i); 540 } 541 if (s.length() > i + 3) { 542 c = s.charAt(i + 3); 543 if (c == '-') { 544 i += 4; 545 sub = s.substring(i, i + 3); 546 eval = getMonthNumber(sub) + 1; 547 if (eval <= 0) { 548 throw new ParseException("Invalid Month value: '" + sub + "'", i); 549 } 550 } 551 } 552 } else if (type == DAY_OF_WEEK) { 553 sval = getDayOfWeekNumber(sub); 554 if (sval < 0) { 555 throw new ParseException("Invalid Day-of-Week value: '" 556 + sub + "'", i); 557 } 558 if (s.length() > i + 3) { 559 c = s.charAt(i + 3); 560 if (c == '-') { 561 i += 4; 562 sub = s.substring(i, i + 3); 563 eval = getDayOfWeekNumber(sub); 564 if (eval < 0) { 565 throw new ParseException( 566 "Invalid Day-of-Week value: '" + sub 567 + "'", i); 568 } 569 } else if (c == '#') { 570 try { 571 i += 4; 572 nthdayOfWeek = Integer.parseInt(s.substring(i)); 573 if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { 574 throw new Exception(); 575 } 576 } catch (Exception e) { 577 throw new ParseException( 578 "A numeric value between 1 and 5 must follow the '#' option", 579 i); 580 } 581 } else if (c == 'L') { 582 lastdayOfWeek = true; 583 i++; 584 } 585 } 586 587 } else { 588 throw new ParseException( 589 "Illegal characters for this position: '" + sub + "'", 590 i); 591 } 592 if (eval != -1) { 593 incr = 1; 594 } 595 addToSet(sval, eval, incr, type); 596 return (i + 3); 597 } 598 599 if (c == '?') { 600 i++; 601 if ((i + 1) < s.length() 602 && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { 603 throw new ParseException("Illegal character after '?': " 604 + s.charAt(i), i); 605 } 606 if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { 607 throw new ParseException( 608 "'?' can only be specfied for Day-of-Month or Day-of-Week.", 609 i); 610 } 611 if (type == DAY_OF_WEEK && !lastdayOfMonth) { 612 int val = daysOfMonth.last(); 613 if (val == NO_SPEC_INT) { 614 throw new ParseException( 615 "'?' can only be specfied for Day-of-Month -OR- Day-of-Week.", 616 i); 617 } 618 } 619 620 addToSet(NO_SPEC_INT, -1, 0, type); 621 return i; 622 } 623 624 if (c == '*' || c == '/') { 625 if (c == '*' && (i + 1) >= s.length()) { 626 addToSet(ALL_SPEC_INT, -1, incr, type); 627 return i + 1; 628 } else if (c == '/' 629 && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s 630 .charAt(i + 1) == '\t')) { 631 throw new ParseException("'/' must be followed by an integer.", i); 632 } else if (c == '*') { 633 i++; 634 } 635 c = s.charAt(i); 636 if (c == '/') { // is an increment specified? 637 i++; 638 if (i >= s.length()) { 639 throw new ParseException("Unexpected end of string.", i); 640 } 641 642 incr = getNumericValue(s, i); 643 644 i++; 645 if (incr > 10) { 646 i++; 647 } 648 if (incr > 59 && (type == SECOND || type == MINUTE)) { 649 throw new ParseException("Increment > 60 : " + incr, i); 650 } else if (incr > 23 && (type == HOUR)) { 651 throw new ParseException("Increment > 24 : " + incr, i); 652 } else if (incr > 31 && (type == DAY_OF_MONTH)) { 653 throw new ParseException("Increment > 31 : " + incr, i); 654 } else if (incr > 7 && (type == DAY_OF_WEEK)) { 655 throw new ParseException("Increment > 7 : " + incr, i); 656 } else if (incr > 12 && (type == MONTH)) { 657 throw new ParseException("Increment > 12 : " + incr, i); 658 } 659 } else { 660 incr = 1; 661 } 662 663 addToSet(ALL_SPEC_INT, -1, incr, type); 664 return i; 665 } else if (c == 'L') { 666 i++; 667 if (type == DAY_OF_MONTH) { 668 lastdayOfMonth = true; 669 } 670 if (type == DAY_OF_WEEK) { 671 addToSet(7, 7, 0, type); 672 } 673 if(type == DAY_OF_MONTH && s.length() > i) { 674 c = s.charAt(i); 675 if(c == '-') { 676 ValueSet vs = getValue(0, s, i+1); 677 lastdayOffset = vs.value; 678 if(lastdayOffset > 30) 679 throw new ParseException("Offset from last day must be <= 30", i+1); 680 i = vs.pos; 681 } 682 if(s.length() > i) { 683 c = s.charAt(i); 684 if(c == 'W') { 685 nearestWeekday = true; 686 i++; 687 } 688 } 689 } 690 return i; 691 } else if (c >= '0' && c <= '9') { 692 int val = Integer.parseInt(String.valueOf(c)); 693 i++; 694 if (i >= s.length()) { 695 addToSet(val, -1, -1, type); 696 } else { 697 c = s.charAt(i); 698 if (c >= '0' && c <= '9') { 699 ValueSet vs = getValue(val, s, i); 700 val = vs.value; 701 i = vs.pos; 702 } 703 i = checkNext(i, s, val, type); 704 return i; 705 } 706 } else { 707 throw new ParseException("Unexpected character: " + c, i); 708 } 709 710 return i; 711 } 712 713 protected int checkNext(int pos, String s, int val, int type) 714 throws ParseException { 715 716 int end = -1; 717 int i = pos; 718 719 if (i >= s.length()) { 720 addToSet(val, end, -1, type); 721 return i; 722 } 723 724 char c = s.charAt(pos); 725 726 if (c == 'L') { 727 if (type == DAY_OF_WEEK) { 728 if(val < 1 || val > 7) 729 throw new ParseException("Day-of-Week values must be between 1 and 7", -1); 730 lastdayOfWeek = true; 731 } else { 732 throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); 733 } 734 TreeSet<Integer> set = getSet(type); 735 set.add(val); 736 i++; 737 return i; 738 } 739 740 if (c == 'W') { 741 if (type == DAY_OF_MONTH) { 742 nearestWeekday = true; 743 } else { 744 throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); 745 } 746 if(val > 31) 747 throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); 748 TreeSet<Integer> set = getSet(type); 749 set.add(val); 750 i++; 751 return i; 752 } 753 754 if (c == '#') { 755 if (type != DAY_OF_WEEK) { 756 throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); 757 } 758 i++; 759 try { 760 nthdayOfWeek = Integer.parseInt(s.substring(i)); 761 if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { 762 throw new Exception(); 763 } 764 } catch (Exception e) { 765 throw new ParseException( 766 "A numeric value between 1 and 5 must follow the '#' option", 767 i); 768 } 769 770 TreeSet<Integer> set = getSet(type); 771 set.add(val); 772 i++; 773 return i; 774 } 775 776 if (c == '-') { 777 i++; 778 c = s.charAt(i); 779 int v = Integer.parseInt(String.valueOf(c)); 780 end = v; 781 i++; 782 if (i >= s.length()) { 783 addToSet(val, end, 1, type); 784 return i; 785 } 786 c = s.charAt(i); 787 if (c >= '0' && c <= '9') { 788 ValueSet vs = getValue(v, s, i); 789 end = vs.value; 790 i = vs.pos; 791 } 792 if (i < s.length() && ((c = s.charAt(i)) == '/')) { 793 i++; 794 c = s.charAt(i); 795 int v2 = Integer.parseInt(String.valueOf(c)); 796 i++; 797 if (i >= s.length()) { 798 addToSet(val, end, v2, type); 799 return i; 800 } 801 c = s.charAt(i); 802 if (c >= '0' && c <= '9') { 803 ValueSet vs = getValue(v2, s, i); 804 int v3 = vs.value; 805 addToSet(val, end, v3, type); 806 i = vs.pos; 807 return i; 808 } else { 809 addToSet(val, end, v2, type); 810 return i; 811 } 812 } else { 813 addToSet(val, end, 1, type); 814 return i; 815 } 816 } 817 818 if (c == '/') { 819 i++; 820 c = s.charAt(i); 821 int v2 = Integer.parseInt(String.valueOf(c)); 822 i++; 823 if (i >= s.length()) { 824 addToSet(val, end, v2, type); 825 return i; 826 } 827 c = s.charAt(i); 828 if (c >= '0' && c <= '9') { 829 ValueSet vs = getValue(v2, s, i); 830 int v3 = vs.value; 831 addToSet(val, end, v3, type); 832 i = vs.pos; 833 return i; 834 } else { 835 throw new ParseException("Unexpected character '" + c + "' after '/'", i); 836 } 837 } 838 839 addToSet(val, end, 0, type); 840 i++; 841 return i; 842 } 843 844 public String getCronExpression() { 845 return cronExpression; 846 } 847 848 public String getExpressionSummary() { 849 StringBuilder buf = new StringBuilder(); 850 851 buf.append("seconds: "); 852 buf.append(getExpressionSetSummary(seconds)); 853 buf.append("\n"); 854 buf.append("minutes: "); 855 buf.append(getExpressionSetSummary(minutes)); 856 buf.append("\n"); 857 buf.append("hours: "); 858 buf.append(getExpressionSetSummary(hours)); 859 buf.append("\n"); 860 buf.append("daysOfMonth: "); 861 buf.append(getExpressionSetSummary(daysOfMonth)); 862 buf.append("\n"); 863 buf.append("months: "); 864 buf.append(getExpressionSetSummary(months)); 865 buf.append("\n"); 866 buf.append("daysOfWeek: "); 867 buf.append(getExpressionSetSummary(daysOfWeek)); 868 buf.append("\n"); 869 buf.append("lastdayOfWeek: "); 870 buf.append(lastdayOfWeek); 871 buf.append("\n"); 872 buf.append("nearestWeekday: "); 873 buf.append(nearestWeekday); 874 buf.append("\n"); 875 buf.append("NthDayOfWeek: "); 876 buf.append(nthdayOfWeek); 877 buf.append("\n"); 878 buf.append("lastdayOfMonth: "); 879 buf.append(lastdayOfMonth); 880 buf.append("\n"); 881 buf.append("years: "); 882 buf.append(getExpressionSetSummary(years)); 883 buf.append("\n"); 884 885 return buf.toString(); 886 } 887 888 protected String getExpressionSetSummary(java.util.Set<Integer> set) { 889 890 if (set.contains(NO_SPEC)) { 891 return "?"; 892 } 893 if (set.contains(ALL_SPEC)) { 894 return "*"; 895 } 896 897 StringBuilder buf = new StringBuilder(); 898 899 Iterator<Integer> itr = set.iterator(); 900 boolean first = true; 901 while (itr.hasNext()) { 902 Integer iVal = itr.next(); 903 String val = iVal.toString(); 904 if (!first) { 905 buf.append(","); 906 } 907 buf.append(val); 908 first = false; 909 } 910 911 return buf.toString(); 912 } 913 914 protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) { 915 916 if (list.contains(NO_SPEC)) { 917 return "?"; 918 } 919 if (list.contains(ALL_SPEC)) { 920 return "*"; 921 } 922 923 StringBuilder buf = new StringBuilder(); 924 925 Iterator<Integer> itr = list.iterator(); 926 boolean first = true; 927 while (itr.hasNext()) { 928 Integer iVal = itr.next(); 929 String val = iVal.toString(); 930 if (!first) { 931 buf.append(","); 932 } 933 buf.append(val); 934 first = false; 935 } 936 937 return buf.toString(); 938 } 939 940 protected int skipWhiteSpace(int i, String s) { 941 for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { 942 ; 943 } 944 945 return i; 946 } 947 948 protected int findNextWhiteSpace(int i, String s) { 949 for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { 950 ; 951 } 952 953 return i; 954 } 955 956 protected void addToSet(int val, int end, int incr, int type) 957 throws ParseException { 958 959 TreeSet<Integer> set = getSet(type); 960 961 if (type == SECOND || type == MINUTE) { 962 if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { 963 throw new ParseException( 964 "Minute and Second values must be between 0 and 59", 965 -1); 966 } 967 } else if (type == HOUR) { 968 if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { 969 throw new ParseException( 970 "Hour values must be between 0 and 23", -1); 971 } 972 } else if (type == DAY_OF_MONTH) { 973 if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) 974 && (val != NO_SPEC_INT)) { 975 throw new ParseException( 976 "Day of month values must be between 1 and 31", -1); 977 } 978 } else if (type == MONTH) { 979 if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { 980 throw new ParseException( 981 "Month values must be between 1 and 12", -1); 982 } 983 } else if (type == DAY_OF_WEEK) { 984 if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) 985 && (val != NO_SPEC_INT)) { 986 throw new ParseException( 987 "Day-of-Week values must be between 1 and 7", -1); 988 } 989 } 990 991 if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { 992 if (val != -1) { 993 set.add(val); 994 } else { 995 set.add(NO_SPEC); 996 } 997 998 return; 999 } 1000 1001 int startAt = val; 1002 int stopAt = end; 1003 1004 if (val == ALL_SPEC_INT && incr <= 0) { 1005 incr = 1; 1006 set.add(ALL_SPEC); // put in a marker, but also fill values 1007 } 1008 1009 if (type == SECOND || type == MINUTE) { 1010 if (stopAt == -1) { 1011 stopAt = 59; 1012 } 1013 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1014 startAt = 0; 1015 } 1016 } else if (type == HOUR) { 1017 if (stopAt == -1) { 1018 stopAt = 23; 1019 } 1020 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1021 startAt = 0; 1022 } 1023 } else if (type == DAY_OF_MONTH) { 1024 if (stopAt == -1) { 1025 stopAt = 31; 1026 } 1027 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1028 startAt = 1; 1029 } 1030 } else if (type == MONTH) { 1031 if (stopAt == -1) { 1032 stopAt = 12; 1033 } 1034 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1035 startAt = 1; 1036 } 1037 } else if (type == DAY_OF_WEEK) { 1038 if (stopAt == -1) { 1039 stopAt = 7; 1040 } 1041 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1042 startAt = 1; 1043 } 1044 } else if (type == YEAR) { 1045 if (stopAt == -1) { 1046 stopAt = MAX_YEAR; 1047 } 1048 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1049 startAt = 1970; 1050 } 1051 } 1052 1053 // if the end of the range is before the start, then we need to overflow into 1054 // the next day, month etc. This is done by adding the maximum amount for that 1055 // type, and using modulus max to determine the value being added. 1056 int max = -1; 1057 if (stopAt < startAt) { 1058 switch (type) { 1059 case SECOND : max = 60; break; 1060 case MINUTE : max = 60; break; 1061 case HOUR : max = 24; break; 1062 case MONTH : max = 12; break; 1063 case DAY_OF_WEEK : max = 7; break; 1064 case DAY_OF_MONTH : max = 31; break; 1065 case YEAR : throw new IllegalArgumentException("Start year must be less than stop year"); 1066 default : throw new IllegalArgumentException("Unexpected type encountered"); 1067 } 1068 stopAt += max; 1069 } 1070 1071 for (int i = startAt; i <= stopAt; i += incr) { 1072 if (max == -1) { 1073 // ie: there's no max to overflow over 1074 set.add(i); 1075 } else { 1076 // take the modulus to get the real value 1077 int i2 = i % max; 1078 1079 // 1-indexed ranges should not include 0, and should include their max 1080 if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) { 1081 i2 = max; 1082 } 1083 1084 set.add(i2); 1085 } 1086 } 1087 } 1088 1089 TreeSet<Integer> getSet(int type) { 1090 switch (type) { 1091 case SECOND: 1092 return seconds; 1093 case MINUTE: 1094 return minutes; 1095 case HOUR: 1096 return hours; 1097 case DAY_OF_MONTH: 1098 return daysOfMonth; 1099 case MONTH: 1100 return months; 1101 case DAY_OF_WEEK: 1102 return daysOfWeek; 1103 case YEAR: 1104 return years; 1105 default: 1106 return null; 1107 } 1108 } 1109 1110 protected ValueSet getValue(int v, String s, int i) { 1111 char c = s.charAt(i); 1112 StringBuilder s1 = new StringBuilder(String.valueOf(v)); 1113 while (c >= '0' && c <= '9') { 1114 s1.append(c); 1115 i++; 1116 if (i >= s.length()) { 1117 break; 1118 } 1119 c = s.charAt(i); 1120 } 1121 ValueSet val = new ValueSet(); 1122 1123 val.pos = (i < s.length()) ? i : i + 1; 1124 val.value = Integer.parseInt(s1.toString()); 1125 return val; 1126 } 1127 1128 protected int getNumericValue(String s, int i) { 1129 int endOfVal = findNextWhiteSpace(i, s); 1130 String val = s.substring(i, endOfVal); 1131 return Integer.parseInt(val); 1132 } 1133 1134 protected int getMonthNumber(String s) { 1135 Integer integer = monthMap.get(s); 1136 1137 if (integer == null) { 1138 return -1; 1139 } 1140 1141 return integer; 1142 } 1143 1144 protected int getDayOfWeekNumber(String s) { 1145 Integer integer = dayMap.get(s); 1146 1147 if (integer == null) { 1148 return -1; 1149 } 1150 1151 return integer; 1152 } 1153 1154 //////////////////////////////////////////////////////////////////////////// 1155 // 1156 // Computation Functions 1157 // 1158 //////////////////////////////////////////////////////////////////////////// 1159 1160 public Date getTimeAfter(Date afterTime) { 1161 1162 // Computation is based on Gregorian year only. 1163 Calendar cl = new java.util.GregorianCalendar(getTimeZone()); 1164 1165 // move ahead one second, since we're computing the time *after* the 1166 // given time 1167 afterTime = new Date(afterTime.getTime() + 1000); 1168 // CronTrigger does not deal with milliseconds 1169 cl.setTime(afterTime); 1170 cl.set(Calendar.MILLISECOND, 0); 1171 1172 boolean gotOne = false; 1173 // loop until we've computed the next time, or we've past the endTime 1174 while (!gotOne) { 1175 1176 //if (endTime != null && cl.getTime().after(endTime)) return null; 1177 if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... 1178 return null; 1179 } 1180 1181 SortedSet<Integer> st = null; 1182 int t = 0; 1183 1184 int sec = cl.get(Calendar.SECOND); 1185 int min = cl.get(Calendar.MINUTE); 1186 1187 // get second................................................. 1188 st = seconds.tailSet(sec); 1189 if (st != null && st.size() != 0) { 1190 sec = st.first(); 1191 } else { 1192 sec = seconds.first(); 1193 min++; 1194 cl.set(Calendar.MINUTE, min); 1195 } 1196 cl.set(Calendar.SECOND, sec); 1197 1198 min = cl.get(Calendar.MINUTE); 1199 int hr = cl.get(Calendar.HOUR_OF_DAY); 1200 t = -1; 1201 1202 // get minute................................................. 1203 st = minutes.tailSet(min); 1204 if (st != null && st.size() != 0) { 1205 t = min; 1206 min = st.first(); 1207 } else { 1208 min = minutes.first(); 1209 hr++; 1210 } 1211 if (min != t) { 1212 cl.set(Calendar.SECOND, 0); 1213 cl.set(Calendar.MINUTE, min); 1214 setCalendarHour(cl, hr); 1215 continue; 1216 } 1217 cl.set(Calendar.MINUTE, min); 1218 1219 hr = cl.get(Calendar.HOUR_OF_DAY); 1220 int day = cl.get(Calendar.DAY_OF_MONTH); 1221 t = -1; 1222 1223 // get hour................................................... 1224 st = hours.tailSet(hr); 1225 if (st != null && st.size() != 0) { 1226 t = hr; 1227 hr = st.first(); 1228 } else { 1229 hr = hours.first(); 1230 day++; 1231 } 1232 if (hr != t) { 1233 cl.set(Calendar.SECOND, 0); 1234 cl.set(Calendar.MINUTE, 0); 1235 cl.set(Calendar.DAY_OF_MONTH, day); 1236 setCalendarHour(cl, hr); 1237 continue; 1238 } 1239 cl.set(Calendar.HOUR_OF_DAY, hr); 1240 1241 day = cl.get(Calendar.DAY_OF_MONTH); 1242 int mon = cl.get(Calendar.MONTH) + 1; 1243 // '+ 1' because calendar is 0-based for this field, and we are 1244 // 1-based 1245 t = -1; 1246 int tmon = mon; 1247 1248 // get day................................................... 1249 boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); 1250 boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); 1251 if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule 1252 st = daysOfMonth.tailSet(day); 1253 if (lastdayOfMonth) { 1254 if(!nearestWeekday) { 1255 t = day; 1256 day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1257 day -= lastdayOffset; 1258 if(t > day) { 1259 mon++; 1260 if(mon > 12) { 1261 mon = 1; 1262 tmon = 3333; // ensure test of mon != tmon further below fails 1263 cl.add(Calendar.YEAR, 1); 1264 } 1265 day = 1; 1266 } 1267 } else { 1268 t = day; 1269 day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1270 day -= lastdayOffset; 1271 1272 java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); 1273 tcal.set(Calendar.SECOND, 0); 1274 tcal.set(Calendar.MINUTE, 0); 1275 tcal.set(Calendar.HOUR_OF_DAY, 0); 1276 tcal.set(Calendar.DAY_OF_MONTH, day); 1277 tcal.set(Calendar.MONTH, mon - 1); 1278 tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); 1279 1280 int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1281 int dow = tcal.get(Calendar.DAY_OF_WEEK); 1282 1283 if(dow == Calendar.SATURDAY && day == 1) { 1284 day += 2; 1285 } else if(dow == Calendar.SATURDAY) { 1286 day -= 1; 1287 } else if(dow == Calendar.SUNDAY && day == ldom) { 1288 day -= 2; 1289 } else if(dow == Calendar.SUNDAY) { 1290 day += 1; 1291 } 1292 1293 tcal.set(Calendar.SECOND, sec); 1294 tcal.set(Calendar.MINUTE, min); 1295 tcal.set(Calendar.HOUR_OF_DAY, hr); 1296 tcal.set(Calendar.DAY_OF_MONTH, day); 1297 tcal.set(Calendar.MONTH, mon - 1); 1298 Date nTime = tcal.getTime(); 1299 if(nTime.before(afterTime)) { 1300 day = 1; 1301 mon++; 1302 } 1303 } 1304 } else if(nearestWeekday) { 1305 t = day; 1306 day = daysOfMonth.first(); 1307 1308 java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); 1309 tcal.set(Calendar.SECOND, 0); 1310 tcal.set(Calendar.MINUTE, 0); 1311 tcal.set(Calendar.HOUR_OF_DAY, 0); 1312 tcal.set(Calendar.DAY_OF_MONTH, day); 1313 tcal.set(Calendar.MONTH, mon - 1); 1314 tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); 1315 1316 int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1317 int dow = tcal.get(Calendar.DAY_OF_WEEK); 1318 1319 if(dow == Calendar.SATURDAY && day == 1) { 1320 day += 2; 1321 } else if(dow == Calendar.SATURDAY) { 1322 day -= 1; 1323 } else if(dow == Calendar.SUNDAY && day == ldom) { 1324 day -= 2; 1325 } else if(dow == Calendar.SUNDAY) { 1326 day += 1; 1327 } 1328 1329 1330 tcal.set(Calendar.SECOND, sec); 1331 tcal.set(Calendar.MINUTE, min); 1332 tcal.set(Calendar.HOUR_OF_DAY, hr); 1333 tcal.set(Calendar.DAY_OF_MONTH, day); 1334 tcal.set(Calendar.MONTH, mon - 1); 1335 Date nTime = tcal.getTime(); 1336 if(nTime.before(afterTime)) { 1337 day = daysOfMonth.first(); 1338 mon++; 1339 } 1340 } else if (st != null && st.size() != 0) { 1341 t = day; 1342 day = st.first(); 1343 // make sure we don't over-run a short month, such as february 1344 int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1345 if (day > lastDay) { 1346 day = daysOfMonth.first(); 1347 mon++; 1348 } 1349 } else { 1350 day = daysOfMonth.first(); 1351 mon++; 1352 } 1353 1354 if (day != t || mon != tmon) { 1355 cl.set(Calendar.SECOND, 0); 1356 cl.set(Calendar.MINUTE, 0); 1357 cl.set(Calendar.HOUR_OF_DAY, 0); 1358 cl.set(Calendar.DAY_OF_MONTH, day); 1359 cl.set(Calendar.MONTH, mon - 1); 1360 // '- 1' because calendar is 0-based for this field, and we 1361 // are 1-based 1362 continue; 1363 } 1364 } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule 1365 if (lastdayOfWeek) { // are we looking for the last XXX day of 1366 // the month? 1367 int dow = daysOfWeek.first(); // desired 1368 // d-o-w 1369 int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w 1370 int daysToAdd = 0; 1371 if (cDow < dow) { 1372 daysToAdd = dow - cDow; 1373 } 1374 if (cDow > dow) { 1375 daysToAdd = dow + (7 - cDow); 1376 } 1377 1378 int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1379 1380 if (day + daysToAdd > lDay) { // did we already miss the 1381 // last one? 1382 cl.set(Calendar.SECOND, 0); 1383 cl.set(Calendar.MINUTE, 0); 1384 cl.set(Calendar.HOUR_OF_DAY, 0); 1385 cl.set(Calendar.DAY_OF_MONTH, 1); 1386 cl.set(Calendar.MONTH, mon); 1387 // no '- 1' here because we are promoting the month 1388 continue; 1389 } 1390 1391 // find date of last occurrence of this day in this month... 1392 while ((day + daysToAdd + 7) <= lDay) { 1393 daysToAdd += 7; 1394 } 1395 1396 day += daysToAdd; 1397 1398 if (daysToAdd > 0) { 1399 cl.set(Calendar.SECOND, 0); 1400 cl.set(Calendar.MINUTE, 0); 1401 cl.set(Calendar.HOUR_OF_DAY, 0); 1402 cl.set(Calendar.DAY_OF_MONTH, day); 1403 cl.set(Calendar.MONTH, mon - 1); 1404 // '- 1' here because we are not promoting the month 1405 continue; 1406 } 1407 1408 } else if (nthdayOfWeek != 0) { 1409 // are we looking for the Nth XXX day in the month? 1410 int dow = daysOfWeek.first(); // desired 1411 // d-o-w 1412 int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w 1413 int daysToAdd = 0; 1414 if (cDow < dow) { 1415 daysToAdd = dow - cDow; 1416 } else if (cDow > dow) { 1417 daysToAdd = dow + (7 - cDow); 1418 } 1419 1420 boolean dayShifted = false; 1421 if (daysToAdd > 0) { 1422 dayShifted = true; 1423 } 1424 1425 day += daysToAdd; 1426 int weekOfMonth = day / 7; 1427 if (day % 7 > 0) { 1428 weekOfMonth++; 1429 } 1430 1431 daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; 1432 day += daysToAdd; 1433 if (daysToAdd < 0 1434 || day > getLastDayOfMonth(mon, cl 1435 .get(Calendar.YEAR))) { 1436 cl.set(Calendar.SECOND, 0); 1437 cl.set(Calendar.MINUTE, 0); 1438 cl.set(Calendar.HOUR_OF_DAY, 0); 1439 cl.set(Calendar.DAY_OF_MONTH, 1); 1440 cl.set(Calendar.MONTH, mon); 1441 // no '- 1' here because we are promoting the month 1442 continue; 1443 } else if (daysToAdd > 0 || dayShifted) { 1444 cl.set(Calendar.SECOND, 0); 1445 cl.set(Calendar.MINUTE, 0); 1446 cl.set(Calendar.HOUR_OF_DAY, 0); 1447 cl.set(Calendar.DAY_OF_MONTH, day); 1448 cl.set(Calendar.MONTH, mon - 1); 1449 // '- 1' here because we are NOT promoting the month 1450 continue; 1451 } 1452 } else { 1453 int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w 1454 int dow = daysOfWeek.first(); // desired 1455 // d-o-w 1456 st = daysOfWeek.tailSet(cDow); 1457 if (st != null && st.size() > 0) { 1458 dow = st.first(); 1459 } 1460 1461 int daysToAdd = 0; 1462 if (cDow < dow) { 1463 daysToAdd = dow - cDow; 1464 } 1465 if (cDow > dow) { 1466 daysToAdd = dow + (7 - cDow); 1467 } 1468 1469 int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1470 1471 if (day + daysToAdd > lDay) { // will we pass the end of 1472 // the month? 1473 cl.set(Calendar.SECOND, 0); 1474 cl.set(Calendar.MINUTE, 0); 1475 cl.set(Calendar.HOUR_OF_DAY, 0); 1476 cl.set(Calendar.DAY_OF_MONTH, 1); 1477 cl.set(Calendar.MONTH, mon); 1478 // no '- 1' here because we are promoting the month 1479 continue; 1480 } else if (daysToAdd > 0) { // are we swithing days? 1481 cl.set(Calendar.SECOND, 0); 1482 cl.set(Calendar.MINUTE, 0); 1483 cl.set(Calendar.HOUR_OF_DAY, 0); 1484 cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); 1485 cl.set(Calendar.MONTH, mon - 1); 1486 // '- 1' because calendar is 0-based for this field, 1487 // and we are 1-based 1488 continue; 1489 } 1490 } 1491 } else { // dayOfWSpec && !dayOfMSpec 1492 throw new UnsupportedOperationException( 1493 "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); 1494 } 1495 cl.set(Calendar.DAY_OF_MONTH, day); 1496 1497 mon = cl.get(Calendar.MONTH) + 1; 1498 // '+ 1' because calendar is 0-based for this field, and we are 1499 // 1-based 1500 int year = cl.get(Calendar.YEAR); 1501 t = -1; 1502 1503 // test for expressions that never generate a valid fire date, 1504 // but keep looping... 1505 if (year > MAX_YEAR) { 1506 return null; 1507 } 1508 1509 // get month................................................... 1510 st = months.tailSet(mon); 1511 if (st != null && st.size() != 0) { 1512 t = mon; 1513 mon = st.first(); 1514 } else { 1515 mon = months.first(); 1516 year++; 1517 } 1518 if (mon != t) { 1519 cl.set(Calendar.SECOND, 0); 1520 cl.set(Calendar.MINUTE, 0); 1521 cl.set(Calendar.HOUR_OF_DAY, 0); 1522 cl.set(Calendar.DAY_OF_MONTH, 1); 1523 cl.set(Calendar.MONTH, mon - 1); 1524 // '- 1' because calendar is 0-based for this field, and we are 1525 // 1-based 1526 cl.set(Calendar.YEAR, year); 1527 continue; 1528 } 1529 cl.set(Calendar.MONTH, mon - 1); 1530 // '- 1' because calendar is 0-based for this field, and we are 1531 // 1-based 1532 1533 year = cl.get(Calendar.YEAR); 1534 t = -1; 1535 1536 // get year................................................... 1537 st = years.tailSet(year); 1538 if (st != null && st.size() != 0) { 1539 t = year; 1540 year = st.first(); 1541 } else { 1542 return null; // ran out of years... 1543 } 1544 1545 if (year != t) { 1546 cl.set(Calendar.SECOND, 0); 1547 cl.set(Calendar.MINUTE, 0); 1548 cl.set(Calendar.HOUR_OF_DAY, 0); 1549 cl.set(Calendar.DAY_OF_MONTH, 1); 1550 cl.set(Calendar.MONTH, 0); 1551 // '- 1' because calendar is 0-based for this field, and we are 1552 // 1-based 1553 cl.set(Calendar.YEAR, year); 1554 continue; 1555 } 1556 cl.set(Calendar.YEAR, year); 1557 1558 gotOne = true; 1559 } // while( !done ) 1560 1561 return cl.getTime(); 1562 } 1563 1564 /** 1565 * Advance the calendar to the particular hour paying particular attention 1566 * to daylight saving problems. 1567 * 1568 * @param cal the calendar to operate on 1569 * @param hour the hour to set 1570 */ 1571 protected void setCalendarHour(Calendar cal, int hour) { 1572 cal.set(java.util.Calendar.HOUR_OF_DAY, hour); 1573 if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) { 1574 cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1); 1575 } 1576 } 1577 1578 /** 1579 * NOT YET IMPLEMENTED: Returns the time before the given time 1580 * that the <code>CronExpression</code> matches. 1581 */ 1582 public Date getTimeBefore(Date endTime) { 1583 // FUTURE_TODO: implement QUARTZ-423 1584 return null; 1585 } 1586 1587 /** 1588 * NOT YET IMPLEMENTED: Returns the final time that the 1589 * <code>CronExpression</code> will match. 1590 */ 1591 public Date getFinalFireTime() { 1592 // FUTURE_TODO: implement QUARTZ-423 1593 return null; 1594 } 1595 1596 protected boolean isLeapYear(int year) { 1597 return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); 1598 } 1599 1600 protected int getLastDayOfMonth(int monthNum, int year) { 1601 1602 switch (monthNum) { 1603 case 1: 1604 return 31; 1605 case 2: 1606 return (isLeapYear(year)) ? 29 : 28; 1607 case 3: 1608 return 31; 1609 case 4: 1610 return 30; 1611 case 5: 1612 return 31; 1613 case 6: 1614 return 30; 1615 case 7: 1616 return 31; 1617 case 8: 1618 return 31; 1619 case 9: 1620 return 30; 1621 case 10: 1622 return 31; 1623 case 11: 1624 return 30; 1625 case 12: 1626 return 31; 1627 default: 1628 throw new IllegalArgumentException("Illegal month number: " 1629 + monthNum); 1630 } 1631 } 1632 1633 1634 private void readObject(java.io.ObjectInputStream stream) 1635 throws java.io.IOException, ClassNotFoundException { 1636 1637 stream.defaultReadObject(); 1638 try { 1639 buildExpression(cronExpression); 1640 } catch (Exception ignore) { 1641 } // never happens 1642 } 1643 1644 @Override 1645 @Deprecated 1646 public Object clone() { 1647 return new CronExpression(this); 1648 } 1649 } 1650 1651 class ValueSet { 1652 public int value; 1653 1654 public int pos; 1655 }