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}