Merge remote-tracking branch stable-2.13'

* stable-2.13:
  Get commentLink w/ association from project.config
  Move the association policy to the plugin's config
  Restore enforcement constraint by parent projects

Change-Id: I7073bdca41473cfe57f7dc253b8a952bcb016fc5
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 581ef99..17d31a1 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
@@ -14,9 +14,14 @@
 
 package com.googlesource.gerrit.plugins.its.base.its;
 
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -28,6 +33,7 @@
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefPatternMatcher;
@@ -39,10 +45,14 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.regex.Pattern;
 
 
 public class ItsConfig {
+  private static final String PLUGIN = "plugin";
+
   private static final Logger log = LoggerFactory.getLogger(ItsConfig.class);
 
   private final String pluginName;
@@ -50,6 +60,18 @@
   private final PluginConfigFactory pluginCfgFactory;
   private final Config gerritConfig;
 
+  private static final ThreadLocal<Project.NameKey> currentProjectName =
+      new ThreadLocal<Project.NameKey>() {
+        @Override
+        protected Project.NameKey initialValue() {
+          return null;
+        }
+      };
+
+  public static void setCurrentProjectName(Project.NameKey projectName) {
+    currentProjectName.set(projectName);
+  }
+
   @Inject
   public ItsConfig(@PluginName String pluginName, ProjectCache projectCache,
       PluginConfigFactory pluginCfgFactory, @GerritServerConfig Config gerritConfig) {
@@ -97,13 +119,8 @@
       return false;
     }
 
-    for (ProjectState parentState : projectState.treeInOrder()) {
-      PluginConfig parentCfg =
-          pluginCfgFactory.getFromProjectConfig(parentState, pluginName);
-      if (!"false".equals(parentCfg.getString("enabled"))
-          && isEnabledForBranch(parentState, refName)) {
-        return true;
-      }
+    if(isEnforcedByAnyParentProject(refName, projectState)) {
+      return true;
     }
 
     return !"false".equals(pluginCfgFactory.getFromProjectConfigWithInheritance(
@@ -111,6 +128,19 @@
         && isEnabledForBranch(projectState, refName);
   }
 
+  private boolean isEnforcedByAnyParentProject(String refName,
+      ProjectState projectState) {
+    for (ProjectState parentState : projectState.treeInOrder()) {
+      PluginConfig parentCfg =
+          pluginCfgFactory.getFromProjectConfig(parentState, pluginName);
+      if ("enforced".equals(parentCfg.getString("enabled", "false"))
+          && isEnabledForBranch(parentState, refName)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private boolean isEnabledForBranch(ProjectState project, String refName) {
     String[] refPatterns =
         pluginCfgFactory.getFromProjectConfigWithInheritance(project,
@@ -140,7 +170,7 @@
   public String getCommentLinkName() {
     String ret;
 
-    ret = gerritConfig.getString(pluginName, null, "commentlink");
+    ret = getPluginConfigString("commentlink");
     if (ret == null) {
       ret = pluginName;
     }
@@ -158,9 +188,26 @@
    *    to match issue ids.
    */
   public Pattern getIssuePattern() {
+    String match =
+        FluentIterable
+            .from(getCommitLinkInfo(getCommentLinkName()))
+            .filter(new Predicate<CommentLinkInfo>() {
+              @Override
+              public boolean apply(CommentLinkInfo input) {
+                return input.match != null && !input.match.trim().isEmpty();
+              }
+            })
+            .transform(new Function<CommentLinkInfo, String>() {
+              @Override
+              public String apply(CommentLinkInfo input) {
+                return input.match;
+              }
+            })
+            .last()
+            .or(gerritConfig.getString("commentlink", getCommentLinkName(),
+                "match"));
     Pattern ret = null;
-    String match = gerritConfig.getString("commentlink",
-        getCommentLinkName(), "match");
+
     if (match != null) {
       ret = Pattern.compile(match);
     }
@@ -178,7 +225,7 @@
   public int getIssuePatternGroupIndex() {
     Pattern pattern = getIssuePattern();
     int groupCount = pattern.matcher("").groupCount();
-    int index = gerritConfig.getInt(pluginName, "commentlinkGroupIndex", 1);
+    int index = getPluginConfigInt("commentlinkGroupIndex", 1);
     if (index < 0 || index > groupCount) {
       index = (groupCount == 0 ? 0 : 1);
     }
@@ -187,10 +234,59 @@
 
   /**
    * Gets how necessary it is to associate commits with issues
+   *
    * @return policy on how necessary association with issues is
    */
   public ItsAssociationPolicy getItsAssociationPolicy() {
-    return gerritConfig.getEnum("commentlink", getCommentLinkName(),
-        "association", ItsAssociationPolicy.OPTIONAL);
+    ItsAssociationPolicy legacyItsAssociationPolicy =
+        gerritConfig.getEnum("commentLink", getCommentLinkName(),
+            "association", ItsAssociationPolicy.OPTIONAL);
+
+    return getPluginConfigEnum("association", legacyItsAssociationPolicy);
+  }
+
+  private String getPluginConfigString(String key) {
+    return getCurrentPluginConfig().getString(key,
+        gerritConfig.getString(PLUGIN, pluginName, key));
+  }
+
+  private int getPluginConfigInt(String key, int defaultValue) {
+    return getCurrentPluginConfig().getInt(key,
+        gerritConfig.getInt(PLUGIN, pluginName, key, defaultValue));
+  }
+
+  private <T extends Enum<?>> T getPluginConfigEnum(String key, T defaultValue) {
+    return getCurrentPluginConfig().getEnum(key,
+        gerritConfig.getEnum(PLUGIN, pluginName, key, defaultValue));
+  }
+
+  private PluginConfig getCurrentPluginConfig() {
+    NameKey projectName = currentProjectName.get();
+    if (projectName != null) {
+      try {
+        return pluginCfgFactory.getFromProjectConfigWithInheritance(
+            projectName, pluginName);
+      } catch (NoSuchProjectException e) {
+        log.error("Cannot access " + projectName + " configuration for plugin "
+            + pluginName, e);
+      }
+    }
+    return new PluginConfig(pluginName, new Config());
+  }
+
+  private List<CommentLinkInfo> getCommitLinkInfo(final String commentLinkName) {
+    NameKey projectName = currentProjectName.get();
+    if (projectName != null) {
+      List<CommentLinkInfo> commentLinks =
+          projectCache.get(projectName).getCommentLinks();
+      return FluentIterable.from(commentLinks)
+          .filter(new Predicate<CommentLinkInfo>() {
+            @Override
+            public boolean apply(CommentLinkInfo input) {
+              return input.name.equals(commentLinkName);
+            }
+          }).toList();
+    }
+    return Collections.emptyList();
   }
 }
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 07ae258..3d8816f 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
@@ -143,6 +143,8 @@
   @Override
   public List<CommitValidationMessage> onCommitReceived(
       CommitReceivedEvent receiveEvent) throws CommitValidationException {
+    ItsConfig.setCurrentProjectName(receiveEvent.getProjectNameKey());
+
     if (itsConfig.isEnabled(receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
       return validCommit(receiveEvent.commit);
     }
diff --git a/src/main/resources/Documentation/config-common.md b/src/main/resources/Documentation/config-common.md
index 02565e3..d53b0c6 100644
--- a/src/main/resources/Documentation/config-common.md
+++ b/src/main/resources/Documentation/config-common.md
@@ -26,7 +26,6 @@
 [commentlink "@PLUGIN@"]
     match = [Bb][Uu][Gg][ ]*([1-9][0-9]*)
     html = "<a href=\"http://my.issure.tracker.example.org/show_bug.cgi?id=$1\">(bug $1)</a>"
-    association = SUGGESTED
 ```
 
 in `etc/gerrit.config` would allow to match the issues `4711`, `167`
@@ -36,7 +35,9 @@
 Sample commit message relating to bug 4711, and bug 167.
 ```
 
-By setting a `commentlink`'s `association` (see above's example), it
+[upstream-comment-link-doc]: ../../../Documentation/config-gerrit.html#commentlink
+
+By setting a `commentlink`'s `association` on the plugin's @PLUGIN@ configuration, it
 is possible to require commits to carry ITS references; the following
 values are supported (default is `OPTIONAL`):
 
@@ -52,9 +53,24 @@
 :	 Bug-ids are liked when found in the git commit message, no warning is
 	 displayed otherwise.
 
-[upstream-comment-link-doc]: ../../../Documentation/config-gerrit.html#commentlink
+Example:
 
+```
+[plugin "@PLUGIN@"]
+    association = MANDATORY
+```
 
+in `etc/gerrit.config` would accept only commits that contain a valid issue id
+in the comment, matching the commentLink defined previously.
+
+NOTE: Historically the association has been defined in the Gerrit's commentLink
+section. That setting is deprecated but still supported for the current release.
+You are encouraged to move the association policy to the plugin section, the
+commentLink.association will be discontinued in the next major release.
+
+The association can be overridden at project level in the project.config
+using the same syntax used in the gerrit.config. Project's hierarchy will be respected
+when evaluating the links configuration and association policy.
 
 [enabling-its-integration]: #enabling-its-integration
 <a name="enabling-its-integration">Enabling ITS integration</a>