001package com.hfg.util.scheduler; 002 003import com.hfg.util.StringUtil; 004import com.hfg.util.collection.CollectionUtil; 005import com.hfg.util.collection.OrderedSet; 006 007import java.util.ArrayList; 008import java.util.Calendar; 009import java.util.Collections; 010import java.util.Date; 011import java.util.GregorianCalendar; 012import java.util.List; 013 014//------------------------------------------------------------------------------ 015/** 016 Scheduler that uses the same specification syntax as the Unix cron utility. 017 See <a href='http://en.wikipedia.org/wiki/Cron'>Wikipedia</a>. 018 The text-based specification has five fields: 019 <pre> 020 * * * * * command to execute 021 | | | | | 022 | | | | | 023 | | | | +------ day of week (0 - 6) (0 to 6 are Sunday to Saturday, or use names; 7 is Sunday, the same as 0) 024 | | | +----------- month (1 - 12) 025 | | +---------------- day of month (1 - 31) 026 | +--------------------- hour (0 - 23) 027 +-------------------------- min (0 - 59) 028 </pre> 029 <div> 030 @author J. Alex Taylor, hairyfatguy.com 031 </div> 032 */ 033//------------------------------------------------------------------------------ 034// com.hfg Library 035// 036// This library is free software; you can redistribute it and/or 037// modify it under the terms of the GNU Lesser General Public 038// License as published by the Free Software Foundation; either 039// version 2.1 of the License, or (at your option) any later version. 040// 041// This library is distributed in the hope that it will be useful, 042// but WITHOUT ANY WARRANTY; without even the implied warranty of 043// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 044// Lesser General Public License for more details. 045// 046// You should have received a copy of the GNU Lesser General Public 047// License along with this library; if not, write to the Free Software 048// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 049// 050// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 051// jataylor@hairyfatguy.com 052//------------------------------------------------------------------------------ 053// TODO: More to do to fully support cron syntax. 054 055public class CronSchedule implements Schedule 056{ 057 private String mMinuteSpec; 058 private String mHourSpec; 059 private String mDayOfMonthSpec; 060 private String mMonthSpec; 061 private String mDayOfWeekSpec; 062 063 private OrderedSet<Integer> mMinuteValues; 064 private OrderedSet<Integer> mHourValues; 065 private OrderedSet<Integer> mDayOfMonthValues; 066 private OrderedSet<Integer> mMonthValues; 067 private OrderedSet<Integer> mDayOfWeekValues; 068 069 //########################################################################### 070 // CONSTRUCTORS 071 //########################################################################### 072 073 //--------------------------------------------------------------------------- 074 public CronSchedule(String inCronString) 075 { 076 parse(inCronString); 077 } 078 079 //########################################################################### 080 // PUBLIC METHODS 081 //########################################################################### 082 083 //--------------------------------------------------------------------------- 084 public Date next() 085 { 086 return nextAfter(new Date()); 087 } 088 089 //--------------------------------------------------------------------------- 090 public Date nextAfter(Date inReferenceDate) 091 { 092 Calendar calendar = new GregorianCalendar(); 093 calendar.setTime(inReferenceDate); 094 095 // Cron doesn't specify times with a precision below minutes 096 calendar.set(Calendar.MILLISECOND, 0); 097 calendar.set(Calendar.SECOND, 0); 098 099 calendar.add(Calendar.MINUTE, 1); 100 101 // Every minute value is valid for execution unless values have been specified 102 if (CollectionUtil.hasValues(mMinuteValues)) 103 { 104 while (! mMinuteValues.contains(calendar.get(Calendar.MINUTE))) 105 { 106 calendar.add(Calendar.MINUTE, 1); 107 } 108 } 109 110 // Every hour value is valid for execution unless values have been specified 111 if (CollectionUtil.hasValues(mHourValues)) 112 { 113 while (! mHourValues.contains(calendar.get(Calendar.HOUR_OF_DAY))) 114 { 115 calendar.add(Calendar.HOUR_OF_DAY, 1); 116 117 if (null == mMinuteValues) 118 { 119 // Set the minute value to zero 120 zeroCalendarField(calendar, Calendar.MINUTE); 121 } 122 } 123 } 124 125 if (CollectionUtil.hasValues(mDayOfMonthValues) 126 || CollectionUtil.hasValues(mDayOfWeekValues)) 127 { 128 boolean modified = false; 129 while ((mDayOfMonthValues != null 130 && ! mDayOfMonthValues.contains(calendar.get(Calendar.DAY_OF_MONTH))) 131 || (mDayOfWeekValues != null 132 && ! mDayOfWeekValues.contains(calendar.get(Calendar.DAY_OF_WEEK)))) 133 { 134 calendar.add(Calendar.DAY_OF_MONTH, 1); 135 modified = true; 136 } 137 138 if (modified) 139 { 140 if (null == mHourValues) 141 { 142 // Set the hour value to zero 143 zeroCalendarField(calendar, Calendar.HOUR_OF_DAY); 144 } 145 146 if (null == mMinuteValues) 147 { 148 // Set the minute value to zero 149 zeroCalendarField(calendar, Calendar.MINUTE); 150 } 151 } 152 } 153 154 if (CollectionUtil.hasValues(mMonthValues)) 155 { 156 boolean modified = false; 157 while ((mMonthValues != null 158 && ! mMonthValues.contains(calendar.get(Calendar.MONTH) + 1)) 159 || (mDayOfMonthValues != null 160 && ! mDayOfMonthValues.contains(calendar.get(Calendar.DAY_OF_MONTH))) 161 || (mDayOfWeekValues != null 162 && ! mDayOfWeekValues.contains(calendar.get(Calendar.DAY_OF_WEEK)))) 163 { 164 modified = true; 165 166 // Are we already at the end of the month? 167 if (calendar.get(Calendar.DAY_OF_MONTH) == calendar.getActualMaximum(Calendar.DAY_OF_MONTH)) 168 { 169 calendar.set(Calendar.DAY_OF_MONTH, 1); 170 calendar.add(Calendar.MONTH, 1); 171 while (! mMonthValues.contains(calendar.get(Calendar.MONTH) + 1)) 172 { 173 calendar.add(Calendar.MONTH, 1); 174 } 175 } 176 else 177 { 178 calendar.add(Calendar.DAY_OF_MONTH, 1); 179 } 180 } 181 182 if (modified) 183 { 184 if (null == mHourValues) 185 { 186 // Set the hour value to zero 187 zeroCalendarField(calendar, Calendar.HOUR_OF_DAY); 188 } 189 190 if (null == mMinuteValues) 191 { 192 // Set the minute value to zero 193 zeroCalendarField(calendar, Calendar.MINUTE); 194 } 195 } 196 } 197 198 return calendar.getTime(); 199 } 200 201 //--------------------------------------------------------------------------- 202 public CronSchedule setMinuteSpec(String inMinuteSpec) 203 { 204 mMinuteValues = null; 205 206 if (StringUtil.isSet(inMinuteSpec)) 207 { 208 String spec = inMinuteSpec.trim(); 209 if (! spec.equals("*")) 210 { 211 List<Integer> values = new ArrayList<Integer>(60); 212 String[] pieces = spec.split(","); 213 for (String piece : pieces) 214 { 215 int startValue; 216 int endValue; 217 int dashIdx = piece.indexOf("-"); 218 if (dashIdx > 0) 219 { 220 startValue = Integer.parseInt(piece.substring(0, dashIdx)); 221 endValue = Integer.parseInt(piece.substring(dashIdx + 1)); 222 } 223 else 224 { 225 startValue = endValue = Integer.parseInt(piece); 226 } 227 228 for (int i = startValue; i <= endValue; i++) 229 { 230 if (i < 0 231 || i > 59) 232 { 233 throw new RuntimeException("Invalid minute value: " + StringUtil.singleQuote(i) + "! Valid value range: 0-59."); 234 } 235 236 values.add(i); 237 } 238 } 239 240 // Make sure the values are sorted low to high 241 Collections.sort(values); 242 243 mMinuteValues = new OrderedSet<Integer>(values); 244 } 245 } 246 247 return this; 248 } 249 250 //--------------------------------------------------------------------------- 251 public CronSchedule setHourSpec(String inHourSpec) 252 { 253 mHourValues = null; 254 255 if (StringUtil.isSet(inHourSpec)) 256 { 257 String spec = inHourSpec.trim(); 258 if (! spec.equals("*")) 259 { 260 List<Integer> values = new ArrayList<Integer>(24); 261 String[] pieces = spec.split(","); 262 for (String piece : pieces) 263 { 264 int startValue; 265 int endValue; 266 int dashIdx = piece.indexOf("-"); 267 if (dashIdx > 0) 268 { 269 startValue = Integer.parseInt(piece.substring(0, dashIdx)); 270 endValue = Integer.parseInt(piece.substring(dashIdx + 1)); 271 } 272 else 273 { 274 startValue = endValue = Integer.parseInt(piece); 275 } 276 277 for (int i = startValue; i <= endValue; i++) 278 { 279 if (i < 0 280 || i > 23) 281 { 282 throw new RuntimeException("Invalid hour value: " + StringUtil.singleQuote(i) + "! Valid value range: 0-23."); 283 } 284 285 values.add(i); 286 } 287 } 288 289 // Make sure the values are sorted low to high 290 Collections.sort(values); 291 292 mHourValues = new OrderedSet<Integer>(values); 293 } 294 } 295 296 return this; 297 } 298 299 //--------------------------------------------------------------------------- 300 public CronSchedule setDayOfMonthSpec(String inDayOfMonthSpec) 301 { 302 mDayOfMonthValues = null; 303 304 if (StringUtil.isSet(inDayOfMonthSpec)) 305 { 306 String spec = inDayOfMonthSpec.trim(); 307 if (! spec.equals("*")) 308 { 309 List<Integer> values = new ArrayList<Integer>(32); 310 String[] pieces = spec.split(","); 311 for (String piece : pieces) 312 { 313 int startValue; 314 int endValue; 315 int dashIdx = piece.indexOf("-"); 316 if (dashIdx > 0) 317 { 318 startValue = Integer.parseInt(piece.substring(0, dashIdx)); 319 endValue = Integer.parseInt(piece.substring(dashIdx + 1)); 320 } 321 else 322 { 323 startValue = endValue = Integer.parseInt(piece); 324 } 325 326 for (int i = startValue; i <= endValue; i++) 327 { 328 if (i < 1 329 || i > 31) 330 { 331 throw new RuntimeException("Invalid day of month value: " + StringUtil.singleQuote(i) + "! Valid value range: 1-31."); 332 } 333 334 values.add(i); 335 } 336 } 337 338 // Make sure the values are sorted low to high 339 Collections.sort(values); 340 341 mDayOfMonthValues = new OrderedSet<Integer>(values); 342 } 343 } 344 345 return this; 346 } 347 348 //--------------------------------------------------------------------------- 349 public CronSchedule setMonthSpec(String inMonthSpec) 350 { 351 mMonthValues = null; 352 353 if (StringUtil.isSet(inMonthSpec)) 354 { 355 String spec = inMonthSpec.trim(); 356 if (! spec.equals("*")) 357 { 358 List<Integer> values = new ArrayList<Integer>(13); 359 String[] pieces = spec.split(","); 360 for (String piece : pieces) 361 { 362 int startValue; 363 int endValue; 364 int dashIdx = piece.indexOf("-"); 365 if (dashIdx > 0) 366 { 367 startValue = Integer.parseInt(piece.substring(0, dashIdx)); 368 endValue = Integer.parseInt(piece.substring(dashIdx + 1)); 369 } 370 else 371 { 372 startValue = endValue = Integer.parseInt(piece); 373 } 374 375 for (int i = startValue; i <= endValue; i++) 376 { 377 if (i < 1 378 || i > 12) 379 { 380 throw new RuntimeException("Invalid day of month value: " + StringUtil.singleQuote(i) + "! Valid value range: 1-12."); 381 } 382 383 values.add(i); 384 } 385 } 386 387 // Make sure the values are sorted low to high 388 Collections.sort(values); 389 390 mMonthValues = new OrderedSet<Integer>(values); 391 } 392 } 393 394 return this; 395 } 396 397 //--------------------------------------------------------------------------- 398 public CronSchedule setDayOfWeekSpec(String inDayOfWeekSpec) 399 { 400 mDayOfWeekValues = null; 401 402 if (StringUtil.isSet(inDayOfWeekSpec)) 403 { 404 String spec = inDayOfWeekSpec.trim(); 405 if (! spec.equals("*")) 406 { 407 List<Integer> values = new ArrayList<Integer>(13); 408 String[] pieces = spec.split(","); 409 for (String piece : pieces) 410 { 411 int startValue; 412 int endValue; 413 int dashIdx = piece.indexOf("-"); 414 if (dashIdx > 0) 415 { 416 startValue = Integer.parseInt(piece.substring(0, dashIdx)); 417 endValue = Integer.parseInt(piece.substring(dashIdx + 1)); 418 } 419 else 420 { 421 startValue = endValue = Integer.parseInt(piece); 422 } 423 424 for (int i = startValue; i <= endValue; i++) 425 { 426 if (i < 0 427 || i > 6) 428 { 429 throw new RuntimeException("Invalid day of month value: " + StringUtil.singleQuote(i) + "! Valid value range: 0-6."); 430 } 431 432 values.add(i + 1); 433 } 434 } 435 436 // Make sure the values are sorted low to high 437 Collections.sort(values); 438 439 mDayOfWeekValues = new OrderedSet<Integer>(values); 440 } 441 } 442 443 return this; 444 } 445 446 //########################################################################### 447 // PRIVATE METHODS 448 //########################################################################### 449 450 //--------------------------------------------------------------------------- 451 private void parse(String inCronString) 452 { 453 String[] pieces = inCronString.trim().split("\\s+"); 454 if (pieces.length != 5) 455 { 456 throw new RuntimeException("The cron string " + StringUtil.singleQuote(inCronString) + " did not have the expected 5 cron fields!"); 457 } 458 459 mMinuteSpec = pieces[0]; 460 mHourSpec = pieces[1]; 461 mDayOfMonthSpec = pieces[2]; 462 mMonthSpec = pieces[3]; 463 mDayOfWeekSpec = pieces[4]; 464 465 setMinuteSpec(pieces[0]); 466 setHourSpec(pieces[1]); 467 setDayOfMonthSpec(pieces[2]); 468 setMonthSpec(pieces[3]); 469 setDayOfWeekSpec(pieces[4]); 470 471 } 472 473 //--------------------------------------------------------------------------- 474 private void zeroCalendarField(Calendar inCalendar, int inField) 475 { 476 inCalendar.add(inField, - inCalendar.get(inField)); 477 } 478 479} 480