Add optional chainable subtask support

Use the "|" character to indicate that it is valid for a subtask to not
exist. Follow the "|" with the name of an alternate subtask if desired.

Change-Id: Idff6d5142def0208305fad616a0b6c5db690baba
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 1d7b9d5..b1fe1a1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -36,6 +36,8 @@
 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;
 
 /**
@@ -46,6 +48,8 @@
  */
 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;
@@ -166,11 +170,28 @@
     protected List<Task> getSubTasks() {
       List<Task> tasks = new ArrayList<>();
       for (String subTask : definition.subTasks) {
-        tasks.add(definition.config.getTask(subTask));
+        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;
+        }
+      }
+      String last = subTaskEntry.substring(end);
+      if (!"".equals(last)) { // Last entry was not optional
+        tasks.add(definition.config.getTask(subTaskEntry.substring(end)));
+      }
+    }
+
     protected List<Task> getTasks(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 6c11074..326c550 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -172,6 +172,29 @@
 ```
     subtask = "Code Review"
     subtask = "License Approval"
+    ...
+    [task "Code Review"]
+    ...
+    [task "License Approval"]
+    ...
+```
+
+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`
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index 33794e5..5f41202 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -77,6 +77,10 @@
    in-progress = NOT is:open
    pass = NOT is:open
 
+[root "Root Optional subtasks"]
+   subtask = OPTIONAL MISSING |
+   subtask = Subtask Optional |
+
 [root "Subtasks File"]
   subtasks-file = common.config
 
@@ -143,6 +147,12 @@
   applicable = is:open
   pass = is:open
 
+[task "Subtask Optional"]
+   subtask = Subtask PASS |
+   subtask = OPTIONAL MISSING | Subtask FAIL
+   subtask = OPTIONAL MISSING | OPTIONAL MISSING |
+   subtask = OPTIONAL MISSING | OPTIONAL MISSING | Subtask READY
+
 [task "Subtask NA"]
   applicable = NOT is:open # Assumes test query is "is:open"
 
@@ -264,6 +274,9 @@
 [task "Subtask INVALID"]
   fail-hint = Use when an INVALID subtask is needed, not meant as a test case in itself
 
+[task "Subtask Optional"]
+   subtask = MISSING | MISSING
+
 [task "NA Bad PASS query"]
   applicable = NOT is:open # Assumes test query is "is:open"
   fail = True
@@ -454,6 +467,42 @@
             },
             {
                "hasPass" : false,
+               "name" : "Root Optional subtasks",
+               "status" : "WAITING",
+               "subTasks" : [
+                  {
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask PASS",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask FAIL",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask READY",
+                           "status" : "READY",
+                           "subTasks" : [
+                              {
+                                 "hasPass" : true,
+                                 "name" : "Subtask PASS",
+                                 "status" : "PASS"
+                              }
+                           ]
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "hasPass" : false,
                "name" : "Subtasks File",
                "status" : "WAITING",
                "subTasks" : [
@@ -747,6 +796,17 @@
                   },
                   {
                      "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
                      "name" : "Looping",
                      "status" : "WAITING",
                      "subTasks" : [
diff --git a/test/all b/test/all
index a993e95..c99aa48 100644
--- a/test/all
+++ b/test/all
@@ -190,6 +190,48 @@
             {
                "applicable" : true,
                "hasPass" : false,
+               "name" : "Root Optional subtasks",
+               "status" : "WAITING",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask PASS",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask FAIL",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask READY",
+                           "status" : "READY",
+                           "subTasks" : [
+                              {
+                                 "applicable" : true,
+                                 "hasPass" : true,
+                                 "name" : "Subtask PASS",
+                                 "status" : "PASS"
+                              }
+                           ]
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
+               "hasPass" : false,
                "name" : "Subtasks File",
                "status" : "WAITING",
                "subTasks" : [
@@ -527,6 +569,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -663,6 +717,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
diff --git a/test/invalid b/test/invalid
index 27df0ec..840c3da 100644
--- a/test/invalid
+++ b/test/invalid
@@ -135,6 +135,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -259,6 +271,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
diff --git a/test/invalid-applicable b/test/invalid-applicable
index 514fca9..8e52fa0 100644
--- a/test/invalid-applicable
+++ b/test/invalid-applicable
@@ -103,6 +103,17 @@
                   },
                   {
                      "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
                      "name" : "Looping",
                      "status" : "WAITING",
                      "subTasks" : [
diff --git a/test/preview b/test/preview
index 60ba53f..e2706ea 100644
--- a/test/preview
+++ b/test/preview
@@ -91,6 +91,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -262,6 +274,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
diff --git a/test/preview.invalid b/test/preview.invalid
index be1bb7f..86d12f1 100644
--- a/test/preview.invalid
+++ b/test/preview.invalid
@@ -91,6 +91,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -215,6 +227,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",