From 20f73bd1f2eac1aeccfeea9da83294f58ecd723a Mon Sep 17 00:00:00 2001
From: Chris Ridd <chris.ridd@forgerock.com>
Date: Wed, 03 Dec 2014 14:53:53 +0000
Subject: [PATCH] Backport fix OPENDJ-1614 (CR-5290) Improve crontab(5) support in recurring tasks

---
 opends/tests/unit-tests-testng/src/server/org/opends/server/backends/task/TaskBackendTestCase.java |  102 +++++++++++---------
 opends/src/messages/messages/admin_tool.properties                                                 |    6 
 opends/src/server/org/opends/server/backends/task/RecurringTask.java                               |  171 ++++++++++++++++++---------------
 3 files changed, 153 insertions(+), 126 deletions(-)

diff --git a/opends/src/messages/messages/admin_tool.properties b/opends/src/messages/messages/admin_tool.properties
index 545da64..da2f031 100644
--- a/opends/src/messages/messages/admin_tool.properties
+++ b/opends/src/messages/messages/admin_tool.properties
@@ -2825,9 +2825,9 @@
 #
 # Note that the following property contains line breaks in HTML format (<br>).
 #
-INFO_CTRL_PANEL_CRON_HELP=Use ',' to separate values. For example: \
- '1,4,5'.<br>Use '-' to indicate intervals.  For example '1-5'.<br>Use '*' to \
- indicate any value.
+INFO_CTRL_PANEL_CRON_HELP=Use ',' to separate values and intervals. For example: '1-3,5'.<br>\
+ Use '-' to indicate intervals. Append '/' and a number to skip through the interval. For example '1-5/2'.<br>\
+ Use '*' to indicate any value. Append '/' and a number to skip through the values. For example '*&#x2f;2'.
 SEVERE_ERR_CTRL_PANEL_INVALID_HOUR=The provided hour value is not valid.
 SEVERE_ERR_CTRL_PANEL_INVALID_MINUTE=The provided minute value is not valid.
 SEVERE_ERR_CTRL_PANEL_INVALID_DAY=The provided day value is not valid.
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 2b91d0f..f37ad8c 100644
--- a/opends/src/server/org/opends/server/backends/task/RecurringTask.java
+++ b/opends/src/server/org/opends/server/backends/task/RecurringTask.java
@@ -22,21 +22,20 @@
  *
  *
  *      Copyright 2006-2010 Sun Microsystems, Inc.
+ *      Portions Copyright 2014 ForgeRock, AS
  */
 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;
-
-
-
 import java.util.Iterator;
 import java.util.List;
-
 import java.util.StringTokenizer;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+
 import org.opends.server.core.DirectoryServer;
 import org.opends.server.types.Attribute;
 import org.opends.server.types.AttributeType;
@@ -58,8 +57,6 @@
 import static org.opends.server.util.StaticUtils.*;
 import static org.opends.server.util.ServerConstants.*;
 
-
-
 /**
  * This class defines a information about a recurring task, which will be used
  * to repeatedly schedule tasks for processing.
@@ -74,63 +71,57 @@
    */
   private static final DebugTracer TRACER = getTracer();
 
-
-
-
-  // The DN of the entry that actually defines this task.
+  /** The DN of the entry that actually defines this task. */
   private final DN recurringTaskEntryDN;
 
-  // The entry that actually defines this task.
+  /** The entry that actually defines this task. */
   private final Entry recurringTaskEntry;
 
-  // The unique ID for this recurring task.
+  /** The unique ID for this recurring task. */
   private final String recurringTaskID;
 
-  // The fully-qualified name of the class that will be used to implement the
-  // class.
+  /**
+   * The fully-qualified name of the class that will be used to implement the
+   * class.
+   */
   private final String taskClassName;
 
-  // Task instance.
+  /** Task instance. */
   private Task task;
 
-  // Task scheduler for this task.
+  /** Task scheduler for this task. */
   private final TaskScheduler taskScheduler;
 
-  // Number of tokens in the task schedule tab.
+  /** Number of tokens in the task schedule tab. */
   private static final int TASKTAB_NUM_TOKENS = 5;
 
-  // Maximum year month days.
+  /** Maximum year month days. */
   static final int MONTH_LENGTH[]
         = {31,28,31,30,31,30,31,31,30,31,30,31};
 
-  // Maximum leap year month days.
+  /** Maximum leap year month days. */
   static final int LEAP_MONTH_LENGTH[]
         = {31,29,31,30,31,30,31,31,30,31,30,31};
 
-  /**
-   * Task tab fields.
-   */
+  /** Task tab fields. */
   private static enum TaskTab {MINUTE, HOUR, DAY, MONTH, WEEKDAY};
 
-  private final static int MINUTE_INDEX = 0;
-  private final static int HOUR_INDEX = 1;
-  private final static int DAY_INDEX = 2;
-  private final static int MONTH_INDEX = 3;
-  private final static int WEEKDAY_INDEX = 4;
+  private static final int MINUTE_INDEX = 0;
+  private static final int HOUR_INDEX = 1;
+  private static final int DAY_INDEX = 2;
+  private static final int MONTH_INDEX = 3;
+  private static final int WEEKDAY_INDEX = 4;
 
-  // Exact match pattern.
-  private static final Pattern exactPattern =
-    Pattern.compile("\\d+");
+  /** Wildcard match pattern. */
+  private static final Pattern wildcardPattern = Pattern.compile("^\\*(?:/(\\d+))?");
 
-  // Range match pattern.
-  private static final Pattern rangePattern =
-    Pattern.compile("\\d+[-]\\d+");
+  /** Exact match pattern. */
+  private static final Pattern exactPattern = Pattern.compile("(\\d+)");
 
-  // List match pattern.
-  private static final Pattern listPattern =
-    Pattern.compile("^(\\d+,)(.*)(\\d+)$");
+  /** Range match pattern. */
+  private static final Pattern rangePattern = Pattern.compile("(\\d+)-(\\d+)(?:/(\\d+))?");
 
-  // Boolean arrays holding task tab slots.
+  /** Boolean arrays holding task tab slots. */
   private final boolean[] minutesArray;
   private final boolean[] hoursArray;
   private final boolean[] daysArray;
@@ -612,6 +603,7 @@
 
   /**
    * 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.
@@ -623,57 +615,82 @@
     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++) {
+    // Wildcard with optional increment.
+    Matcher m = wildcardPattern.matcher(tabField);
+    if (m.matches() && m.groupCount() == 1)
+    {
+      String stepString = m.group(1);
+      int increment = isValueAbsent(stepString) ? 1 : Integer.parseInt(stepString);
+      for (int i = minValue; i <= maxValue; i += increment)
+      {
         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 {
+    for (String listVal : tabField.split(","))
+    {
+      // Single number.
+      m = exactPattern.matcher(listVal);
+      if (m.matches() && m.groupCount() == 1)
+      {
+        String exactValue = m.group(1);
+        if (isValueAbsent(exactValue))
+        {
           throw new IllegalArgumentException();
         }
+        int value = Integer.parseInt(exactValue);
+        if (value < minValue || value > maxValue)
+        {
+          throw new IllegalArgumentException();
+        }
+        valueList[value] = true;
+        continue;
       }
-      return valueList;
+
+      // Range of numbers with optional increment.
+      m = rangePattern.matcher(listVal);
+      if (m.matches() && m.groupCount() == 3) {
+        String startString = m.group(1);
+        String endString = m.group(2);
+        String stepString = m.group(3);
+        int increment = isValueAbsent(stepString) ? 1 : Integer.parseInt(stepString);
+        if (isValueAbsent(startString) || isValueAbsent(endString))
+        {
+          throw new IllegalArgumentException();
+        }
+        int startValue = Integer.parseInt(startString);
+        int endValue = Integer.parseInt(endString);
+        if (startValue > endValue || startValue < minValue || endValue > maxValue)
+        {
+          throw new IllegalArgumentException();
+        }
+        for (int i = startValue; i <= endValue; i += increment)
+        {
+          valueList[i] = true;
+        }
+        continue;
+      }
+
+      // Can only have a list of numbers and ranges.
+      throw new IllegalArgumentException();
     }
 
-    throw new IllegalArgumentException();
+    return valueList;
+  }
+
+  /**
+   * Check if a String from a Matcher group is absent. Matcher returns empty strings
+   * for optional groups that are absent.
+   *
+   * @param s A string returned from Matcher.group()
+   * @return true if the string is unusable, false if it is usable.
+   */
+  private static boolean isValueAbsent(String s)
+  {
+    return (s == null || s.length() == 0) ? true : false;
   }
 
   /**
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 cc95e13..2eee4fa 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
@@ -22,6 +22,7 @@
  *
  *
  *      Copyright 2008-2010 Sun Microsystems, Inc.
+ *      Portions Copyright 2014 ForgeRock, AS
  */
 package org.opends.server.backends.task;
 
@@ -413,52 +414,61 @@
   @DataProvider(name="recurringTaskSchedules")
   public Object[][] createRecurringTaskSchedules() {
     return new Object[][] {
-        { "* * * *",       false },
-        { "* * * * * *",   false },
-        { "*:*:*:*:*",     false },
-        { "60 * * * *",    false },
-        { "-1 * * * *",    false },
-        { "1-60 * * * *",  false },
-        { "1,60 * * * *",  false },
-        { "* 24 * * *",    false },
-        { "* -1 * * *",    false },
-        { "* 1-24 * * *",  false },
-        { "* 1,24 * * *",  false },
-        { "* * 32 * *",    false },
-        { "* * 0 * *",     false },
-        { "* * 1-32 * *",  false },
-        { "* * 1,32 * *",  false },
-        { "* * * 13 *",    false },
-        { "* * * 0 *",     false },
-        { "* * * 1-13 *",  false },
-        { "* * * 1,13 *",  false },
-        { "* * * * 7",     false },
-        { "* * * * -1",    false },
-        { "* * * * 1-7",   false },
-        { "* * * * 1,7",   false },
-        { "* * 31 2 *",    false },
-        { "* * 29 2 *",    true },
-        { "* * * * *",     true },
-        { "59 * * * *",    true },
-        { "0 * * * *",     true },
-        { "0-59 * * * *",  true },
-        { "0,59 * * * *",  true },
-        { "* 23 * * *",    true },
-        { "* 0 * * *",     true },
-        { "* 0-23 * * *",  true },
-        { "* 0,23 * * *",  true },
-        { "* * 31 * *",    true },
-        { "* * 1 * *",     true },
-        { "* * 1-31 * *",  true },
-        { "* * 1,31 * *",  true },
-        { "* * * 12 *",    true },
-        { "* * * 1 *",     true },
-        { "* * * 1-12 *",  true },
-        { "* * * 1,12 *",  true },
-        { "* * * * 6",     true },
-        { "* * * * 0",     true },
-        { "* * * * 0-6",   true },
-        { "* * * * 0,6",   true }
+        { "* * * *",               false },
+        { "* * * * * *",           false },
+        { "*:*:*:*:*",             false },
+        { "60 * * * *",            false },
+        { "-1 * * * *",            false },
+        { "1-60 * * * *",          false },
+        { "1,60 * * * *",          false },
+        { "* 24 * * *",            false },
+        { "* -1 * * *",            false },
+        { "* 1-24 * * *",          false },
+        { "* 1,24 * * *",          false },
+        { "* * 32 * *",            false },
+        { "* * 0 * *",             false },
+        { "* * 1-32 * *",          false },
+        { "* * 1,32 * *",          false },
+        { "* * * 13 *",            false },
+        { "* * * 0 *",             false },
+        { "* * * 1-13 *",          false },
+        { "* * * 1,13 *",          false },
+        { "* * * * 7",             false },
+        { "* * * * -1",            false },
+        { "* * * * 1-7",           false },
+        { "* * * * 1,7",           false },
+        { "* * 31 2 *",            false },
+        { "*/foo * * * *",         false },
+        { "1-3,10/4,13 * * * *",   false },
+        { "1-5/,10,13 * * * *",    false },
+        { "1-5/foo,10,13 * * * *", false },
+        { "* * 29 2 *",            true },
+        { "* * * * *",             true },
+        { "59 * * * *",            true },
+        { "0 * * * *",             true },
+        { "0-59 * * * *",          true },
+        { "0,59 * * * *",          true },
+        { "* 23 * * *",            true },
+        { "* 0 * * *",             true },
+        { "* 0-23 * * *",          true },
+        { "* 0,23 * * *",          true },
+        { "* * 31 * *",            true },
+        { "* * 1 * *",             true },
+        { "* * 1-31 * *",          true },
+        { "* * 1,31 * *",          true },
+        { "* * * 12 *",            true },
+        { "* * * 1 *",             true },
+        { "* * * 1-12 *",          true },
+        { "* * * 1,12 *",          true },
+        { "* * * * 6",             true },
+        { "* * * * 0",             true },
+        { "* * * * 0-6",           true },
+        { "* * * * 0,6",           true },
+        { "*/2 * * * *",           true },
+        { "1-3,10-13 * * * *",     true },
+        { "1-3,10,13 * * * *",     true },
+        { "1-5/2,10,13 * * * *",   true },
+        { "1-5/2,11-15/2 * * * *", true }
     };
   }
 

--
Gitblit v1.10.0