001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2006-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2014-2015 ForgeRock AS 026 */ 027package org.opends.server.backends.task; 028 029import java.text.SimpleDateFormat; 030import java.util.Date; 031import java.util.GregorianCalendar; 032import java.util.Iterator; 033import java.util.List; 034import java.util.StringTokenizer; 035import java.util.regex.Matcher; 036import java.util.regex.Pattern; 037 038import org.forgerock.i18n.LocalizableMessage; 039import org.forgerock.i18n.slf4j.LocalizedLogger; 040import org.forgerock.opendj.ldap.ByteString; 041import org.forgerock.opendj.ldap.ResultCode; 042import org.opends.server.core.DirectoryServer; 043import org.opends.server.core.ServerContext; 044import org.opends.server.types.Attribute; 045import org.opends.server.types.AttributeType; 046import org.opends.server.types.Attributes; 047import org.opends.server.types.DN; 048import org.opends.server.types.DirectoryException; 049import org.opends.server.types.Entry; 050import org.opends.server.types.InitializationException; 051import org.opends.server.types.RDN; 052 053import static java.util.Calendar.*; 054 055import static org.opends.messages.BackendMessages.*; 056import static org.opends.server.config.ConfigConstants.*; 057import static org.opends.server.util.ServerConstants.*; 058import static org.opends.server.util.StaticUtils.*; 059 060/** 061 * This class defines a information about a recurring task, which will be used 062 * to repeatedly schedule tasks for processing. 063 * <br> 064 * It also provides some static methods that allow to validate strings in 065 * crontab (5) format. 066 */ 067public class RecurringTask 068{ 069 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 070 071 /** The DN of the entry that actually defines this task. */ 072 private final DN recurringTaskEntryDN; 073 074 /** The entry that actually defines this task. */ 075 private final Entry recurringTaskEntry; 076 077 /** The unique ID for this recurring task. */ 078 private final String recurringTaskID; 079 080 /** 081 * The fully-qualified name of the class that will be used to implement the 082 * class. 083 */ 084 private final String taskClassName; 085 086 /** Task instance. */ 087 private Task task; 088 089 /** Task scheduler for this task. */ 090 private final TaskScheduler taskScheduler; 091 092 /** Number of tokens in the task schedule tab. */ 093 private static final int TASKTAB_NUM_TOKENS = 5; 094 095 /** Maximum year month days. */ 096 static final int MONTH_LENGTH[] 097 = {31,28,31,30,31,30,31,31,30,31,30,31}; 098 099 /** Maximum leap year month days. */ 100 static final int LEAP_MONTH_LENGTH[] 101 = {31,29,31,30,31,30,31,31,30,31,30,31}; 102 103 /** Task tab fields. */ 104 private static enum TaskTab {MINUTE, HOUR, DAY, MONTH, WEEKDAY} 105 106 private static final int MINUTE_INDEX = 0; 107 private static final int HOUR_INDEX = 1; 108 private static final int DAY_INDEX = 2; 109 private static final int MONTH_INDEX = 3; 110 private static final int WEEKDAY_INDEX = 4; 111 112 /** Wildcard match pattern. */ 113 private static final Pattern wildcardPattern = Pattern.compile("^\\*(?:/(\\d+))?"); 114 115 /** Exact match pattern. */ 116 private static final Pattern exactPattern = Pattern.compile("(\\d+)"); 117 118 /** Range match pattern. */ 119 private static final Pattern rangePattern = Pattern.compile("(\\d+)-(\\d+)(?:/(\\d+))?"); 120 121 /** Boolean arrays holding task tab slots. */ 122 private final boolean[] minutesArray; 123 private final boolean[] hoursArray; 124 private final boolean[] daysArray; 125 private final boolean[] monthArray; 126 private final boolean[] weekdayArray; 127 128 private final ServerContext serverContext; 129 130 /** 131 * Creates a new recurring task based on the information in the provided 132 * entry. 133 * 134 * @param serverContext 135 * The server context. 136 * 137 * @param taskScheduler A reference to the task scheduler that may be 138 * used to schedule new tasks. 139 * @param recurringTaskEntry The entry containing the information to use to 140 * define the task to process. 141 * 142 * @throws DirectoryException If the provided entry does not contain a valid 143 * recurring task definition. 144 */ 145 public RecurringTask(ServerContext serverContext, TaskScheduler taskScheduler, Entry recurringTaskEntry) 146 throws DirectoryException 147 { 148 this.serverContext = serverContext; 149 this.taskScheduler = taskScheduler; 150 this.recurringTaskEntry = recurringTaskEntry; 151 this.recurringTaskEntryDN = recurringTaskEntry.getName(); 152 153 // Get the recurring task ID from the entry. If there isn't one, then fail. 154 AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault( 155 ATTR_RECURRING_TASK_ID.toLowerCase(), ATTR_RECURRING_TASK_ID); 156 List<Attribute> attrList = recurringTaskEntry.getAttribute(attrType); 157 if (attrList == null || attrList.isEmpty()) 158 { 159 LocalizableMessage message = 160 ERR_RECURRINGTASK_NO_ID_ATTRIBUTE.get(ATTR_RECURRING_TASK_ID); 161 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 162 } 163 164 if (attrList.size() > 1) 165 { 166 LocalizableMessage message = 167 ERR_RECURRINGTASK_MULTIPLE_ID_TYPES.get(ATTR_RECURRING_TASK_ID); 168 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 169 } 170 171 Attribute attr = attrList.get(0); 172 if (attr.isEmpty()) 173 { 174 LocalizableMessage message = ERR_RECURRINGTASK_NO_ID.get(ATTR_RECURRING_TASK_ID); 175 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 176 } 177 178 Iterator<ByteString> iterator = attr.iterator(); 179 ByteString value = iterator.next(); 180 if (iterator.hasNext()) 181 { 182 LocalizableMessage message = 183 ERR_RECURRINGTASK_MULTIPLE_ID_VALUES.get(ATTR_RECURRING_TASK_ID); 184 throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); 185 } 186 187 recurringTaskID = value.toString(); 188 189 190 // Get the schedule for this task. 191 attrType = DirectoryServer.getAttributeTypeOrDefault( 192 ATTR_RECURRING_TASK_SCHEDULE.toLowerCase(), ATTR_RECURRING_TASK_SCHEDULE); 193 194 attrList = recurringTaskEntry.getAttribute(attrType); 195 if (attrList == null || attrList.isEmpty()) 196 { 197 LocalizableMessage message = ERR_RECURRINGTASK_NO_SCHEDULE_ATTRIBUTE.get( 198 ATTR_RECURRING_TASK_SCHEDULE); 199 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 200 } 201 202 if (attrList.size() > 1) 203 { 204 LocalizableMessage message = ERR_RECURRINGTASK_MULTIPLE_SCHEDULE_TYPES.get( 205 ATTR_RECURRING_TASK_SCHEDULE); 206 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 207 } 208 209 attr = attrList.get(0); 210 if (attr.isEmpty()) 211 { 212 LocalizableMessage message = ERR_RECURRINGTASK_NO_SCHEDULE_VALUES.get( 213 ATTR_RECURRING_TASK_SCHEDULE); 214 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 215 } 216 217 iterator = attr.iterator(); 218 value = iterator.next(); 219 if (iterator.hasNext()) 220 { 221 LocalizableMessage message = ERR_RECURRINGTASK_MULTIPLE_SCHEDULE_VALUES.get(ATTR_RECURRING_TASK_SCHEDULE); 222 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 223 } 224 225 String taskScheduleTab = value.toString(); 226 227 boolean[][] taskArrays = new boolean[][]{null, null, null, null, null}; 228 229 parseTaskTab(taskScheduleTab, taskArrays, true); 230 231 minutesArray = taskArrays[MINUTE_INDEX]; 232 hoursArray = taskArrays[HOUR_INDEX]; 233 daysArray = taskArrays[DAY_INDEX]; 234 monthArray = taskArrays[MONTH_INDEX]; 235 weekdayArray = taskArrays[WEEKDAY_INDEX]; 236 237 // Get the class name from the entry. If there isn't one, then fail. 238 attrType = DirectoryServer.getAttributeTypeOrDefault(ATTR_TASK_CLASS.toLowerCase(), ATTR_TASK_CLASS); 239 240 attrList = recurringTaskEntry.getAttribute(attrType); 241 if (attrList == null || attrList.isEmpty()) 242 { 243 LocalizableMessage message = ERR_TASKSCHED_NO_CLASS_ATTRIBUTE.get(ATTR_TASK_CLASS); 244 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 245 } 246 247 if (attrList.size() > 1) 248 { 249 LocalizableMessage message = ERR_TASKSCHED_MULTIPLE_CLASS_TYPES.get(ATTR_TASK_CLASS); 250 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 251 } 252 253 attr = attrList.get(0); 254 if (attr.isEmpty()) 255 { 256 LocalizableMessage message = ERR_TASKSCHED_NO_CLASS_VALUES.get(ATTR_TASK_CLASS); 257 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 258 } 259 260 iterator = attr.iterator(); 261 value = iterator.next(); 262 if (iterator.hasNext()) 263 { 264 LocalizableMessage message = ERR_TASKSCHED_MULTIPLE_CLASS_VALUES.get(ATTR_TASK_CLASS); 265 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 266 } 267 268 taskClassName = value.toString(); 269 270 271 // Make sure that the specified class can be loaded. 272 Class<?> taskClass; 273 try 274 { 275 taskClass = DirectoryServer.loadClass(taskClassName); 276 } 277 catch (Exception e) 278 { 279 logger.traceException(e); 280 281 LocalizableMessage message = ERR_RECURRINGTASK_CANNOT_LOAD_CLASS. 282 get(taskClassName, ATTR_TASK_CLASS, getExceptionMessage(e)); 283 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message, e); 284 } 285 286 287 // Make sure that the specified class can be instantiated as a task. 288 try 289 { 290 task = (Task) taskClass.newInstance(); 291 } 292 catch (Exception e) 293 { 294 logger.traceException(e); 295 296 LocalizableMessage message = ERR_RECURRINGTASK_CANNOT_INSTANTIATE_CLASS_AS_TASK.get( 297 taskClassName, Task.class.getName()); 298 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message, e); 299 } 300 301 302 // Make sure that we can initialize the task with the information in the 303 // provided entry. 304 try 305 { 306 task.initializeTaskInternal(serverContext, taskScheduler, recurringTaskEntry); 307 } 308 catch (InitializationException ie) 309 { 310 logger.traceException(ie); 311 312 LocalizableMessage message = ERR_RECURRINGTASK_CANNOT_INITIALIZE_INTERNAL.get( taskClassName, ie.getMessage()); 313 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, ie); 314 } 315 316 task.initializeTask(); 317 } 318 319 320 321 /** 322 * Retrieves the unique ID assigned to this recurring task. 323 * 324 * @return The unique ID assigned to this recurring task. 325 */ 326 public String getRecurringTaskID() 327 { 328 return recurringTaskID; 329 } 330 331 332 333 /** 334 * Retrieves the DN of the entry containing the data for this recurring task. 335 * 336 * @return The DN of the entry containing the data for this recurring task. 337 */ 338 public DN getRecurringTaskEntryDN() 339 { 340 return recurringTaskEntryDN; 341 } 342 343 344 345 /** 346 * Retrieves the entry containing the data for this recurring task. 347 * 348 * @return The entry containing the data for this recurring task. 349 */ 350 public Entry getRecurringTaskEntry() 351 { 352 return recurringTaskEntry; 353 } 354 355 356 357 /** 358 * Retrieves the fully-qualified name of the Java class that provides the 359 * implementation logic for this recurring task. 360 * 361 * @return The fully-qualified name of the Java class that provides the 362 * implementation logic for this recurring task. 363 */ 364 public String getTaskClassName() 365 { 366 return taskClassName; 367 } 368 369 370 371 /** 372 * Schedules the next iteration of this recurring task for processing. 373 * @param calendar date and time to schedule next iteration from. 374 * @return The task that has been scheduled for processing. 375 * @throws DirectoryException to indicate an error. 376 */ 377 public Task scheduleNextIteration(GregorianCalendar calendar) 378 throws DirectoryException 379 { 380 Task nextTask = null; 381 Date nextTaskDate = null; 382 383 try { 384 nextTaskDate = getNextIteration(calendar); 385 } catch (IllegalArgumentException e) { 386 logger.traceException(e); 387 388 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 389 ERR_RECURRINGTASK_INVALID_TOKENS_COMBO.get( 390 ATTR_RECURRING_TASK_SCHEDULE)); 391 } 392 393 SimpleDateFormat dateFormat = new SimpleDateFormat( 394 DATE_FORMAT_COMPACT_LOCAL_TIME); 395 String nextTaskStartTime = dateFormat.format(nextTaskDate); 396 397 try { 398 // Make a regular task iteration from this recurring task. 399 nextTask = task.getClass().newInstance(); 400 Entry nextTaskEntry = recurringTaskEntry.duplicate(false); 401 SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS"); 402 String nextTaskID = task.getTaskID() + "-" + df.format(nextTaskDate); 403 String nextTaskIDName = NAME_PREFIX_TASK + "id"; 404 AttributeType taskIDAttrType = DirectoryServer.getAttributeTypeOrNull(nextTaskIDName); 405 Attribute nextTaskIDAttr = Attributes.create(taskIDAttrType, nextTaskID); 406 nextTaskEntry.replaceAttribute(nextTaskIDAttr); 407 RDN nextTaskRDN = RDN.decode(nextTaskIDName + "=" + nextTaskID); 408 DN nextTaskDN = new DN(nextTaskRDN, 409 taskScheduler.getTaskBackend().getScheduledTasksParentDN()); 410 nextTaskEntry.setDN(nextTaskDN); 411 412 String nextTaskStartTimeName = NAME_PREFIX_TASK + "scheduled-start-time"; 413 AttributeType taskStartTimeAttrType = DirectoryServer.getAttributeTypeOrNull(nextTaskStartTimeName); 414 Attribute nextTaskStartTimeAttr = Attributes.create(taskStartTimeAttrType, nextTaskStartTime); 415 nextTaskEntry.replaceAttribute(nextTaskStartTimeAttr); 416 417 nextTask.initializeTaskInternal(serverContext, taskScheduler, nextTaskEntry); 418 nextTask.initializeTask(); 419 } catch (Exception e) { 420 // Should not happen, debug log it otherwise. 421 logger.traceException(e); 422 } 423 424 return nextTask; 425 } 426 427 /** 428 * Parse and validate recurring task schedule. 429 * @param taskSchedule recurring task schedule tab in crontab(5) format. 430 * @throws DirectoryException to indicate an error. 431 */ 432 public static void parseTaskTab(String taskSchedule) throws DirectoryException 433 { 434 parseTaskTab(taskSchedule, new boolean[][]{null, null, null, null, null}, 435 false); 436 } 437 438 /** 439 * Parse and validate recurring task schedule. 440 * @param taskSchedule recurring task schedule tab in crontab(5) format. 441 * @param arrays an array of 5 boolean arrays. The array has the following 442 * structure: {minutesArray, hoursArray, daysArray, monthArray, weekdayArray}. 443 * @param referToTaskEntryAttribute whether the error messages must refer 444 * to the task entry attribute or not. This is used to have meaningful 445 * messages when the {@link #parseTaskTab(String)} is called to validate 446 * a crontab formatted string. 447 * @throws DirectoryException to indicate an error. 448 */ 449 private static void parseTaskTab(String taskSchedule, boolean[][] arrays, 450 boolean referToTaskEntryAttribute) throws DirectoryException 451 { 452 StringTokenizer st = new StringTokenizer(taskSchedule); 453 454 if (st.countTokens() != TASKTAB_NUM_TOKENS) { 455 if (referToTaskEntryAttribute) 456 { 457 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 458 ERR_RECURRINGTASK_INVALID_N_TOKENS.get( 459 ATTR_RECURRING_TASK_SCHEDULE)); 460 } 461 else 462 { 463 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 464 ERR_RECURRINGTASK_INVALID_N_TOKENS_SIMPLE.get()); 465 } 466 } 467 468 for (TaskTab taskTabToken : TaskTab.values()) { 469 String token = st.nextToken(); 470 switch (taskTabToken) { 471 case MINUTE: 472 try { 473 arrays[MINUTE_INDEX] = parseTaskTabField(token, 0, 59); 474 } catch (IllegalArgumentException e) { 475 if (referToTaskEntryAttribute) 476 { 477 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 478 ERR_RECURRINGTASK_INVALID_MINUTE_TOKEN.get( 479 ATTR_RECURRING_TASK_SCHEDULE)); 480 } 481 else 482 { 483 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 484 ERR_RECURRINGTASK_INVALID_MINUTE_TOKEN_SIMPLE.get()); 485 } 486 } 487 break; 488 case HOUR: 489 try { 490 arrays[HOUR_INDEX] = parseTaskTabField(token, 0, 23); 491 } catch (IllegalArgumentException e) { 492 if (referToTaskEntryAttribute) 493 { 494 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 495 ERR_RECURRINGTASK_INVALID_HOUR_TOKEN.get( 496 ATTR_RECURRING_TASK_SCHEDULE)); 497 } 498 else 499 { 500 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 501 ERR_RECURRINGTASK_INVALID_HOUR_TOKEN_SIMPLE.get()); 502 } 503 } 504 break; 505 case DAY: 506 try { 507 arrays[DAY_INDEX] = parseTaskTabField(token, 1, 31); 508 } catch (IllegalArgumentException e) { 509 if (referToTaskEntryAttribute) 510 { 511 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 512 ERR_RECURRINGTASK_INVALID_DAY_TOKEN.get( 513 ATTR_RECURRING_TASK_SCHEDULE)); 514 } 515 else 516 { 517 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 518 ERR_RECURRINGTASK_INVALID_DAY_TOKEN_SIMPLE.get()); 519 } 520 } 521 break; 522 case MONTH: 523 try { 524 arrays[MONTH_INDEX] = parseTaskTabField(token, 1, 12); 525 } catch (IllegalArgumentException e) { 526 if (referToTaskEntryAttribute) 527 { 528 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 529 ERR_RECURRINGTASK_INVALID_MONTH_TOKEN.get( 530 ATTR_RECURRING_TASK_SCHEDULE)); 531 } 532 else 533 { 534 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 535 ERR_RECURRINGTASK_INVALID_MONTH_TOKEN_SIMPLE.get()); 536 } 537 } 538 break; 539 case WEEKDAY: 540 try { 541 arrays[WEEKDAY_INDEX] = parseTaskTabField(token, 0, 6); 542 } catch (IllegalArgumentException e) { 543 if (referToTaskEntryAttribute) 544 { 545 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 546 ERR_RECURRINGTASK_INVALID_WEEKDAY_TOKEN.get( 547 ATTR_RECURRING_TASK_SCHEDULE)); 548 } 549 else 550 { 551 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 552 ERR_RECURRINGTASK_INVALID_WEEKDAY_TOKEN_SIMPLE.get()); 553 } 554 } 555 break; 556 } 557 } 558 } 559 560 /** 561 * Parse and validate recurring task schedule field. 562 * 563 * @param tabField recurring task schedule field in crontab(5) format. 564 * @param minValue minimum value allowed for this field. 565 * @param maxValue maximum value allowed for this field. 566 * @return boolean schedule slots range set according to the schedule field. 567 * @throws IllegalArgumentException if tab field is invalid. 568 */ 569 public static boolean[] parseTaskTabField(String tabField, 570 int minValue, int maxValue) throws IllegalArgumentException 571 { 572 boolean[] valueList = new boolean[maxValue + 1]; 573 574 // Wildcard with optional increment. 575 Matcher m = wildcardPattern.matcher(tabField); 576 if (m.matches() && m.groupCount() == 1) 577 { 578 String stepString = m.group(1); 579 int increment = isValueAbsent(stepString) ? 1 : Integer.parseInt(stepString); 580 for (int i = minValue; i <= maxValue; i += increment) 581 { 582 valueList[i] = true; 583 } 584 return valueList; 585 } 586 587 // List. 588 for (String listVal : tabField.split(",")) 589 { 590 // Single number. 591 m = exactPattern.matcher(listVal); 592 if (m.matches() && m.groupCount() == 1) 593 { 594 String exactValue = m.group(1); 595 if (isValueAbsent(exactValue)) 596 { 597 throw new IllegalArgumentException(); 598 } 599 int value = Integer.parseInt(exactValue); 600 if (value < minValue || value > maxValue) 601 { 602 throw new IllegalArgumentException(); 603 } 604 valueList[value] = true; 605 continue; 606 } 607 608 // Range of numbers with optional increment. 609 m = rangePattern.matcher(listVal); 610 if (m.matches() && m.groupCount() == 3) { 611 String startString = m.group(1); 612 String endString = m.group(2); 613 String stepString = m.group(3); 614 int increment = isValueAbsent(stepString) ? 1 : Integer.parseInt(stepString); 615 if (isValueAbsent(startString) || isValueAbsent(endString)) 616 { 617 throw new IllegalArgumentException(); 618 } 619 int startValue = Integer.parseInt(startString); 620 int endValue = Integer.parseInt(endString); 621 if (startValue > endValue || startValue < minValue || endValue > maxValue) 622 { 623 throw new IllegalArgumentException(); 624 } 625 for (int i = startValue; i <= endValue; i += increment) 626 { 627 valueList[i] = true; 628 } 629 continue; 630 } 631 632 // Can only have a list of numbers and ranges. 633 throw new IllegalArgumentException(); 634 } 635 636 return valueList; 637 } 638 639 /** 640 * Check if a String from a Matcher group is absent. Matcher returns empty strings 641 * for optional groups that are absent. 642 * 643 * @param s A string returned from Matcher.group() 644 * @return true if the string is unusable, false if it is usable. 645 */ 646 private static boolean isValueAbsent(String s) 647 { 648 return s == null || s.length() == 0; 649 } 650 /** 651 * Get next recurring slot from the range. 652 * @param timesList the range. 653 * @param fromNow the current slot. 654 * @return next recurring slot in the range. 655 */ 656 private int getNextTimeSlice(boolean[] timesList, int fromNow) 657 { 658 for (int i = fromNow; i < timesList.length; i++) { 659 if (timesList[i]) { 660 return i; 661 } 662 } 663 return -1; 664 } 665 666 /** 667 * Get next task iteration date according to recurring schedule. 668 * @param calendar date and time to schedule from. 669 * @return next task iteration date. 670 * @throws IllegalArgumentException if recurring schedule is invalid. 671 */ 672 private Date getNextIteration(GregorianCalendar calendar) 673 throws IllegalArgumentException 674 { 675 int minute, hour, day, month, weekday; 676 calendar.setFirstDayOfWeek(GregorianCalendar.SUNDAY); 677 calendar.add(GregorianCalendar.MINUTE, 1); 678 calendar.set(GregorianCalendar.SECOND, 0); 679 calendar.set(GregorianCalendar.MILLISECOND, 0); 680 calendar.setLenient(false); 681 682 // Weekday 683 for (;;) { 684 // Month 685 for (;;) { 686 // Day 687 for (;;) { 688 // Hour 689 for (;;) { 690 // Minute 691 for (;;) { 692 minute = getNextTimeSlice(minutesArray, calendar.get(MINUTE)); 693 if (minute == -1) { 694 calendar.set(GregorianCalendar.MINUTE, 0); 695 calendar.add(GregorianCalendar.HOUR_OF_DAY, 1); 696 } else { 697 calendar.set(GregorianCalendar.MINUTE, minute); 698 break; 699 } 700 } 701 hour = getNextTimeSlice(hoursArray, 702 calendar.get(GregorianCalendar.HOUR_OF_DAY)); 703 if (hour == -1) { 704 calendar.set(GregorianCalendar.HOUR_OF_DAY, 0); 705 calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); 706 } else { 707 calendar.set(GregorianCalendar.HOUR_OF_DAY, hour); 708 break; 709 } 710 } 711 day = getNextTimeSlice(daysArray, 712 calendar.get(GregorianCalendar.DAY_OF_MONTH)); 713 if (day == -1 || day > calendar.getActualMaximum(DAY_OF_MONTH)) 714 { 715 calendar.set(GregorianCalendar.DAY_OF_MONTH, 1); 716 calendar.add(GregorianCalendar.MONTH, 1); 717 } else { 718 calendar.set(GregorianCalendar.DAY_OF_MONTH, day); 719 break; 720 } 721 } 722 month = getNextTimeSlice(monthArray, calendar.get(MONTH) + 1); 723 if (month == -1) { 724 calendar.set(GregorianCalendar.MONTH, 0); 725 calendar.add(GregorianCalendar.YEAR, 1); 726 } 727 else if (day > LEAP_MONTH_LENGTH[month - 1] 728 && (getNextTimeSlice(daysArray, 1) != day 729 || getNextTimeSlice(monthArray, 1) != month)) 730 { 731 calendar.set(DAY_OF_MONTH, 1); 732 calendar.add(MONTH, 1); 733 } else if (day > MONTH_LENGTH[month - 1] 734 && !calendar.isLeapYear(calendar.get(YEAR))) { 735 calendar.add(YEAR, 1); 736 } else { 737 calendar.set(MONTH, month - 1); 738 break; 739 } 740 } 741 weekday = getNextTimeSlice(weekdayArray, calendar.get(DAY_OF_WEEK) - 1); 742 if (weekday == -1 743 || weekday != calendar.get(DAY_OF_WEEK) - 1) 744 { 745 calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); 746 } else { 747 break; 748 } 749 } 750 751 return calendar.getTime(); 752 } 753}