Tasks: add support for a ${_name} property

The ${_name} token will be replaced in any task field with the value of
the current task name. This can be used to call subtasks based on the
name of the current task reducing the chance for typos while adding
templated tasks.

Change-Id: I268545326f27fa4138e86e6537a42c41b2173534
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 a67653b..56e7f03 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -27,6 +27,7 @@
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
 import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -97,6 +98,7 @@
       this.definition = definition;
       this.path.addAll(path);
       this.path.add(definition);
+      expandProperties(definition);
     }
 
     public List<Node> getSubNodes() throws OrmException {
@@ -181,4 +183,36 @@
       return new Branch.NameKey(allUsers.get(), RefNames.refsUsers(acct.getId()));
     }
   }
+
+  protected static void expandProperties(Task definition) {
+    for (Field field : Task.class.getFields()) {
+      try {
+        field.setAccessible(true);
+        Object o = field.get(definition);
+        if (o instanceof String) {
+          o = expandProperties((String) o, definition);
+        } else if (o instanceof List) {
+          o = expandProperties((List<String>) o, definition);
+        }
+        field.set(definition, o);
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  protected static List<String> expandProperties(List<String> strings, Task definition) {
+    if (strings == null) {
+      return null;
+    }
+    List<String> expanded = new ArrayList<>(strings.size());
+    for (String string : strings) {
+      expanded.add(expandProperties(string, definition));
+    }
+    return expanded;
+  }
+
+  protected static String expandProperties(String string, Task definition) {
+    return string == null ? null : string.replaceAll("\\$\\{_name\\}", definition.name);
+  }
 }
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 35ede41..96782eb 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -262,6 +262,16 @@
     user = first-user # references the sharded user ref refs/users/01/1000001
 ```
 
+Properties
+----------
+The task plugin supplies the `${_name}` property which may be used anywhere in
+a task definition as a token representing the name of the current task.
+
+Example:
+```
+    fail-hint = {$_name} needs to be fixed
+```
+
 Change Query Output
 -------------------
 It is possible to add a task section to the query output of changes using
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index 50df58f..dcd1026 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -121,6 +121,11 @@
   applicable = -is:open
   subtasks-file = invalids.config
 
+[root "Root Properties"]
+  fail = True
+  fail-hint = Name(${_name})
+  subtask = Subtask Properties
+
 [task "Subtask FAIL"]
   applicable = is:open
   fail = is:open
@@ -138,6 +143,14 @@
 [task "Subtask NA"]
   applicable = NOT is:open
 
+[task "Subtask Properties"]
+  fail = True
+  fail-hint = Name(${_name})
+  subtask = Chained ${_name}
+
+[task "Chained Subtask Properties"]
+  pass = True
+
 [external "user special"]
   user = testuser
   file = special.config
@@ -535,6 +548,27 @@
                      "status" : "INVALID"
                   }
                ]
+            },
+            {
+               "hasPass" : true,
+               "hint" : "Name(Root Properties)",
+               "name" : "Root Properties",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "hasPass" : true,
+                     "hint" : "Name(Subtask Properties)",
+                     "name" : "Subtask Properties",
+                     "status" : "FAIL",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "name" : "Chained Subtask Properties",
+                           "status" : "PASS"
+                        }
+                     ]
+                  }
+               ]
             }
          ]
       }
diff --git a/test/all b/test/all
index fb98668..7691bc2 100644
--- a/test/all
+++ b/test/all
@@ -500,6 +500,30 @@
                      "status" : "FAIL"
                   }
                ]
+            },
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "hint" : "Name(Root Properties)",
+               "name" : "Root Properties",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "hint" : "Name(Subtask Properties)",
+                     "name" : "Subtask Properties",
+                     "status" : "FAIL",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Chained Subtask Properties",
+                           "status" : "PASS"
+                        }
+                     ]
+                  }
+               ]
             }
          ]
       }