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