Allow tasks attributes to be preloaded from other tasks

This is similar to inheritance and can help consolidate many task
properties such as hints and common subtasks.  The name "preload" helps
hint at the general policy of overriding scalars, and extending
lists/maps.

This will be particularly useful to at least consolidate the PW
warehouse hints and pass criteria onto the base warehouse task.

Change-Id: I645147e3ce68436cba7fdff8d824fc3c9ef49542
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
new file mode 100644
index 0000000..e7b91a8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import java.lang.IllegalAccessException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Use to pre-load a task definition with values from its preload-task definition. */
+public class Preloader {
+  public static void preload(Task definition) {
+    String name = definition.preloadTask;
+    if (name != null) {
+      Task task = definition.config.getTask(name);
+      if (task == null) {
+        throw new RuntimeException("Unknown preload-task.");
+      }
+      preload(task);
+      preloadFrom(definition, task);
+    }
+  }
+
+  protected static void preloadFrom(Task definition, Task preloadFrom) {
+    for (Field field : definition.getClass().getFields()) {
+      String name = field.getName();
+      if (name.equals("isVisible") || name.equals("isTrusted") || name.equals("config")) {
+        continue;
+      }
+
+      try {
+        field.setAccessible(true);
+        Object pre = field.get(preloadFrom);
+        if (pre != null) {
+          Object val = field.get(definition);
+          if (val == null) {
+            field.set(definition, pre);
+          } else if (val instanceof List) {
+            field.set(definition, preloadListFrom((List) val, (List) pre));
+          } else if (val instanceof Map) {
+            field.set(definition, preloadMapFrom((Map) val, (Map) pre));
+          } // nothing to do for overridden preloaded scalars
+        }
+      } catch (IllegalAccessException | IllegalArgumentException e) {
+        throw new RuntimeException();
+      }
+    }
+  }
+
+  protected static List preloadListFrom(List list, List preList) {
+    List extended = list;
+    if (!preList.isEmpty()) {
+      extended = preList;
+      if (!list.isEmpty()) {
+        extended = new ArrayList(list.size() + preList.size());
+        extended.addAll(preList);
+        extended.addAll(list);
+      }
+    }
+    return extended;
+  }
+
+  protected static Map preloadMapFrom(Map map, Map preMap) {
+    Map extended = map;
+    if (!preMap.isEmpty()) {
+      extended = preMap;
+      if (!map.isEmpty()) {
+        extended = new HashMap(map.size() + preMap.size());
+        extended.putAll(preMap);
+        extended.putAll(map);
+      }
+    }
+    return extended;
+  }
+}
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 0fa89d6..0eac473 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -43,6 +43,7 @@
     public String inProgress;
     public String name;
     public String pass;
+    public String preloadTask;
     public Map<String, String> properties;
     public String readyHint;
     public List<String> subTasks;
@@ -62,6 +63,7 @@
       inProgress = getString(s, KEY_IN_PROGRESS, null);
       name = s.subSection;
       pass = getString(s, KEY_PASS, null);
+      preloadTask = getString(s, KEY_PRELOAD_TASK, null);
       properties = getProperties(s, KEY_PROPERTIES_PREFIX);
       readyHint = getString(s, KEY_READY_HINT, null);
       subTasks = getStringList(s, KEY_SUBTASK);
@@ -93,6 +95,7 @@
   protected static final String KEY_IN_PROGRESS = "in-progress";
   protected static final String KEY_NAME = "name";
   protected static final String KEY_PASS = "pass";
+  protected static final String KEY_PRELOAD_TASK = "preload-task";
   protected static final String KEY_PROPERTIES_PREFIX = "set-";
   protected static final String KEY_READY_HINT = "ready-hint";
   protected static final String KEY_SUBTASK = "subtask";
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 ad8117d..1d7b9d5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -116,6 +116,7 @@
       this.definition = definition;
       this.path.addAll(path);
       this.path.add(definition.name);
+      Preloader.preload(definition);
       new Properties(definition, parentProperties);
     }
 
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 6980a98..6c11074 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -146,6 +146,21 @@
     fail-hint = Blocked by a negative review score
 ```
 
+`preload-task`
+
+: This key defines a task whose attributes will be preloaded into the current
+task before the current task's attributes are set. Most attributes defined
+in the preload-task will be loaded first, and will be overridden by attributes
+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.
+
+Example:
+```
+    preload-task = Base Jenkins Verification # has a pass criteria and hints
+```
+
 `subtask`
 
 : This key lists the name of a subtask of the current task. This key may be
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index 95ae507..33794e5 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -106,6 +106,10 @@
   fail-hint = Name(${_name})
   subtask = Subtask Properties
 
+[root "Root Preload"]
+   preload-task = Subtask FAIL
+   subtask = Subtask Preload
+
 [root "INVALIDS"]
   subtasks-file = invalids.config
 
@@ -143,6 +147,7 @@
   applicable = NOT is:open # Assumes test query is "is:open"
 
 [task "Subtask Properties"]
+  export-subtask = ${_name}
   subtask = Subtask Properties Hints
   subtask = Chained ${_name}
   subtask = Subtask Properties Reset
@@ -151,7 +156,6 @@
   set-first-property = first-value
   set-second-property = ${first-property} second-extra ${third-property}
   set-third-property = third-value
-  export-subtask = ${_name}
   fail = True
   fail-hint = Name(${_name}) root-property(${root-property}) first-property(${first-property}) second-property(${second-property}) root(${root})
 
@@ -163,6 +167,51 @@
   set-first-property = reset-first-value
   fail-hint = first-property(${first-property})
 
+[task "Subtask Preload"]
+  preload-task = Subtask READY
+  subtask = Subtask Preload Preload
+  subtask = Subtask Preload Hints PASS
+  subtask = Subtask Preload Hints FAIL
+  subtask = Subtask Preload Override Pass
+  subtask = Subtask Preload Override Fail
+  subtask = Subtask Preload Extend Subtasks
+  subtask = Subtask Preload Properties
+
+[task "Subtask Preload Preload"]
+  preload-task = Subtask Preload with Preload
+
+[task "Subtask Preload with Preload"]
+  preload-task = Subtask PASS
+
+[task "Subtask Preload Hints PASS"]
+  preload-task = Subtask Hints
+  pass = False
+
+[task "Subtask Preload Hints FAIL"]
+  preload-task = Subtask Hints
+  fail = True
+
+[task "Subtask Preload Override Pass"]
+  preload-task = Subtask PASS
+  pass = False
+
+[task "Subtask Preload Override Fail"]
+  preload-task = Subtask FAIL
+  fail = False
+
+[task "Subtask Preload Extend Subtasks"]
+  preload-task = Subtask READY
+  subtask = Subtask APPLICABLE
+
+[task "Subtask Preload Properties"]
+  preload-task = Subtask Properties Hints
+  set-fourth-property = fourth-value
+  fail-hint = second-property(${second-property}) fourth-property(${fourth-property})
+
+[task "Subtask Hints"] # meant to be preloaded, not a test case in itself
+  ready-hint = Task is ready
+  fail-hint = Task failed
+
 [external "user special"]
   user = testuser
   file = special.config
@@ -523,14 +572,14 @@
                "status" : "FAIL",
                "subTasks" : [
                   {
+                     "exported" : {
+                        "subtask" : "Subtask Properties"
+                     },
                      "hasPass" : false,
                      "name" : "Subtask Properties",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "exported" : {
-                              "subtask" : "Subtask Properties Hints"
-                           },
                            "hasPass" : true,
                            "hint" : "Name(Subtask Properties Hints) root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value) root(Root Properties)",
                            "name" : "Subtask Properties Hints",
@@ -551,6 +600,75 @@
                ]
             },
             {
+               "hasPass" : true,
+               "name" : "Root Preload",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "hasPass" : true,
+                     "name" : "Subtask Preload",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask PASS",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Preload",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "hint" : "Task is ready",
+                           "name" : "Subtask Preload Hints PASS",
+                           "status" : "READY"
+                        },
+                        {
+                           "hasPass" : true,
+                           "hint" : "Task failed",
+                           "name" : "Subtask Preload Hints FAIL",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Override Pass",
+                           "status" : "READY"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Override Fail",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Extend Subtasks",
+                           "status" : "READY",
+                           "subTasks" : [
+                              {
+                                 "hasPass" : true,
+                                 "name" : "Subtask PASS",
+                                 "status" : "PASS"
+                              },
+                              {
+                                 "hasPass" : true,
+                                 "name" : "Subtask APPLICABLE",
+                                 "status" : "PASS"
+                              }
+                           ]
+                        },
+                        {
+                           "hasPass" : true,
+                           "hint" : "second-property(first-value second-extra third-value) fourth-property(fourth-value)",
+                           "name" : "Subtask Preload Properties",
+                           "status" : "FAIL"
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
                "hasPass" : false,
                "name" : "INVALIDS",
                "status" : "WAITING",
diff --git a/test/all b/test/all
index b3c79a8..a993e95 100644
--- a/test/all
+++ b/test/all
@@ -327,15 +327,15 @@
                "subTasks" : [
                   {
                      "applicable" : true,
+                     "exported" : {
+                        "subtask" : "Subtask Properties"
+                     },
                      "hasPass" : false,
                      "name" : "Subtask Properties",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
-                           "exported" : {
-                              "subtask" : "Subtask Properties Hints"
-                           },
                            "hasPass" : true,
                            "hint" : "Name(Subtask Properties Hints) root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value) root(Root Properties)",
                            "name" : "Subtask Properties Hints",
@@ -359,6 +359,87 @@
             },
             {
                "applicable" : true,
+               "hasPass" : true,
+               "name" : "Root Preload",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "Subtask Preload",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask PASS",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Preload",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "Task is ready",
+                           "name" : "Subtask Preload Hints PASS",
+                           "status" : "READY"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "Task failed",
+                           "name" : "Subtask Preload Hints FAIL",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Override Pass",
+                           "status" : "READY"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Override Fail",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Extend Subtasks",
+                           "status" : "READY",
+                           "subTasks" : [
+                              {
+                                 "applicable" : true,
+                                 "hasPass" : true,
+                                 "name" : "Subtask PASS",
+                                 "status" : "PASS"
+                              },
+                              {
+                                 "applicable" : true,
+                                 "hasPass" : true,
+                                 "name" : "Subtask APPLICABLE",
+                                 "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"
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
                "hasPass" : false,
                "name" : "INVALIDS",
                "status" : "WAITING",