Support optional preload-task

Optionally preloading a task can be used to customize certain tasks
created dynamically with a task-factory.

Change-Id: I0c64735aaf47ff8364d6af6a6e0392257d4006d7
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
index e7b91a8..e0b76c1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -21,18 +21,18 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** Use to pre-load a task definition with values from its preload-task definition. */
 public class Preloader {
-  public static void preload(Task definition) {
+  public static void preload(Task definition) throws ConfigInvalidException {
     String name = definition.preloadTask;
     if (name != null) {
-      Task task = definition.config.getTask(name);
-      if (task == null) {
-        throw new RuntimeException("Unknown preload-task.");
+      Task task = definition.config.getTaskOptional(name);
+      if (task != null) {
+        preload(task);
+        preloadFrom(definition, task);
       }
-      preload(task);
-      preloadFrom(definition, task);
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
index 0eac473..5fcc8f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -24,6 +24,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** Task Configuration file living in git */
 public class TaskConfig extends AbstractVersionedMetaData {
@@ -84,6 +87,9 @@
     }
   }
 
+  protected static final Pattern OPTIONAL_TASK_PATTERN =
+      Pattern.compile("([^ |]*( *[^ |])*) *\\| *");
+
   protected static final String SECTION_EXTERNAL = "external";
   protected static final String SECTION_ROOT = "root";
   protected static final String SECTION_TASK = "task";
@@ -138,7 +144,31 @@
     return externals;
   }
 
-  public Task getTask(String name) {
+  /* returs null only if optional and not found */
+  public Task getTaskOptional(String name) throws ConfigInvalidException {
+    int end = 0;
+    Matcher m = OPTIONAL_TASK_PATTERN.matcher(name);
+    while (m.find()) {
+      end = m.end();
+      Task task = getTaskOrNull(m.group(1));
+      if (task != null) {
+        return task;
+      }
+    }
+
+    String last = name.substring(end);
+    if (!"".equals(last)) { // Last entry was not optional
+      Task task = getTaskOrNull(last);
+      if (task != null) {
+        return task;
+      }
+      throw new ConfigInvalidException("task not defined");
+    }
+    return null;
+  }
+
+  /* returns null if not found */
+  protected Task getTaskOrNull(String name) {
     SubSection subSection = new SubSection(SECTION_TASK, name);
     return getNames(subSection).isEmpty() ? null : new Task(subSection, isVisible, isTrusted);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
index b1fe1a1..263043c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -36,8 +36,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /**
@@ -48,8 +46,6 @@
  */
 public class TaskTree {
   protected static final String TASK_DIR = "task";
-  protected static final Pattern OPTIONAL_TASK_PATTERN =
-      Pattern.compile("([^ |]*( *[^ |])*) *\\| *");
 
   protected final AccountResolver accountResolver;
   protected final AllUsersNameProvider allUsers;
@@ -116,7 +112,8 @@
   public class Node extends NodeList {
     public final Task definition;
 
-    public Node(Task definition, List<String> path, Map<String, String> parentProperties) {
+    public Node(Task definition, List<String> path, Map<String, String> parentProperties)
+        throws ConfigInvalidException {
       this.definition = definition;
       this.path.addAll(path);
       this.path.add(definition.name);
@@ -133,7 +130,7 @@
     }
 
     protected void addSubDefinitions() throws OrmException {
-      addSubDefinitions(getSubTasks());
+      addSubDefinitions(getSubDefinitions());
       addSubFileDefinitions();
       addExternalDefinitions();
     }
@@ -159,7 +156,7 @@
           if (ext == null) {
             nodes.add(null);
           } else {
-            addSubDefinitions(getTasks(ext));
+            addSubDefinitions(getTaskDefinitions(ext));
           }
         } catch (ConfigInvalidException | IOException e) {
           nodes.add(null);
@@ -167,32 +164,22 @@
       }
     }
 
-    protected List<Task> getSubTasks() {
-      List<Task> tasks = new ArrayList<>();
-      for (String subTask : definition.subTasks) {
-        addSubTaskTo(subTask, tasks);
-      }
-      return tasks;
-    }
-
-    protected void addSubTaskTo(String subTaskEntry, List<Task> tasks) {
-      int end = 0;
-      Matcher m = OPTIONAL_TASK_PATTERN.matcher(subTaskEntry);
-      while (m.find()) {
-        end = m.end();
-        Task subTask = definition.config.getTask(m.group(1));
-        if (subTask != null) {
-          tasks.add(subTask);
-          return;
+    protected List<Task> getSubDefinitions() {
+      List<Task> defs = new ArrayList<>();
+      for (String name : definition.subTasks) {
+        try {
+          Task def = definition.config.getTaskOptional(name);
+          if (def != null) {
+            defs.add(def);
+          }
+        } catch (ConfigInvalidException e) {
+          defs.add(null);
         }
       }
-      String last = subTaskEntry.substring(end);
-      if (!"".equals(last)) { // Last entry was not optional
-        tasks.add(definition.config.getTask(subTaskEntry.substring(end)));
-      }
+      return defs;
     }
 
-    protected List<Task> getTasks(External external)
+    protected List<Task> getTaskDefinitions(External external)
         throws ConfigInvalidException, IOException, OrmException {
       return getTasks(resolveUserBranch(external.user), external.file);
     }
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 326c550..a642426 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -154,7 +154,8 @@
 from the current task if they redefined in the current task. Attributes
 which are lists (such as subtasks) or maps (such as properties), will be
 preloaded by the preload-task and then extended with the attributes from the
-current task.
+current task. See [Optional Tasks](#optional_tasks) for how to define optional
+preload-tasks.
 
 Example:
 ```
@@ -165,7 +166,8 @@
 
 : This key lists the name of a subtask of the current task. This key may be
 used several times in a task section to define more than one subtask for a
-particular task.
+particular task. See [Optional Tasks](#optional_tasks) for how to define
+optional subtasks.
 
 Example:
 
@@ -179,24 +181,6 @@
     ...
 ```
 
-To define a subtask that may not exist and that will not cause the parent task
-to be INVALID, follow the subtask name with pipe (`|`) character. This feature
-is particularly useful when a property is used in the subtask name.
-
-```
-    subtask = Optional Subtask {$_name} |
-```
-
-To define an alternate subtask to load when an optional subtask does not exist,
-list the alterante subtask name after the pipe (`|`) character. This feature
-may be chained together as many times as needed.
-
-```
-    subtask = Optional Subtask {$_name} |
-              Backup Optional Subtask {$_name} Backup |
-              Default Subtask # Must exist if the above two don't!
-```
-
 `subtasks-external`
 
 : This key defines a file containing subtasks of the current task. This
@@ -270,6 +254,27 @@
     fail = label:code-review-2
 ```
 
+<a id="optional_tasks"/>
+Optional Tasks
+--------------
+To define a task that may not exist and that will not cause the task referencing
+it to be INVALID, follow the task name with pipe (`|`) character. This feature
+is particularly useful when a property is used in the task name.
+
+```
+    preload-task = Optional Subtask {$_name} |
+```
+
+To define an alternate task to load when an optional task does not exist,
+list the alterante task name after the pipe (`|`) character. This feature
+may be chained together as many times as needed.
+
+```
+    subtask = Optional Subtask {$_name} |
+              Backup Optional Subtask {$_name} Backup |
+              Default Subtask # Must exist if the above two don't!
+```
+
 External Entries
 ----------------
 A name for external task files on other projects and branches may be given
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index 5f41202..721032f 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -185,6 +185,7 @@
   subtask = Subtask Preload Override Pass
   subtask = Subtask Preload Override Fail
   subtask = Subtask Preload Extend Subtasks
+  subtask = Subtask Preload Optional
   subtask = Subtask Preload Properties
 
 [task "Subtask Preload Preload"]
@@ -213,6 +214,9 @@
   preload-task = Subtask READY
   subtask = Subtask APPLICABLE
 
+[task "Subtask Preload Optional"]
+  preload-task = Missing | Subtask PASS
+
 [task "Subtask Preload Properties"]
   preload-task = Subtask Properties Hints
   set-fourth-property = fourth-value
@@ -709,6 +713,11 @@
                         },
                         {
                            "hasPass" : true,
+                           "name" : "Subtask Preload Optional",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
                            "hint" : "second-property(first-value second-extra third-value) fourth-property(fourth-value)",
                            "name" : "Subtask Preload Properties",
                            "status" : "FAIL"
diff --git a/test/all b/test/all
index c99aa48..82347ab 100644
--- a/test/all
+++ b/test/all
@@ -472,6 +472,12 @@
                         {
                            "applicable" : true,
                            "hasPass" : true,
+                           "name" : "Subtask Preload Optional",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
                            "hint" : "second-property(first-value second-extra third-value) fourth-property(fourth-value)",
                            "name" : "Subtask Preload Properties",
                            "status" : "FAIL"