From 2273c26793fe6e3abfd90a400823e8e46b3303bb Mon Sep 17 00:00:00 2001
From: abobrov <abobrov@localhost>
Date: Mon, 15 Dec 2008 16:07:29 +0000
Subject: [PATCH] - [Issue 274]  Recurring Tasks

---
 opends/resource/schema/02-config.ldif                                                              |   12 
 opends/src/server/org/opends/server/tools/tasks/TaskEntry.java                                     |   14 
 opends/src/server/org/opends/server/backends/task/TaskState.java                                   |   32 +
 opends/src/server/org/opends/server/tools/tasks/TaskScheduleInformation.java                       |   17 
 opends/src/server/org/opends/server/backends/task/Task.java                                        |   12 
 opends/src/messages/messages/backend.properties                                                    |   41 +
 opends/tests/unit-tests-testng/src/server/org/opends/server/backends/task/TaskBackendTestCase.java |  131 +++++
 opends/src/server/org/opends/server/tasks/BackupTask.java                                          |   10 
 opends/src/server/org/opends/server/tools/ToolConstants.java                                       |   10 
 opends/src/messages/messages/tools.properties                                                      |    8 
 opends/src/server/org/opends/server/tools/RestoreDB.java                                           |   11 
 opends/src/server/org/opends/server/backends/task/TaskScheduler.java                               |  184 ++++++-
 opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java                     |   65 +
 opends/src/server/org/opends/server/tools/BackUpDB.java                                            |   11 
 opends/src/server/org/opends/server/tools/ImportLDIF.java                                          |    8 
 opends/src/server/org/opends/server/tools/ManageTasks.java                                         |   40 +
 opends/src/server/org/opends/server/backends/task/TaskThread.java                                  |    7 
 opends/src/server/org/opends/server/tools/ExportLDIF.java                                          |    8 
 opends/src/server/org/opends/server/tools/tasks/TaskTool.java                                      |  135 +++--
 opends/src/messages/messages/task.properties                                                       |    1 
 opends/src/server/org/opends/server/tools/tasks/TaskClient.java                                    |   94 +++
 opends/src/server/org/opends/server/backends/task/RecurringTask.java                               |  406 +++++++++++++++-
 opends/src/server/org/opends/server/backends/task/TaskBackend.java                                 |  140 +++--
 opends/src/server/org/opends/server/config/ConfigConstants.java                                    |    8 
 24 files changed, 1,169 insertions(+), 236 deletions(-)

diff --git a/opends/resource/schema/02-config.ldif b/opends/resource/schema/02-config.ldif
index e2a9fc0..137ba88 100644
--- a/opends/resource/schema/02-config.ldif
+++ b/opends/resource/schema/02-config.ldif
@@ -311,8 +311,8 @@
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
   SINGLE-VALUE
   X-ORIGIN 'OpenDS Directory Server' )
-attributeTypes: ( 1.3.6.1.4.1.26027.1.1.60
-  NAME 'ds-recurring-task-class-name'
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.515
+  NAME 'ds-recurring-task-schedule'
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
   SINGLE-VALUE
   X-ORIGIN 'OpenDS Directory Server' )
@@ -2728,14 +2728,12 @@
         ds-cfg-profile-sample-interval $
         ds-cfg-profile-action )
   X-ORIGIN 'OpenDS Directory Server' )
-objectClasses: ( 1.3.6.1.4.1.26027.1.2.38
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.198
   NAME 'ds-recurring-task'
   SUP top
-  STRUCTURAL
-  MUST ( ds-recurring-task-class-name $
+  AUXILIARY
+  MUST ( ds-recurring-task-schedule $
          ds-recurring-task-id )
-  MAY ( ds-task-notify-on-completion $
-        ds-task-notify-on-error )
   X-ORIGIN 'OpenDS Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.39
   NAME 'ds-cfg-root-config'
diff --git a/opends/src/messages/messages/backend.properties b/opends/src/messages/messages/backend.properties
index 2839dd2..48e4025 100644
--- a/opends/src/messages/messages/backend.properties
+++ b/opends/src/messages/messages/backend.properties
@@ -318,18 +318,18 @@
 SEVERE_ERR_RECURRINGTASK_MULTIPLE_ID_VALUES_103=The provided recurring task \
  entry contains multiple values for the %s attribute, which is used to specify \
  the recurring task ID, but only a single value is allowed
-SEVERE_ERR_RECURRINGTASK_NO_CLASS_ATTRIBUTE_104=The provided recurring task \
- entry does not contain attribute %s which is needed to specify the \
- fully-qualified name of the class providing the task logic
-SEVERE_ERR_RECURRINGTASK_MULTIPLE_CLASS_TYPES_105=The provided recurring task \
- entry contains multiple attributes with type %s, which is used to hold the \
- task class name, but only a single instance is allowed
-SEVERE_ERR_RECURRINGTASK_NO_CLASS_VALUES_106=The provided recurring task \
+SEVERE_ERR_RECURRINGTASK_NO_SCHEDULE_ATTRIBUTE_104=The provided recurring task \
+ entry does not contain attribute %s which is needed to specify recurring task \
+ schedule
+SEVERE_ERR_RECURRINGTASK_MULTIPLE_SCHEDULE_TYPES_105=The provided recurring \
+ task entry contains multiple attributes with type %s, which is used to hold \
+ recurring task schedule, but only a single instance is allowed
+SEVERE_ERR_RECURRINGTASK_NO_SCHEDULE_VALUES_106=The provided recurring task \
  entry does not contain any values for the %s attribute, which is used to \
- specify the fully-qualified name of the class providing the task logic
-SEVERE_ERR_RECURRINGTASK_MULTIPLE_CLASS_VALUES_107=The provided recurring \
+ specify recurring task schedule
+SEVERE_ERR_RECURRINGTASK_MULTIPLE_SCHEDULE_VALUES_107=The provided recurring \
  task entry contains multiple values for the %s attribute, which is used to \
- specify the task class name, but only a single value is allowed
+ specify recurring task schedule, but only a single value is allowed
 SEVERE_ERR_RECURRINGTASK_CANNOT_LOAD_CLASS_108=An error occurred while \
  attempting to load class %s specified in attribute %s of the provided \
  recurring task entry:  %s.  Does this class exist in the Directory Server \
@@ -1035,3 +1035,24 @@
 MILD_ERR_NUM_SUBORDINATES_NOT_SUPPORTED_369=This backend does not provide \
  support for the numSubordinates operational attribute
 NOTICE_BACKEND_OFFLINE_370=The backend %s is now taken offline
+SEVERE_ERR_RECURRINGTASK_INVALID_N_TOKENS_371=The provided recurring task \
+ entry attribute %s holding the recurring task schedule has invalid number \
+ of tokens
+SEVERE_ERR_RECURRINGTASK_INVALID_MINUTE_TOKEN_372=The provided recurring task \
+ entry attribute %s holding the recurring task schedule has invalid minute \
+ token
+SEVERE_ERR_RECURRINGTASK_INVALID_HOUR_TOKEN_373=The provided recurring task \
+ entry attribute %s holding the recurring task schedule has invalid hour \
+ token
+SEVERE_ERR_RECURRINGTASK_INVALID_DAY_TOKEN_374=The provided recurring task \
+ entry attribute %s holding the recurring task schedule has invalid day of \
+ the month token
+SEVERE_ERR_RECURRINGTASK_INVALID_MONTH_TOKEN_375=The provided recurring task \
+ entry attribute %s holding the recurring task schedule has invalid month of \
+ the year token
+SEVERE_ERR_RECURRINGTASK_INVALID_WEEKDAY_TOKEN_376=The provided recurring task \
+ entry attribute %s holding the recurring task schedule has invalid day of the \
+ week token
+SEVERE_ERR_RECURRINGTASK_INVALID_TOKENS_COMBO_377=The provided recurring task \
+ entry attribute %s holding the recurring task schedule has invalid tokens \
+ combination yielding a nonexistent calendar date
diff --git a/opends/src/messages/messages/task.properties b/opends/src/messages/messages/task.properties
index c8ce673..c1e756a 100644
--- a/opends/src/messages/messages/task.properties
+++ b/opends/src/messages/messages/task.properties
@@ -193,3 +193,4 @@
 INFO_IMPORT_ARG_RANDOM_SEED_105=Random Seed
 SEVERE_ERR_TASK_LDAP_FAILED_TO_CONNECT_WRONG_PORT_106=Unable to connect to the \
  server at %s on port %s. Check this port is an administration port
+INFO_TASK_STATE_RECURRING_107=Recurring
diff --git a/opends/src/messages/messages/tools.properties b/opends/src/messages/messages/tools.properties
index cada3b3..d6d99ee 100644
--- a/opends/src/messages/messages/tools.properties
+++ b/opends/src/messages/messages/tools.properties
@@ -2144,7 +2144,7 @@
   be canceled
 INFO_TASKINFO_NO_CANCELABLE_TASKS_1452=There are currently no cancelable tasks
 SEVERE_ERR_TASK_CLIENT_UNKNOWN_TASK_1453=There are no tasks defined with ID '%s'
-SEVERE_ERR_TASK_CLIENT_UNCANCELABLE_TASK_1454=Task '%s' is has finished and \
+SEVERE_ERR_TASK_CLIENT_UNCANCELABLE_TASK_1454=Task '%s' has finished and \
   cannot be canceled
 SEVERE_ERR_TASK_CLIENT_TASK_STATE_UNKNOWN_1455=State for task '%s' cannot be \
   determined
@@ -2431,3 +2431,9 @@
 cannot be backuped is the directory %s: this directory is already a backup \
 directory for backend %s
 
+INFO_RECURRING_TASK_PLACEHOLDER_1651={schedulePattern}
+INFO_DESCRIPTION_RECURRING_TASK_1652=Indicates the task is recurring and will \
+ be scheduled according to the value argument expressed in crontab(5) \
+ compatible time/date pattern
+INFO_TASK_TOOL_RECURRING_TASK_SCHEDULED_1653=Recurring %s task %s scheduled \
+ successfully
diff --git a/opends/src/server/org/opends/server/backends/task/RecurringTask.java b/opends/src/server/org/opends/server/backends/task/RecurringTask.java
index ab117ae..67fcec9 100644
--- a/opends/src/server/org/opends/server/backends/task/RecurringTask.java
+++ b/opends/src/server/org/opends/server/backends/task/RecurringTask.java
@@ -25,6 +25,10 @@
  *      Copyright 2006-2008 Sun Microsystems, Inc.
  */
 package org.opends.server.backends.task;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.GregorianCalendar;
 import org.opends.messages.Message;
 
 
@@ -32,6 +36,8 @@
 import java.util.Iterator;
 import java.util.List;
 
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
 import org.opends.server.core.DirectoryServer;
 import org.opends.server.types.Attribute;
 import org.opends.server.types.AttributeType;
@@ -45,9 +51,13 @@
 import static org.opends.server.config.ConfigConstants.*;
 import static org.opends.server.loggers.debug.DebugLogger.*;
 import org.opends.server.loggers.debug.DebugTracer;
+import org.opends.server.types.Attributes;
 import org.opends.server.types.DebugLogLevel;
+import org.opends.server.types.RDN;
+
 import static org.opends.messages.BackendMessages.*;
 import static org.opends.server.util.StaticUtils.*;
+import static org.opends.server.util.ServerConstants.*;
 
 
 
@@ -78,7 +88,38 @@
   // class.
   private String taskClassName;
 
+  // Task instance.
+  private Task task;
 
+  // Task scheduler for this task.
+  private TaskScheduler taskScheduler;
+
+  // Number of tokens in the task schedule tab.
+  private static final int TASKTAB_NUM_TOKENS = 5;
+
+  /**
+   * Task tab fields.
+   */
+  private static enum TaskTab {MINUTE, HOUR, DAY, MONTH, WEEKDAY};
+
+  // Exact match pattern.
+  private static final Pattern exactPattern =
+    Pattern.compile("\\d+");
+
+  // Range match pattern.
+  private static final Pattern rangePattern =
+    Pattern.compile("\\d+[-]\\d+");
+
+  // List match pattern.
+  private static final Pattern listPattern =
+    Pattern.compile("^(\\d+,)(.*)(\\d+)$");
+
+  // Boolean arrays holding task tab slots.
+  private boolean[] minutesArray;
+  private boolean[] hoursArray;
+  private boolean[] daysArray;
+  private boolean[] monthArray;
+  private boolean[] weekdayArray;
 
   /**
    * Creates a new recurring task based on the information in the provided
@@ -95,10 +136,10 @@
   public RecurringTask(TaskScheduler taskScheduler, Entry recurringTaskEntry)
          throws DirectoryException
   {
-    this.recurringTaskEntry   = recurringTaskEntry;
+    this.taskScheduler = taskScheduler;
+    this.recurringTaskEntry = recurringTaskEntry;
     this.recurringTaskEntryDN = recurringTaskEntry.getDN();
 
-
     // Get the recurring task ID from the entry.  If there isn't one, then fail.
     AttributeType attrType = DirectoryServer.getAttributeType(
                                   ATTR_RECURRING_TASK_ID.toLowerCase());
@@ -142,39 +183,35 @@
     recurringTaskID = value.getStringValue();
 
 
-    // FIXME -- Need to have some method of getting the scheduling information
-    //          from the recurring task entry.
-
-
-    // Get the class name from the entry.  If there isn't one, then fail.
+    // Get the schedule for this task.
     attrType = DirectoryServer.getAttributeType(
-                    ATTR_RECURRING_TASK_CLASS_NAME.toLowerCase());
+                    ATTR_RECURRING_TASK_SCHEDULE.toLowerCase());
     if (attrType == null)
     {
       attrType = DirectoryServer.getDefaultAttributeType(
-                                      ATTR_RECURRING_TASK_CLASS_NAME);
+        ATTR_RECURRING_TASK_SCHEDULE);
     }
 
     attrList = recurringTaskEntry.getAttribute(attrType);
     if ((attrList == null) || attrList.isEmpty())
     {
-      Message message = ERR_RECURRINGTASK_NO_CLASS_ATTRIBUTE.get(
-          ATTR_RECURRING_TASK_CLASS_NAME);
+      Message message = ERR_RECURRINGTASK_NO_SCHEDULE_ATTRIBUTE.get(
+          ATTR_RECURRING_TASK_SCHEDULE);
       throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
     }
 
-    if (attrList.size() > 0)
+    if (attrList.size() > 1)
     {
-      Message message = ERR_RECURRINGTASK_MULTIPLE_CLASS_TYPES.get(
-          ATTR_RECURRING_TASK_CLASS_NAME);
+      Message message = ERR_RECURRINGTASK_MULTIPLE_SCHEDULE_TYPES.get(
+          ATTR_RECURRING_TASK_SCHEDULE);
       throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
     }
 
     attr = attrList.get(0);
     if (attr.isEmpty())
     {
-      Message message =
-          ERR_RECURRINGTASK_NO_CLASS_VALUES.get(ATTR_RECURRING_TASK_CLASS_NAME);
+      Message message = ERR_RECURRINGTASK_NO_SCHEDULE_VALUES.get(
+        ATTR_RECURRING_TASK_SCHEDULE);
       throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
     }
 
@@ -182,8 +219,51 @@
     value = iterator.next();
     if (iterator.hasNext())
     {
-      Message message = ERR_RECURRINGTASK_MULTIPLE_CLASS_VALUES.get(
-          ATTR_RECURRING_TASK_CLASS_NAME);
+      Message message = ERR_RECURRINGTASK_MULTIPLE_SCHEDULE_VALUES.get(
+          ATTR_RECURRING_TASK_SCHEDULE);
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
+    }
+
+    String taskScheduleTab = value.getStringValue();
+    parseTaskTab(taskScheduleTab);
+
+    // Get the class name from the entry.  If there isn't one, then fail.
+    attrType = DirectoryServer.getAttributeType(
+                    ATTR_TASK_CLASS.toLowerCase());
+    if (attrType == null)
+    {
+      attrType = DirectoryServer.getDefaultAttributeType(ATTR_TASK_CLASS);
+    }
+
+    attrList = recurringTaskEntry.getAttribute(attrType);
+    if ((attrList == null) || attrList.isEmpty())
+    {
+      Message message = ERR_TASKSCHED_NO_CLASS_ATTRIBUTE.get(
+          ATTR_TASK_CLASS);
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
+    }
+
+    if (attrList.size() > 1)
+    {
+      Message message = ERR_TASKSCHED_MULTIPLE_CLASS_TYPES.get(
+          ATTR_TASK_CLASS);
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
+    }
+
+    attr = attrList.get(0);
+    if (attr.isEmpty())
+    {
+      Message message =
+          ERR_TASKSCHED_NO_CLASS_VALUES.get(ATTR_TASK_CLASS);
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
+    }
+
+    iterator = attr.iterator();
+    value = iterator.next();
+    if (iterator.hasNext())
+    {
+      Message message = ERR_TASKSCHED_MULTIPLE_CLASS_VALUES.get(
+          ATTR_TASK_CLASS);
       throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
     }
 
@@ -204,7 +284,7 @@
       }
 
       Message message = ERR_RECURRINGTASK_CANNOT_LOAD_CLASS.
-          get(String.valueOf(taskClassName), ATTR_RECURRING_TASK_CLASS_NAME,
+          get(String.valueOf(taskClassName), ATTR_TASK_CLASS,
               getExceptionMessage(e));
       throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
                                    e);
@@ -212,7 +292,6 @@
 
 
     // Make sure that the specified class can be instantiated as a task.
-    Task task;
     try
     {
       task = (Task) taskClass.newInstance();
@@ -308,12 +387,289 @@
   /**
    * Schedules the next iteration of this recurring task for processing.
    *
-   * @return  The task that has been scheduled for processing.
+   * @return The task that has been scheduled for processing.
+   * @throws DirectoryException to indicate an error.
    */
-  public Task scheduleNextIteration()
+  public Task scheduleNextIteration() throws DirectoryException
   {
-    // NYI
-    return null;
+    Task nextTask = null;
+    Date nextTaskDate = null;
+
+    try {
+      nextTaskDate = getNextIteration();
+    } catch (IllegalArgumentException e) {
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+        ERR_RECURRINGTASK_INVALID_TOKENS_COMBO.get(
+        ATTR_RECURRING_TASK_SCHEDULE));
+    }
+
+    SimpleDateFormat dateFormat = new SimpleDateFormat(
+      DATE_FORMAT_COMPACT_LOCAL_TIME);
+    String nextTaskStartTime = dateFormat.format(nextTaskDate);
+
+    try {
+      // Make a regular task iteration from this recurring task.
+      nextTask = task.getClass().newInstance();
+      Entry nextTaskEntry = recurringTaskEntry.duplicate(false);
+      String nextTaskID = task.getTaskID() + " - " +
+        nextTaskDate.toString();
+      String nextTaskIDName = NAME_PREFIX_TASK + "id";
+      AttributeType taskIDAttrType =
+        DirectoryServer.getAttributeType(nextTaskIDName);
+      Attribute nextTaskIDAttr = Attributes.create(
+        taskIDAttrType, nextTaskID);
+      nextTaskEntry.replaceAttribute(nextTaskIDAttr);
+      RDN nextTaskRDN = RDN.decode(nextTaskIDName + "=" + nextTaskID);
+      DN nextTaskDN = new DN(nextTaskRDN,
+        taskScheduler.getTaskBackend().getScheduledTasksParentDN());
+      nextTaskEntry.setDN(nextTaskDN);
+
+      String nextTaskStartTimeName = NAME_PREFIX_TASK +
+        "scheduled-start-time";
+      AttributeType taskStartTimeAttrType =
+        DirectoryServer.getAttributeType(nextTaskStartTimeName);
+      Attribute nextTaskStartTimeAttr = Attributes.create(
+        taskStartTimeAttrType, nextTaskStartTime);
+      nextTaskEntry.replaceAttribute(nextTaskStartTimeAttr);
+
+      nextTask.initializeTaskInternal(taskScheduler, nextTaskEntry);
+      nextTask.initializeTask();
+    } catch (Exception e) {
+      // Should not happen, debug log it otherwise.
+      if (debugEnabled())
+      {
+        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+      }
+    }
+
+    return nextTask;
+  }
+
+  /**
+   * Parse and validate recurring task schedule.
+   * @param taskSchedule recurring task schedule tab in crontab(5) format.
+   * @throws DirectoryException to indicate an error.
+   */
+  private void parseTaskTab(String taskSchedule) throws DirectoryException
+  {
+    StringTokenizer st = new StringTokenizer(taskSchedule);
+
+    if (st.countTokens() != TASKTAB_NUM_TOKENS) {
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+        ERR_RECURRINGTASK_INVALID_N_TOKENS.get(
+        ATTR_RECURRING_TASK_SCHEDULE));
+    }
+
+    for (TaskTab taskTabToken : TaskTab.values()) {
+      String token = st.nextToken();
+      switch (taskTabToken) {
+        case MINUTE:
+          try {
+            minutesArray = parseTaskTabField(token, 0, 59);
+          } catch (IllegalArgumentException e) {
+            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+              ERR_RECURRINGTASK_INVALID_MINUTE_TOKEN.get(
+              ATTR_RECURRING_TASK_SCHEDULE));
+          }
+          break;
+        case HOUR:
+          try {
+            hoursArray = parseTaskTabField(token, 0, 23);
+          } catch (IllegalArgumentException e) {
+            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+              ERR_RECURRINGTASK_INVALID_HOUR_TOKEN.get(
+              ATTR_RECURRING_TASK_SCHEDULE));
+          }
+          break;
+        case DAY:
+          try {
+            daysArray = parseTaskTabField(token, 1, 31);
+          } catch (IllegalArgumentException e) {
+            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+              ERR_RECURRINGTASK_INVALID_DAY_TOKEN.get(
+              ATTR_RECURRING_TASK_SCHEDULE));
+          }
+          break;
+        case MONTH:
+          try {
+            monthArray = parseTaskTabField(token, 1, 12);
+          } catch (IllegalArgumentException e) {
+            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+              ERR_RECURRINGTASK_INVALID_MONTH_TOKEN.get(
+              ATTR_RECURRING_TASK_SCHEDULE));
+          }
+          break;
+        case WEEKDAY:
+          try {
+            weekdayArray = parseTaskTabField(token, 0, 6);
+          } catch (IllegalArgumentException e) {
+            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+              ERR_RECURRINGTASK_INVALID_WEEKDAY_TOKEN.get(
+              ATTR_RECURRING_TASK_SCHEDULE));
+          }
+          break;
+      }
+    }
+  }
+
+  /**
+   * Parse and validate recurring task schedule field.
+   * @param tabField recurring task schedule field in crontab(5) format.
+   * @param minValue minimum value allowed for this field.
+   * @param maxValue maximum value allowed for this field.
+   * @return boolean schedule slots range set according to
+   *         the schedule field.
+   * @throws IllegalArgumentException if tab field is invalid.
+   */
+  private boolean[] parseTaskTabField(String tabField,
+    int minValue, int maxValue) throws IllegalArgumentException
+  {
+    boolean[] valueList = new boolean[maxValue + 1];
+    Arrays.fill(valueList, false);
+
+    // Blanket.
+    if (tabField.equals("*")) {
+      for (int i = minValue; i <= maxValue; i++) {
+        valueList[i] = true;
+      }
+      return valueList;
+    }
+
+    // Exact.
+    if (exactPattern.matcher(tabField).matches()) {
+      int value = Integer.parseInt(tabField);
+      if ((value >= minValue) && (value <= maxValue)) {
+        valueList[value] = true;
+        return valueList;
+      }
+      throw new IllegalArgumentException();
+    }
+
+    // Range.
+    if (rangePattern.matcher(tabField).matches()) {
+      StringTokenizer st = new StringTokenizer(tabField, "-");
+      int startValue = Integer.parseInt(st.nextToken());
+      int endValue = Integer.parseInt(st.nextToken());
+      if ((startValue < endValue) &&
+          ((startValue >= minValue) && (endValue <= maxValue)))
+      {
+        for (int i = startValue; i <= endValue; i++) {
+          valueList[i] = true;
+        }
+        return valueList;
+      }
+      throw new IllegalArgumentException();
+    }
+
+    // List.
+    if (listPattern.matcher(tabField).matches()) {
+      StringTokenizer st = new StringTokenizer(tabField, ",");
+      while (st.hasMoreTokens()) {
+        int value = Integer.parseInt(st.nextToken());
+        if ((value >= minValue) && (value <= maxValue)) {
+          valueList[value] = true;
+        } else {
+          throw new IllegalArgumentException();
+        }
+      }
+      return valueList;
+    }
+
+    throw new IllegalArgumentException();
+  }
+
+  /**
+   * Get next reccuring slot from the range.
+   * @param timesList the range.
+   * @param fromNow the current slot.
+   * @return next recurring slot in the range.
+   */
+  private int getNextTimeSlice(boolean[] timesList, int fromNow)
+  {
+    for (int i = fromNow; i < timesList.length; i++) {
+      if (timesList[i]) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Get next task iteration date according to recurring schedule.
+   * @return next task iteration date.
+   * @throws IllegalArgumentException if recurring schedule is invalid.
+   */
+  private Date getNextIteration() throws IllegalArgumentException
+  {
+    int minute, hour, day, month, weekday;
+    GregorianCalendar calendar = new GregorianCalendar();
+    calendar.setFirstDayOfWeek(GregorianCalendar.SUNDAY);
+    calendar.add(GregorianCalendar.MINUTE, 1);
+    calendar.set(GregorianCalendar.SECOND, 0);
+    calendar.setLenient(false);
+
+    // Weekday
+    for (;;) {
+      // Month
+      for (;;) {
+        // Day
+        for (;;) {
+          // Hour
+          for (;;) {
+            // Minute
+            for (;;) {
+              minute = getNextTimeSlice(minutesArray,
+                calendar.get(GregorianCalendar.MINUTE));
+              if (minute == -1) {
+                calendar.set(GregorianCalendar.MINUTE, 0);
+                calendar.add(GregorianCalendar.HOUR_OF_DAY, 1);
+              } else {
+                calendar.set(GregorianCalendar.MINUTE, minute);
+                break;
+              }
+            }
+            hour = getNextTimeSlice(hoursArray,
+              calendar.get(GregorianCalendar.HOUR_OF_DAY));
+            if (hour == -1) {
+              calendar.set(GregorianCalendar.HOUR_OF_DAY, 0);
+              calendar.add(GregorianCalendar.DAY_OF_MONTH, 1);
+            } else {
+              calendar.set(GregorianCalendar.HOUR_OF_DAY, hour);
+              break;
+            }
+          }
+          day = getNextTimeSlice(daysArray,
+            calendar.get(GregorianCalendar.DAY_OF_MONTH));
+          if (day == -1) {
+            calendar.set(GregorianCalendar.DAY_OF_MONTH, 1);
+            calendar.add(GregorianCalendar.MONTH, 1);
+          } else {
+            calendar.set(GregorianCalendar.DAY_OF_MONTH, day);
+            break;
+          }
+        }
+        month = getNextTimeSlice(monthArray,
+          (calendar.get(GregorianCalendar.MONTH) + 1));
+        if (month == -1) {
+          calendar.set(GregorianCalendar.MONTH, 0);
+          calendar.add(GregorianCalendar.YEAR, 1);
+        } else {
+          calendar.set(GregorianCalendar.MONTH, (month - 1));
+          break;
+        }
+      }
+      weekday = getNextTimeSlice(weekdayArray,
+        (calendar.get(GregorianCalendar.DAY_OF_WEEK) - 1));
+      if ((weekday == -1) ||
+          (weekday != (calendar.get(
+           GregorianCalendar.DAY_OF_WEEK) - 1)))
+      {
+        calendar.add(GregorianCalendar.DAY_OF_MONTH, 1);
+      } else {
+        break;
+      }
+    }
+
+    return calendar.getTime();
   }
 }
-
diff --git a/opends/src/server/org/opends/server/backends/task/Task.java b/opends/src/server/org/opends/server/backends/task/Task.java
index 82fc38f..d8e6ec3 100644
--- a/opends/src/server/org/opends/server/backends/task/Task.java
+++ b/opends/src/server/org/opends/server/backends/task/Task.java
@@ -580,6 +580,18 @@
   }
 
   /**
+   * Indicates whether or not this task is an iteration of
+   * some recurring task.
+   *
+   * @return boolean where true indicates that this task is
+   *         recurring, false otherwise.
+   */
+  public boolean isRecurring()
+  {
+    return (recurringTaskID != null);
+  }
+
+  /**
    * Indicates whether or not this task has been cancelled.
    *
    * @return boolean where true indicates that this task was
diff --git a/opends/src/server/org/opends/server/backends/task/TaskBackend.java b/opends/src/server/org/opends/server/backends/task/TaskBackend.java
index b158520..6029483 100644
--- a/opends/src/server/org/opends/server/backends/task/TaskBackend.java
+++ b/opends/src/server/org/opends/server/backends/task/TaskBackend.java
@@ -617,7 +617,11 @@
       TaskState state = t.getTaskState();
       if (TaskState.isPending(state))
       {
-        taskScheduler.removePendingTask(t.getTaskID());
+        if (t.isRecurring()) {
+          taskScheduler.removeRecurringTaskIteration(t.getTaskID());
+        } else {
+          taskScheduler.removePendingTask(t.getTaskID());
+        }
       }
       else if (TaskState.isDone(t.getTaskState()))
       {
@@ -641,9 +645,6 @@
         throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
       }
 
-
-      // Try to remove the recurring task.  This will fail if there are any
-      // associated iterations pending or running.
       taskScheduler.removeRecurringTask(rt.getRecurringTaskID());
     }
     else
@@ -707,13 +708,12 @@
           throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
         }
 
-
         // Look at the state of the task.  We will allow anything to be altered
         // for a pending task.  For a running task, we will only allow the state
         // to be altered in order to cancel it.  We will not allow any
         // modifications for completed tasks.
         TaskState state = t.getTaskState();
-        if (TaskState.isPending(state))
+        if (TaskState.isPending(state) && !t.isRecurring())
         {
           Task newTask = taskScheduler.entryToScheduledTask(newEntry,
               modifyOperation);
@@ -727,50 +727,7 @@
           // This will only be allowed using the replace modification type on
           // the ds-task-state attribute if the value starts with "cancel" or
           // "stop".  In that case, we'll cancel the task.
-          boolean acceptable = true;
-          for (Modification m : modifyOperation.getModifications())
-          {
-            if (m.isInternal())
-            {
-              continue;
-            }
-
-            if (m.getModificationType() != ModificationType.REPLACE)
-            {
-              acceptable = false;
-              break;
-            }
-
-            Attribute a = m.getAttribute();
-            AttributeType at = a.getAttributeType();
-            if (! at.hasName(ATTR_TASK_STATE))
-            {
-              acceptable = false;
-              break;
-            }
-
-            Iterator<AttributeValue> iterator = a.iterator();
-            if (! iterator.hasNext())
-            {
-              acceptable = false;
-              break;
-            }
-
-            AttributeValue v = iterator.next();
-            String valueString = toLowerCase(v.getStringValue());
-            if (! (valueString.startsWith("cancel") ||
-                   valueString.startsWith("stop")))
-            {
-              acceptable = false;
-              break;
-            }
-
-            if (iterator.hasNext())
-            {
-              acceptable = false;
-              break;
-            }
-          }
+          boolean acceptable = isReplaceEntryAcceptable(modifyOperation);
 
           if (acceptable)
           {
@@ -786,6 +743,37 @@
                                          message);
           }
         }
+        else if (TaskState.isPending(state) && t.isRecurring())
+        {
+          // Pending recurring task iterations can only be canceled.
+          boolean acceptable = isReplaceEntryAcceptable(modifyOperation);
+
+          if (acceptable)
+          {
+            Task newTask = taskScheduler.entryToScheduledTask(newEntry,
+              modifyOperation);
+            if (newTask.getTaskState() ==
+              TaskState.CANCELED_BEFORE_STARTING)
+            {
+              taskScheduler.removePendingTask(t.getTaskID());
+              taskScheduler.scheduleTask(newTask, true);
+            }
+            else if (newTask.getTaskState() ==
+              TaskState.STOPPED_BY_ADMINISTRATOR)
+            {
+              Message message = INFO_TASKBE_RUNNING_TASK_CANCELLED.get();
+              t.interruptTask(TaskState.STOPPED_BY_ADMINISTRATOR, message);
+            }
+              return;
+          }
+          else
+          {
+            Message message =
+              ERR_TASKBE_MODIFY_RECURRING.get(String.valueOf(entryDN));
+            throw new DirectoryException(
+              ResultCode.UNWILLING_TO_PERFORM, message);
+          }
+        }
         else
         {
           Message message =
@@ -820,6 +808,58 @@
 
 
   /**
+   * Helper to determine if requested modifications are acceptable.
+   * @param modifyOperation associated with requested modifications.
+   * @return <CODE>true</CODE> if requested modifications are
+   *         acceptable, <CODE>false</CODE> otherwise.
+   */
+  private boolean isReplaceEntryAcceptable(ModifyOperation modifyOperation)
+  {
+    boolean acceptable = true;
+
+    for (Modification m : modifyOperation.getModifications()) {
+      if (m.isInternal()) {
+        continue;
+      }
+
+      if (m.getModificationType() != ModificationType.REPLACE) {
+        acceptable = false;
+        break;
+      }
+
+      Attribute a = m.getAttribute();
+      AttributeType at = a.getAttributeType();
+      if (!at.hasName(ATTR_TASK_STATE)) {
+        acceptable = false;
+        break;
+      }
+
+      Iterator<AttributeValue> iterator = a.iterator();
+      if (!iterator.hasNext()) {
+        acceptable = false;
+        break;
+      }
+
+      AttributeValue v = iterator.next();
+      String valueString = toLowerCase(v.getStringValue());
+      if (!(valueString.startsWith("cancel") ||
+        valueString.startsWith("stop"))) {
+        acceptable = false;
+        break;
+      }
+
+      if (iterator.hasNext()) {
+        acceptable = false;
+        break;
+      }
+    }
+
+    return acceptable;
+  }
+
+
+
+  /**
    * {@inheritDoc}
    */
   @Override()
diff --git a/opends/src/server/org/opends/server/backends/task/TaskScheduler.java b/opends/src/server/org/opends/server/backends/task/TaskScheduler.java
index 7dcd46b..6c30a95 100644
--- a/opends/src/server/org/opends/server/backends/task/TaskScheduler.java
+++ b/opends/src/server/org/opends/server/backends/task/TaskScheduler.java
@@ -37,6 +37,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -55,6 +56,7 @@
 import org.opends.server.types.Attribute;
 import org.opends.server.types.AttributeType;
 import org.opends.server.types.AttributeValue;
+import org.opends.server.types.Attributes;
 import org.opends.server.types.DN;
 import org.opends.server.types.DebugLogLevel;
 import org.opends.server.types.DirectoryException;
@@ -191,6 +193,27 @@
     DirectoryServer.registerAlertGenerator(this);
 
     initializeTasksFromBackingFile();
+
+    for (RecurringTask recurringTask : recurringTasks.values()) {
+      Task task = null;
+      try {
+        task = recurringTask.scheduleNextIteration();
+      } catch (DirectoryException de) {
+        logError(de.getMessageObject());
+      }
+      if (task != null) {
+        try {
+          scheduleTask(task, false);
+        } catch (DirectoryException de) {
+          // This task might have been already scheduled from before
+          // and thus got initialized from backing file, otherwise
+          // log error and continue.
+          if (de.getResultCode() != ResultCode.ENTRY_ALREADY_EXISTS) {
+            logError(de.getMessageObject());
+          }
+        }
+      }
+    }
   }
 
 
@@ -224,7 +247,12 @@
         throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, message);
       }
 
-      recurringTasks.put(id, recurringTask);
+      Attribute attr = Attributes.create(ATTR_TASK_STATE,
+        TaskState.RECURRING.toString());
+      ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
+      attrList.add(attr);
+      Entry recurringTaskEntry = recurringTask.getRecurringTaskEntry();
+      recurringTaskEntry.putAttribute(attr.getAttributeType(), attrList);
 
       if (scheduleIteration)
       {
@@ -236,6 +264,7 @@
         }
       }
 
+      recurringTasks.put(id, recurringTask);
       writeState();
     }
     finally
@@ -264,24 +293,19 @@
 
     try
     {
+      RecurringTask recurringTask = recurringTasks.remove(recurringTaskID);
+      writeState();
+
       for (Task t : tasks.values())
       {
         if ((t.getRecurringTaskID() != null) &&
             (t.getRecurringTaskID().equals(recurringTaskID)) &&
-            (! TaskState.isDone(t.getTaskState())))
+            (!TaskState.isDone(t.getTaskState())))
         {
-          Message message = ERR_TASKSCHED_REMOVE_RECURRING_EXISTING_ITERATION.
-              get(String.valueOf(recurringTaskID),
-                  String.valueOf(t.getTaskID()));
-          throw new DirectoryException(
-                  ResultCode.UNWILLING_TO_PERFORM, message);
+          cancelTask(t.getTaskID());
         }
       }
 
-
-      RecurringTask recurringTask = recurringTasks.remove(recurringTaskID);
-      writeState();
-
       return recurringTask;
     }
     finally
@@ -348,7 +372,15 @@
       }
       else if (TaskState.isDone(state))
       {
-        completedTasks.add(task);
+        if ((state == TaskState.CANCELED_BEFORE_STARTING) &&
+          task.isRecurring())
+        {
+          pendingTasks.add(task);
+        }
+        else
+        {
+          completedTasks.add(task);
+        }
       }
       else
       {
@@ -459,6 +491,53 @@
 
 
   /**
+   * Removes the specified pending iteration of recurring task. It will
+   * be removed from the task set but still be kept in the pending set.
+   *
+   * @param  taskID  The task ID of the pending iteration to remove.
+   *
+   * @return  The task that was removed.
+   *
+   * @throws  DirectoryException  If the requested task is not in the
+   *                              pending queue.
+   */
+  public Task removeRecurringTaskIteration(String taskID)
+         throws DirectoryException
+  {
+    schedulerLock.lock();
+
+    try
+    {
+      Task t = tasks.get(taskID);
+      if (t == null)
+      {
+        Message message = ERR_TASKSCHED_REMOVE_PENDING_NO_SUCH_TASK.get(
+            String.valueOf(taskID));
+        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
+      }
+
+      if (TaskState.isPending(t.getTaskState()))
+      {
+        tasks.remove(taskID);
+        writeState();
+        return t;
+      }
+      else
+      {
+        Message message = ERR_TASKSCHED_REMOVE_PENDING_NOT_PENDING.get(
+            String.valueOf(taskID));
+        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
+      }
+    }
+    finally
+    {
+      schedulerLock.unlock();
+    }
+  }
+
+
+
+  /**
    * Removes the specified completed task.
    *
    * @param  taskID  The task ID of the completed task to remove.
@@ -546,7 +625,12 @@
         }
         else
         {
-          Task newIteration = recurringTask.scheduleNextIteration();
+          Task newIteration = null;
+          try {
+            newIteration = recurringTask.scheduleNextIteration();
+          } catch (DirectoryException de) {
+            logError(de.getMessageObject());
+          }
           if (newIteration != null)
           {
             // FIXME -- What to do if new iteration is null?
@@ -746,6 +830,7 @@
    * Operates in a loop, launching tasks at the appropriate time and performing
    * any necessary periodic cleanup.
    */
+  @Override
   public void run()
   {
     isRunning       = true;
@@ -797,6 +882,31 @@
               long waitTime = t.getScheduledStartTime() - TimeThread.getTime();
               sleepTime = Math.min(sleepTime, waitTime);
             }
+            // Recurring task iteration has to spawn the next one
+            // even if the current iteration has been canceled.
+            else if ((state == TaskState.CANCELED_BEFORE_STARTING) &&
+                     t.isRecurring())
+            {
+              if (t.getScheduledStartTime() > TimeThread.getTime()) {
+                // If we're waiting for the start time to arrive,
+                // then see if that will come before the next
+                // sleep time is up.
+                long waitTime =
+                  t.getScheduledStartTime() - TimeThread.getTime();
+                sleepTime = Math.min(sleepTime, waitTime);
+              } else {
+                TaskThread taskThread;
+                if (idleThreads.isEmpty()) {
+                  taskThread = new TaskThread(this, nextThreadID++);
+                  taskThread.start();
+                } else {
+                  taskThread = idleThreads.removeFirst();
+                }
+                runningTasks.add(t);
+                activeThreads.put(t.getTaskID(), taskThread);
+                taskThread.setTask(t);
+              }
+            }
 
             if (state != t.getTaskState())
             {
@@ -876,7 +986,13 @@
   {
     // If the task has finished we don't want to restart it
     TaskState state = task.getTaskState();
-    if (state != null && TaskState.isDone(state))
+
+    // Reset task state if recurring.
+    if (state == TaskState.RECURRING) {
+      state = null;
+    }
+
+    if ((state != null) && TaskState.isDone(state))
     {
       return state;
     }
@@ -1011,26 +1127,6 @@
                     String.valueOf(taskBackend.getTaskRootDN()));
             logError(message);
           }
-          else if (parentDN.equals(taskBackend.getRecurringTasksParentDN()))
-          {
-            try
-            {
-              RecurringTask recurringTask = entryToRecurringTask(entry);
-              addRecurringTask(recurringTask, false);
-            }
-            catch (DirectoryException de)
-            {
-              if (debugEnabled())
-              {
-                TRACER.debugCaught(DebugLogLevel.ERROR, de);
-              }
-
-              Message message =
-                  ERR_TASKSCHED_CANNOT_SCHEDULE_RECURRING_TASK_FROM_ENTRY.
-                    get(String.valueOf(entryDN), de.getMessageObject());
-              logError(message);
-            }
-          }
           else if (parentDN.equals(taskBackend.getScheduledTasksParentDN()))
           {
             try
@@ -1057,6 +1153,26 @@
               logError(message);
             }
           }
+          else if (parentDN.equals(taskBackend.getRecurringTasksParentDN()))
+          {
+            try
+            {
+              RecurringTask recurringTask = entryToRecurringTask(entry);
+              addRecurringTask(recurringTask, false);
+            }
+            catch (DirectoryException de)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, de);
+              }
+
+              Message message =
+                  ERR_TASKSCHED_CANNOT_SCHEDULE_RECURRING_TASK_FROM_ENTRY.
+                    get(String.valueOf(entryDN), de.getMessageObject());
+              logError(message);
+            }
+          }
           else
           {
             Message message = ERR_TASKSCHED_INVALID_TASK_ENTRY_DN.get(
diff --git a/opends/src/server/org/opends/server/backends/task/TaskState.java b/opends/src/server/org/opends/server/backends/task/TaskState.java
index d3c10ba..be920e4 100644
--- a/opends/src/server/org/opends/server/backends/task/TaskState.java
+++ b/opends/src/server/org/opends/server/backends/task/TaskState.java
@@ -76,6 +76,13 @@
 
 
   /**
+   * The task state that indicates that the task is recurring.
+   */
+  RECURRING(INFO_TASK_STATE_RECURRING.get()),
+
+
+
+  /**
    * The task state that indicates that the task has completed without any
    * errors.
    */
@@ -173,6 +180,27 @@
 
 
   /**
+   * Indicates whether a task with the specified state is recurring.
+   *
+   * @param  taskState  The task state for which to make the determination.
+   *
+   * @return  <CODE>true</CODE> if the task state indicates that the task
+   *          is recurring, or <CODE>false</CODE> otherwise.
+   */
+  public static boolean isRecurring(TaskState taskState)
+  {
+    switch (taskState)
+    {
+      case RECURRING:
+        return true;
+      default:
+        return false;
+    }
+  }
+
+
+
+  /**
    * Indicates whether a task with the specified state has completed all the
    * processing that it will do, regardless of whether it completed its
    * intended goal.
@@ -278,6 +306,10 @@
     {
       return RUNNING;
     }
+    else if (lowerString.equals("recurring"))
+    {
+      return RECURRING;
+    }
     else if (lowerString.equals("completed_successfully"))
     {
       return COMPLETED_SUCCESSFULLY;
diff --git a/opends/src/server/org/opends/server/backends/task/TaskThread.java b/opends/src/server/org/opends/server/backends/task/TaskThread.java
index 440bcc2..b9eb145 100644
--- a/opends/src/server/org/opends/server/backends/task/TaskThread.java
+++ b/opends/src/server/org/opends/server/backends/task/TaskThread.java
@@ -194,8 +194,11 @@
 
       try
       {
-        TaskState returnState = getAssociatedTask().execute();
-        getAssociatedTask().setTaskState(returnState);
+        if (!TaskState.isDone(getAssociatedTask().getTaskState()))
+        {
+          TaskState returnState = getAssociatedTask().execute();
+          getAssociatedTask().setTaskState(returnState);
+        }
       }
       catch (Exception e)
       {
diff --git a/opends/src/server/org/opends/server/config/ConfigConstants.java b/opends/src/server/org/opends/server/config/ConfigConstants.java
index 96b52ee..8190143 100644
--- a/opends/src/server/org/opends/server/config/ConfigConstants.java
+++ b/opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -2109,11 +2109,11 @@
 
 
   /**
-   * The name of the configuration attribute that holds the name of the class
-   * used to provide the implementation logic for a recurring task.
+   * The name of the configuration attribute that holds the
+   * schedule for a recurring task.
    */
-  public static final String ATTR_RECURRING_TASK_CLASS_NAME =
-       NAME_PREFIX_RECURRING_TASK + "class-name";
+  public static final String ATTR_RECURRING_TASK_SCHEDULE =
+       NAME_PREFIX_RECURRING_TASK + "schedule";
 
 
 
diff --git a/opends/src/server/org/opends/server/tasks/BackupTask.java b/opends/src/server/org/opends/server/tasks/BackupTask.java
index e611468..1547d06 100644
--- a/opends/src/server/org/opends/server/tasks/BackupTask.java
+++ b/opends/src/server/org/opends/server/tasks/BackupTask.java
@@ -147,6 +147,7 @@
   /**
    * {@inheritDoc}
    */
+  @Override
   public Message getDisplayName() {
     return INFO_TASK_BACKUP_NAME.get();
   }
@@ -154,6 +155,7 @@
   /**
    * {@inheritDoc}
    */
+  @Override
   public Message getAttributeDisplayName(String attrName) {
     return argDisplayMap.get(attrName);
   }
@@ -284,6 +286,12 @@
     }
 
 
+    // Use task id for backup id in case of recurring task.
+    if (super.isRecurring()) {
+      backupID = super.getTaskID();
+    }
+
+
     // If no backup ID was provided, then create one with the current timestamp.
     if (backupID == null)
     {
@@ -575,6 +583,7 @@
   /**
    * {@inheritDoc}
    */
+  @Override
   public void interruptTask(TaskState interruptState, Message interruptReason)
   {
     if (TaskState.STOPPED_BY_ADMINISTRATOR.equals(interruptState) &&
@@ -591,6 +600,7 @@
   /**
    * {@inheritDoc}
    */
+  @Override
   public boolean isInterruptable() {
     return true;
   }
diff --git a/opends/src/server/org/opends/server/tools/BackUpDB.java b/opends/src/server/org/opends/server/tools/BackUpDB.java
index d26e4d9..fa9c78b 100644
--- a/opends/src/server/org/opends/server/tools/BackUpDB.java
+++ b/opends/src/server/org/opends/server/tools/BackUpDB.java
@@ -1109,5 +1109,16 @@
     }
     return ret;
   }
+
+  /**
+   * {@inheritDoc}
+   */
+  public String getTaskId() {
+    if (backupIDString != null) {
+      return backupIDString.getValue();
+    } else {
+      return null;
+    }
+  }
 }
 
diff --git a/opends/src/server/org/opends/server/tools/ExportLDIF.java b/opends/src/server/org/opends/server/tools/ExportLDIF.java
index 9046833..bc6c706 100644
--- a/opends/src/server/org/opends/server/tools/ExportLDIF.java
+++ b/opends/src/server/org/opends/server/tools/ExportLDIF.java
@@ -1029,5 +1029,13 @@
       return 1;
     }
   }
+
+  /**
+   * {@inheritDoc}
+   */
+  public String getTaskId() {
+    // NYI.
+    return null;
+  }
 }
 
diff --git a/opends/src/server/org/opends/server/tools/ImportLDIF.java b/opends/src/server/org/opends/server/tools/ImportLDIF.java
index 4a7e3a8..4d002fc 100644
--- a/opends/src/server/org/opends/server/tools/ImportLDIF.java
+++ b/opends/src/server/org/opends/server/tools/ImportLDIF.java
@@ -1414,5 +1414,13 @@
     importConfig.close();
     return retCode;
   }
+
+  /**
+   * {@inheritDoc}
+   */
+  public String getTaskId() {
+    // NYI.
+    return null;
+  }
 }
 
diff --git a/opends/src/server/org/opends/server/tools/ManageTasks.java b/opends/src/server/org/opends/server/tools/ManageTasks.java
index a8f6429..9a3a806 100644
--- a/opends/src/server/org/opends/server/tools/ManageTasks.java
+++ b/opends/src/server/org/opends/server/tools/ManageTasks.java
@@ -62,6 +62,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import org.opends.server.backends.task.TaskState;
 
 /**
  * Tool for getting information and managing tasks in the Directory Server.
@@ -423,12 +424,11 @@
                 new TaskDrilldownMenu(taskId),
                 taskEntry.getType(), taskEntry.getState());
         index++;
-        if (taskEntry.isCancelable() && !taskEntry.isDone()) {
+        if (taskEntry.isCancelable()) {
           cancelableIndices.add(index);
         }
       }
     } else {
-      // println();
       getOutputStream().println(INFO_TASKINFO_NO_TASKS.get());
       getOutputStream().println();
     }
@@ -621,6 +621,7 @@
     public MenuResult<TaskEntry> invoke(ManageTasks app)
             throws CLIException
     {
+      Message m = null;
       TaskEntry taskEntry = null;
       try {
         taskEntry = app.getTaskClient().getTaskEntry(taskId);
@@ -640,23 +641,30 @@
         table.appendCell(INFO_TASKINFO_FIELD_STATUS.get());
         table.appendCell(taskEntry.getState());
 
-        table.startRow();
-        table.appendCell(INFO_TASKINFO_FIELD_SCHEDULED_START.get());
-        Message m = taskEntry.getScheduledStartTime();
-        if (m == null || m.equals(Message.EMPTY)) {
-          table.appendCell(INFO_TASKINFO_IMMEDIATE_EXECUTION.get());
-        } else {
+        if (TaskState.isRecurring(taskEntry.getTaskState())) {
+          table.startRow();
+          table.appendCell(INFO_TASKINFO_FIELD_SCHEDULED_START.get());
+          m = taskEntry.getScheduleTab();
           table.appendCell(m);
+        } else {
+          table.startRow();
+          table.appendCell(INFO_TASKINFO_FIELD_SCHEDULED_START.get());
+          m = taskEntry.getScheduledStartTime();
+          if (m == null || m.equals(Message.EMPTY)) {
+            table.appendCell(INFO_TASKINFO_IMMEDIATE_EXECUTION.get());
+          } else {
+            table.appendCell(m);
+          }
+
+          table.startRow();
+          table.appendCell(INFO_TASKINFO_FIELD_ACTUAL_START.get());
+          table.appendCell(taskEntry.getActualStartTime());
+
+          table.startRow();
+          table.appendCell(INFO_TASKINFO_FIELD_COMPLETION_TIME.get());
+          table.appendCell(taskEntry.getCompletionTime());
         }
 
-        table.startRow();
-        table.appendCell(INFO_TASKINFO_FIELD_ACTUAL_START.get());
-        table.appendCell(taskEntry.getActualStartTime());
-
-        table.startRow();
-        table.appendCell(INFO_TASKINFO_FIELD_COMPLETION_TIME.get());
-        table.appendCell(taskEntry.getCompletionTime());
-
         writeMultiValueCells(
                 table,
                 INFO_TASKINFO_FIELD_DEPENDENCY.get(),
diff --git a/opends/src/server/org/opends/server/tools/RestoreDB.java b/opends/src/server/org/opends/server/tools/RestoreDB.java
index aad5a92..94f07ad 100644
--- a/opends/src/server/org/opends/server/tools/RestoreDB.java
+++ b/opends/src/server/org/opends/server/tools/RestoreDB.java
@@ -701,5 +701,16 @@
     }
     return 0;
   }
+
+  /**
+   * {@inheritDoc}
+   */
+  public String getTaskId() {
+    if (backupIDString != null) {
+      return backupIDString.getValue();
+    } else {
+      return null;
+    }
+  }
 }
 
diff --git a/opends/src/server/org/opends/server/tools/ToolConstants.java b/opends/src/server/org/opends/server/tools/ToolConstants.java
index 97432ba..553f3ce 100644
--- a/opends/src/server/org/opends/server/tools/ToolConstants.java
+++ b/opends/src/server/org/opends/server/tools/ToolConstants.java
@@ -576,6 +576,16 @@
   public static final Character OPTION_SHORT_START_DATETIME = 't';
 
   /**
+   * Recurring task option long form.
+   */
+  public static final String OPTION_LONG_RECURRING_TASK = "recurringTask";
+
+  /**
+   * Recurring task option short form.
+   */
+  public static final Character OPTION_SHORT_RECURRING_TASK = null;
+
+  /**
    * The value for the long option propertiesFilePAth .
    */
   public static final String OPTION_LONG_PROP_FILE_PATH = "propertiesFilePath";
diff --git a/opends/src/server/org/opends/server/tools/tasks/TaskClient.java b/opends/src/server/org/opends/server/tools/tasks/TaskClient.java
index 868bdce..8d58d55 100644
--- a/opends/src/server/org/opends/server/tools/tasks/TaskClient.java
+++ b/opends/src/server/org/opends/server/tools/tasks/TaskClient.java
@@ -69,7 +69,10 @@
 import java.util.Date;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
+import org.opends.server.protocols.ldap.DeleteRequestProtocolOp;
+import org.opends.server.protocols.ldap.DeleteResponseProtocolOp;
 
 /**
  * Helper class for interacting with the task backend on behalf of utilities
@@ -109,16 +112,33 @@
   public synchronized TaskEntry schedule(TaskScheduleInformation information)
           throws LDAPException, IOException, ASN1Exception, TaskClientException
   {
+    String taskID = null;
+    ASN1OctetString entryDN = null;
+    boolean scheduleRecurring = false;
+
     LDAPReader reader = connection.getLDAPReader();
     LDAPWriter writer = connection.getLDAPWriter();
 
-    // Use a formatted time/date for the ID so that is remotely useful
-    SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssMM");
-    String taskID = df.format(new Date());
+    if (information.getRecurringDateTime() != null) {
+      scheduleRecurring = true;
+    }
 
-    ASN1OctetString entryDN =
-         new ASN1OctetString(ATTR_TASK_ID + "=" + taskID + "," +
-                             SCHEDULED_TASK_BASE_RDN + "," + DN_TASK_ROOT);
+    if (scheduleRecurring) {
+      taskID = information.getTaskId();
+      if ((taskID == null) || taskID.length() == 0) {
+        taskID = information.getTaskClass().getSimpleName() +
+          "-" + UUID.randomUUID().toString();
+      }
+      entryDN = new ASN1OctetString(ATTR_RECURRING_TASK_ID + "=" +
+        taskID + "," + RECURRING_TASK_BASE_RDN + "," + DN_TASK_ROOT);
+    } else {
+      // Use a formatted time/date for the ID so that is remotely useful
+      SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssMM");
+      taskID = df.format(new Date());
+
+      entryDN = new ASN1OctetString(ATTR_TASK_ID + "=" + taskID + "," +
+        SCHEDULED_TASK_BASE_RDN + "," + DN_TASK_ROOT);
+    }
 
     ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
 
@@ -127,11 +147,20 @@
     ArrayList<ASN1OctetString> ocValues = new ArrayList<ASN1OctetString>(3);
     ocValues.add(new ASN1OctetString("top"));
     ocValues.add(new ASN1OctetString(ConfigConstants.OC_TASK));
+
+    if (scheduleRecurring) {
+      ocValues.add(new ASN1OctetString(ConfigConstants.OC_RECURRING_TASK));
+    }
+
     ocValues.add(new ASN1OctetString(information.getTaskObjectclass()));
     attributes.add(new LDAPAttribute(ATTR_OBJECTCLASS, ocValues));
 
     ArrayList<ASN1OctetString> taskIDValues = new ArrayList<ASN1OctetString>(1);
     taskIDValues.add(new ASN1OctetString(taskID));
+
+    if (scheduleRecurring) {
+      attributes.add(new LDAPAttribute(ATTR_RECURRING_TASK_ID, taskIDValues));
+    }
     attributes.add(new LDAPAttribute(ATTR_TASK_ID, taskIDValues));
 
     ArrayList<ASN1OctetString> classValues = new ArrayList<ASN1OctetString>(1);
@@ -149,6 +178,15 @@
               startDateValues));
     }
 
+    if (scheduleRecurring) {
+      ArrayList<ASN1OctetString> recurringPatternValues =
+        new ArrayList<ASN1OctetString>(1);
+      recurringPatternValues.add(new ASN1OctetString(
+        information.getRecurringDateTime()));
+      attributes.add(new LDAPAttribute(ATTR_RECURRING_TASK_SCHEDULE,
+        recurringPatternValues));
+    }
+
     // add dependency IDs
     List<String> dependencyIds = information.getDependencyIds();
     if (dependencyIds != null && dependencyIds.size() > 0) {
@@ -340,14 +378,13 @@
    * Changes that the state of the task in the backend to a canceled state.
    *
    * @param  id if the task to cancel
-   * @return Entry of the task before the modification
    * @throws IOException if there is a stream communication problem
    * @throws LDAPException if there is a problem getting information
    *         out to the directory
    * @throws ASN1Exception if there is a problem with the encoding
    * @throws TaskClientException if there is no task with the requested id
    */
-  public synchronized TaskEntry cancelTask(String id)
+  public synchronized void cancelTask(String id)
           throws TaskClientException, IOException, ASN1Exception, LDAPException
   {
     LDAPReader reader = connection.getLDAPReader();
@@ -373,11 +410,6 @@
         LDAPAttribute attr = new LDAPAttribute(ATTR_TASK_STATE, values);
         mods.add(new LDAPModification(ModificationType.REPLACE, attr));
 
-        // We have to reset the start time or the scheduler will
-        // reschedule to task.
-        // attr = new LDAPAttribute(ATTR_TASK_SCHEDULED_START_TIME);
-        // mods.add(new LDAPModification(ModificationType.DELETE, attr));
-
         ModifyRequestProtocolOp modRequest =
                 new ModifyRequestProtocolOp(dn, mods);
         LDAPMessage requestMessage =
@@ -409,6 +441,41 @@
                   LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
                   errorMessage);
         }
+      } else if (TaskState.isRecurring(state)) {
+
+        ASN1OctetString dn = new ASN1OctetString(entry.getDN().toString());
+        DeleteRequestProtocolOp deleteRequest =
+          new DeleteRequestProtocolOp(dn);
+
+        LDAPMessage requestMessage = new LDAPMessage(
+          nextMessageID.getAndIncrement(), deleteRequest, null);
+
+        writer.writeMessage(requestMessage);
+
+        LDAPMessage responseMessage = reader.readMessage();
+
+        if (responseMessage == null) {
+          Message message = ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get();
+          throw new LDAPException(UNAVAILABLE.getIntValue(), message);
+        }
+
+        if (responseMessage.getProtocolOpType() !=
+                LDAPConstants.OP_TYPE_DELETE_RESPONSE)
+        {
+          throw new LDAPException(
+                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
+                  ERR_TASK_CLIENT_INVALID_RESPONSE_TYPE.get(
+                    responseMessage.getProtocolOpName()));
+        }
+
+        DeleteResponseProtocolOp deleteResponse =
+                responseMessage.getDeleteResponseProtocolOp();
+        Message errorMessage = deleteResponse.getErrorMessage();
+        if (errorMessage != null) {
+          throw new LDAPException(
+                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
+                  errorMessage);
+        }
       } else {
         throw new TaskClientException(
                 ERR_TASK_CLIENT_UNCANCELABLE_TASK.get(id));
@@ -417,7 +484,6 @@
       throw new TaskClientException(
               ERR_TASK_CLIENT_TASK_STATE_UNKNOWN.get(id));
     }
-    return getTaskEntry(id);
   }
 
 
diff --git a/opends/src/server/org/opends/server/tools/tasks/TaskEntry.java b/opends/src/server/org/opends/server/tools/tasks/TaskEntry.java
index e84e048..30448f7 100644
--- a/opends/src/server/org/opends/server/tools/tasks/TaskEntry.java
+++ b/opends/src/server/org/opends/server/tools/tasks/TaskEntry.java
@@ -82,6 +82,8 @@
     supAttrNames.add("ds-task-log-message");
     supAttrNames.add("ds-task-notify-on-completion");
     supAttrNames.add("ds-task-notify-on-error");
+    supAttrNames.add("ds-recurring-task-id");
+    supAttrNames.add("ds-recurring-task-schedule");
   }
 
   private String id;
@@ -90,6 +92,7 @@
   private String schedStart;
   private String actStart;
   private String compTime;
+  private String schedTab;
   private List<String> depends;
   private String depFailAct;
   private List<String> logs;
@@ -126,6 +129,7 @@
     logs =       getMultiStringValue(entry,  p + "log-message");
     notifyErr =  getMultiStringValue(entry,  p + "notify-on-error");
     notifyComp = getMultiStringValue(entry,  p + "notify-on-completion");
+    schedTab =   getSingleStringValue(entry, "ds-recurring-task-schedule");
 
 
     // Build a map of non-superior attribute value pairs for display
@@ -223,6 +227,15 @@
   }
 
   /**
+   * Gets recurring schedule tab.
+   *
+   * @return Message tab string
+   */
+  public Message getScheduleTab() {
+    return Message.raw(schedTab);
+  }
+
+  /**
    * Gets the IDs of tasks upon which this task depends.
    *
    * @return array of IDs
@@ -326,6 +339,7 @@
     if (state != null) {
       Task task = getTask();
       cancelable = (TaskState.isPending(state) ||
+        TaskState.isRecurring(state) ||
               (TaskState.isRunning(state) &&
                       task != null &&
                       task.isInterruptable()));
diff --git a/opends/src/server/org/opends/server/tools/tasks/TaskScheduleInformation.java b/opends/src/server/org/opends/server/tools/tasks/TaskScheduleInformation.java
index 87b4081..bd5ad99 100644
--- a/opends/src/server/org/opends/server/tools/tasks/TaskScheduleInformation.java
+++ b/opends/src/server/org/opends/server/tools/tasks/TaskScheduleInformation.java
@@ -77,6 +77,23 @@
 
 
   /**
+   * Gets an arbitrary task id assigned to this task.
+   *
+   * @return assigned task id if any or <CODE>null</CODE> otherwise.
+   */
+  String getTaskId();
+
+
+  /**
+   * Gets the date/time pattern for recurring task schedule.
+   *
+   * @return recurring date/time pattern at which the task
+   *         should be scheduled.
+   */
+  String getRecurringDateTime();
+
+
+  /**
    * Gets a list of task IDs upon which this task is dependent.
    *
    * @return list of task IDs
diff --git a/opends/src/server/org/opends/server/tools/tasks/TaskTool.java b/opends/src/server/org/opends/server/tools/tasks/TaskTool.java
index 0b77a00..344f22e 100644
--- a/opends/src/server/org/opends/server/tools/tasks/TaskTool.java
+++ b/opends/src/server/org/opends/server/tools/tasks/TaskTool.java
@@ -88,6 +88,9 @@
   // Argument for describing the task's start time
   StringArgument startArg;
 
+  // Argument to indicate a recurring task
+  StringArgument recurringArg;
+
   // Argument for specifying completion notifications
   StringArgument completionNotificationArg;
 
@@ -133,79 +136,88 @@
    * @return LDAPConnectionArgumentParser for processing CLI input
    */
   protected LDAPConnectionArgumentParser createArgParser(String className,
-      Message toolDescription)
-    {
+    Message toolDescription)
+  {
     ArgumentGroup ldapGroup = new ArgumentGroup(
-            INFO_DESCRIPTION_TASK_LDAP_ARGS.get(), 1001);
+      INFO_DESCRIPTION_TASK_LDAP_ARGS.get(), 1001);
 
     argParser = new LDAPConnectionArgumentParser(className,
-            toolDescription, false, ldapGroup, alwaysSSL);
+      toolDescription, false, ldapGroup, alwaysSSL);
 
     ArgumentGroup taskGroup = new ArgumentGroup(
-            INFO_DESCRIPTION_TASK_TASK_ARGS.get(), 1000);
+      INFO_DESCRIPTION_TASK_TASK_ARGS.get(), 1000);
 
     try {
       StringArgument propertiesFileArgument = new StringArgument(
-          "propertiesFilePath",
-          null, OPTION_LONG_PROP_FILE_PATH,
-          false, false, true, INFO_PROP_FILE_PATH_PLACEHOLDER.get(), null, null,
-          INFO_DESCRIPTION_PROP_FILE_PATH.get());
+        "propertiesFilePath",
+        null, OPTION_LONG_PROP_FILE_PATH,
+        false, false, true, INFO_PROP_FILE_PATH_PLACEHOLDER.get(), null, null,
+        INFO_DESCRIPTION_PROP_FILE_PATH.get());
       argParser.addArgument(propertiesFileArgument);
       argParser.setFilePropertiesArgument(propertiesFileArgument);
 
-     BooleanArgument noPropertiesFileArgument = new BooleanArgument(
-          "noPropertiesFileArgument", null, OPTION_LONG_NO_PROP_FILE,
-          INFO_DESCRIPTION_NO_PROP_FILE.get());
-     argParser.addArgument(noPropertiesFileArgument);
-     argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+      BooleanArgument noPropertiesFileArgument = new BooleanArgument(
+        "noPropertiesFileArgument", null, OPTION_LONG_NO_PROP_FILE,
+        INFO_DESCRIPTION_NO_PROP_FILE.get());
+      argParser.addArgument(noPropertiesFileArgument);
+      argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
 
       startArg = new StringArgument(
-              OPTION_LONG_START_DATETIME,
-              OPTION_SHORT_START_DATETIME,
-              OPTION_LONG_START_DATETIME, false, false,
-              true, INFO_START_DATETIME_PLACEHOLDER.get(),
-              null, null,
-              INFO_DESCRIPTION_START_DATETIME.get());
+        OPTION_LONG_START_DATETIME,
+        OPTION_SHORT_START_DATETIME,
+        OPTION_LONG_START_DATETIME, false, false,
+        true, INFO_START_DATETIME_PLACEHOLDER.get(),
+        null, null,
+        INFO_DESCRIPTION_START_DATETIME.get());
       argParser.addArgument(startArg, taskGroup);
 
+      recurringArg = new StringArgument(
+        OPTION_LONG_RECURRING_TASK,
+        OPTION_SHORT_RECURRING_TASK,
+        OPTION_LONG_RECURRING_TASK, false, false,
+        true, INFO_RECURRING_TASK_PLACEHOLDER.get(),
+        null, null,
+        INFO_DESCRIPTION_RECURRING_TASK.get());
+      argParser.addArgument(recurringArg, taskGroup);
+
       completionNotificationArg = new StringArgument(
-              OPTION_LONG_COMPLETION_NOTIFICATION_EMAIL,
-              OPTION_SHORT_COMPLETION_NOTIFICATION_EMAIL,
-              OPTION_LONG_COMPLETION_NOTIFICATION_EMAIL,
-              false, true, true, INFO_EMAIL_ADDRESS_PLACEHOLDER.get(),
-              null, null, INFO_DESCRIPTION_TASK_COMPLETION_NOTIFICATION.get());
+        OPTION_LONG_COMPLETION_NOTIFICATION_EMAIL,
+        OPTION_SHORT_COMPLETION_NOTIFICATION_EMAIL,
+        OPTION_LONG_COMPLETION_NOTIFICATION_EMAIL,
+        false, true, true, INFO_EMAIL_ADDRESS_PLACEHOLDER.get(),
+        null, null, INFO_DESCRIPTION_TASK_COMPLETION_NOTIFICATION.get());
       argParser.addArgument(completionNotificationArg, taskGroup);
 
       errorNotificationArg = new StringArgument(
-              OPTION_LONG_ERROR_NOTIFICATION_EMAIL,
-              OPTION_SHORT_ERROR_NOTIFICATION_EMAIL,
-              OPTION_LONG_ERROR_NOTIFICATION_EMAIL,
-              false, true, true, INFO_EMAIL_ADDRESS_PLACEHOLDER.get(),
-              null, null, INFO_DESCRIPTION_TASK_ERROR_NOTIFICATION.get());
+        OPTION_LONG_ERROR_NOTIFICATION_EMAIL,
+        OPTION_SHORT_ERROR_NOTIFICATION_EMAIL,
+        OPTION_LONG_ERROR_NOTIFICATION_EMAIL,
+        false, true, true, INFO_EMAIL_ADDRESS_PLACEHOLDER.get(),
+        null, null, INFO_DESCRIPTION_TASK_ERROR_NOTIFICATION.get());
       argParser.addArgument(errorNotificationArg, taskGroup);
 
       dependencyArg = new StringArgument(
-              OPTION_LONG_DEPENDENCY,
-              OPTION_SHORT_DEPENDENCY,
-              OPTION_LONG_DEPENDENCY,
-              false, true, true, INFO_TASK_ID_PLACEHOLDER.get(),
-              null, null, INFO_DESCRIPTION_TASK_DEPENDENCY_ID.get());
+        OPTION_LONG_DEPENDENCY,
+        OPTION_SHORT_DEPENDENCY,
+        OPTION_LONG_DEPENDENCY,
+        false, true, true, INFO_TASK_ID_PLACEHOLDER.get(),
+        null, null, INFO_DESCRIPTION_TASK_DEPENDENCY_ID.get());
       argParser.addArgument(dependencyArg, taskGroup);
 
       Set fdaValSet = EnumSet.allOf(FailedDependencyAction.class);
       failedDependencyActionArg = new StringArgument(
-              OPTION_LONG_FAILED_DEPENDENCY_ACTION,
-              OPTION_SHORT_FAILED_DEPENDENCY_ACTION,
-              OPTION_LONG_FAILED_DEPENDENCY_ACTION,
-              false, true, true, INFO_ACTION_PLACEHOLDER.get(),
-              null, null, INFO_DESCRIPTION_TASK_FAILED_DEPENDENCY_ACTION.get(
-                StaticUtils.collectionToString(fdaValSet, ","),
-                FailedDependencyAction.defaultValue().name()));
+        OPTION_LONG_FAILED_DEPENDENCY_ACTION,
+        OPTION_SHORT_FAILED_DEPENDENCY_ACTION,
+        OPTION_LONG_FAILED_DEPENDENCY_ACTION,
+        false, true, true, INFO_ACTION_PLACEHOLDER.get(),
+        null, null, INFO_DESCRIPTION_TASK_FAILED_DEPENDENCY_ACTION.get(
+        StaticUtils.collectionToString(fdaValSet, ","),
+        FailedDependencyAction.defaultValue().name()));
       argParser.addArgument(failedDependencyActionArg, taskGroup);
 
       testIfOfflineArg = new BooleanArgument(
-          "testIfOffline", null, "testIfOffline",
-          INFO_DESCRIPTION_TEST_IF_OFFLINE.get());
+        "testIfOffline", null, "testIfOffline",
+        INFO_DESCRIPTION_TEST_IF_OFFLINE.get());
       testIfOfflineArg.setHidden(true);
       argParser.addArgument(testIfOfflineArg);
 
@@ -311,6 +323,19 @@
   /**
    * {@inheritDoc}
    */
+  public String getRecurringDateTime() {
+    String pattern = null;
+
+    // If the recurring task arg is present parse its value
+    if (recurringArg != null && recurringArg.isPresent()) {
+      pattern = recurringArg.getValue();
+    }
+    return pattern;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
   public List<String> getDependencyIds() {
     if (dependencyArg.isPresent()) {
       return dependencyArg.getValues();
@@ -405,13 +430,18 @@
         TaskClient tc = new TaskClient(conn);
         TaskEntry taskEntry = tc.schedule(this);
         Message startTime = taskEntry.getScheduledStartTime();
-        if (startTime == null || startTime.length() == 0) {
+        if (taskEntry.getTaskState() == TaskState.RECURRING) {
+          out.println(
+                  wrapText(INFO_TASK_TOOL_RECURRING_TASK_SCHEDULED.get(
+                          taskEntry.getType(),
+                          taskEntry.getId()),
+                  MAX_LINE_WIDTH));
+        } else if (startTime == null || startTime.length() == 0) {
           out.println(
                   wrapText(INFO_TASK_TOOL_TASK_SCHEDULED_NOW.get(
                           taskEntry.getType(),
                           taskEntry.getId()),
                   MAX_LINE_WIDTH));
-
         } else {
           out.println(
                   wrapText(INFO_TASK_TOOL_TASK_SCHEDULED_FUTURE.get(
@@ -443,12 +473,13 @@
 
           } while (!taskEntry.isDone());
           if (TaskState.isSuccessful(taskEntry.getTaskState())) {
-            out.println(
-                wrapText(INFO_TASK_TOOL_TASK_SUCESSFULL.get(
-                        taskEntry.getType(),
-                        taskEntry.getId()),
-                MAX_LINE_WIDTH));
-
+            if (taskEntry.getTaskState() != TaskState.RECURRING) {
+              out.println(
+                  wrapText(INFO_TASK_TOOL_TASK_SUCESSFULL.get(
+                          taskEntry.getType(),
+                          taskEntry.getId()),
+                  MAX_LINE_WIDTH));
+            }
             return 0;
           } else {
             out.println(
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java
index 6e4e313..09bb550 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java
@@ -168,7 +168,7 @@
    * cases that depend on this specific value of "o=test".
    */
   public static final String TEST_ROOT_DN_STRING = "o=test";
-  
+
   /**
    * The backend if for the test backend
    */
@@ -178,7 +178,7 @@
    * The string representation of the OpenDMK jar file location
    * that will be used as base to determine if snmp is included or not
    */
-  public static final String PROPERTY_OPENDMK_LOCATION = 
+  public static final String PROPERTY_OPENDMK_LOCATION =
           "org.opends.server.snmp.opendmk";
 
   /**
@@ -304,7 +304,7 @@
         testInstallRoot.mkdirs();
         testInstanceRoot.mkdirs();
       }
-      
+
       File testInstanceSchema =
         new File (testInstanceRoot, "config" + File.separator + "schema");
       testInstanceSchema.mkdirs();
@@ -341,34 +341,34 @@
       File testClassesDir   = new File(testInstanceRoot, "classes");
       File testLibDir       = new File(testInstallRoot, "lib");
       File testBinDir       = new File(testInstallRoot, "bin");
-      
+
       // Snmp resource
-      String opendmkJarFileLocation = 
+      String opendmkJarFileLocation =
               System.getProperty(PROPERTY_OPENDMK_LOCATION);
-      
+
       File opendmkJar = new File(opendmkJarFileLocation, "jdmkrt.jar");
-      
+
       File   snmpResourceDir = new File(buildRoot + File.separator + "src" +
                                     File.separator + "snmp" + File.separator +
                                     "resource");
-      
+
       File snmpConfigDir = new File(snmpResourceDir, "config");
-      
+
       File testSnmpResourceDir = new File (testConfigDir + File.separator +
                                     "snmp");
-      
+
       if (Boolean.getBoolean(PROPERTY_COPY_CLASSES_TO_TEST_PKG))
       {
         copyDirectory(serverClassesDir, testClassesDir);
         copyDirectory(unitClassesDir, testClassesDir);
       }
-      
+
       if (installedRoot != null)
       {
         copyDirectory(new File(installedRoot), testInstallRoot);
-        
+
         // Get the instance location
-        
+
       }
       else
       {
@@ -508,7 +508,7 @@
       DirectoryEnvironmentConfig config = new DirectoryEnvironmentConfig();
       config.setServerRoot(testInstallRoot);
       config.setInstanceRoot(testInstanceRoot);
-      
+
       config.setForceDaemonThreads(true);
       config.setConfigClass(ConfigFileHandler.class);
       config.setConfigFile(new File(testConfigDir, "config.ldif"));
@@ -648,7 +648,7 @@
     if (testConfigDir == null) {
       throw new RuntimeException("The testConfigDir variable is not set yet!");
     }
-    
+
     return (testConfigDir);
   }
 
@@ -974,7 +974,7 @@
     in.close();
     out.close();
   }
-  
+
   public static void appendFile(File src, File dst) throws IOException {
     InputStream in = new FileInputStream(src);
     OutputStream out = new FileOutputStream(dst, true);
@@ -988,7 +988,7 @@
     in.close();
     out.close();
   }
-   
+
 
   /**
    * Get the LDAP port the test environment Directory Server instance is
@@ -1305,6 +1305,33 @@
 
 
   /**
+   * Adds the provided entry to the Directory Server using an internal
+   * operation.
+   *
+   * @param  lines  The lines that make up the entry to be added.
+   *
+   * @return result code for this operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  public static ResultCode addEntryOperation(String... lines)
+         throws Exception
+  {
+    Entry entry = makeEntry(lines);
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    AddOperation addOperation = conn.processAdd(entry.getDN(),
+                                     entry.getObjectClasses(),
+                                     entry.getUserAttributes(),
+                                     entry.getOperationalAttributes());
+    return addOperation.getResultCode();
+  }
+
+
+
+  /**
    * Adds the provided set of entries to the Directory Server using internal
    * operations.
    *
@@ -1387,7 +1414,7 @@
       "-a",
       "-f", path
     };
-    
+
     if (useAdminPort) {
       return LDAPModify.mainModify(adminArgs, false, null, null);
     } else {
@@ -1749,7 +1776,7 @@
     } catch (Exception e) {
        hostName="Unknown (" + e + ")";
     }
-    
+
     String[] fullArgs = new String[args.length + 11];
     fullArgs[0] = "-h";
     fullArgs[1] = hostName;
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/backends/task/TaskBackendTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/backends/task/TaskBackendTestCase.java
index 0293b9a..beafe6b 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/backends/task/TaskBackendTestCase.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/backends/task/TaskBackendTestCase.java
@@ -30,8 +30,10 @@
 
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.TimeZone;
 
+import java.util.UUID;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -42,10 +44,10 @@
 import org.opends.server.tasks.TasksTestCase;
 import org.opends.server.types.DN;
 
+import org.opends.server.types.ResultCode;
 import static org.testng.Assert.*;
 
 import static org.opends.server.util.ServerConstants.*;
-import static org.opends.server.util.StaticUtils.*;
 
 
 
@@ -405,5 +407,130 @@
     assertEquals(resultCode, 0);
     assertFalse(DirectoryServer.entryExists(DN.decode(taskDN)));
   }
-}
 
+
+
+  /**
+   * Tests basic recurring task functionality and parser.
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRecurringTask()
+         throws Exception
+  {
+    String taskID = "testRecurringTask";
+    String taskDN = "ds-recurring-task-id=" +
+      taskID + ",cn=Recurring Tasks,cn=tasks";
+    String taskSchedule = "00 * * * *";
+
+    String[] invalidTaskSchedules = {
+      "* * * *", "* * * * * *", "*:*:*:*:*",
+      "60 * * * *", "-1 * * * *", "1-60 * * * *", "1,60 * * * *",
+      "* 24 * * *", "* -1 * * *", "* 1-24 * * *", "* 1,24 * * *",
+      "* * 32 * *", "* * 0 * *", "* * 1-32 * *", "* * 1,32 * *",
+      "* * * 13 *", "* * * 0 *", "* * * 1-13 *", "* * * 1,13 *",
+      "* * * * 7", "* * * * -1", "* * * * 1-7", "* * * * 1,7",
+      "* * 31 2 *" };
+    String[] validTaskSchedules = {
+      "* * * * *",
+      "59 * * * *", "0 * * * *", "0-59 * * * *", "0,59 * * * *",
+      "* 23 * * *", "* 0 * * *", "* 0-23 * * *", "* 0,23 * * *",
+      "* * 31 * *", "* * 1 * *", "* * 1-31 * *", "* * 1,31 * *",
+      "* * * 12 *", "* * * 1 *", "* * * 1-12 *", "* * * 1,12 *",
+      "* * * * 6", "* * * * 0", "* * * * 0-6", "* * * * 0,6" };
+
+    GregorianCalendar calendar = new GregorianCalendar();
+    calendar.setFirstDayOfWeek(GregorianCalendar.SUNDAY);
+    calendar.setLenient(false);
+    calendar.add(GregorianCalendar.HOUR_OF_DAY, 1);
+    calendar.set(GregorianCalendar.MINUTE, 0);
+    calendar.set(GregorianCalendar.SECOND, 0);
+
+    Date scheduledDate = calendar.getTime();
+    String scheduledTaskID = taskID + " - " + scheduledDate.toString();
+    String scheduledTaskDN = "ds-task-id=" + scheduledTaskID +
+      ",cn=Scheduled Tasks,cn=tasks";
+
+    assertTrue(addRecurringTask(taskID, taskSchedule));
+
+    Task scheduledTask = TasksTestCase.getTask(DN.decode(scheduledTaskDN));
+    assertTrue(TaskState.isPending(scheduledTask.getTaskState()));
+
+    // Perform a modification to update a non-state attribute.
+    int resultCode = TestCaseUtils.applyModifications(true,
+      "dn: " + taskDN,
+      "changetype: modify",
+      "replace: ds-recurring-task-schedule",
+      "ds-recurring-task-schedule: * * * * *");
+    assertFalse(resultCode == 0);
+
+    // Delete recurring task.
+    resultCode = TestCaseUtils.applyModifications(true,
+      "dn: " + taskDN,
+      "changetype: delete");
+    assertEquals(resultCode, 0);
+    assertFalse(DirectoryServer.entryExists(DN.decode(taskDN)));
+
+    // Make sure scheduled task got canceled.
+    scheduledTask = TasksTestCase.getTask(DN.decode(scheduledTaskDN));
+    assertTrue(TaskState.isCancelled(scheduledTask.getTaskState()));
+
+    // Test parser with invalid schedules.
+    for (String invalidSchedule : invalidTaskSchedules) {
+      assertFalse(addRecurringTask(taskID, invalidSchedule));
+    }
+
+    // Test parser with valid schedules.
+    for (String validSchedule : validTaskSchedules) {
+      taskID = "testRecurringTask" + "-" + UUID.randomUUID();
+      taskDN = "ds-recurring-task-id=" + taskID +
+        ",cn=Recurring Tasks,cn=tasks";
+      assertTrue(addRecurringTask(taskID, validSchedule));
+      // Delete recurring task.
+      resultCode = TestCaseUtils.applyModifications(true,
+        "dn: " + taskDN,
+        "changetype: delete");
+      assertEquals(resultCode, 0);
+      assertFalse(DirectoryServer.entryExists(DN.decode(taskDN)));
+    }
+  }
+
+
+
+  /**
+   * Adds recurring task to the task backend.
+   *
+   * @param  taskID  recurring task id.
+   *
+   * @param  taskSchedule  recurring task schedule.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   *
+   * @return <CODE>true</CODE> if task successfully added to
+   *         the task backend, <CODE>false</CODE> otherwise.
+   */
+  @Test(enabled=false) // This isn't a test method, but TestNG thinks it is.
+  private boolean addRecurringTask(String taskID, String taskSchedule)
+          throws Exception
+  {
+    String taskDN = "ds-recurring-task-id=" +
+      taskID + ",cn=Recurring Tasks,cn=tasks";
+
+    ResultCode rc = TestCaseUtils.addEntryOperation(
+      "dn: " + taskDN,
+      "objectClass: top",
+      "objectClass: ds-task",
+      "objectClass: ds-recurring-task",
+      "objectClass: extensibleObject",
+      "ds-recurring-task-id: " + taskID,
+      "ds-recurring-task-schedule: " + taskSchedule,
+      "ds-task-id: " + taskID,
+      "ds-task-class-name: org.opends.server.tasks.DummyTask",
+      "ds-task-dummy-sleep-time: 0");
+
+    if (rc != ResultCode.SUCCESS) {
+      return false;
+    }
+    return DirectoryServer.entryExists(DN.decode(taskDN));
+  }
+}

--
Gitblit v1.10.0