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.getAttributeType(ATTR_RECURRING_TASK_SCHEDULE.toLowerCase()); 192 if (attrType == null) 193 { 194 attrType = DirectoryServer.getDefaultAttributeType(ATTR_RECURRING_TASK_SCHEDULE); 195 } 196 197 attrList = recurringTaskEntry.getAttribute(attrType); 198 if (attrList == null || attrList.isEmpty()) 199 { 200 LocalizableMessage message = ERR_RECURRINGTASK_NO_SCHEDULE_ATTRIBUTE.get( 201 ATTR_RECURRING_TASK_SCHEDULE); 202 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 203 } 204 205 if (attrList.size() > 1) 206 { 207 LocalizableMessage message = ERR_RECURRINGTASK_MULTIPLE_SCHEDULE_TYPES.get( 208 ATTR_RECURRING_TASK_SCHEDULE); 209 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 210 } 211 212 attr = attrList.get(0); 213 if (attr.isEmpty()) 214 { 215 LocalizableMessage message = ERR_RECURRINGTASK_NO_SCHEDULE_VALUES.get( 216 ATTR_RECURRING_TASK_SCHEDULE); 217 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 218 } 219 220 iterator = attr.iterator(); 221 value = iterator.next(); 222 if (iterator.hasNext()) 223 { 224 LocalizableMessage message = ERR_RECURRINGTASK_MULTIPLE_SCHEDULE_VALUES.get(ATTR_RECURRING_TASK_SCHEDULE); 225 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 226 } 227 228 String taskScheduleTab = value.toString(); 229 230 boolean[][] taskArrays = new boolean[][]{null, null, null, null, null}; 231 232 parseTaskTab(taskScheduleTab, taskArrays, true); 233 234 minutesArray = taskArrays[MINUTE_INDEX]; 235 hoursArray = taskArrays[HOUR_INDEX]; 236 daysArray = taskArrays[DAY_INDEX]; 237 monthArray = taskArrays[MONTH_INDEX]; 238 weekdayArray = taskArrays[WEEKDAY_INDEX]; 239 240 // Get the class name from the entry. If there isn't one, then fail. 241 attrType = DirectoryServer.getAttributeType(ATTR_TASK_CLASS.toLowerCase()); 242 if (attrType == null) 243 { 244 attrType = DirectoryServer.getDefaultAttributeType(ATTR_TASK_CLASS); 245 } 246 247 attrList = recurringTaskEntry.getAttribute(attrType); 248 if (attrList == null || attrList.isEmpty()) 249 { 250 LocalizableMessage message = ERR_TASKSCHED_NO_CLASS_ATTRIBUTE.get(ATTR_TASK_CLASS); 251 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 252 } 253 254 if (attrList.size() > 1) 255 { 256 LocalizableMessage message = ERR_TASKSCHED_MULTIPLE_CLASS_TYPES.get(ATTR_TASK_CLASS); 257 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 258 } 259 260 attr = attrList.get(0); 261 if (attr.isEmpty()) 262 { 263 LocalizableMessage message = ERR_TASKSCHED_NO_CLASS_VALUES.get(ATTR_TASK_CLASS); 264 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 265 } 266 267 iterator = attr.iterator(); 268 value = iterator.next(); 269 if (iterator.hasNext()) 270 { 271 LocalizableMessage message = ERR_TASKSCHED_MULTIPLE_CLASS_VALUES.get(ATTR_TASK_CLASS); 272 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 273 } 274 275 taskClassName = value.toString(); 276 277 278 // Make sure that the specified class can be loaded. 279 Class<?> taskClass; 280 try 281 { 282 taskClass = DirectoryServer.loadClass(taskClassName); 283 } 284 catch (Exception e) 285 { 286 logger.traceException(e); 287 288 LocalizableMessage message = ERR_RECURRINGTASK_CANNOT_LOAD_CLASS. 289 get(taskClassName, ATTR_TASK_CLASS, getExceptionMessage(e)); 290 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message, e); 291 } 292 293 294 // Make sure that the specified class can be instantiated as a task. 295 try 296 { 297 task = (Task) taskClass.newInstance(); 298 } 299 catch (Exception e) 300 { 301 logger.traceException(e); 302 303 LocalizableMessage message = ERR_RECURRINGTASK_CANNOT_INSTANTIATE_CLASS_AS_TASK.get( 304 taskClassName, Task.class.getName()); 305 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message, e); 306 } 307 308 309 // Make sure that we can initialize the task with the information in the 310 // provided entry. 311 try 312 { 313 task.initializeTaskInternal(serverContext, taskScheduler, recurringTaskEntry); 314 } 315 catch (InitializationException ie) 316 { 317 logger.traceException(ie); 318 319 LocalizableMessage message = ERR_RECURRINGTASK_CANNOT_INITIALIZE_INTERNAL.get( taskClassName, ie.getMessage()); 320 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, ie); 321 } 322 323 task.initializeTask(); 324 } 325 326 327 328 /** 329 * Retrieves the unique ID assigned to this recurring task. 330 * 331 * @return The unique ID assigned to this recurring task. 332 */ 333 public String getRecurringTaskID() 334 { 335 return recurringTaskID; 336 } 337 338 339 340 /** 341 * Retrieves the DN of the entry containing the data for this recurring task. 342 * 343 * @return The DN of the entry containing the data for this recurring task. 344 */ 345 public DN getRecurringTaskEntryDN() 346 { 347 return recurringTaskEntryDN; 348 } 349 350 351 352 /** 353 * Retrieves the entry containing the data for this recurring task. 354 * 355 * @return The entry containing the data for this recurring task. 356 */ 357 public Entry getRecurringTaskEntry() 358 { 359 return recurringTaskEntry; 360 } 361 362 363 364 /** 365 * Retrieves the fully-qualified name of the Java class that provides the 366 * implementation logic for this recurring task. 367 * 368 * @return The fully-qualified name of the Java class that provides the 369 * implementation logic for this recurring task. 370 */ 371 public String getTaskClassName() 372 { 373 return taskClassName; 374 } 375 376 377 378 /** 379 * Schedules the next iteration of this recurring task for processing. 380 * @param calendar date and time to schedule next iteration from. 381 * @return The task that has been scheduled for processing. 382 * @throws DirectoryException to indicate an error. 383 */ 384 public Task scheduleNextIteration(GregorianCalendar calendar) 385 throws DirectoryException 386 { 387 Task nextTask = null; 388 Date nextTaskDate = null; 389 390 try { 391 nextTaskDate = getNextIteration(calendar); 392 } catch (IllegalArgumentException e) { 393 logger.traceException(e); 394 395 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 396 ERR_RECURRINGTASK_INVALID_TOKENS_COMBO.get( 397 ATTR_RECURRING_TASK_SCHEDULE)); 398 } 399 400 SimpleDateFormat dateFormat = new SimpleDateFormat( 401 DATE_FORMAT_COMPACT_LOCAL_TIME); 402 String nextTaskStartTime = dateFormat.format(nextTaskDate); 403 404 try { 405 // Make a regular task iteration from this recurring task. 406 nextTask = task.getClass().newInstance(); 407 Entry nextTaskEntry = recurringTaskEntry.duplicate(false); 408 SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS"); 409 String nextTaskID = task.getTaskID() + "-" + df.format(nextTaskDate); 410 String nextTaskIDName = NAME_PREFIX_TASK + "id"; 411 AttributeType taskIDAttrType = DirectoryServer.getAttributeType(nextTaskIDName); 412 Attribute nextTaskIDAttr = Attributes.create(taskIDAttrType, nextTaskID); 413 nextTaskEntry.replaceAttribute(nextTaskIDAttr); 414 RDN nextTaskRDN = RDN.decode(nextTaskIDName + "=" + nextTaskID); 415 DN nextTaskDN = new DN(nextTaskRDN, 416 taskScheduler.getTaskBackend().getScheduledTasksParentDN()); 417 nextTaskEntry.setDN(nextTaskDN); 418 419 String nextTaskStartTimeName = NAME_PREFIX_TASK + 420 "scheduled-start-time"; 421 AttributeType taskStartTimeAttrType = 422 DirectoryServer.getAttributeType(nextTaskStartTimeName); 423 Attribute nextTaskStartTimeAttr = Attributes.create( 424 taskStartTimeAttrType, nextTaskStartTime); 425 nextTaskEntry.replaceAttribute(nextTaskStartTimeAttr); 426 427 nextTask.initializeTaskInternal(serverContext, taskScheduler, nextTaskEntry); 428 nextTask.initializeTask(); 429 } catch (Exception e) { 430 // Should not happen, debug log it otherwise. 431 logger.traceException(e); 432 } 433 434 return nextTask; 435 } 436 437 /** 438 * Parse and validate recurring task schedule. 439 * @param taskSchedule recurring task schedule tab in crontab(5) format. 440 * @throws DirectoryException to indicate an error. 441 */ 442 public static void parseTaskTab(String taskSchedule) throws DirectoryException 443 { 444 parseTaskTab(taskSchedule, new boolean[][]{null, null, null, null, null}, 445 false); 446 } 447 448 /** 449 * Parse and validate recurring task schedule. 450 * @param taskSchedule recurring task schedule tab in crontab(5) format. 451 * @param arrays an array of 5 boolean arrays. The array has the following 452 * structure: {minutesArray, hoursArray, daysArray, monthArray, weekdayArray}. 453 * @param referToTaskEntryAttribute whether the error messages must refer 454 * to the task entry attribute or not. This is used to have meaningful 455 * messages when the {@link #parseTaskTab(String)} is called to validate 456 * a crontab formatted string. 457 * @throws DirectoryException to indicate an error. 458 */ 459 private static void parseTaskTab(String taskSchedule, boolean[][] arrays, 460 boolean referToTaskEntryAttribute) throws DirectoryException 461 { 462 StringTokenizer st = new StringTokenizer(taskSchedule); 463 464 if (st.countTokens() != TASKTAB_NUM_TOKENS) { 465 if (referToTaskEntryAttribute) 466 { 467 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 468 ERR_RECURRINGTASK_INVALID_N_TOKENS.get( 469 ATTR_RECURRING_TASK_SCHEDULE)); 470 } 471 else 472 { 473 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 474 ERR_RECURRINGTASK_INVALID_N_TOKENS_SIMPLE.get()); 475 } 476 } 477 478 for (TaskTab taskTabToken : TaskTab.values()) { 479 String token = st.nextToken(); 480 switch (taskTabToken) { 481 case MINUTE: 482 try { 483 arrays[MINUTE_INDEX] = parseTaskTabField(token, 0, 59); 484 } catch (IllegalArgumentException e) { 485 if (referToTaskEntryAttribute) 486 { 487 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 488 ERR_RECURRINGTASK_INVALID_MINUTE_TOKEN.get( 489 ATTR_RECURRING_TASK_SCHEDULE)); 490 } 491 else 492 { 493 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 494 ERR_RECURRINGTASK_INVALID_MINUTE_TOKEN_SIMPLE.get()); 495 } 496 } 497 break; 498 case HOUR: 499 try { 500 arrays[HOUR_INDEX] = parseTaskTabField(token, 0, 23); 501 } catch (IllegalArgumentException e) { 502 if (referToTaskEntryAttribute) 503 { 504 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 505 ERR_RECURRINGTASK_INVALID_HOUR_TOKEN.get( 506 ATTR_RECURRING_TASK_SCHEDULE)); 507 } 508 else 509 { 510 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 511 ERR_RECURRINGTASK_INVALID_HOUR_TOKEN_SIMPLE.get()); 512 } 513 } 514 break; 515 case DAY: 516 try { 517 arrays[DAY_INDEX] = parseTaskTabField(token, 1, 31); 518 } catch (IllegalArgumentException e) { 519 if (referToTaskEntryAttribute) 520 { 521 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 522 ERR_RECURRINGTASK_INVALID_DAY_TOKEN.get( 523 ATTR_RECURRING_TASK_SCHEDULE)); 524 } 525 else 526 { 527 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 528 ERR_RECURRINGTASK_INVALID_DAY_TOKEN_SIMPLE.get()); 529 } 530 } 531 break; 532 case MONTH: 533 try { 534 arrays[MONTH_INDEX] = parseTaskTabField(token, 1, 12); 535 } catch (IllegalArgumentException e) { 536 if (referToTaskEntryAttribute) 537 { 538 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 539 ERR_RECURRINGTASK_INVALID_MONTH_TOKEN.get( 540 ATTR_RECURRING_TASK_SCHEDULE)); 541 } 542 else 543 { 544 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 545 ERR_RECURRINGTASK_INVALID_MONTH_TOKEN_SIMPLE.get()); 546 } 547 } 548 break; 549 case WEEKDAY: 550 try { 551 arrays[WEEKDAY_INDEX] = parseTaskTabField(token, 0, 6); 552 } catch (IllegalArgumentException e) { 553 if (referToTaskEntryAttribute) 554 { 555 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 556 ERR_RECURRINGTASK_INVALID_WEEKDAY_TOKEN.get( 557 ATTR_RECURRING_TASK_SCHEDULE)); 558 } 559 else 560 { 561 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 562 ERR_RECURRINGTASK_INVALID_WEEKDAY_TOKEN_SIMPLE.get()); 563 } 564 } 565 break; 566 } 567 } 568 } 569 570 /** 571 * Parse and validate recurring task schedule field. 572 * 573 * @param tabField recurring task schedule field in crontab(5) format. 574 * @param minValue minimum value allowed for this field. 575 * @param maxValue maximum value allowed for this field. 576 * @return boolean schedule slots range set according to the schedule field. 577 * @throws IllegalArgumentException if tab field is invalid. 578 */ 579 public static boolean[] parseTaskTabField(String tabField, 580 int minValue, int maxValue) throws IllegalArgumentException 581 { 582 boolean[] valueList = new boolean[maxValue + 1]; 583 584 // Wildcard with optional increment. 585 Matcher m = wildcardPattern.matcher(tabField); 586 if (m.matches() && m.groupCount() == 1) 587 { 588 String stepString = m.group(1); 589 int increment = isValueAbsent(stepString) ? 1 : Integer.parseInt(stepString); 590 for (int i = minValue; i <= maxValue; i += increment) 591 { 592 valueList[i] = true; 593 } 594 return valueList; 595 } 596 597 // List. 598 for (String listVal : tabField.split(",")) 599 { 600 // Single number. 601 m = exactPattern.matcher(listVal); 602 if (m.matches() && m.groupCount() == 1) 603 { 604 String exactValue = m.group(1); 605 if (isValueAbsent(exactValue)) 606 { 607 throw new IllegalArgumentException(); 608 } 609 int value = Integer.parseInt(exactValue); 610 if (value < minValue || value > maxValue) 611 { 612 throw new IllegalArgumentException(); 613 } 614 valueList[value] = true; 615 continue; 616 } 617 618 // Range of numbers with optional increment. 619 m = rangePattern.matcher(listVal); 620 if (m.matches() && m.groupCount() == 3) { 621 String startString = m.group(1); 622 String endString = m.group(2); 623 String stepString = m.group(3); 624 int increment = isValueAbsent(stepString) ? 1 : Integer.parseInt(stepString); 625 if (isValueAbsent(startString) || isValueAbsent(endString)) 626 { 627 throw new IllegalArgumentException(); 628 } 629 int startValue = Integer.parseInt(startString); 630 int endValue = Integer.parseInt(endString); 631 if (startValue > endValue || startValue < minValue || endValue > maxValue) 632 { 633 throw new IllegalArgumentException(); 634 } 635 for (int i = startValue; i <= endValue; i += increment) 636 { 637 valueList[i] = true; 638 } 639 continue; 640 } 641 642 // Can only have a list of numbers and ranges. 643 throw new IllegalArgumentException(); 644 } 645 646 return valueList; 647 } 648 649 /** 650 * Check if a String from a Matcher group is absent. Matcher returns empty strings 651 * for optional groups that are absent. 652 * 653 * @param s A string returned from Matcher.group() 654 * @return true if the string is unusable, false if it is usable. 655 */ 656 private static boolean isValueAbsent(String s) 657 { 658 return s == null || s.length() == 0; 659 } 660 /** 661 * Get next recurring slot from the range. 662 * @param timesList the range. 663 * @param fromNow the current slot. 664 * @return next recurring slot in the range. 665 */ 666 private int getNextTimeSlice(boolean[] timesList, int fromNow) 667 { 668 for (int i = fromNow; i < timesList.length; i++) { 669 if (timesList[i]) { 670 return i; 671 } 672 } 673 return -1; 674 } 675 676 /** 677 * Get next task iteration date according to recurring schedule. 678 * @param calendar date and time to schedule from. 679 * @return next task iteration date. 680 * @throws IllegalArgumentException if recurring schedule is invalid. 681 */ 682 private Date getNextIteration(GregorianCalendar calendar) 683 throws IllegalArgumentException 684 { 685 int minute, hour, day, month, weekday; 686 calendar.setFirstDayOfWeek(GregorianCalendar.SUNDAY); 687 calendar.add(GregorianCalendar.MINUTE, 1); 688 calendar.set(GregorianCalendar.SECOND, 0); 689 calendar.set(GregorianCalendar.MILLISECOND, 0); 690 calendar.setLenient(false); 691 692 // Weekday 693 for (;;) { 694 // Month 695 for (;;) { 696 // Day 697 for (;;) { 698 // Hour 699 for (;;) { 700 // Minute 701 for (;;) { 702 minute = getNextTimeSlice(minutesArray, calendar.get(MINUTE)); 703 if (minute == -1) { 704 calendar.set(GregorianCalendar.MINUTE, 0); 705 calendar.add(GregorianCalendar.HOUR_OF_DAY, 1); 706 } else { 707 calendar.set(GregorianCalendar.MINUTE, minute); 708 break; 709 } 710 } 711 hour = getNextTimeSlice(hoursArray, 712 calendar.get(GregorianCalendar.HOUR_OF_DAY)); 713 if (hour == -1) { 714 calendar.set(GregorianCalendar.HOUR_OF_DAY, 0); 715 calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); 716 } else { 717 calendar.set(GregorianCalendar.HOUR_OF_DAY, hour); 718 break; 719 } 720 } 721 day = getNextTimeSlice(daysArray, 722 calendar.get(GregorianCalendar.DAY_OF_MONTH)); 723 if (day == -1 || day > calendar.getActualMaximum(DAY_OF_MONTH)) 724 { 725 calendar.set(GregorianCalendar.DAY_OF_MONTH, 1); 726 calendar.add(GregorianCalendar.MONTH, 1); 727 } else { 728 calendar.set(GregorianCalendar.DAY_OF_MONTH, day); 729 break; 730 } 731 } 732 month = getNextTimeSlice(monthArray, calendar.get(MONTH) + 1); 733 if (month == -1) { 734 calendar.set(GregorianCalendar.MONTH, 0); 735 calendar.add(GregorianCalendar.YEAR, 1); 736 } 737 else if (day > LEAP_MONTH_LENGTH[month - 1] 738 && (getNextTimeSlice(daysArray, 1) != day 739 || getNextTimeSlice(monthArray, 1) != month)) 740 { 741 calendar.set(DAY_OF_MONTH, 1); 742 calendar.add(MONTH, 1); 743 } else if (day > MONTH_LENGTH[month - 1] 744 && !calendar.isLeapYear(calendar.get(YEAR))) { 745 calendar.add(YEAR, 1); 746 } else { 747 calendar.set(MONTH, month - 1); 748 break; 749 } 750 } 751 weekday = getNextTimeSlice(weekdayArray, calendar.get(DAY_OF_WEEK) - 1); 752 if (weekday == -1 753 || weekday != calendar.get(DAY_OF_WEEK) - 1) 754 { 755 calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); 756 } else { 757 break; 758 } 759 } 760 761 return calendar.getTime(); 762 } 763}