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}