Merge branch 'stable-2.15'

* stable-2.15:
  Update bazlets to latest stable-2.15 to use 2.15rc3

Change-Id: I1a026ec2be41f470164090139ffba41d0c8ac5c5
diff --git a/.gitignore b/.gitignore
index dc4fc0a..cabf34f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@
 .project
 /.settings
 /bazel-*
+.DS_Store
+/eclipse-out
diff --git a/WORKSPACE b/WORKSPACE
index 3629f3c..5591045 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,24 +3,24 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "34d5ef186f73c80e61dcfa057ee400b2725164bf",
+    commit = "42bffc66c0e92753133e4cea2debe65abc359c4d",
     # local_path = "/home/<user>/projects/bazlets",
 )
 
 # Snapshot Plugin API
-#load(
-#    "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
-#    "gerrit_api_maven_local",
-#)
-
-# Release Plugin API
 load(
-    "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
-    "gerrit_api",
+    "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+    "gerrit_api_maven_local",
 )
 
+# Release Plugin API
+#load(
+#    "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+#    "gerrit_api",
+#)
+
 # Load release Plugin API
-gerrit_api()
+#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 91b26a0..691a037 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
@@ -14,14 +14,17 @@
 
 package com.googlesource.gerrit.plugins.its.base;
 
-import com.google.gerrit.common.EventListener;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.events.EventListener;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
 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;
@@ -33,11 +36,14 @@
 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.nio.file.Path;
 
 public class ItsHookModule extends FactoryModule {
 
+  /** Folder where rules configuration files are located */
+  private static final String ITS_FOLDER = "its";
+
   private final String pluginName;
   private final PluginConfigFactory pluginCfgFactory;
 
@@ -61,7 +67,13 @@
     factory(AddComment.Factory.class);
     factory(AddSoyComment.Factory.class);
     factory(AddStandardComment.Factory.class);
-    factory(AddVelocityComment.Factory.class);
     factory(LogEvent.Factory.class);
   }
+
+  @Provides
+  @ItsPath
+  @Inject
+  Path itsPath(SitePaths sitePaths) {
+    return sitePaths.etc_dir.normalize().resolve(ITS_FOLDER);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsPath.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsPath.java
new file mode 100644
index 0000000..dfd5119
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsPath.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2018 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;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ItsPath {}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/InitIts.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/InitIts.java
index 1cd2b74..9fb2b3d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/InitIts.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/InitIts.java
@@ -21,7 +21,10 @@
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.EnumSet;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
@@ -41,6 +44,8 @@
     ENFORCED
   }
 
+  @Inject private SitePaths sitePaths;
+
   private final String pluginName;
   private final String itsDisplayName;
   protected final ConsoleUI ui;
@@ -61,7 +66,18 @@
   }
 
   @Override
-  public void run() throws IOException, ConfigInvalidException {}
+  public void run() throws IOException, ConfigInvalidException {
+    Path deprecatedRules = sitePaths.etc_dir.normalize().resolve("its").resolve("action.config");
+    if (deprecatedRules.toFile().exists()) {
+      ui.error(
+          "Deprecated rules file '%s' (No trailing 's' in 'action') will not be read. "
+              + "Please migrate to 'etc/its/actions.config' (Trailing 's' in 'actions') and retry "
+              + "the init step.\n",
+          deprecatedRules);
+      throw new ConfigInvalidException(
+          "Deprecated configuration file found: " + deprecatedRules.toRealPath());
+    }
+  }
 
   @Override
   public void postRun() throws IOException, ConfigInvalidException {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/InvalidTransitionException.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/InvalidTransitionException.java
index 78c6ec5..949031b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/InvalidTransitionException.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/InvalidTransitionException.java
@@ -14,9 +14,7 @@
 
 package com.googlesource.gerrit.plugins.its.base.its;
 
-import java.io.IOException;
-
-public class InvalidTransitionException extends IOException {
+public class InvalidTransitionException extends Exception {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
index 99fe80c..de22f9c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
@@ -28,8 +28,8 @@
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.ChangeRestoredEvent;
 import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -76,29 +76,17 @@
 
   // Plugin enablement --------------------------------------------------------
 
-  public boolean isEnabled(Event event) {
-    if (event instanceof PatchSetCreatedEvent) {
-      PatchSetCreatedEvent e = (PatchSetCreatedEvent) event;
-      return isEnabled(e.getProjectNameKey(), e.getRefName());
-    } else if (event instanceof CommentAddedEvent) {
-      CommentAddedEvent e = (CommentAddedEvent) event;
-      return isEnabled(e.getProjectNameKey(), e.getRefName());
-    } else if (event instanceof ChangeMergedEvent) {
-      ChangeMergedEvent e = (ChangeMergedEvent) event;
-      return isEnabled(e.getProjectNameKey(), e.getRefName());
-    } else if (event instanceof ChangeAbandonedEvent) {
-      ChangeAbandonedEvent e = (ChangeAbandonedEvent) event;
-      return isEnabled(e.getProjectNameKey(), e.getRefName());
-    } else if (event instanceof ChangeRestoredEvent) {
-      ChangeRestoredEvent e = (ChangeRestoredEvent) event;
-      return isEnabled(e.getProjectNameKey(), e.getRefName());
-    } else if (event instanceof RefUpdatedEvent) {
-      RefUpdatedEvent e = (RefUpdatedEvent) event;
-      return isEnabled(e.getProjectNameKey(), e.getRefName());
-    } else {
-      log.debug("Event {} not recognised and ignored", event);
-      return false;
+  public boolean isEnabled(RefEvent event) {
+    if (event instanceof PatchSetCreatedEvent
+        || event instanceof CommentAddedEvent
+        || event instanceof ChangeMergedEvent
+        || event instanceof ChangeAbandonedEvent
+        || event instanceof ChangeRestoredEvent
+        || event instanceof RefUpdatedEvent) {
+      return isEnabled(event.getProjectNameKey(), event.getRefName());
     }
+    log.debug("Event {} not recognised and ignored", event);
+    return false;
   }
 
   public boolean isEnabled(Project.NameKey projectNK, String refName) {
@@ -110,17 +98,8 @@
           projectNK.get());
       return false;
     }
-
-    if (isEnforcedByAnyParentProject(refName, projectState)) {
-      return true;
-    }
-
-    return !"false"
-            .equals(
-                pluginCfgFactory
-                    .getFromProjectConfigWithInheritance(projectState, pluginName)
-                    .getString("enabled", "false"))
-        && isEnabledForBranch(projectState, refName);
+    return isEnforcedByAnyParentProject(refName, projectState)
+        || (isEnabledForProject(projectState) && isEnabledForBranch(projectState, refName));
   }
 
   private boolean isEnforcedByAnyParentProject(String refName, ProjectState projectState) {
@@ -134,6 +113,14 @@
     return false;
   }
 
+  private boolean isEnabledForProject(ProjectState projectState) {
+    return !"false"
+        .equals(
+            pluginCfgFactory
+                .getFromProjectConfigWithInheritance(projectState, pluginName)
+                .getString("enabled", "false"));
+  }
+
   private boolean isEnabledForBranch(ProjectState project, String refName) {
     String[] refPatterns =
         pluginCfgFactory
@@ -215,6 +202,17 @@
   }
 
   /**
+   * Pattern to skip the mandatory check for an issue. Can be used to explicitly bypass the
+   * mandatory issue pattern check for some commits.
+   *
+   * <p>When no pattern is specified, it will return a pattern which never matches.
+   */
+  public Optional<Pattern> getDummyIssuePattern() {
+    return Optional.ofNullable(getPluginConfigString("dummyIssuePattern"))
+        .map(Pattern::compile);
+  }
+
+  /**
    * Gets how necessary it is to associate commits with issues
    *
    * @return policy on how necessary association with issues is
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/IssueExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/IssueExtractor.java
index 5c344f1..fd37249 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/IssueExtractor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/IssueExtractor.java
@@ -1,5 +1,7 @@
 package com.googlesource.gerrit.plugins.its.base.util;
 
+import static java.util.Arrays.copyOfRange;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
@@ -17,7 +19,6 @@
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -156,13 +157,11 @@
       if (footerStart == -1) {
         // No footer could be found. So all lines after the first one (that's
         // the subject) is the body.
-        //body = String[] templateParameters =
-        //  Arrays.copyOfRange(allParameters, 1, allParameters.length);
         if (lines.length > 0) {
-          body = StringUtils.join(lines, "\n", 1, lines.length);
+          body = String.join("\n", copyOfRange(lines, 1, lines.length));
         }
       } else {
-        body = StringUtils.join(lines, "\n", 1, footerStart - 1);
+        body = String.join("\n", copyOfRange(lines, 1, footerStart - 1));
 
         StringBuilder footerBuilder = new StringBuilder();
         for (int lineIdx = footerStart; lineIdx < footerEnd; lineIdx++) {
@@ -182,7 +181,7 @@
           }
           footerBuilder.append(line);
         }
-        footer = StringUtils.join(lines, "\n", footerStart, footerEnd);
+        footer = String.join("\n", copyOfRange(lines, footerStart, footerEnd));
       }
       if (body != null) {
         addIssuesOccurrence(body, "body", ret);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
index b04bcb0..6cc93da 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
@@ -27,9 +27,9 @@
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.ChangeRestoredEvent;
 import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.PatchSetEvent;
+import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.its.base.workflow.Property;
@@ -169,7 +169,7 @@
    * @param event The event to extract property sets from.
    * @return sets of property sets extracted from the event.
    */
-  public Set<Set<Property>> extractFrom(Event event) {
+  public Set<Set<Property>> extractFrom(RefEvent event) {
     Map<String, Set<String>> associations = null;
     Set<Set<Property>> ret = Sets.newHashSet();
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/validation/ItsValidateComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/validation/ItsValidateComment.java
index 969eeb0..d45d0a6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/validation/ItsValidateComment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/validation/ItsValidateComment.java
@@ -92,7 +92,10 @@
 
             ret.add(commitValidationFailure(synopsis, details));
           }
-        } else {
+        } else if (!itsConfig
+            .getDummyIssuePattern()
+            .map(p -> p.matcher(commitMessage).find())
+            .orElse(false)) {
           synopsis = "Missing issue-id in commit message";
 
           StringBuilder sb = new StringBuilder();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionController.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionController.java
index b1c0982..844ac91 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionController.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionController.java
@@ -14,8 +14,9 @@
 
 package com.googlesource.gerrit.plugins.its.base.workflow;
 
-import com.google.gerrit.common.EventListener;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventListener;
+import com.google.gerrit.server.events.RefEvent;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.its.base.its.ItsConfig;
 import com.googlesource.gerrit.plugins.its.base.util.PropertyExtractor;
@@ -48,11 +49,16 @@
 
   @Override
   public void onEvent(Event event) {
-    if (!itsConfig.isEnabled(event)) {
-      return;
+    if (event instanceof RefEvent) {
+      RefEvent refEvent = (RefEvent) event;
+      if (itsConfig.isEnabled(refEvent)) {
+        handleEvent(refEvent);
+      }
     }
+  }
 
-    Set<Set<Property>> propertiesCollections = propertyExtractor.extractFrom(event);
+  private void handleEvent(RefEvent refEvent) {
+    Set<Set<Property>> propertiesCollections = propertyExtractor.extractFrom(refEvent);
     for (Set<Property> properties : propertiesCollections) {
       Collection<ActionRequest> actions = ruleBase.actionRequestsFor(properties);
       if (!actions.isEmpty()) {
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 0e31b93..fdf1e8e 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
@@ -20,7 +20,6 @@
 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;
@@ -34,7 +33,6 @@
   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;
 
@@ -43,33 +41,33 @@
       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;
   }
 
+  private Action getAction(String actionName) {
+    switch (actionName) {
+      case "add-comment":
+        return addCommentFactory.create();
+      case "add-standard-comment":
+        return addStandardCommentFactory.create();
+      case "add-soy-comment":
+        return addSoyCommentFactory.create();
+      case "log-event":
+        return logEventFactory.create();
+      default:
+        return null;
+    }
+  }
+
   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();
-      } 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();
-      }
-
+      Action action = getAction(actionRequest.getName());
       if (action == null) {
         its.performAction(issue, actionRequest.getUnparsed());
       } else {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionRequest.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionRequest.java
index dec671e..964c01d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionRequest.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionRequest.java
@@ -49,11 +49,7 @@
    * @return The name of the requested action, if a name has been given. "" otherwise.
    */
   public String getName() {
-    String ret = "";
-    if (chopped.length > 0) {
-      ret = chopped[0];
-    }
-    return ret;
+    return getParameter(0);
   }
 
   /**
@@ -63,11 +59,7 @@
    * @return The name of the requested parameter, if the requested parameter exists. "" otherwise.
    */
   public String getParameter(int i) {
-    String ret = "";
-    if (chopped.length > i) {
-      ret = chopped[i];
-    }
-    return ret;
+    return chopped.length > i ? chopped[i] : "";
   }
 
   /**
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBase.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBase.java
index 41dc9fa..8dd0016 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBase.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBase.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.server.config.SitePath;
 import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.ItsPath;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -28,16 +28,12 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-/** Collection and matcher agains {@link Rule}s. */
+/** Collection and matcher against {@link Rule}s. */
 public class RuleBase {
   private static final Logger log = LoggerFactory.getLogger(RuleBase.class);
 
-  /** File beginning (relative to site) to load rules from */
-  private static final String ITS_CONFIG_FILE_START =
-      "etc" + File.separatorChar + "its" + File.separator + "actions";
-
-  /** File end to load rules from */
-  private static final String ITS_CONFIG_FILE_END = ".config";
+  /** Rules configuration filename pattern */
+  private static final String CONFIG_FILE_NAME = "actions%s.config";
 
   /** The section for rules within rulebases */
   private static final String RULE_SECTION = "rule";
@@ -45,7 +41,7 @@
   /** The key for actions within rulebases */
   private static final String ACTION_KEY = "action";
 
-  private final Path sitePath;
+  private final Path itsPath;
   private final Rule.Factory ruleFactory;
   private final Condition.Factory conditionFactory;
   private final ActionRequest.Factory actionRequestFactory;
@@ -59,12 +55,12 @@
 
   @Inject
   public RuleBase(
-      @SitePath Path sitePath,
+      @ItsPath Path itsPath,
       Rule.Factory ruleFactory,
       Condition.Factory conditionFactory,
       ActionRequest.Factory actionRequestFactory,
       @PluginName String pluginName) {
-    this.sitePath = sitePath;
+    this.itsPath = itsPath;
     this.ruleFactory = ruleFactory;
     this.conditionFactory = conditionFactory;
     this.actionRequestFactory = actionRequestFactory;
@@ -73,7 +69,7 @@
   }
 
   /**
-   * Adds rules from a file to the the RuleBase.
+   * Adds rules from a file to the RuleBase.
    *
    * <p>If the given file does not exist, it is silently ignored
    *
@@ -116,49 +112,20 @@
   private void reloadRules() {
     rules = Lists.newArrayList();
 
-    // Add rules from file with typo in filename
-    //
-    // While the documentation called for "actions.config" (Trailing "s" in
-    // "actions"), the code previously only loaded "action.config" (No
-    // trailing "s" in "action"). To give users time to gracefully migrate to
-    // "actions.config" (with trailing "s", we (for now) load files from both
-    // locations, but consider "actions.config" (with trailing "s" the
-    // canonical place.
-    File faultyNameRuleFile =
-        new File(
-            sitePath.toFile(),
-            "etc" + File.separatorChar + "its" + File.separator + "action.config");
-    if (faultyNameRuleFile.exists()) {
-      log.warn(
-          "Loading rules from deprecated 'etc/its/action.config' (No "
-              + "trailing 's' in 'action'). Please migrate to "
-              + "'etc/its/actions.config' (Trailing 's' in 'actions').");
-      addRulesFromFile(faultyNameRuleFile);
-    }
-
     // Add global rules
-    File globalRuleFile = new File(sitePath.toFile(), ITS_CONFIG_FILE_START + ITS_CONFIG_FILE_END);
+    File globalRuleFile = itsPath.resolve(String.format(CONFIG_FILE_NAME, "")).toFile();
     addRulesFromFile(globalRuleFile);
 
     // Add its-specific rules
     File itsSpecificRuleFile =
-        new File(sitePath.toFile(), ITS_CONFIG_FILE_START + "-" + pluginName + ITS_CONFIG_FILE_END);
+        itsPath.resolve(String.format(CONFIG_FILE_NAME, "-" + pluginName)).toFile();
     addRulesFromFile(itsSpecificRuleFile);
 
     if (!globalRuleFile.exists() && !itsSpecificRuleFile.exists()) {
-      try {
-        log.warn(
-            "Neither global rule file "
-                + globalRuleFile.getCanonicalPath()
-                + " nor Its specific rule file"
-                + itsSpecificRuleFile.getCanonicalPath()
-                + " exist. Please configure "
-                + "rules.");
-      } catch (IOException e) {
-        log.warn(
-            "Neither global rule file nor Its specific rule files exist. "
-                + "Please configure rules.");
-      }
+      log.warn(
+          "Neither global rule file {} nor Its specific rule file {} exist. Please configure rules.",
+          globalRuleFile,
+          itsSpecificRuleFile);
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddComment.java
index 16855d3..51f976c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddComment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddComment.java
@@ -21,7 +21,6 @@
 import com.googlesource.gerrit.plugins.its.base.workflow.Property;
 import java.io.IOException;
 import java.util.Set;
-import org.apache.commons.lang.StringUtils;
 
 /**
  * Adds a fixed comment to an issue.
@@ -43,8 +42,7 @@
   @Override
   public void execute(String issue, ActionRequest actionRequest, Set<Property> properties)
       throws IOException {
-    String[] parameters = actionRequest.getParameters();
-    String comment = StringUtils.join(parameters, " ");
+    String comment = String.join(" ", actionRequest.getParameters());
     if (!Strings.isNullOrEmpty(comment)) {
       its.addComment(issue, comment);
     }
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
index bbad6e9..0ddefd5 100644
--- 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
@@ -16,16 +16,15 @@
 
 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.ItsPath;
 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.nio.charset.StandardCharsets;
@@ -48,17 +47,13 @@
     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;
+  private final Path templateDir;
   protected HashMap<String, Object> soyContext;
 
   @Inject
-  public AddSoyComment(@SitePath Path sitePath, ItsFacade its) {
-    this.sitePath = sitePath;
+  public AddSoyComment(@ItsPath Path itsPath, ItsFacade its) {
+    this.templateDir = itsPath.resolve("templates");
     this.its = its;
   }
 
@@ -82,7 +77,6 @@
       String template,
       SanitizedContent.ContentKind kind,
       Set<Property> properties) {
-    Path templateDir = sitePath.resolve(ITS_TEMPLATE_DIR);
     Path templatePath = templateDir.resolve(template + ".soy");
     String content;
 
@@ -115,17 +109,18 @@
   @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);
+    String comment = buildComment(actionRequest, properties);
+    if (!Strings.isNullOrEmpty(comment)) {
       its.addComment(issue, comment);
     }
   }
+
+  private String buildComment(ActionRequest actionRequest, Set<Property> properties) {
+    String template = actionRequest.getParameter(1);
+    if (!template.isEmpty()) {
+      return soyTextTemplate(SoyFileSet.builder(), template, properties);
+    }
+    log.error("No template name given in {}", actionRequest);
+    return "";
+  }
 }
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 eb8c4a6..4777d4b 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
@@ -41,16 +41,6 @@
     this.its = its;
   }
 
-  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;
-  }
-
   private String getCommentChangeEvent(String action, String prefix, Map<String, String> map) {
     String ret = "";
     String changeNumber = getValueFromMap(map, "", "change-number", "changeNumber");
@@ -87,26 +77,16 @@
     return "";
   }
 
-  private String getCommentChangeAbandoned(Map<String, String> map) {
-    return getCommentChangeEvent("abandoned", "abandoner", map);
-  }
-
-  private String getCommentChangeMerged(Map<String, String> map) {
-    return getCommentChangeEvent("merged", "submitter", map);
-  }
-
-  private String getCommentChangeRestored(Map<String, String> map) {
-    return getCommentChangeEvent("restored", "restorer", map);
-  }
-
-  private String getCommentPatchSetCreated(Map<String, String> map) {
-    return getCommentChangeEvent("had a related patch set uploaded", "uploader", map);
-  }
-
   @Override
   public void execute(String issue, ActionRequest actionRequest, Set<Property> properties)
       throws IOException {
-    String comment = "";
+    String comment = buildComment(properties);
+    if (!Strings.isNullOrEmpty(comment)) {
+      its.addComment(issue, comment);
+    }
+  }
+
+  private String buildComment(Set<Property> properties) {
     Map<String, String> map = Maps.newHashMap();
     for (Property property : properties) {
       String current = property.getValue();
@@ -119,18 +99,17 @@
         map.put(key, old + current);
       }
     }
-    String eventType = map.get("event-type");
-    if ("change-abandoned".equals(eventType)) {
-      comment = getCommentChangeAbandoned(map);
-    } else if ("change-merged".equals(eventType)) {
-      comment = getCommentChangeMerged(map);
-    } else if ("change-restored".equals(eventType)) {
-      comment = getCommentChangeRestored(map);
-    } else if ("patchset-created".equals(eventType)) {
-      comment = getCommentPatchSetCreated(map);
-    }
-    if (!Strings.isNullOrEmpty(comment)) {
-      its.addComment(issue, comment);
+    switch (map.get("event-type")) {
+      case "change-abandoned":
+        return getCommentChangeEvent("abandoned", "abandoner", map);
+      case "change-merged":
+        return getCommentChangeEvent("merged", "submitter", map);
+      case "change-restored":
+        return getCommentChangeEvent("restored", "restorer", map);
+      case "patchset-created":
+        return getCommentChangeEvent("had a related patch set uploaded", "uploader", map);
+      default:
+        return "";
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddVelocityComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddVelocityComment.java
deleted file mode 100644
index 13a6655..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddVelocityComment.java
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright (C) 2013 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.gerrit.server.config.SitePath;
-import com.google.inject.Inject;
-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.StringWriter;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Set;
-import org.apache.commons.lang.StringUtils;
-import org.apache.velocity.VelocityContext;
-import org.apache.velocity.runtime.RuntimeInstance;
-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 AddVelocityComment implements Action {
-  private static final Logger log = LoggerFactory.getLogger(AddVelocityComment.class);
-
-  public interface Factory {
-    AddVelocityComment 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;
-  private final RuntimeInstance velocityRuntime;
-
-  @Inject
-  public AddVelocityComment(
-      RuntimeInstance velocityRuntime, @SitePath Path sitePath, ItsFacade its) {
-    this.velocityRuntime = velocityRuntime;
-    this.sitePath = sitePath;
-    this.its = its;
-  }
-
-  private VelocityContext getVelocityContext(Set<Property> properties) {
-    VelocityContext velocityContext = new VelocityContext();
-    for (Property property : properties) {
-      String key = property.getKey();
-      if (!Strings.isNullOrEmpty(key)) {
-        String value = property.getValue();
-        if (!Strings.isNullOrEmpty(value)) {
-          velocityContext.put(key, value);
-        }
-      }
-    }
-
-    velocityContext.put("its", new VelocityAdapterItsFacade(its));
-
-    return velocityContext;
-  }
-
-  private String velocify(String template, Set<Property> properties) {
-    VelocityContext context = getVelocityContext(properties);
-    StringWriter w = new StringWriter();
-    velocityRuntime.evaluate(context, w, "ItsComment", template);
-    return w.toString();
-  }
-
-  @Override
-  public void execute(String issue, ActionRequest actionRequest, Set<Property> properties)
-      throws IOException {
-    String template = null;
-    String templateName = actionRequest.getParameter(1);
-    if ("inline".equals(templateName)) {
-      String[] allParameters = actionRequest.getParameters();
-      String[] templateParameters = Arrays.copyOfRange(allParameters, 1, allParameters.length);
-      template = StringUtils.join(templateParameters, " ");
-    } else {
-      if (templateName.isEmpty()) {
-        log.error("No template name given in {}", actionRequest);
-      } else {
-        Path templateDir = sitePath.resolve(ITS_TEMPLATE_DIR);
-        Path templatePath = templateDir.resolve(templateName + ".vm");
-        if (Files.isReadable(templatePath)) {
-          template = new String(Files.readAllBytes(templatePath));
-        } else {
-          log.error("Cannot read template {}", templatePath);
-        }
-      }
-    }
-    if (!Strings.isNullOrEmpty(template)) {
-      String comment = velocify(template, properties);
-      its.addComment(issue, comment);
-    }
-  }
-
-  /** Adapter for ItsFacade to be used through Velocity */
-  // Although we'd prefer to keep this class private, Velocity will only pick
-  // it up, if it is public.
-  public class VelocityAdapterItsFacade {
-
-    private final ItsFacade facade;
-
-    private VelocityAdapterItsFacade(ItsFacade facade) {
-      this.facade = facade;
-    }
-
-    /**
-     * Format a link to a URL in the used Its' syntax.
-     *
-     * @param url URL to link to
-     * @param caption Text used to represent the link
-     * @return Link to the given URL in the used Its' syntax.
-     */
-    public String formatLink(String url, String caption) {
-      return facade.createLinkForWebui(url, caption);
-    }
-
-    /**
-     * Format a link to an URL.
-     *
-     * <p>The provided URL is used as caption for the formatted link.
-     *
-     * @param url URL to link to
-     * @return Link to the given URL in the used Its' syntax.
-     */
-    public String formatLink(String url) {
-      return facade.createLinkForWebui(url, url);
-    }
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/LogEvent.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/LogEvent.java
index 3a46202..4abff71 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/LogEvent.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/action/LogEvent.java
@@ -34,7 +34,18 @@
     ERROR,
     WARN,
     INFO,
-    DEBUG
+    DEBUG;
+
+    static Level fromString(String s) {
+      if (s != null) {
+        for (Level level : Level.values()) {
+          if (s.toUpperCase().equals(level.toString())) {
+            return level;
+          }
+        }
+      }
+      return INFO;
+    }
   }
 
   public interface Factory {
@@ -67,21 +78,7 @@
   @Override
   public void execute(String issue, ActionRequest actionRequest, Set<Property> properties)
       throws IOException {
-    String levelParameter = actionRequest.getParameter(1);
-    if (levelParameter != null) {
-      levelParameter = levelParameter.toLowerCase();
-    }
-    Level level = Level.INFO;
-    if ("error".equals(levelParameter)) {
-      level = Level.ERROR;
-    } else if ("warn".equals(levelParameter)) {
-      level = Level.WARN;
-    } else if ("info".equals(levelParameter)) {
-      level = Level.INFO;
-    } else if ("debug".equals(levelParameter)) {
-      level = Level.DEBUG;
-    }
-
+    Level level = Level.fromString(actionRequest.getParameter(1));
     for (Property property : properties) {
       logProperty(level, property);
     }
diff --git a/src/main/resources/Documentation/config-common.md b/src/main/resources/Documentation/config-common.md
index d53b0c6..9532510 100644
--- a/src/main/resources/Documentation/config-common.md
+++ b/src/main/resources/Documentation/config-common.md
@@ -42,8 +42,8 @@
 values are supported (default is `OPTIONAL`):
 
 MANDATORY
-:	 One or more issue-ids are required in the git commit message, otherwise
-	 the git push will be rejected.
+:	 One or more issue-ids are required in the git commit message.  The git push will
+	 be rejected otherwise.
 
 SUGGESTED
 :	 Whenever the git commit message does not contain one or more issue-ids,
@@ -185,6 +185,12 @@
     the `@PLUGIN@.commentlink` comment link, and the default is `0`, if there
     are no such groups.
 
+<a name="common-config-dummyIssuePattern">`@PLUGIN@.dummyIssuePattern`
+:   Pattern which can be specified to match a dummy issue.
+
+    This setting is useful to bypass the MANDATORY check for commits matching
+    a specific pattern.
+
 [Back to @PLUGIN@ documentation index][index]
 
 [index]: index.html
diff --git a/src/main/resources/Documentation/config-rulebase-common.md b/src/main/resources/Documentation/config-rulebase-common.md
index 16f2de5..2409a63 100644
--- a/src/main/resources/Documentation/config-rulebase-common.md
+++ b/src/main/resources/Documentation/config-rulebase-common.md
@@ -175,50 +175,50 @@
 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>
@@ -482,10 +482,7 @@
 [`add-standard-comment`][action-add-standard-comment]
 : adds a predefined standard comment for certain events
 
-[`add-velocity-comment`][action-add-velocity-comment]
-: adds a rendered Velocity template as issue comment
-
-[`add-soy-comment`][action-add-velocity-comment]
+[`add-soy-comment`][action-add-soy-comment]
 : adds a rendered Closure Template (soy) template as issue comment
 
 [`log-event`][action-log-event]
@@ -523,67 +520,6 @@
 (abandoner, merger, ...), the change's subject, a reason (if one has
 been given), and a link to the change.
 
-[action-add-velocity-comment]: #action-add-velocity-comment
-### <a name="action-add-velocity-comment">Action: add-velocity-comment</a>
-
-The `add-velocity-comment` action renders a Velocity template for the
-event and adds the output as comment to any associated issue.
-
-So for example
-
-```
-  action = add-velocity-comment TemplateName
-```
-
-would render the template `etc/its/templates/TemplateName.vm` add the
-output as comment to associated issues.
-
-If 'TemplateName' is `inline`, the Velocity template to render is not
-loaded from a file, but the template is built by joining the remaining
-parameters. So for example
-
-```
-  action = add-velocity-comment inline Sample template using $subject property.
-```
-
-would render “Sample template using $subject property.” as Velocity
-template.
-
-If 'TemplateName' is not `inline`, further parameters get ignored.
-
-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.
-
-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.
-
-    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($changeUrl)}
-```
-
-`formatLink( url, caption )`
-:   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:
-
-    ```
-[rule "formatLinkSampleRule"]
-  event-type = comment-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>
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
index 6561d00..9664542 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
@@ -28,8 +28,8 @@
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.ChangeRestoredEvent;
 import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
@@ -469,7 +469,7 @@
   }
 
   public void BROKEN_testIsEnabledUnknownEvent() {
-    Event event = new Event("foo") {};
+    RefEvent event = createMock(RefEvent.class);
 
     ItsConfig itsConfig = createItsConfig();
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
index 981eeae..7255f43 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ApprovalAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
@@ -31,8 +32,8 @@
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.ChangeRestoredEvent;
 import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -330,7 +331,7 @@
   }
 
   private void eventHelper(
-      Event event, String className, String type, Set<Property> common, boolean withRevision) {
+      RefEvent event, String className, String type, Set<Property> common, boolean withRevision) {
     PropertyExtractor propertyExtractor = injector.getInstance(PropertyExtractor.class);
 
     Property propertyItsName = createMock(Property.class);
@@ -421,9 +422,19 @@
     }
   }
 
-  private class DummyEvent extends Event {
+  private class DummyEvent extends RefEvent {
     public DummyEvent() {
       super(null);
     }
+
+    @Override
+    public String getRefName() {
+      return null;
+    }
+
+    @Override
+    public NameKey getProjectNameKey() {
+      return null;
+    }
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/validation/ItsValidateCommentTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/validation/ItsValidateCommentTest.java
index 5573878..14744c4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/validation/ItsValidateCommentTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/validation/ItsValidateCommentTest.java
@@ -30,6 +30,7 @@
 import com.googlesource.gerrit.plugins.its.base.util.IssueExtractor;
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 import java.util.regex.Pattern;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -75,6 +76,7 @@
     expect(itsConfig.getItsAssociationPolicy())
         .andReturn(ItsAssociationPolicy.SUGGESTED)
         .atLeastOnce();
+    expect(itsConfig.getDummyIssuePattern()).andReturn(Optional.empty()).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("TestMessage").atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
     expect(commit.getName()).andReturn("TestCommit").anyTimes();
@@ -99,6 +101,7 @@
     expect(itsConfig.getItsAssociationPolicy())
         .andReturn(ItsAssociationPolicy.MANDATORY)
         .atLeastOnce();
+    expect(itsConfig.getDummyIssuePattern()).andReturn(Optional.empty()).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("TestMessage").atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
     expect(commit.getName()).andReturn("TestCommit").anyTimes();
@@ -116,6 +119,31 @@
     }
   }
 
+  public void testOnlySkipMatching() throws CommitValidationException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = newCommitReceivedEvent(command, project, null, commit, null);
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.MANDATORY)
+        .atLeastOnce();
+    expect(itsConfig.getDummyIssuePattern())
+        .andReturn(Optional.of(Pattern.compile("SKIP")))
+        .atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("TestMessage SKIP").atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("TestMessage SKIP")).andReturn(new String[] {}).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEmptyList(ret);
+  }
+
   public void testSuggestedMatchingSingleExisting() throws CommitValidationException, IOException {
     List<CommitValidationMessage> ret;
     ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionControllerTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionControllerTest.java
index d2926f9..7c85f7f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionControllerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionControllerTest.java
@@ -20,7 +20,7 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.events.ChangeEvent;
-import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.RefEvent;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.googlesource.gerrit.plugins.its.base.its.ItsConfig;
@@ -173,7 +173,7 @@
   }
 
   private void setupCommonMocks() {
-    expect(itsConfig.isEnabled(anyObject(Event.class))).andReturn(true).anyTimes();
+    expect(itsConfig.isEnabled(anyObject(RefEvent.class))).andReturn(true).anyTimes();
   }
 
   @Override
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 f2b6a55..4dbc779 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
@@ -25,7 +25,6 @@
 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.Collections;
@@ -37,7 +36,6 @@
   private ItsFacade its;
   private AddComment.Factory addCommentFactory;
   private AddStandardComment.Factory addStandardCommentFactory;
-  private AddVelocityComment.Factory addVelocityCommentFactory;
   private AddSoyComment.Factory addSoyCommentFactory;
   private LogEvent.Factory logEventFactory;
 
@@ -176,23 +174,6 @@
     actionExecutor.execute("4711", actionRequest, properties);
   }
 
-  public void testAddVelocityCommentDelegation() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getName()).andReturn("add-velocity-comment");
-
-    Set<Property> properties = Collections.emptySet();
-
-    AddVelocityComment addVelocityComment = createMock(AddVelocityComment.class);
-    expect(addVelocityCommentFactory.create()).andReturn(addVelocityComment);
-
-    addVelocityComment.execute("4711", actionRequest, properties);
-
-    replayMocks();
-
-    ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute("4711", actionRequest, properties);
-  }
-
   public void testLogEventDelegation() throws IOException {
     ActionRequest actionRequest = createMock(ActionRequest.class);
     expect(actionRequest.getName()).andReturn("log-event");
@@ -235,9 +216,6 @@
       addStandardCommentFactory = createMock(AddStandardComment.Factory.class);
       bind(AddStandardComment.Factory.class).toInstance(addStandardCommentFactory);
 
-      addVelocityCommentFactory = createMock(AddVelocityComment.Factory.class);
-      bind(AddVelocityComment.Factory.class).toInstance(addVelocityCommentFactory);
-
       logEventFactory = createMock(LogEvent.Factory.class);
       bind(LogEvent.Factory.class).toInstance(logEventFactory);
     }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBaseTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBaseTest.java
index 80e12fb..2ba4d4a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBaseTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBaseTest.java
@@ -18,13 +18,10 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.server.config.SitePath;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.ItsPath;
 import com.googlesource.gerrit.plugins.its.base.testutil.LoggingMockingTestCase;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -38,7 +35,7 @@
 public class RuleBaseTest extends LoggingMockingTestCase {
   private Injector injector;
 
-  private Path sitePath;
+  private Path itsPath;
   private Rule.Factory ruleFactory;
   private Condition.Factory conditionFactory;
   private ActionRequest.Factory actionRequestFactory;
@@ -46,9 +43,14 @@
   private boolean cleanupSitePath;
 
   private enum RuleBaseKind {
-    GLOBAL,
-    ITS,
-    FAULTY
+    GLOBAL("actions"),
+    ITS("actions-ItsTestName");
+
+    String fileName;
+
+    RuleBaseKind(String fileName) {
+      this.fileName = fileName + ".config";
+    }
   }
 
   public void testWarnNonExistingRuleBase() {
@@ -221,41 +223,6 @@
     assertEquals("Matched actionRequests do not match", expected, actual);
   }
 
-  public void testWarnExistingFaultyNameRuleBaseFile() throws IOException {
-    injectRuleBase("", RuleBaseKind.FAULTY);
-
-    replayMocks();
-
-    createRuleBase();
-
-    assertLogMessageContains("Please migrate"); // Migration warning for old name
-    assertLogMessageContains("Neither global"); // For rule file at at usual places
-  }
-
-  public void testSimpleFaultyNameRuleBase() throws IOException {
-    injectRuleBase(
-        "[rule \"rule1\"]\n" + "\tconditionA = value1\n" + "\taction = action1",
-        RuleBaseKind.FAULTY);
-
-    Rule rule1 = createMock(Rule.class);
-    expect(ruleFactory.create("rule1")).andReturn(rule1);
-
-    Condition condition1 = createMock(Condition.class);
-    expect(conditionFactory.create("conditionA", "value1")).andReturn(condition1);
-    rule1.addCondition(condition1);
-
-    ActionRequest actionRequest1 = createMock(ActionRequest.class);
-    expect(actionRequestFactory.create("action1")).andReturn(actionRequest1);
-    rule1.addActionRequest(actionRequest1);
-
-    replayMocks();
-
-    createRuleBase();
-
-    assertLogMessageContains("Please migrate"); // Migration warning for old name
-    assertLogMessageContains("Neither global"); // For rule file at at usual places
-  }
-
   public void testSimpleItsRuleBase() throws IOException {
     injectRuleBase(
         "[rule \"rule1\"]\n" + "\tconditionA = value1\n" + "\taction = action1", RuleBaseKind.ITS);
@@ -277,25 +244,12 @@
   }
 
   public void testAllRuleBaseFilesAreLoaded() throws IOException {
-    injectRuleBase("[rule \"rule1\"]\n" + "\taction = action1", RuleBaseKind.FAULTY);
-
     injectRuleBase("[rule \"rule2\"]\n" + "\taction = action2", RuleBaseKind.GLOBAL);
 
     injectRuleBase("[rule \"rule3\"]\n" + "\taction = action3", RuleBaseKind.ITS);
 
     Collection<Property> properties = Collections.emptySet();
 
-    Rule rule1 = createMock(Rule.class);
-    expect(ruleFactory.create("rule1")).andReturn(rule1);
-
-    ActionRequest actionRequest1 = createMock(ActionRequest.class);
-    expect(actionRequestFactory.create("action1")).andReturn(actionRequest1);
-    rule1.addActionRequest(actionRequest1);
-
-    List<ActionRequest> rule1Match = Lists.newArrayListWithCapacity(1);
-    rule1Match.add(actionRequest1);
-    expect(rule1.actionRequestsFor(properties)).andReturn(rule1Match);
-
     Rule rule2 = createMock(Rule.class);
     expect(ruleFactory.create("rule2")).andReturn(rule2);
 
@@ -325,13 +279,10 @@
     Collection<ActionRequest> actual = ruleBase.actionRequestsFor(properties);
 
     List<ActionRequest> expected = Lists.newArrayListWithCapacity(3);
-    expected.add(actionRequest1);
     expected.add(actionRequest2);
     expected.add(actionRequest3);
 
     assertEquals("Matched actionRequests do not match", expected, actual);
-
-    assertLogMessageContains("Please migrate"); // Migration warning for old name
   }
 
   private RuleBase createRuleBase() {
@@ -343,35 +294,9 @@
   }
 
   private void injectRuleBase(String rules, RuleBaseKind ruleBaseKind) throws IOException {
-    String baseName = "";
-    switch (ruleBaseKind) {
-      case GLOBAL:
-        baseName = "actions";
-        break;
-      case ITS:
-        baseName = "actions-ItsTestName";
-        break;
-      case FAULTY:
-        baseName = "action";
-        break;
-      default:
-        fail("Unknown ruleBaseKind");
-    }
-    File ruleBaseFile =
-        new File(
-            sitePath.toFile(),
-            "etc" + File.separatorChar + "its" + File.separator + baseName + ".config");
-
-    File ruleBaseParentFile = ruleBaseFile.getParentFile();
-    if (!ruleBaseParentFile.exists()) {
-      assertTrue(
-          "Failed to create parent (" + ruleBaseParentFile + ") for " + "rule base",
-          ruleBaseParentFile.mkdirs());
-    }
-    try (FileWriter unbufferedWriter = new FileWriter(ruleBaseFile);
-        BufferedWriter writer = new BufferedWriter(unbufferedWriter)) {
-      writer.write(rules);
-    }
+    Path ruleBaseFile = itsPath.resolve(ruleBaseKind.fileName);
+    Files.createDirectories(ruleBaseFile.getParent());
+    Files.write(ruleBaseFile, rules.getBytes());
   }
 
   @Override
@@ -384,8 +309,8 @@
   @Override
   public void tearDown() throws Exception {
     if (cleanupSitePath) {
-      if (Files.exists(sitePath)) {
-        FileUtils.delete(sitePath.toFile(), FileUtils.RECURSIVE);
+      if (Files.exists(itsPath)) {
+        FileUtils.delete(itsPath.toFile(), FileUtils.RECURSIVE);
       }
     }
     super.tearDown();
@@ -401,11 +326,11 @@
 
       bind(String.class).annotatedWith(PluginName.class).toInstance("ItsTestName");
 
-      sitePath = randomTargetPath();
-      assertFalse("sitePath already (" + sitePath + ") already exists", Files.exists(sitePath));
+      itsPath = randomTargetPath().resolve("etc").resolve("its");
+      assertFalse("itsPath (" + itsPath + ") already exists", Files.exists(itsPath));
       cleanupSitePath = true;
 
-      bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
+      bind(Path.class).annotatedWith(ItsPath.class).toInstance(itsPath);
 
       ruleFactory = createMock(Rule.Factory.class);
       bind(Rule.Factory.class).toInstance(ruleFactory);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddCommentTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddCommentTest.java
index 3039b12..d16bab2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddCommentTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddCommentTest.java
@@ -21,7 +21,6 @@
 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.ActionRequest;
-import com.googlesource.gerrit.plugins.its.base.workflow.Property;
 import java.io.IOException;
 import java.util.HashSet;
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddVelocityCommentTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddVelocityCommentTest.java
deleted file mode 100644
index 61042a4..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/AddVelocityCommentTest.java
+++ /dev/null
@@ -1,450 +0,0 @@
-// Copyright (C) 2013 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 static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.capture;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.server.config.SitePath;
-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.ActionRequest;
-import com.googlesource.gerrit.plugins.its.base.workflow.Property;
-import com.googlesource.gerrit.plugins.its.base.workflow.action.AddVelocityComment.VelocityAdapterItsFacade;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.Writer;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.UUID;
-import org.apache.velocity.VelocityContext;
-import org.apache.velocity.runtime.RuntimeInstance;
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.easymock.IAnswer;
-import org.eclipse.jgit.util.FileUtils;
-
-public class AddVelocityCommentTest extends LoggingMockingTestCase {
-  private Injector injector;
-
-  private Path sitePath;
-  private ItsFacade its;
-  private RuntimeInstance velocityRuntime;
-
-  private boolean cleanupSitePath;
-
-  public void testWarnNoTemplateNameGiven() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("");
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, new HashSet<>());
-
-    assertLogMessageContains("No template name");
-  }
-
-  public void testInlinePlain() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("inline");
-    expect(actionRequest.getParameters()).andReturn(new String[] {"inline", "Simple-text"});
-
-    IAnswer<Boolean> answer = new VelocityWriterFiller("Simple-text");
-    expect(
-            velocityRuntime.evaluate(
-                (VelocityContext) anyObject(),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq("Simple-text")))
-        .andAnswer(answer);
-
-    its.addComment("4711", "Simple-text");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, new HashSet<>());
-  }
-
-  public void testInlineWithMultipleParameters() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("inline");
-    expect(actionRequest.getParameters()).andReturn(new String[] {"inline", "Param2", "Param3"});
-
-    Set<Property> properties = Sets.newHashSet();
-
-    IAnswer<Boolean> answer = new VelocityWriterFiller("Param2 Param3");
-    expect(
-            velocityRuntime.evaluate(
-                (VelocityContext) anyObject(),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq("Param2 Param3")))
-        .andAnswer(answer);
-
-    its.addComment("4711", "Param2 Param3");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, properties);
-  }
-
-  public void testInlineWithSingleProperty() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("inline");
-    expect(actionRequest.getParameters()).andReturn(new String[] {"inline", "${subject}"});
-
-    Set<Property> properties = Sets.newHashSet();
-
-    Property propertySubject = createMock(Property.class);
-    expect(propertySubject.getKey()).andReturn("subject").anyTimes();
-    expect(propertySubject.getValue()).andReturn("Rosebud").anyTimes();
-    properties.add(propertySubject);
-
-    IAnswer<Boolean> answer = new VelocityWriterFiller("Rosebud");
-    Capture<VelocityContext> contextCapture = createCapture();
-    expect(
-            velocityRuntime.evaluate(
-                capture(contextCapture),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq("${subject}")))
-        .andAnswer(answer);
-
-    its.addComment("4711", "Rosebud");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, properties);
-
-    VelocityContext context = contextCapture.getValue();
-    assertEquals("Subject property of context did not match", "Rosebud", context.get("subject"));
-  }
-
-  public void testInlineWithUnusedProperty() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("inline");
-    expect(actionRequest.getParameters()).andReturn(new String[] {"inline", "Test"});
-
-    Set<Property> properties = Sets.newHashSet();
-
-    Property propertySubject = createMock(Property.class);
-    expect(propertySubject.getKey()).andReturn("subject").anyTimes();
-    expect(propertySubject.getValue()).andReturn("Rosebud").anyTimes();
-    properties.add(propertySubject);
-
-    IAnswer<Boolean> answer = new VelocityWriterFiller("Test");
-    expect(
-            velocityRuntime.evaluate(
-                (VelocityContext) anyObject(),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq("Test")))
-        .andAnswer(answer);
-
-    its.addComment("4711", "Test");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, properties);
-  }
-
-  public void testInlineWithMultipleProperties() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("inline");
-    expect(actionRequest.getParameters())
-        .andReturn(new String[] {"inline", "${subject}", "${reason}", "${subject}"});
-
-    Set<Property> properties = Sets.newHashSet();
-
-    Property propertySubject = createMock(Property.class);
-    expect(propertySubject.getKey()).andReturn("subject").anyTimes();
-    expect(propertySubject.getValue()).andReturn("Rosebud").anyTimes();
-    properties.add(propertySubject);
-
-    Property propertyReason = createMock(Property.class);
-    expect(propertyReason.getKey()).andReturn("reason").anyTimes();
-    expect(propertyReason.getValue()).andReturn("Life").anyTimes();
-    properties.add(propertyReason);
-
-    IAnswer<Boolean> answer = new VelocityWriterFiller("Rosebud Life Rosebud");
-    Capture<VelocityContext> contextCapture = createCapture();
-    expect(
-            velocityRuntime.evaluate(
-                capture(contextCapture),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq("${subject} ${reason} ${subject}")))
-        .andAnswer(answer);
-
-    its.addComment("4711", "Rosebud Life Rosebud");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, properties);
-
-    VelocityContext context = contextCapture.getValue();
-    assertEquals("Subject property of context did not match", "Rosebud", context.get("subject"));
-    assertEquals("Reason property of context did not match", "Life", context.get("reason"));
-  }
-
-  public void testItsWrapperFormatLink1Parameter()
-      throws IOException, SecurityException, IllegalArgumentException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("inline");
-    expect(actionRequest.getParameters()).andReturn(new String[] {"inline", "Simple-Text"});
-
-    IAnswer<Boolean> answer = new VelocityWriterFiller("Simple-Text");
-    Capture<VelocityContext> contextCapture = createCapture();
-    expect(
-            velocityRuntime.evaluate(
-                capture(contextCapture),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq("Simple-Text")))
-        .andAnswer(answer);
-
-    its.addComment("4711", "Simple-Text");
-
-    expect(its.createLinkForWebui("http://www.example.org/", "http://www.example.org/"))
-        .andReturn("Formatted Link");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, new HashSet<>());
-
-    VelocityContext context = contextCapture.getValue();
-    Object itsAdapterObj = context.get("its");
-    assertNotNull("its property is null", itsAdapterObj);
-    assertTrue(
-        "Its is not a VelocityAdapterItsFacade instance",
-        itsAdapterObj instanceof VelocityAdapterItsFacade);
-    VelocityAdapterItsFacade itsAdapter = (VelocityAdapterItsFacade) itsAdapterObj;
-    String formattedLink = itsAdapter.formatLink("http://www.example.org/");
-    assertEquals("Result of formatLink does not match", "Formatted Link", formattedLink);
-  }
-
-  public void testItsWrapperFormatLink2Parameters()
-      throws IOException, SecurityException, IllegalArgumentException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("inline");
-    expect(actionRequest.getParameters()).andReturn(new String[] {"inline", "Simple-Text"});
-
-    IAnswer<Boolean> answer = new VelocityWriterFiller("Simple-Text");
-    Capture<VelocityContext> contextCapture = createCapture();
-    expect(
-            velocityRuntime.evaluate(
-                capture(contextCapture),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq("Simple-Text")))
-        .andAnswer(answer);
-
-    its.addComment("4711", "Simple-Text");
-
-    expect(its.createLinkForWebui("http://www.example.org/", "Caption"))
-        .andReturn("Formatted Link");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, new HashSet<>());
-
-    VelocityContext context = contextCapture.getValue();
-    Object itsAdapterObj = context.get("its");
-    assertNotNull("its property is null", itsAdapterObj);
-    assertTrue(
-        "Its is not a VelocityAdapterItsFacade instance",
-        itsAdapterObj instanceof VelocityAdapterItsFacade);
-    VelocityAdapterItsFacade itsAdapter = (VelocityAdapterItsFacade) itsAdapterObj;
-    String formattedLink = itsAdapter.formatLink("http://www.example.org/", "Caption");
-    assertEquals("Result of formatLink does not match", "Formatted Link", formattedLink);
-  }
-
-  public void testWarnTemplateNotFound() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("non-existing-template");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, new HashSet<>());
-
-    assertLogMessageContains("non-existing-template");
-  }
-
-  public void testTemplateSimple() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("test-template");
-
-    injectTestTemplate("Simple Test Template");
-
-    IAnswer<Boolean> answer = new VelocityWriterFiller("Simple Test Template");
-    expect(
-            velocityRuntime.evaluate(
-                (VelocityContext) anyObject(),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq("Simple Test Template")))
-        .andAnswer(answer);
-
-    its.addComment("4711", "Simple Test Template");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, new HashSet<>());
-  }
-
-  public void testTemplateMultipleParametersAndProperties() throws IOException {
-    ActionRequest actionRequest = createMock(ActionRequest.class);
-    expect(actionRequest.getParameter(1)).andReturn("test-template");
-
-    Set<Property> properties = Sets.newHashSet();
-
-    Property propertySubject = createMock(Property.class);
-    expect(propertySubject.getKey()).andReturn("subject").anyTimes();
-    expect(propertySubject.getValue()).andReturn("Rosebud").anyTimes();
-    properties.add(propertySubject);
-
-    Property propertyReason = createMock(Property.class);
-    expect(propertyReason.getKey()).andReturn("reason").anyTimes();
-    expect(propertyReason.getValue()).andReturn("Life").anyTimes();
-    properties.add(propertyReason);
-
-    injectTestTemplate(
-        "Test Template with subject: ${subject}.\n" + "${reason} is the reason for ${subject}.");
-
-    IAnswer<Boolean> answer =
-        new VelocityWriterFiller(
-            "Test Template with subject: Rosebud.\n" + "Life is the reason for Rosebud.");
-    Capture<VelocityContext> contextCapture = createCapture();
-    expect(
-            velocityRuntime.evaluate(
-                capture(contextCapture),
-                (Writer) anyObject(),
-                (String) anyObject(),
-                eq(
-                    "Test Template with subject: ${subject}.\n"
-                        + "${reason} is the reason for ${subject}.")))
-        .andAnswer(answer);
-
-    its.addComment(
-        "4711", "Test Template with subject: Rosebud.\n" + "Life is the reason for Rosebud.");
-
-    replayMocks();
-
-    AddVelocityComment addVelocityComment = createAddVelocityComment();
-    addVelocityComment.execute("4711", actionRequest, properties);
-
-    VelocityContext context = contextCapture.getValue();
-    assertEquals("Subject property of context did not match", "Rosebud", context.get("subject"));
-    assertEquals("Reason property of context did not match", "Life", context.get("reason"));
-  }
-
-  private AddVelocityComment createAddVelocityComment() {
-    return injector.getInstance(AddVelocityComment.class);
-  }
-
-  private void injectTestTemplate(String template) throws IOException {
-    File templateParentFile =
-        new File(
-            sitePath.toFile(), "etc" + File.separatorChar + "its" + File.separator + "templates");
-    assertTrue(
-        "Failed to create parent (" + templateParentFile + ") for " + "rule base",
-        templateParentFile.mkdirs());
-    File templateFile = new File(templateParentFile, "test-template.vm");
-
-    try (FileWriter unbufferedWriter = new FileWriter(templateFile);
-        BufferedWriter writer = new BufferedWriter(unbufferedWriter)) {
-      writer.write(template);
-    }
-  }
-
-  @Override
-  public void setUp() throws Exception {
-    super.setUp();
-    cleanupSitePath = false;
-    injector = Guice.createInjector(new TestModule());
-  }
-
-  @Override
-  public void tearDown() throws Exception {
-    if (cleanupSitePath) {
-      if (Files.exists(sitePath)) {
-        FileUtils.delete(sitePath.toFile(), FileUtils.RECURSIVE);
-      }
-    }
-    super.tearDown();
-  }
-
-  private Path randomTargetPath() {
-    return Paths.get("target", "random-name-" + UUID.randomUUID().toString());
-  }
-
-  private class TestModule extends FactoryModule {
-    @Override
-    protected void configure() {
-      sitePath = randomTargetPath();
-      assertFalse("sitePath already (" + sitePath + ") already exists", Files.exists(sitePath));
-      cleanupSitePath = true;
-
-      bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
-
-      its = createMock(ItsFacade.class);
-      bind(ItsFacade.class).toInstance(its);
-
-      velocityRuntime = createMock(RuntimeInstance.class);
-      bind(RuntimeInstance.class).toInstance(velocityRuntime);
-    }
-  }
-
-  private class VelocityWriterFiller implements IAnswer<Boolean> {
-    private final String fill;
-    private final boolean returnValue;
-
-    private VelocityWriterFiller(String fill, boolean returnValue) {
-      this.fill = fill;
-      this.returnValue = returnValue;
-    }
-
-    private VelocityWriterFiller(String fill) {
-      this(fill, true);
-    }
-
-    @Override
-    public Boolean answer() throws Throwable {
-      Object[] arguments = EasyMock.getCurrentArguments();
-      Writer writer = (Writer) arguments[1];
-      writer.write(fill);
-      return returnValue;
-    }
-  }
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/LogEventTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/LogEventTest.java
index 99fa2c3..54b9942 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/LogEventTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/action/LogEventTest.java
@@ -30,6 +30,16 @@
 public class LogEventTest extends LoggingMockingTestCase {
   private Injector injector;
 
+  public void testNull() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameter(1)).andReturn(null);
+
+    replayMocks();
+
+    LogEvent logEvent = createLogEvent();
+    logEvent.execute("4711", actionRequest, new HashSet<>());
+  }
+
   public void testEmpty() throws IOException {
     ActionRequest actionRequest = createMock(ActionRequest.class);
     expect(actionRequest.getParameter(1)).andReturn("");