Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Add support for closure template (soy)
  Update bazlets to latest revision on stable-2.14

Change-Id: I407fb908fb215c40dd16935c5c5d1759b45dff85
diff --git a/BUILD b/BUILD
index 3852f4f..92bbdac 100644
--- a/BUILD
+++ b/BUILD
@@ -10,6 +10,7 @@
     name = "its-base",
     srcs = glob(["src/main/java/**/*.java"]),
     resources = glob(["src/main/resources/**/*"]),
+    deps = PLUGIN_DEPS,
 )
 
 TEST_UTIL_SRC = glob(["src/test/java/com/googlesource/gerrit/plugins/its/base/testutil/**/*.java"])
diff --git a/WORKSPACE b/WORKSPACE
index 9ec1b44..b130797 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -23,4 +23,4 @@
 gerrit_api()
 
 # Load snapshot Plugin API
-#gerrit_api_maven_local()
+gerrit_api_maven_local()
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..16f2de5 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.
     ```
 
@@ -175,61 +175,61 @@
 values are:
 
 `somewhere`
-:	issue id occurs somewhere in the commit message of the change/the
-	most recent patch set.
+:   issue id occurs somewhere in the commit message of the change/the
+    most recent patch set.
 
 `subject`
-:	issue id occurs in the first line of the commit message of the
-	change/the most recent patch set.
+:   issue id occurs in the first line of the commit message of the
+    change/the most recent patch set.
 
 `body`
-:	issue id occurs after the subject but before the footer of the
-	commit message of the change/the most recent patch set.
+:   issue id occurs after the subject but before the footer of the
+    commit message of the change/the most recent patch set.
 
 `footer`
-:	issue id occurs in the last paragraph after the subject of the
-	commit message of the change/the most recent patch set
+:   issue id occurs in the last paragraph after the subject of the
+    commit message of the change/the most recent patch set
 
 `footer-<Key>`
-:	issue id occurs in the footer of the commit message of the
-	change/the most recent patch set, and is in a line with a key
-	(part before the colon).
+:   issue id occurs in the footer of the commit message of the
+    change/the most recent patch set, and is in a line with a key
+    (part before the colon).
 
-	So for example, if the footer would contain a line
+    So for example, if the footer would contain a line
 
-	```
+    ```
 Fixes-Issue: issue 4711
 ```
 
-	then a property `association` with value `footer-Fixes-Issue`
-	would get added to the event for issue “4711”.
+    then a property `association` with value `footer-Fixes-Issue`
+    would get added to the event for issue “4711”.
 
 `added@<Association-Value>`
-:	(only for events that allow to determine the patch set number.
-	So for example, this `association` property is not set for
-	RevUpdatedEvents)
+:   (only for events that allow to determine the patch set number.
+    So for example, this `association` property is not set for
+    RevUpdatedEvents)
 
-	issue id occurs at `<Association-Value>` in the most recent
-	patch set of the change, and either the event is for patch set
-	1 or the issue id does not occur at `<Association-Value>` in
-	the previous patch set.
+    issue id occurs at `<Association-Value>` in the most recent
+    patch set of the change, and either the event is for patch set
+    1 or the issue id does not occur at `<Association-Value>` in
+    the previous patch set.
 
-	So for example if issue “4711” occurs in the subject of patch
-	set 3 (the most recent patch set) of a change, but not in
-	patch set 2.  When adding a comment to this change, the event
-	for issue “4711” would get a property 'association' with value
-	`added@subject`.
+    So for example if issue “4711” occurs in the subject of patch
+    set 3 (the most recent patch set) of a change, but not in
+    patch set 2.  When adding a comment to this change, the event
+    for issue “4711” would get a property 'association' with value
+    `added@subject`.
 
 [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,37 +553,72 @@
 
 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
 allows to format links using the its' syntax:
 
 `formatLink( url )`
-:	Formats a link to a url.
+:   Formats a link to a url.
 
-	So for example upon adding a comment to a change, the
-	following rule formats a link to the change:
+    So for example upon adding a comment to a change, the
+    following rule formats a link to the change:
 
-	```
+    ```
 [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 )`
-:	Formats a link to a url using 'caption' to represent the url.
+:   Formats a link to a url using 'caption' to represent the url.
 
-	So for example upon adding a comment to a change, the following rule
-	formats a link to the change using the change number as link
-	capition:
+    So for example upon adding a comment to a change, the following rule
+    formats a link to the change using the change number as link
+    capition:
 
-	```
+    ```
 [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);