Add support for closure template (soy)

Breaking change: Variables that had a dash have been renamed to
exclude the dash and instead capitalise the word that used to be after
the dash.

The renaming of variables to remove the dash in them is because
soy does not support variables with a dash in it.

Anyways velocity 2.0 also dropped support for variables with dash too.

Bug: Issue 6528
Bug: Issue 6411
Change-Id: I847d1697e72314d4d30cfa0006c02a7bdc65c53b
diff --git a/BUILD b/BUILD
index 3852f4f..6c5ea1c 100644
--- a/BUILD
+++ b/BUILD
@@ -10,6 +10,9 @@
     name = "its-base",
     srcs = glob(["src/main/java/**/*.java"]),
     resources = glob(["src/main/resources/**/*"]),
+    deps = [
+      "@soy//jar",
+    ],
 )
 
 TEST_UTIL_SRC = glob(["src/test/java/com/googlesource/gerrit/plugins/its/base/testutil/**/*.java"])
diff --git a/WORKSPACE b/WORKSPACE
index 95cf81a..9920af0 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -24,3 +24,15 @@
 
 # Load snapshot Plugin API
 gerrit_api_maven_local()
+
+load(
+    "@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl",
+    "maven_jar",
+)
+
+# Keep this version of Soy synchronized with the version used in gerrit.
+maven_jar(
+    name = "soy",
+    artifact = "com.google.template:soy:2017-08-08",
+    sha1 = "792aa49e3ec3f61e793e56b499f0724df1c1e16c",
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
index e2fff3f..91b26a0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2017 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.
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
-
 import com.googlesource.gerrit.plugins.its.base.its.ItsConfig;
 import com.googlesource.gerrit.plugins.its.base.its.ItsHookEnabledConfigEntry;
 import com.googlesource.gerrit.plugins.its.base.validation.ItsValidateComment;
@@ -32,6 +31,7 @@
 import com.googlesource.gerrit.plugins.its.base.workflow.Property;
 import com.googlesource.gerrit.plugins.its.base.workflow.Rule;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddComment;
+import com.googlesource.gerrit.plugins.its.base.workflow.action.AddSoyComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddStandardComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddVelocityComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.LogEvent;
@@ -41,8 +41,7 @@
   private final String pluginName;
   private final PluginConfigFactory pluginCfgFactory;
 
-  public ItsHookModule(@PluginName String pluginName,
-      PluginConfigFactory pluginCfgFactory) {
+  public ItsHookModule(@PluginName String pluginName, PluginConfigFactory pluginCfgFactory) {
     this.pluginName = pluginName;
     this.pluginCfgFactory = pluginCfgFactory;
   }
@@ -53,15 +52,14 @@
         .annotatedWith(Exports.named("enabled"))
         .toInstance(new ItsHookEnabledConfigEntry(pluginName, pluginCfgFactory));
     bind(ItsConfig.class);
-    DynamicSet.bind(binder(), CommitValidationListener.class).to(
-        ItsValidateComment.class);
-    DynamicSet.bind(binder(), EventListener.class).to(
-        ActionController.class);
+    DynamicSet.bind(binder(), CommitValidationListener.class).to(ItsValidateComment.class);
+    DynamicSet.bind(binder(), EventListener.class).to(ActionController.class);
     factory(ActionRequest.Factory.class);
     factory(Property.Factory.class);
     factory(Condition.Factory.class);
     factory(Rule.Factory.class);
     factory(AddComment.Factory.class);
+    factory(AddSoyComment.Factory.class);
     factory(AddStandardComment.Factory.class);
     factory(AddVelocityComment.Factory.class);
     factory(LogEvent.Factory.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyAttributeExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyAttributeExtractor.java
index ea1fb70..a306240 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyAttributeExtractor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyAttributeExtractor.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2017 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.
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.inject.Inject;
 
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
 import com.googlesource.gerrit.plugins.its.base.workflow.Property;
 
 import java.util.Set;
@@ -32,9 +33,11 @@
  */
 public class PropertyAttributeExtractor {
   private Property.Factory propertyFactory;
+  private ItsFacade its;
 
   @Inject
-  PropertyAttributeExtractor(Property.Factory propertyFactory) {
+  PropertyAttributeExtractor(ItsFacade its, Property.Factory propertyFactory) {
+    this.its = its;
     this.propertyFactory = propertyFactory;
   }
 
@@ -42,12 +45,21 @@
       String prefix) {
     Set<Property> properties = Sets.newHashSet();
     if (accountAttribute != null) {
+      // deprecated, to be removed soon. migrate to ones without dash.
       properties.add(propertyFactory.create(prefix + "-email",
           accountAttribute.email));
       properties.add(propertyFactory.create(prefix + "-username",
           accountAttribute.username));
       properties.add(propertyFactory.create(prefix + "-name",
           accountAttribute.name));
+
+      // New style configs for vm and soy
+      properties.add(propertyFactory.create(prefix + "Email",
+          accountAttribute.email));
+      properties.add(propertyFactory.create(prefix + "Username",
+          accountAttribute.username));
+      properties.add(propertyFactory.create(prefix + "Name",
+          accountAttribute.name));
     }
     return properties;
   }
@@ -58,11 +70,25 @@
     properties.add(propertyFactory.create("branch", changeAttribute.branch));
     properties.add(propertyFactory.create("topic", changeAttribute.topic));
     properties.add(propertyFactory.create("subject", changeAttribute.subject));
+
+    // deprecated, to be removed soon. migrate to ones without dash.
     properties.add(propertyFactory.create("commit-message", changeAttribute.commitMessage));
     properties.add(propertyFactory.create("change-id", changeAttribute.id));
     properties.add(propertyFactory.create("change-number",
         String.valueOf(changeAttribute.number)));
     properties.add(propertyFactory.create("change-url", changeAttribute.url));
+
+    // New style configs for vm and soy
+    properties.add(propertyFactory.create("commitMessage", changeAttribute.commitMessage));
+    properties.add(propertyFactory.create("changeId", changeAttribute.id));
+    properties.add(propertyFactory.create("changeNumber",
+        String.valueOf(changeAttribute.number)));
+    properties.add(propertyFactory.create("changeUrl", changeAttribute.url));
+
+    // Soy specfic config though will work with Velocity too
+    properties.add(propertyFactory.create("formatChangeUrl",
+        its.createLinkForWebui(changeAttribute.url, changeAttribute.url)));
+
     String status = null;
     if (changeAttribute.status != null) {
       status = changeAttribute.status.toString();
@@ -76,11 +102,24 @@
     Set<Property> properties = Sets.newHashSet();
     properties.add(propertyFactory.create("revision",
         patchSetAttribute.revision));
+    // deprecated, to be removed soon. migrate to ones without dash.
     properties.add(propertyFactory.create("patch-set-number",
         String.valueOf(patchSetAttribute.number)));
+
+    // New style configs for vm and soy
+    properties.add(propertyFactory.create("patchSetNumber",
+        String.valueOf(patchSetAttribute.number)));
+
     properties.add(propertyFactory.create("ref", patchSetAttribute.ref));
+
+    // deprecated, to be removed soon. migrate to ones without dash.
     properties.add(propertyFactory.create("created-on",
         patchSetAttribute.createdOn.toString()));
+
+    // New style configs for vm and soy
+    properties.add(propertyFactory.create("createdOn",
+        patchSetAttribute.createdOn.toString()));
+
     properties.add(propertyFactory.create("parents",
         patchSetAttribute.parents.toString()));
     properties.add(propertyFactory.create("deletions",
@@ -98,8 +137,14 @@
     Set<Property> properties = Sets.newHashSet();
     properties.add(propertyFactory.create("revision",
         refUpdateAttribute.newRev));
+
+    // deprecated, to be removed soon. migrate to ones without dash.
     properties.add(propertyFactory.create("revision-old",
         refUpdateAttribute.oldRev));
+
+    // New style configs for vm and soy
+    properties.add(propertyFactory.create("revisionOld",
+        refUpdateAttribute.oldRev));
     properties.add(propertyFactory.create("project",
         refUpdateAttribute.project));
     properties.add(propertyFactory.create("ref",
@@ -109,8 +154,13 @@
 
   public Set<Property>extractFrom(ApprovalAttribute approvalAttribute) {
     Set<Property> properties = Sets.newHashSet();
+    // deprecated, to be removed soon. migrate to ones without dash.
     properties.add(propertyFactory.create("approval-" +
         approvalAttribute.type, approvalAttribute.value));
+
+    // New style configs for vm and soy
+    properties.add(propertyFactory.create("approval" +
+        approvalAttribute.type.replace("-", ""), approvalAttribute.value));
     return properties;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
index e83feb4..0e31b93 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2017 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.
@@ -15,56 +15,57 @@
 package com.googlesource.gerrit.plugins.its.base.workflow;
 
 import com.google.inject.Inject;
-
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.Action;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddComment;
+import com.googlesource.gerrit.plugins.its.base.workflow.action.AddSoyComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddStandardComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddVelocityComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.LogEvent;
-
+import java.io.IOException;
+import java.util.Set;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.util.Set;
-
-/**
- * Executes an {@link ActionRequest}
- */
+/** Executes an {@link ActionRequest} */
 public class ActionExecutor {
-  private static final Logger log = LoggerFactory.getLogger(
-      ActionExecutor.class);
+  private static final Logger log = LoggerFactory.getLogger(ActionExecutor.class);
 
   private final ItsFacade its;
   private final AddComment.Factory addCommentFactory;
   private final AddStandardComment.Factory addStandardCommentFactory;
   private final AddVelocityComment.Factory addVelocityCommentFactory;
+  private final AddSoyComment.Factory addSoyCommentFactory;
   private final LogEvent.Factory logEventFactory;
 
   @Inject
-  public ActionExecutor(ItsFacade its, AddComment.Factory addCommentFactory,
+  public ActionExecutor(
+      ItsFacade its,
+      AddComment.Factory addCommentFactory,
       AddStandardComment.Factory addStandardCommentFactory,
       AddVelocityComment.Factory addVelocityCommentFactory,
+      AddSoyComment.Factory addSoyCommentFactory,
       LogEvent.Factory logEventFactory) {
     this.its = its;
     this.addCommentFactory = addCommentFactory;
     this.addStandardCommentFactory = addStandardCommentFactory;
     this.addVelocityCommentFactory = addVelocityCommentFactory;
+    this.addSoyCommentFactory = addSoyCommentFactory;
     this.logEventFactory = logEventFactory;
   }
 
-  public void execute(String issue, ActionRequest actionRequest,
-      Set<Property> properties) {
+  public void execute(String issue, ActionRequest actionRequest, Set<Property> properties) {
     try {
       String name = actionRequest.getName();
       Action action = null;
       if ("add-comment".equals(name)) {
         action = addCommentFactory.create();
       } else if ("add-standard-comment".equals(name)) {
-          action = addStandardCommentFactory.create();
+        action = addStandardCommentFactory.create();
       } else if ("add-velocity-comment".equals(name)) {
         action = addVelocityCommentFactory.create();
+      } else if ("add-soy-comment".equals(name)) {
+        action = addSoyCommentFactory.create();
       } else if ("log-event".equals(name)) {
         action = logEventFactory.create();
       }
@@ -79,10 +80,9 @@
     }
   }
 
-  public void execute(String issue, Iterable<ActionRequest> actions,
-      Set<Property> properties) {
+  public void execute(String issue, Iterable<ActionRequest> actions, Set<Property> properties) {
     for (ActionRequest actionRequest : actions) {
-        execute(issue, actionRequest, properties);
+      execute(issue, actionRequest, properties);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddSoyComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddSoyComment.java
new file mode 100644
index 0000000..f65d6c6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddSoyComment.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2017 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.its.base.workflow.action;
+
+import com.google.common.base.Strings;
+import com.google.common.io.CharStreams;
+import com.google.gerrit.server.config.SitePath;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.template.soy.SoyFileSet;
+import com.google.template.soy.data.SanitizedContent;
+import com.google.template.soy.tofu.SoyTofu;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import com.googlesource.gerrit.plugins.its.base.workflow.ActionRequest;
+import com.googlesource.gerrit.plugins.its.base.workflow.Property;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Adds a short predefined comments to an issue.
+ *
+ * <p>Comments are added for merging, abandoning, restoring of changes and adding of patch sets.
+ */
+public class AddSoyComment implements Action {
+  private static final Logger log = LoggerFactory.getLogger(AddSoyComment.class);
+
+  public interface Factory {
+    AddSoyComment create();
+  }
+
+  /** Directory (relative to site) to search templates in */
+  private static final String ITS_TEMPLATE_DIR =
+      "etc" + File.separator + "its" + File.separator + "templates";
+
+  private final ItsFacade its;
+  private final Path sitePath;
+  protected HashMap<String, Object> soyContext;
+
+  @Inject
+  public AddSoyComment(@SitePath Path sitePath, ItsFacade its) {
+    this.sitePath = sitePath;
+    this.its = its;
+  }
+
+  private HashMap getSoyContext(Set<Property> properties) {
+    HashMap<String, Object> soyContext = new HashMap<>();
+    for (Property property : properties) {
+      String key = property.getKey();
+      if (!Strings.isNullOrEmpty(key)) {
+        String value = property.getValue();
+        if (!Strings.isNullOrEmpty(value)) {
+          soyContext.put(key, value);
+        }
+      }
+    }
+
+    return soyContext;
+  }
+
+  private String soyTemplate(SoyFileSet.Builder builder, String template, SanitizedContent.ContentKind kind, Set<Property> properties) {
+    Path templateDir = sitePath.resolve(ITS_TEMPLATE_DIR);
+    Path templatePath = templateDir.resolve(template + ".soy");
+    String content;
+
+    try (Reader r = Files.newBufferedReader(templatePath, StandardCharsets.UTF_8)) {
+       content = CharStreams.toString(r);
+    } catch (IOException err) {
+      throw new ProvisionException(
+          "Failed to read template file " + templatePath.toAbsolutePath().toString(), err);
+    }
+
+    builder.add(content, templatePath.toAbsolutePath().toString());
+
+    HashMap context = getSoyContext(properties);
+
+    SoyTofu.Renderer renderer =
+        builder
+            .build()
+            .compileToTofu()
+            .newRenderer("etc.its.templates." + template)
+            .setContentKind(kind)
+            .setData(context);
+    return renderer.render();
+  }
+
+  protected String soyTextTemplate(SoyFileSet.Builder builder, String template, Set<Property> properties) {
+    return soyTemplate(builder, template, SanitizedContent.ContentKind.TEXT, properties);
+  }
+
+  @Override
+  public void execute(String issue, ActionRequest actionRequest, Set<Property> properties)
+      throws IOException {
+    SoyFileSet.Builder builder = SoyFileSet.builder();
+    String template = null;
+    String templateName = actionRequest.getParameter(1);
+    if (templateName.isEmpty()) {
+      log.error("No template name given in " + actionRequest);
+    } else {
+      template = templateName;
+    }
+    if (!Strings.isNullOrEmpty(template)) {
+      String comment = soyTextTemplate(builder, template, properties);
+      its.addComment(issue, comment);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddStandardComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddStandardComment.java
index 814075d..b033603 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddStandardComment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddStandardComment.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2017 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.
@@ -46,8 +46,10 @@
 
   private String formatPerson(String prefix, Map<String, String> map) {
     String ret = Strings.nullToEmpty(map.get(prefix + "-name"));
+    ret = Strings.nullToEmpty(map.get(prefix + "Name"));
     if (ret.isEmpty()) {
       ret = Strings.nullToEmpty(map.get(prefix + "-username"));
+      ret = Strings.nullToEmpty(map.get(prefix + "Username"));
     }
     return ret;
   }
@@ -56,6 +58,7 @@
       Map<String, String> map) {
     String ret = "";
     String changeNumber = Strings.nullToEmpty(map.get("change-number"));
+    changeNumber = Strings.nullToEmpty(map.get("changeNumber"));
     if (!changeNumber.isEmpty()) {
       changeNumber += " ";
     }
@@ -73,6 +76,7 @@
       ret += "\n\nReason:\n" + reason;
     }
     String url = Strings.nullToEmpty(map.get("change-url"));
+    url = Strings.nullToEmpty(map.get("changeUrl"));
     if (!url.isEmpty()) {
       ret += "\n\n" + its.createLinkForWebui(url, url);
     }
diff --git a/src/main/resources/Documentation/config-rulebase-common.md b/src/main/resources/Documentation/config-rulebase-common.md
index 3630793..ca305a0 100644
--- a/src/main/resources/Documentation/config-rulebase-common.md
+++ b/src/main/resources/Documentation/config-rulebase-common.md
@@ -35,7 +35,7 @@
     action = add-standard-comment
 [rule "rule2"]
     event-type = comment-added
-    approval-Code-Review = -2,-1
+    approvalCodeReview = -2,-1
     action = add-comment Oh my Goodness! Someone gave a negative code review in Gerrit on an associated change.
 ```
 
@@ -140,11 +140,11 @@
     ```
     [rule "someRuleForBugzillaOnly"]
       its-name = its-bugzilla
-      approval-Code-Review = -2
+      approvalCodeReview = -2
       action = add-comment Heya Bugzilla users, the change had a -2 Code-Review approval.
     [rule "someRuleForJiraOnly"]
       its-name = its-jira
-      approval-Code-Review = -2
+      approvalCodeReview = -2
       action = add-comment Dear JIRA users, the change had a -2 Code-Review approval.
     ```
 
@@ -223,13 +223,13 @@
 [event-properties-ChangeAbandonedEvent]: #event-properties-ChangeAbandonedEvent
 ### <a name="event-properties-ChangeAbandonedEvent">ChangeAbandonedEvent</a>
 
-`abandoner-email`
+`abandonerEmail`
 : email address of the user abandoning the change.
 
-`abandoner-name`
+`abandonerName`
 : name of the user abandoning the change.
 
-`abandoner-username`
+`abandonerUsername`
 : username of the user abandoning the change.
 
 `event`
@@ -254,13 +254,13 @@
 `event-type`
 : `change-merged`
 
-`submitter-email`
+`submitterEmail`
 : email address of the user causing the merge of the change.
 
-`submitter-name`
+`submitterName`
 : name of the user causing the merge of the change.
 
-`submitter-username`
+`submitterUsername`
 : username of the user causing the merge of the change.
 
 In addition to the above properties, the event also provides
@@ -279,13 +279,13 @@
 `reason`
 : reason why the change has been restored.
 
-`restorer-email`
+`restorerEmail`
 : email address of the user restoring the change.
 
-`restorer-name`
+`restorerName`
 :  name of the user restoring the change.
 
-`restorer-username`
+`restorerUsername`
 : username of the user restoring the change.
 
 In addition to the above properties, the event also provides
@@ -301,13 +301,13 @@
 author of the comment is accessible via the `commenter-...`
 properties.
 
-`commenter-email`
+`commenterEmail`
 : email address of the comment's author.
 
-`commenter-name`
+`commenterName`
 : name of the comment's author.
 
-`commenter-username`
+`commenterUsername`
 : username of the comment's author.
 
 `comment`
@@ -320,11 +320,11 @@
 : `comment-added`
 
 For each new or changed approval that has been made for this change, a
-property of key `approval-<LabelName>` and the approval's value as
+property of key `approval<LabelName>` and the approval's value as
 value is added. So for example voting “-2” for the approval
 “Code-Review” would add the following property:
 
-`approval-Code-Review`
+`approvalCodeReview`
 : `-2`
 
 In addition to the above properties, the event also provides
@@ -363,16 +363,16 @@
 `revision`
 : git commit hash the rev is pointing to now.
 
-`revision-old`
+`revisionOld`
 : git commit hash the rev was pointing to before.
 
-`submitter-email`
+`submitterEmail`
 : email address of the user that updated the ref.
 
-`submitter-name`
+`submitterName`
 : name of the user that updated the ref.
 
-`submitter-username`
+`submitterUsername`
 : username of the user that updated the ref.
 
 [event-properties-change]: #event-properties-change
@@ -381,22 +381,25 @@
 `branch`
 : name of the branch the change belongs to.
 
-`change-id`
+`changeId`
 : Change-Id for the change („I-followed by 40 hex digits” string).
 
-`change-number`
+`changeNumber`
 : number for the change (plain integer).
 
-`change-url`
+`changeUrl`
 : url of the change.
 
-`owner-email`
+`formatChangeUrl`
+: format the url for changeUrl.
+
+`ownerEmail`
 : email address of the change's owner.
 
-`owner-name`
+`ownerName`
 : name of the change's owner.
 
-`owner-username`
+`ownerUsername`
 : username of the change's owner.
 
 `project`
@@ -405,7 +408,7 @@
 `subject`
 : first line of the change's most recent patch set's commit message.
 
-`commit-message`
+`commitMessage`
 : full commit message of the most recent patch set
 
 `status`
@@ -418,13 +421,13 @@
 [event-properties-patch-set]: #event-properties-patch-set
 ### <a name="event-properties-patch-set">Common properties for events on a patch set</a>
 
-`author-email`
+`authorEmail`
 : email address of this patch set's author.
 
-`author-name`
+`authorName`
 : name of this patch set's author.
 
-`author-username`
+`authorUsername`
 : username of this patch set's author.
 
 `created-on`
@@ -439,7 +442,7 @@
 `parents`
 : A list of git commit hashes that are parents to the patch set.
 
-`patch-set-number`
+`patchSetNumber`
 : patch set's number within the change.
 
 `ref`
@@ -449,13 +452,13 @@
 `revision`
 : git commit hash of the patch set
 
-`uploader-email`
+`uploaderEmail`
 : email address of the user that uploaded this patch set.
 
-`uploader-name`
+`uploaderName`
 : name of the user that uploaded this patch set.
 
-`uploader-username`
+`uploaderUsername`
 : username of the user that uploaded this patch set.
 
 [actions]: #actions
@@ -482,6 +485,9 @@
 [`add-velocity-comment`][action-add-velocity-comment]
 : adds a rendered Velocity template as issue comment
 
+[`add-soy-comment`][action-add-velocity-comment]
+: adds a rendered Closure Template (soy) template as issue comment
+
 [`log-event`][action-log-event]
 : appends the event's properties to Gerrit's log
 
@@ -547,7 +553,7 @@
 
 Any [property][event-properties] of the event may be used from
 templates. So for example `$subject` in the above example refers to
-the event's subject property, and `$change-number` would refer to the
+the event's subject property, and `$changeNumber` would refer to the
 change's number.
 
 Additionally, the context's `its` property provides an object that
@@ -562,7 +568,7 @@
 	```
 [rule "formatLinkSampleRule"]
   event-type = comment-added
-  action = add-velocity-comment inline Comment for change $change-number added. See ${its.formatLink($change-url)}
+  action = add-velocity-comment inline Comment for change $change-number added. See ${its.formatLink($changeUrl)}
 ```
 
 `formatLink( url, caption )`
@@ -575,9 +581,44 @@
 	```
 [rule "formatLinkSampleRule"]
   event-type = comment-added
-  action = add-velocity-comment inline Comment for change ${its.formatLink($change-url, $change-number)} added.
+  action = add-velocity-comment inline Comment for change ${its.formatLink($changeUrl, $changeNumber)} added.
 ```
 
+[action-add-soy-comment]: #action-add-soy-comment
+### <a name="action-add-soy-comment">Action: add-soy-comment</a>
+
+The `add-soy-comment` action renders a Closure template (soy) for the
+event and adds the output as comment to any associated issue.
+
+So for example
+
+```
+  action = add-soy-comment TemplateName
+```
+
+would render the template `etc/its/templates/TemplateName.soy` add the
+output as comment to associated issues.
+
+example for what the soy template will look like (note @param is required with correct variables.)
+
+
+```
+{namespace etc.its.templates}
+
+/**
+ * @param changeNumber
+ * @param formatChangeUrl
+ */
+{template .TemplateName autoescape="strict" kind="text"}
+  inline Comment for change {$changeNumber} added. See {$formatChangeUrl}
+{/template}
+```
+
+Any [property][event-properties] of the event may be used from
+templates. So for example `$subject` in the above example refers to
+the event's subject property, and `$changeNumber` would refer to the
+change's number.
+
 [action-log-event]: #action-log-event
 ### <a name="action-log-event">Action: log-event</a>
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyAttributeExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyAttributeExtractorTest.java
index 9e444f2..41f8921 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyAttributeExtractorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyAttributeExtractorTest.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2017 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.
@@ -27,6 +27,7 @@
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
 import com.googlesource.gerrit.plugins.its.base.testutil.LoggingMockingTestCase;
 import com.googlesource.gerrit.plugins.its.base.workflow.Property;
 
@@ -35,6 +36,7 @@
 public class PropertyAttributeExtractorTest extends LoggingMockingTestCase {
   private Injector injector;
 
+  private ItsFacade facade;
   private Property.Factory propertyFactory;
 
   public void testAccountAttributeNull() {
@@ -56,16 +58,30 @@
     accountAttribute.name = "testName";
     accountAttribute.username = "testUsername";
 
-    Property propertyEmail= createMock(Property.class);
+    // deprecated, to be removed soon. migrate to ones without dash.
+    Property propertyEmail2 = createMock(Property.class);
     expect(propertyFactory.create("prefix-email", "testEmail"))
+        .andReturn(propertyEmail2);
+
+    Property propertyName2 = createMock(Property.class);
+    expect(propertyFactory.create("prefix-name", "testName"))
+        .andReturn(propertyName2);
+
+    Property propertyUsername2 = createMock(Property.class);
+    expect(propertyFactory.create("prefix-username", "testUsername"))
+        .andReturn(propertyUsername2);
+
+    // New style configs for vm and soy
+    Property propertyEmail= createMock(Property.class);
+    expect(propertyFactory.create("prefixEmail", "testEmail"))
         .andReturn(propertyEmail);
 
     Property propertyName = createMock(Property.class);
-    expect(propertyFactory.create("prefix-name", "testName"))
+    expect(propertyFactory.create("prefixName", "testName"))
         .andReturn(propertyName);
 
     Property propertyUsername = createMock(Property.class);
-    expect(propertyFactory.create("prefix-username", "testUsername"))
+    expect(propertyFactory.create("prefixUsername", "testUsername"))
         .andReturn(propertyUsername);
 
     replayMocks();
@@ -79,6 +95,9 @@
     expected.add(propertyEmail);
     expected.add(propertyName);
     expected.add(propertyUsername);
+    expected.add(propertyEmail2);
+    expected.add(propertyName2);
+    expected.add(propertyUsername2);
     assertEquals("Properties do not match", expected, actual);
   }
 
@@ -115,38 +134,72 @@
     expect(propertyFactory.create("subject", "testSubject"))
         .andReturn(propertySubject);
 
-    Property propertyId = createMock(Property.class);
+    Property propertyId2 = createMock(Property.class);
     expect(propertyFactory.create("change-id", "testId"))
+        .andReturn(propertyId2);
+
+    Property propertyId = createMock(Property.class);
+    expect(propertyFactory.create("changeId", "testId"))
         .andReturn(propertyId);
 
-    Property propertyNumber = createMock(Property.class);
+    Property propertyNumber2 = createMock(Property.class);
     expect(propertyFactory.create("change-number", "4711"))
+        .andReturn(propertyNumber2);
+
+    Property propertyNumber = createMock(Property.class);
+    expect(propertyFactory.create("changeNumber", "4711"))
         .andReturn(propertyNumber);
 
-    Property propertyUrl = createMock(Property.class);
+    Property propertyUrl2 = createMock(Property.class);
     expect(propertyFactory.create("change-url", "http://www.example.org/test"))
+        .andReturn(propertyUrl2);
+
+    Property propertyUrl = createMock(Property.class);
+    expect(propertyFactory.create("changeUrl", "http://www.example.org/test"))
         .andReturn(propertyUrl);
 
     Property propertyStatus = createMock(Property.class);
     expect(propertyFactory.create("status", null))
         .andReturn(propertyStatus);
 
-    Property propertyEmail= createMock(Property.class);
-    expect(propertyFactory.create("owner-email", "testEmail"))
+    Property propertyEmail = createMock(Property.class);
+    expect(propertyFactory.create("ownerEmail", "testEmail"))
         .andReturn(propertyEmail);
 
     Property propertyName = createMock(Property.class);
-    expect(propertyFactory.create("owner-name", "testName"))
+    expect(propertyFactory.create("ownerName", "testName"))
         .andReturn(propertyName);
 
     Property propertyUsername = createMock(Property.class);
-    expect(propertyFactory.create("owner-username", "testUsername"))
+    expect(propertyFactory.create("ownerUsername", "testUsername"))
         .andReturn(propertyUsername);
 
     Property propertyCommitMessage = createMock(Property.class);
-    expect(propertyFactory.create("commit-message", "Commit Message"))
+    expect(propertyFactory.create("commitMessage", "Commit Message"))
         .andReturn(propertyCommitMessage);
 
+    Property propertyEmail2 = createMock(Property.class);
+    expect(propertyFactory.create("owner-email", "testEmail"))
+        .andReturn(propertyEmail2);
+
+    Property propertyName2 = createMock(Property.class);
+    expect(propertyFactory.create("owner-name", "testName"))
+        .andReturn(propertyName2);
+
+    Property propertyUsername2 = createMock(Property.class);
+    expect(propertyFactory.create("owner-username", "testUsername"))
+        .andReturn(propertyUsername2);
+
+    Property propertyCommitMessage2 = createMock(Property.class);
+    expect(propertyFactory.create("commit-message", "Commit Message"))
+        .andReturn(propertyCommitMessage2);
+
+    Property propertyFormatChangeUrl = createMock(Property.class);
+    expect(propertyFactory.create("formatChangeUrl", "http://www.example.org/test"))
+        .andReturn(propertyFormatChangeUrl);
+
+    expect(facade.createLinkForWebui("http://www.example.org/test", "http://www.example.org/test"))
+        .andReturn("http://www.example.org/test");
 
     replayMocks();
 
@@ -161,13 +214,21 @@
     expected.add(propertyTopic);
     expected.add(propertySubject);
     expected.add(propertyId);
+    expected.add(propertyId2);
     expected.add(propertyNumber);
+    expected.add(propertyNumber2);
     expected.add(propertyUrl);
+    expected.add(propertyUrl2);
     expected.add(propertyStatus);
     expected.add(propertyEmail);
     expected.add(propertyName);
     expected.add(propertyUsername);
     expected.add(propertyCommitMessage);
+    expected.add(propertyEmail2);
+    expected.add(propertyName2);
+    expected.add(propertyUsername2);
+    expected.add(propertyCommitMessage2);
+    expected.add(propertyFormatChangeUrl);
     assertEquals("Properties do not match", expected, actual);
   }
 
@@ -206,37 +267,72 @@
         .andReturn(propertySubject);
 
     Property propertyId = createMock(Property.class);
-    expect(propertyFactory.create("change-id", "testId"))
+    expect(propertyFactory.create("changeId", "testId"))
         .andReturn(propertyId);
 
     Property propertyNumber = createMock(Property.class);
-    expect(propertyFactory.create("change-number", "4711"))
+    expect(propertyFactory.create("changeNumber", "4711"))
         .andReturn(propertyNumber);
 
     Property propertyUrl = createMock(Property.class);
-    expect(propertyFactory.create("change-url", "http://www.example.org/test"))
+    expect(propertyFactory.create("changeUrl", "http://www.example.org/test"))
         .andReturn(propertyUrl);
 
+    Property propertyId2 = createMock(Property.class);
+    expect(propertyFactory.create("change-id", "testId"))
+        .andReturn(propertyId2);
+
+    Property propertyNumber2 = createMock(Property.class);
+    expect(propertyFactory.create("change-number", "4711"))
+        .andReturn(propertyNumber2);
+
+    Property propertyUrl2 = createMock(Property.class);
+    expect(propertyFactory.create("change-url", "http://www.example.org/test"))
+        .andReturn(propertyUrl2);
+
     Property propertyStatus = createMock(Property.class);
     expect(propertyFactory.create("status", "ABANDONED"))
         .andReturn(propertyStatus);
 
     Property propertyEmail= createMock(Property.class);
-    expect(propertyFactory.create("owner-email", "testEmail"))
+    expect(propertyFactory.create("ownerEmail", "testEmail"))
         .andReturn(propertyEmail);
 
     Property propertyName = createMock(Property.class);
-    expect(propertyFactory.create("owner-name", "testName"))
+    expect(propertyFactory.create("ownerName", "testName"))
         .andReturn(propertyName);
 
     Property propertyUsername = createMock(Property.class);
-    expect(propertyFactory.create("owner-username", "testUsername"))
+    expect(propertyFactory.create("ownerUsername", "testUsername"))
         .andReturn(propertyUsername);
 
     Property propertyCommitMessage = createMock(Property.class);
-    expect(propertyFactory.create("commit-message", "Commit Message"))
+    expect(propertyFactory.create("commitMessage", "Commit Message"))
         .andReturn(propertyCommitMessage);
 
+    Property propertyEmail2= createMock(Property.class);
+    expect(propertyFactory.create("owner-email", "testEmail"))
+        .andReturn(propertyEmail2);
+
+    Property propertyName2 = createMock(Property.class);
+    expect(propertyFactory.create("owner-name", "testName"))
+        .andReturn(propertyName2);
+
+    Property propertyUsername2 = createMock(Property.class);
+    expect(propertyFactory.create("owner-username", "testUsername"))
+        .andReturn(propertyUsername2);
+
+    Property propertyCommitMessage2 = createMock(Property.class);
+    expect(propertyFactory.create("commit-message", "Commit Message"))
+        .andReturn(propertyCommitMessage2);
+
+    Property propertyFormatChangeUrl = createMock(Property.class);
+    expect(propertyFactory.create("formatChangeUrl", "http://www.example.org/test"))
+        .andReturn(propertyFormatChangeUrl);
+
+    expect(facade.createLinkForWebui("http://www.example.org/test", "http://www.example.org/test"))
+        .andReturn("http://www.example.org/test");
+
 
     replayMocks();
 
@@ -253,11 +349,19 @@
     expected.add(propertyId);
     expected.add(propertyNumber);
     expected.add(propertyUrl);
+    expected.add(propertyId2);
+    expected.add(propertyNumber2);
+    expected.add(propertyUrl2);
     expected.add(propertyStatus);
     expected.add(propertyEmail);
     expected.add(propertyName);
     expected.add(propertyUsername);
     expected.add(propertyCommitMessage);
+    expected.add(propertyEmail2);
+    expected.add(propertyName2);
+    expected.add(propertyUsername2);
+    expected.add(propertyCommitMessage2);
+    expected.add(propertyFormatChangeUrl);
     assertEquals("Properties do not match", expected, actual);
   }
 
@@ -289,17 +393,25 @@
         .andReturn(propertyRevision);
 
     Property propertyNumber = createMock(Property.class);
-    expect(propertyFactory.create("patch-set-number", "42"))
+    expect(propertyFactory.create("patchSetNumber", "42"))
         .andReturn(propertyNumber);
 
+    Property propertyNumber2 = createMock(Property.class);
+    expect(propertyFactory.create("patch-set-number", "42"))
+        .andReturn(propertyNumber2);
+
     Property propertyRef = createMock(Property.class);
     expect(propertyFactory.create("ref", "testRef"))
         .andReturn(propertyRef);
 
     Property propertyCreatedOn = createMock(Property.class);
-    expect(propertyFactory.create("created-on", "1234567890"))
+    expect(propertyFactory.create("createdOn", "1234567890"))
         .andReturn(propertyCreatedOn);
 
+    Property propertyCreatedOn2 = createMock(Property.class);
+    expect(propertyFactory.create("created-on", "1234567890"))
+        .andReturn(propertyCreatedOn2);
+
     Property propertyParents = createMock(Property.class);
     expect(propertyFactory.create("parents", "[parent1, parent2]"))
         .andReturn(propertyParents);
@@ -312,29 +424,54 @@
     expect(propertyFactory.create("insertions", "12"))
         .andReturn(propertyInsertions);
 
-    Property propertyEmail1 = createMock(Property.class);
+    Property propertyUploaderEmail = createMock(Property.class);
+    expect(propertyFactory.create("uploaderEmail", "testEmail1"))
+        .andReturn(propertyUploaderEmail);
+
+    Property propertyUploaderName = createMock(Property.class);
+    expect(propertyFactory.create("uploaderName", "testName1"))
+        .andReturn(propertyUploaderName);
+
+    Property propertyUploaderUsername = createMock(Property.class);
+    expect(propertyFactory.create("uploaderUsername", "testUsername1"))
+        .andReturn(propertyUploaderUsername);
+
+    Property propertyUploaderEmail2 = createMock(Property.class);
     expect(propertyFactory.create("uploader-email", "testEmail1"))
-        .andReturn(propertyEmail1);
+        .andReturn(propertyUploaderEmail2);
 
-    Property propertyName1 = createMock(Property.class);
+    Property propertyUploaderName2 = createMock(Property.class);
     expect(propertyFactory.create("uploader-name", "testName1"))
-        .andReturn(propertyName1);
+        .andReturn(propertyUploaderName2);
 
-    Property propertyUsername1 = createMock(Property.class);
+    Property propertyUploaderUsername2 = createMock(Property.class);
     expect(propertyFactory.create("uploader-username", "testUsername1"))
-        .andReturn(propertyUsername1);
+        .andReturn(propertyUploaderUsername2);
 
-    Property propertyEmail2 = createMock(Property.class);
+    Property propertyAuthorEmail = createMock(Property.class);
+    expect(propertyFactory.create("authorEmail", "testEmail2"))
+        .andReturn(propertyAuthorEmail);
+
+    Property propertyAuthorName = createMock(Property.class);
+    expect(propertyFactory.create("authorName", "testName2"))
+        .andReturn(propertyAuthorName);
+
+    Property propertyAuthorUsername = createMock(Property.class);
+    expect(propertyFactory.create("authorUsername", "testUsername2"))
+        .andReturn(propertyAuthorUsername);
+
+    Property propertyAuthorEmail2 = createMock(Property.class);
     expect(propertyFactory.create("author-email", "testEmail2"))
-        .andReturn(propertyEmail2);
+        .andReturn(propertyAuthorEmail2);
 
-    Property propertyName2 = createMock(Property.class);
+    Property propertyAuthorName2 = createMock(Property.class);
     expect(propertyFactory.create("author-name", "testName2"))
-        .andReturn(propertyName2);
+        .andReturn(propertyAuthorName2);
 
-    Property propertyUsername2 = createMock(Property.class);
+    Property propertyAuthorUsername2 = createMock(Property.class);
     expect(propertyFactory.create("author-username", "testUsername2"))
-        .andReturn(propertyUsername2);
+        .andReturn(propertyAuthorUsername2);
+
 
     replayMocks();
 
@@ -346,17 +483,25 @@
     Set<Property> expected = Sets.newHashSet();
     expected.add(propertyRevision);
     expected.add(propertyNumber);
+    expected.add(propertyNumber2);
     expected.add(propertyRef);
     expected.add(propertyCreatedOn);
+    expected.add(propertyCreatedOn2);
     expected.add(propertyParents);
     expected.add(propertyDeletions);
     expected.add(propertyInsertions);
-    expected.add(propertyEmail1);
-    expected.add(propertyName1);
-    expected.add(propertyUsername1);
-    expected.add(propertyEmail2);
-    expected.add(propertyName2);
-    expected.add(propertyUsername2);
+    expected.add(propertyUploaderEmail);
+    expected.add(propertyUploaderName);
+    expected.add(propertyUploaderUsername);
+    expected.add(propertyUploaderEmail2);
+    expected.add(propertyUploaderName2);
+    expected.add(propertyUploaderUsername2);
+    expected.add(propertyAuthorEmail);
+    expected.add(propertyAuthorName);
+    expected.add(propertyAuthorUsername);
+    expected.add(propertyAuthorEmail2);
+    expected.add(propertyAuthorName2);
+    expected.add(propertyAuthorUsername2);
     assertEquals("Properties do not match", expected, actual);
   }
 
@@ -373,10 +518,15 @@
         .andReturn(propertyRevision);
 
     Property propertyRevisionOld = createMock(Property.class);
-    expect(propertyFactory.create("revision-old",
+    expect(propertyFactory.create("revisionOld",
         "9876543211987654321298765432139876543214"))
         .andReturn(propertyRevisionOld);
 
+    Property propertyRevisionOld2 = createMock(Property.class);
+    expect(propertyFactory.create("revision-old",
+        "9876543211987654321298765432139876543214"))
+        .andReturn(propertyRevisionOld2);
+
     Property propertyProject = createMock(Property.class);
     expect(propertyFactory.create("project", "testProject"))
         .andReturn(propertyProject);
@@ -395,6 +545,7 @@
     Set<Property> expected = Sets.newHashSet();
     expected.add(propertyRevision);
     expected.add(propertyRevisionOld);
+    expected.add(propertyRevisionOld2);
     expected.add(propertyProject);
     expected.add(propertyRef);
     assertEquals("Properties do not match", expected, actual);
@@ -406,9 +557,14 @@
     approvalAttribute.value = "TestValue";
 
     Property propertyApproval = createMock(Property.class);
-    expect(propertyFactory.create("approval-TestType", "TestValue"))
+    expect(propertyFactory.create("approvalTestType", "TestValue"))
         .andReturn(propertyApproval);
 
+    Property propertyApproval2 = createMock(Property.class);
+    expect(propertyFactory.create("approval-TestType", "TestValue"))
+        .andReturn(propertyApproval2);
+
+
     replayMocks();
 
     PropertyAttributeExtractor extractor =
@@ -418,6 +574,34 @@
 
     Set<Property> expected = Sets.newHashSet();
     expected.add(propertyApproval);
+    expected.add(propertyApproval2);
+    assertEquals("Properties do not match", expected, actual);
+  }
+
+  public void testApprovalAttributeWithDash() {
+    ApprovalAttribute approvalAttribute = new ApprovalAttribute();
+    approvalAttribute.type = "Test-Type";
+    approvalAttribute.value = "TestValue";
+
+    Property propertyApproval = createMock(Property.class);
+    expect(propertyFactory.create("approvalTestType", "TestValue"))
+        .andReturn(propertyApproval);
+
+    Property propertyApproval2 = createMock(Property.class);
+    expect(propertyFactory.create("approval-Test-Type", "TestValue"))
+        .andReturn(propertyApproval2);
+
+
+    replayMocks();
+
+    PropertyAttributeExtractor extractor =
+        injector.getInstance(PropertyAttributeExtractor.class);
+
+    Set<Property> actual = extractor.extractFrom(approvalAttribute);
+
+    Set<Property> expected = Sets.newHashSet();
+    expected.add(propertyApproval);
+    expected.add(propertyApproval2);
     assertEquals("Properties do not match", expected, actual);
   }
 
@@ -430,8 +614,10 @@
   private class TestModule extends FactoryModule {
     @Override
     protected void configure() {
+      facade = createMock(ItsFacade.class);
+      bind(ItsFacade.class).toInstance(facade);
       propertyFactory = createMock(Property.Factory.class);
       bind(Property.Factory.class).toInstance(propertyFactory);
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
index 2610b22..f3f5916 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2017 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.
@@ -24,6 +24,7 @@
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
 import com.googlesource.gerrit.plugins.its.base.testutil.LoggingMockingTestCase;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddComment;
+import com.googlesource.gerrit.plugins.its.base.workflow.action.AddSoyComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddStandardComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.AddVelocityComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.action.LogEvent;
@@ -39,6 +40,7 @@
   private AddComment.Factory addCommentFactory;
   private AddStandardComment.Factory addStandardCommentFactory;
   private AddVelocityComment.Factory addVelocityCommentFactory;
+  private AddSoyComment.Factory addSoyCommentFactory;
   private LogEvent.Factory logEventFactory;
 
   public void testExecuteItem() throws IOException {
@@ -143,6 +145,24 @@
     actionExecutor.execute("4711", actionRequest, properties);
   }
 
+  public void testAddSoyCommentDelegation() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getName()).andReturn("add-soy-comment");
+
+    Set<Property> properties = Collections.emptySet();
+
+    AddSoyComment addSoyComment =
+        createMock(AddSoyComment.class);
+    expect(addSoyCommentFactory.create()).andReturn(addSoyComment);
+
+    addSoyComment.execute("4711", actionRequest, properties);
+
+    replayMocks();
+
+    ActionExecutor actionExecutor = createActionExecutor();
+    actionExecutor.execute("4711", actionRequest, properties);
+  }
+
   public void testAddStandardCommentDelegation() throws IOException {
     ActionRequest actionRequest = createMock(ActionRequest.class);
     expect(actionRequest.getName()).andReturn("add-standard-comment");
@@ -215,6 +235,10 @@
       addCommentFactory = createMock(AddComment.Factory.class);
       bind(AddComment.Factory.class).toInstance(addCommentFactory);
 
+      addSoyCommentFactory = createMock(AddSoyComment.Factory.class);
+      bind(AddSoyComment.Factory.class).toInstance(
+          addSoyCommentFactory);
+
       addStandardCommentFactory = createMock(AddStandardComment.Factory.class);
       bind(AddStandardComment.Factory.class).toInstance(
           addStandardCommentFactory);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddStandardCommentTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddStandardCommentTest.java
index af013b8..df42702 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddStandardCommentTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddStandardCommentTest.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2017 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.
@@ -66,19 +66,19 @@
     properties.add(propertySubject);
 
     Property propertyChangeNumber = createMock(Property.class);
-    expect(propertyChangeNumber.getKey()).andReturn("change-number")
+    expect(propertyChangeNumber.getKey()).andReturn("changeNumber")
         .anyTimes();
     expect(propertyChangeNumber.getValue()).andReturn("4711").anyTimes();
     properties.add(propertyChangeNumber);
 
     Property propertySubmitterName = createMock(Property.class);
-    expect(propertySubmitterName.getKey()).andReturn("submitter-name")
+    expect(propertySubmitterName.getKey()).andReturn("submitterName")
         .anyTimes();
     expect(propertySubmitterName.getValue()).andReturn("John Doe").anyTimes();
     properties.add(propertySubmitterName);
 
     Property propertyChangeUrl= createMock(Property.class);
-    expect(propertyChangeUrl.getKey()).andReturn("change-url").anyTimes();
+    expect(propertyChangeUrl.getKey()).andReturn("changeUrl").anyTimes();
     expect(propertyChangeUrl.getValue()).andReturn("http://example.org/change")
         .anyTimes();
     properties.add(propertyChangeUrl);
@@ -134,19 +134,19 @@
     properties.add(propertySubject);
 
     Property propertyChangeNumber = createMock(Property.class);
-    expect(propertyChangeNumber.getKey()).andReturn("change-number")
+    expect(propertyChangeNumber.getKey()).andReturn("changeNumber")
         .anyTimes();
     expect(propertyChangeNumber.getValue()).andReturn("4711").anyTimes();
     properties.add(propertyChangeNumber);
 
     Property propertySubmitterName = createMock(Property.class);
-    expect(propertySubmitterName.getKey()).andReturn("abandoner-name")
+    expect(propertySubmitterName.getKey()).andReturn("abandonerName")
         .anyTimes();
     expect(propertySubmitterName.getValue()).andReturn("John Doe").anyTimes();
     properties.add(propertySubmitterName);
 
     Property propertyChangeUrl= createMock(Property.class);
-    expect(propertyChangeUrl.getKey()).andReturn("change-url").anyTimes();
+    expect(propertyChangeUrl.getKey()).andReturn("changeUrl").anyTimes();
     expect(propertyChangeUrl.getValue()).andReturn("http://example.org/change")
         .anyTimes();
     properties.add(propertyChangeUrl);
@@ -205,19 +205,19 @@
     properties.add(propertySubject);
 
     Property propertyChangeNumber = createMock(Property.class);
-    expect(propertyChangeNumber.getKey()).andReturn("change-number")
+    expect(propertyChangeNumber.getKey()).andReturn("changeNumber")
         .anyTimes();
     expect(propertyChangeNumber.getValue()).andReturn("4711").anyTimes();
     properties.add(propertyChangeNumber);
 
     Property propertySubmitterName = createMock(Property.class);
-    expect(propertySubmitterName.getKey()).andReturn("restorer-name")
+    expect(propertySubmitterName.getKey()).andReturn("restorerName")
         .anyTimes();
     expect(propertySubmitterName.getValue()).andReturn("John Doe").anyTimes();
     properties.add(propertySubmitterName);
 
     Property propertyChangeUrl= createMock(Property.class);
-    expect(propertyChangeUrl.getKey()).andReturn("change-url").anyTimes();
+    expect(propertyChangeUrl.getKey()).andReturn("changeUrl").anyTimes();
     expect(propertyChangeUrl.getValue()).andReturn("http://example.org/change")
         .anyTimes();
     properties.add(propertyChangeUrl);
@@ -271,19 +271,19 @@
     properties.add(propertySubject);
 
     Property propertyChangeNumber = createMock(Property.class);
-    expect(propertyChangeNumber.getKey()).andReturn("change-number")
+    expect(propertyChangeNumber.getKey()).andReturn("changeNumber")
         .anyTimes();
     expect(propertyChangeNumber.getValue()).andReturn("4711").anyTimes();
     properties.add(propertyChangeNumber);
 
     Property propertySubmitterName = createMock(Property.class);
-    expect(propertySubmitterName.getKey()).andReturn("uploader-name")
+    expect(propertySubmitterName.getKey()).andReturn("uploaderName")
         .anyTimes();
     expect(propertySubmitterName.getValue()).andReturn("John Doe").anyTimes();
     properties.add(propertySubmitterName);
 
     Property propertyChangeUrl= createMock(Property.class);
-    expect(propertyChangeUrl.getKey()).andReturn("change-url").anyTimes();
+    expect(propertyChangeUrl.getKey()).andReturn("changeUrl").anyTimes();
     expect(propertyChangeUrl.getValue()).andReturn("http://example.org/change")
         .anyTimes();
     properties.add(propertyChangeUrl);