fixup! task: add support to set custom properties

Since property expansion will modify a definition's fields, Lists and
Maps need to be copied during a copy to prevent modifications on one
Task from modifying other Tasks returned from the same tasks-factory.
Also only copy a TaskBase's known field types to avoid potentially
copying a type which is not currently known, and thus may not be copied
properly. Also, add test case to validate the fix.

Change-Id: I551bf8a0532a7fb31ad21f8facc705535e69f3c5
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 d9e8c4b..e0fec78 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.common.primitives.Primitives;
 import com.google.gerrit.common.Container;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.git.meta.AbstractVersionedMetaData;
@@ -90,15 +91,41 @@
     }
 
     protected TaskBase(TaskBase base) {
-      for (Field field : TaskBase.class.getDeclaredFields()) {
+      copyDeclaredFields(TaskBase.class, base);
+    }
+
+    protected <T> void copyDeclaredFields(Class<T> cls, T from) {
+      for (Field field : cls.getDeclaredFields()) {
         try {
           field.setAccessible(true);
-          field.set(this, field.get(base));
+          Class<?> fieldCls = field.getType();
+          Object val = field.get(from);
+          if (field.getType().isPrimitive()
+              || Primitives.isWrapperType(fieldCls)
+              || (val instanceof String)
+              || val == null) {
+            field.set(this, val);
+          } else if (val instanceof List) {
+            List<?> list = List.class.cast(val);
+            field.set(this, new ArrayList<>(list));
+          } else if (val instanceof Map) {
+            Map<?, ?> map = Map.class.cast(val);
+            field.set(this, new HashMap<>(map));
+          } else if (field.getName().equals("this$0")) { // Don't copy internal final field
+          } else {
+            throw new RuntimeException(
+                "Don't know how to deep copy " + fieldValueToString(field, val));
+          }
         } catch (IllegalAccessException e) {
-          throw new RuntimeException(e);
+          throw new RuntimeException(
+              "Cannot access field to copy it " + fieldValueToString(field, "unknown"));
         }
       }
     }
+
+    protected String fieldValueToString(Field field, Object val) {
+      return "field:" + field.getName() + " value:" + val + " type:" + field.getType();
+    }
   }
 
   public class Task extends TaskBase {
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index e990856..ee0ac43 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -137,6 +137,19 @@
   fail-hint = Name(${_name}) Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic})
   subtask = Subtask Properties
 
+[root "Root Properties Expansion"]
+  applicable = status:open
+  subtask = Subtask Property Expansion fail-hint
+
+[task "Subtask Property Expansion fail-hint"]
+  subtasks-factory = tasks-factory Property Expansion fail-hint
+
+[tasks-factory "tasks-factory Property Expansion fail-hint"]
+  set-first-property = first-property ${_name}
+  fail-hint = ${first-property}
+  fail = true
+  names-factory = names-factory static list
+
 [root "Root Preload"]
    preload-task = Subtask FAIL
    subtask = Subtask Preload
@@ -984,6 +997,50 @@
             },
             {
                "applicable" : true,
+               "hasPass" : false,
+               "name" : "Root Properties Expansion",
+               "status" : "WAITING",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Property Expansion fail-hint",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "first-property my a task",
+                           "name" : "my a task",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "first-property my b task",
+                           "name" : "my b task",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "first-property my c task",
+                           "name" : "my c task",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "first-property my d task Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
+                           "name" : "my d task Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
+                           "status" : "FAIL"
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
                "hasPass" : true,
                "name" : "Root Preload",
                "status" : "FAIL",