Merge branch 'stable-2.16'

* stable-2.16:
  Support multiple Jira instances

Change-Id: Ibd8ba989f4ffca8479e0b1c96528feaeefd9c2ff
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java
index 5fd69ba..ab6cba2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java
@@ -53,9 +53,8 @@
    * @param issueKey Jira Issue key
    * @return true if issue exists
    */
-  public boolean issueExists(String issueKey) throws IOException {
-    JiraRestApi<JiraIssue> api = apiBuilder.getIssue();
-
+  public boolean issueExists(JiraItsServerInfo server, String issueKey) throws IOException {
+    JiraRestApi<JiraIssue> api = apiBuilder.getIssue(server);
     api.doGet(issueKey, HTTP_OK, new int[] {HTTP_NOT_FOUND, HTTP_FORBIDDEN});
     Integer code = api.getResponseCode();
     switch (code) {
@@ -78,9 +77,9 @@
    * @return Iterable of available transitions
    * @throws IOException
    */
-  public List<JiraTransition.Item> getTransitions(String issueKey) throws IOException {
-
-    JiraRestApi<JiraTransition> api = apiBuilder.get(JiraTransition.class, "/issue");
+  public List<JiraTransition.Item> getTransitions(JiraItsServerInfo server, String issueKey)
+      throws IOException {
+    JiraRestApi<JiraTransition> api = apiBuilder.get(server, JiraTransition.class, "/issue");
     return Arrays.asList(api.doGet(issueKey + "/transitions", HTTP_OK).getTransitions());
   }
 
@@ -89,12 +88,13 @@
    * @param comment String to be added
    * @throws IOException
    */
-  public void addComment(String issueKey, String comment) throws IOException {
+  public void addComment(JiraItsServerInfo server, String issueKey, String comment)
+      throws IOException {
 
-    if (issueExists(issueKey)) {
+    if (issueExists(server, issueKey)) {
       log.debug("Trying to add comment for issue {}", issueKey);
       apiBuilder
-          .getIssue()
+          .getIssue(server)
           .doPost(issueKey + "/comment", gson.toJson(new JiraComment(comment)), HTTP_CREATED);
       log.debug("Comment added to issue {}", issueKey);
     } else {
@@ -114,33 +114,33 @@
    * @param transition JiraTransition.Item to perform
    * @return true if successful
    */
-  public boolean doTransition(String issueKey, String transition)
+  public boolean doTransition(JiraItsServerInfo server, String issueKey, String transition)
       throws IOException, InvalidTransitionException {
     log.debug("Making transition to {} for {}", transition, issueKey);
-    JiraTransition.Item t = getTransitionByName(issueKey, transition);
+    JiraTransition.Item t = getTransitionByName(server, issueKey, transition);
     if (t == null) {
       throw new InvalidTransitionException(
           "Action " + transition + " not executable on issue " + issueKey);
     }
     log.debug("Transition issue {} to '{}' ({})", issueKey, transition, t.getId());
     return apiBuilder
-        .getIssue()
+        .getIssue(server)
         .doPost(issueKey + "/transitions", gson.toJson(new JiraTransition(t)), HTTP_NO_CONTENT);
   }
 
   /** @return Serverinformation of jira */
-  public JiraServerInfo sysInfo() throws IOException {
-    return apiBuilder.getServerInfo().doGet("", HTTP_OK);
+  public JiraServerInfo sysInfo(JiraItsServerInfo server) throws IOException {
+    return apiBuilder.getServerInfo(server).doGet("", HTTP_OK);
   }
 
   /** @return List of all projects we have access to in jira */
-  public JiraProject[] getProjects() throws IOException {
-    return apiBuilder.getProjects().doGet("", HTTP_OK);
+  public JiraProject[] getProjects(JiraItsServerInfo server) throws IOException {
+    return apiBuilder.getProjects(server).doGet("", HTTP_OK);
   }
 
-  private JiraTransition.Item getTransitionByName(String issueKey, String transition)
-      throws IOException {
-    for (JiraTransition.Item t : getTransitions(issueKey)) {
+  private JiraTransition.Item getTransitionByName(
+      JiraItsServerInfo server, String issueKey, String transition) throws IOException {
+    for (JiraTransition.Item t : getTransitions(server, issueKey)) {
       if (transition.equals(t.getName())) {
         return t;
       }
@@ -148,15 +148,15 @@
     return null;
   }
 
-  public String healthCheckAccess() throws IOException {
-    sysInfo();
+  public String healthCheckAccess(JiraItsServerInfo server) throws IOException {
+    sysInfo(server);
     String result = "{\"status\"=\"ok\"}";
     log.debug("Health check on access result: {}", result);
     return result;
   }
 
-  public String healthCheckSysinfo() throws IOException {
-    JiraServerInfo info = sysInfo();
+  public String healthCheckSysinfo(JiraItsServerInfo server) throws IOException {
+    JiraServerInfo info = sysInfo(server);
     String result =
         "{\"status\"=\"ok\",\"system\"=\"Jira\",\"version\"=\""
             + info.getVersion()
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraConfig.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraConfig.java
index a39e059..5b4e197 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraConfig.java
@@ -14,73 +14,135 @@
 
 package com.googlesource.gerrit.plugins.its.jira;
 
-import static java.lang.String.format;
-
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.project.CommentLinkInfoImpl;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.its.jira.restapi.JiraURL;
-import java.net.MalformedURLException;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-/** The JIRA plugin configuration as read from Gerrit config. */
+/** The JIRA plugin configuration as read from project config. */
 @Singleton
 public class JiraConfig {
-  static final String ERROR_MSG = "Unable to load plugin %s because of invalid configuration: %s";
-  static final String GERRIT_CONFIG_URL = "url";
-  static final String GERRIT_CONFIG_USERNAME = "username";
-  static final String GERRIT_CONFIG_PASSWORD = "password";
+  static final String PROJECT_CONFIG_URL_KEY = "instanceUrl";
+  static final String PROJECT_CONFIG_USERNAME_KEY = "username";
+  static final String PROJECT_CONFIG_PASSWORD_KEY = "password";
 
-  private final JiraURL jiraUrl;
-  private final String jiraUsername;
-  private final String jiraPassword;
+  private static final Logger log = LoggerFactory.getLogger(JiraConfig.class);
+  private static final String COMMENTLINK = "commentlink";
+  private static final String GERRIT_CONFIG_URL = "url";
+  private static final String GERRIT_CONFIG_USERNAME = "username";
+  private static final String GERRIT_CONFIG_PASSWORD = "password";
 
-  /**
-   * Builds an JiraConfig.
-   *
-   * @param config the gerrit server config
-   * @param pluginName the name of this very plugin
-   */
+  private final String pluginName;
+  private final PluginConfigFactory cfgFactory;
+  private final Config gerritConfig;
+  private final JiraItsServerInfo defaultJiraServerInfo;
+  private final GitRepositoryManager repoManager;
+  private final ProjectCache projectCache;
+  private final PersonIdent serverUser;
+
   @Inject
-  JiraConfig(@GerritServerConfig Config config, @PluginName String pluginName) {
-    try {
-      jiraUrl = new JiraURL(config.getString(pluginName, null, GERRIT_CONFIG_URL)).adjustUrlPath();
-    } catch (MalformedURLException e) {
-      throw new RuntimeException(format(ERROR_MSG, pluginName, e.getLocalizedMessage()));
-    }
+  JiraConfig(
+      @GerritServerConfig Config config,
+      @PluginName String pluginName,
+      PluginConfigFactory cfgFactory,
+      @GerritPersonIdent PersonIdent serverUser,
+      ProjectCache projectCache,
+      GitRepositoryManager repoManager) {
+    this.gerritConfig = config;
+    this.pluginName = pluginName;
+    this.cfgFactory = cfgFactory;
+    this.serverUser = serverUser;
+    this.projectCache = projectCache;
+    this.repoManager = repoManager;
+    this.defaultJiraServerInfo = buildDefaultServerInfo(gerritConfig, pluginName);
+  }
 
-    jiraUsername = config.getString(pluginName, null, GERRIT_CONFIG_USERNAME);
-    jiraPassword = config.getString(pluginName, null, GERRIT_CONFIG_PASSWORD);
-    if (jiraUrl == null || jiraUsername == null || jiraPassword == null) {
-      throw new RuntimeException(format(ERROR_MSG, pluginName, "missing username/password"));
+  private static JiraItsServerInfo buildDefaultServerInfo(Config gerritConfig, String pluginName) {
+    return JiraItsServerInfo.builder()
+        .url(gerritConfig.getString(pluginName, null, GERRIT_CONFIG_URL))
+        .username(gerritConfig.getString(pluginName, null, GERRIT_CONFIG_USERNAME))
+        .password(gerritConfig.getString(pluginName, null, GERRIT_CONFIG_PASSWORD))
+        .build();
+  }
+
+  JiraItsServerInfo getDefaultServerInfo() {
+    return defaultJiraServerInfo;
+  }
+
+  String getCommentLinkFromGerritConfig(String key) {
+    return gerritConfig.getString(COMMENTLINK, pluginName, key);
+  }
+
+  JiraItsServerInfo getServerInfoFor(String projectName) {
+    PluginConfig pluginConfig = getPluginConfigFor(projectName);
+    return JiraItsServerInfo.builder()
+        .url(pluginConfig.getString(PROJECT_CONFIG_URL_KEY, null))
+        .username(pluginConfig.getString(PROJECT_CONFIG_USERNAME_KEY, null))
+        .password(pluginConfig.getString(PROJECT_CONFIG_PASSWORD_KEY, null))
+        .build();
+  }
+
+  void addCommentLinksSection(Project.NameKey projectName, JiraItsServerInfo jiraItsServerInfo) {
+    try (Repository git = repoManager.openRepository(projectName);
+        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, projectName, git)) {
+      ProjectConfig config = ProjectConfig.read(md);
+      String link =
+          CharMatcher.is('/').trimFrom(jiraItsServerInfo.getUrl().toString()) + JiraURL.URL_SUFFIX;
+      if (!commentLinksExist(config, link)) {
+        String match = getCommentLinkFromGerritConfig("match");
+        CommentLinkInfoImpl commentlinkSection =
+            new CommentLinkInfoImpl(pluginName, match, link, null, true);
+        config.addCommentLinkSection(commentlinkSection);
+        md.getCommitBuilder().setAuthor(serverUser);
+        md.getCommitBuilder().setCommitter(serverUser);
+        projectCache.evict(config.getProject());
+        config.commit(md);
+      }
+    } catch (ConfigInvalidException | IOException e) {
+      throw new RuntimeException(e);
     }
   }
 
-  /**
-   * The Jira url to connect to.
-   *
-   * @return the jira url
-   */
-  public JiraURL getJiraUrl() {
-    return jiraUrl;
+  private boolean commentLinksExist(ProjectConfig config, String link) {
+    return config.getCommentLinkSections().stream().map(c -> c.link).anyMatch(link::equals);
   }
 
-  /**
-   * The username to connect to a Jira server.
-   *
-   * @return the username
-   */
-  public String getUsername() {
-    return jiraUsername;
-  }
-
-  /**
-   * The password to connect to a Jira server.
-   *
-   * @return the password
-   */
-  public String getPassword() {
-    return jiraPassword;
+  @VisibleForTesting
+  PluginConfig getPluginConfigFor(String projectName) {
+    if (!Strings.isNullOrEmpty(projectName)) {
+      try {
+        return cfgFactory.getFromProjectConfigWithInheritance(
+            new Project.NameKey(projectName), pluginName);
+      } catch (NoSuchProjectException e) {
+        log.warn(
+            "Unable to get project configuration for {}: project '{}' not found ",
+            pluginName,
+            projectName,
+            e);
+      }
+    }
+    return new PluginConfig(pluginName, new Config());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsFacade.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsFacade.java
index 4d066d5..4480c08 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsFacade.java
@@ -17,8 +17,6 @@
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.its.base.its.InvalidTransitionException;
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
-import com.googlesource.gerrit.plugins.its.jira.restapi.JiraProject;
-import com.googlesource.gerrit.plugins.its.jira.restapi.JiraServerInfo;
 import java.io.IOException;
 import java.net.URL;
 import java.util.concurrent.Callable;
@@ -33,19 +31,11 @@
 
   private final JiraClient jiraClient;
 
+  private JiraItsServerInfo itsServerInfo;
+
   @Inject
   public JiraItsFacade(JiraClient jiraClient) {
     this.jiraClient = jiraClient;
-    try {
-      JiraServerInfo info = this.jiraClient.sysInfo();
-      log.info(
-          "Connected to JIRA at {}, reported version is {}", info.getBaseUri(), info.getVersion());
-      for (JiraProject p : this.jiraClient.getProjects()) {
-        log.info("Found project: {} (key: {})", p.getName(), p.getKey());
-      }
-    } catch (Exception ex) {
-      log.warn("Jira is currently not available", ex);
-    }
   }
 
   @Override
@@ -54,19 +44,18 @@
     return execute(
         () -> {
           if (check.equals(Check.ACCESS)) {
-            return jiraClient.healthCheckAccess();
+            return jiraClient.healthCheckAccess(getJiraServerInstance());
           }
-          return jiraClient.healthCheckSysinfo();
+          return jiraClient.healthCheckSysinfo(getJiraServerInstance());
         });
   }
 
   @Override
   public void addComment(String issueKey, String comment) throws IOException {
-
     execute(
         () -> {
           log.debug("Adding comment {} to issue {}", comment, issueKey);
-          jiraClient.addComment(issueKey, comment);
+          jiraClient.addComment(getJiraServerInstance(), issueKey, comment);
           log.debug("Added comment {} to issue {}", comment, issueKey);
           return issueKey;
         });
@@ -81,7 +70,6 @@
 
   @Override
   public void performAction(String issueKey, String actionName) throws IOException {
-
     execute(
         () -> {
           log.debug("Performing action {} on issue {}", actionName, issueKey);
@@ -93,7 +81,7 @@
   private void doPerformAction(String issueKey, String actionName)
       throws IOException, InvalidTransitionException {
     log.debug("Trying to perform action: {} on issue {}", actionName, issueKey);
-    boolean ret = jiraClient.doTransition(issueKey, actionName);
+    boolean ret = jiraClient.doTransition(getJiraServerInstance(), issueKey, actionName);
     if (ret) {
       log.debug("Action {} successful on Issue {}", actionName, issueKey);
     } else {
@@ -113,7 +101,15 @@
 
   @Override
   public boolean exists(String issueKey) throws IOException {
-    return execute(() -> jiraClient.issueExists(issueKey));
+    return execute(() -> jiraClient.issueExists(getJiraServerInstance(), issueKey));
+  }
+
+  private JiraItsServerInfo getJiraServerInstance() {
+    return itsServerInfo;
+  }
+
+  public void setJiraServerInstance(JiraItsServerInfo server) {
+    itsServerInfo = server;
   }
 
   private <P> P execute(Callable<P> function) throws IOException {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServer.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServer.java
new file mode 100644
index 0000000..0a698aa
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServer.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2018 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.jira;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacadeFactory;
+
+/**
+ * Provides information about the single/current server configured. The information is tunneled back
+ * to its-base to perform the its-actions.
+ */
+public class JiraItsServer implements ItsFacadeFactory {
+  private final JiraConfig jiraConfig;
+  private final JiraItsFacade itsFacade;
+  private final JiraItsServerCache serverCache;
+
+  @Inject
+  public JiraItsServer(
+      JiraConfig jiraConfig, JiraItsFacade itsFacade, JiraItsServerCache serverCache) {
+    this.jiraConfig = jiraConfig;
+    this.itsFacade = itsFacade;
+    this.serverCache = serverCache;
+  }
+
+  /**
+   * Gets the server configuration from project.config. If the project config values are valid, it
+   * creates a commentlinks section for "its-jira" in the project config. Returns default
+   * configuration values from gerrit.config if no project config was provided. In case of invalid
+   * project config, its-jira tells the user that it is not able to connect.
+   *
+   * @param projectName the oroject for which the Jira server configuration should be returned
+   * @return the Jira server configuration for the project or the default Jira server configuration
+   *     if the project does not define a project-level Jira configuration
+   */
+  @Override
+  public JiraItsFacade getFacade(Project.NameKey projectName) {
+    JiraItsServerInfo jiraItsServerInfo = serverCache.get(projectName.get());
+    if (jiraItsServerInfo.isValid()) {
+      jiraConfig.addCommentLinksSection(projectName, jiraItsServerInfo);
+    } else {
+      jiraItsServerInfo = jiraConfig.getDefaultServerInfo();
+    }
+
+    if (!jiraItsServerInfo.isValid()) {
+      throw new RuntimeException(
+          String.format(
+              "No valid Jira server configuration was found for project '%s' %n."
+                  + "Missing one or more configuration values: url: %s, username: %s, password: %s",
+              projectName.get(),
+              jiraItsServerInfo.getUrl(),
+              jiraItsServerInfo.getUsername(),
+              jiraItsServerInfo.getPassword()));
+    }
+
+    itsFacade.setJiraServerInstance(jiraItsServerInfo);
+    return itsFacade;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerCache.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerCache.java
new file mode 100644
index 0000000..23e7310
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerCache.java
@@ -0,0 +1,34 @@
+// 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.jira;
+
+/** Cache of project-specific Jira servers */
+interface JiraItsServerCache {
+
+  /**
+   * Get the cached Jira server for a project
+   *
+   * @param projectName name of the project.
+   * @return the cached Jira server.
+   */
+  JiraItsServerInfo get(String projectName);
+
+  /**
+   * Invalidate the cached Jira server for the given project.
+   *
+   * @param projectName project for which the Jira server is being evicted.
+   */
+  void evict(String projectName);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerCacheImpl.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerCacheImpl.java
new file mode 100644
index 0000000..1e2c43b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerCacheImpl.java
@@ -0,0 +1,81 @@
+// 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.jira;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.name.Named;
+import java.util.concurrent.ExecutionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class JiraItsServerCacheImpl implements JiraItsServerCache {
+  private static final Logger log = LoggerFactory.getLogger(JiraItsServerCacheImpl.class);
+  private static final String CACHE_NAME = "jira_server_project";
+
+  private final LoadingCache<String, JiraItsServerInfo> cache;
+
+  @Inject
+  JiraItsServerCacheImpl(@Named(CACHE_NAME) LoadingCache<String, JiraItsServerInfo> cache) {
+    this.cache = cache;
+  }
+
+  @Override
+  public JiraItsServerInfo get(String projectName) {
+    try {
+      return cache.get(projectName);
+    } catch (ExecutionException e) {
+      log.warn("Cannot get project specific rules for project {}", projectName, e);
+      return JiraItsServerInfo.builder().url(null).username(null).password(null).build();
+    }
+  }
+
+  @Override
+  public void evict(String projectName) {
+    cache.invalidate(projectName);
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE_NAME, String.class, JiraItsServerInfo.class).loader(Loader.class);
+        bind(JiraItsServerCacheImpl.class);
+        bind(JiraItsServerCache.class).to(JiraItsServerCacheImpl.class);
+        DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+            .to(JiraItsServerProjectCacheRefresher.class);
+      }
+    };
+  }
+
+  static class Loader extends CacheLoader<String, JiraItsServerInfo> {
+    private final JiraConfig jiraConfig;
+
+    @Inject
+    Loader(JiraConfig jiraConfig) {
+      this.jiraConfig = jiraConfig;
+    }
+
+    @Override
+    public JiraItsServerInfo load(String projectName) {
+      return jiraConfig.getServerInfoFor(projectName);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerInfo.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerInfo.java
new file mode 100644
index 0000000..b384189
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerInfo.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2018 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.jira;
+
+import com.googlesource.gerrit.plugins.its.jira.restapi.JiraURL;
+import java.net.MalformedURLException;
+
+public class JiraItsServerInfo {
+  public static class Builder {
+    private JiraItsServerInfo instance = new JiraItsServerInfo();
+
+    private Builder() {}
+
+    public Builder url(String projectUrl) {
+      try {
+        instance.url = projectUrl != null ? new JiraURL(projectUrl) : null;
+        return this;
+      } catch (MalformedURLException e) {
+        throw new IllegalArgumentException("Unable to resolve URL", e);
+      }
+    }
+
+    public Builder username(String username) {
+      instance.username = username;
+      return this;
+    }
+
+    public Builder password(String password) {
+      instance.password = password;
+      return this;
+    }
+
+    public JiraItsServerInfo build() {
+      return instance;
+    }
+  }
+
+  private JiraURL url;
+  private String username;
+  private String password;
+
+  public static Builder builder() {
+    return new JiraItsServerInfo.Builder();
+  }
+
+  public JiraURL getUrl() {
+    return url;
+  }
+
+  public String getUsername() {
+    return username;
+  }
+
+  public String getPassword() {
+    return password;
+  }
+
+  public boolean isValid() {
+    return url != null && username != null && password != null;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerProjectCacheRefresher.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerProjectCacheRefresher.java
new file mode 100644
index 0000000..def23ea
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerProjectCacheRefresher.java
@@ -0,0 +1,54 @@
+// 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.jira;
+
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.inject.Inject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class JiraItsServerProjectCacheRefresher implements GitReferenceUpdatedListener {
+  private static final Logger log =
+      LoggerFactory.getLogger(JiraItsServerProjectCacheRefresher.class);
+
+  private final GerritApi gApi;
+  private final JiraItsServerCache jiraItsServerCache;
+
+  @Inject
+  JiraItsServerProjectCacheRefresher(GerritApi gApi, JiraItsServerCache jiraItsServerCache) {
+    this.gApi = gApi;
+    this.jiraItsServerCache = jiraItsServerCache;
+  }
+
+  @Override
+  public void onGitReferenceUpdated(Event event) {
+    if (!event.getRefName().equals(RefNames.REFS_CONFIG)) {
+      return;
+    }
+    String project = event.getProjectName();
+    jiraItsServerCache.evict(project);
+    try {
+      for (ProjectInfo childProject : gApi.projects().name(project).children()) {
+        jiraItsServerCache.evict(childProject.name);
+      }
+    } catch (RestApiException e) {
+      log.warn("Unable to evict its-jira server cache", e);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java
index 0a0a2dc..b9e98b3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java
@@ -14,16 +14,20 @@
 
 package com.googlesource.gerrit.plugins.its.jira;
 
+import static com.googlesource.gerrit.plugins.its.jira.JiraConfig.PROJECT_CONFIG_PASSWORD_KEY;
+import static com.googlesource.gerrit.plugins.its.jira.JiraConfig.PROJECT_CONFIG_URL_KEY;
+import static com.googlesource.gerrit.plugins.its.jira.JiraConfig.PROJECT_CONFIG_USERNAME_KEY;
+
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.its.base.ItsHookModule;
+import com.googlesource.gerrit.plugins.its.base.its.ItsConfig;
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacadeFactory;
-import com.googlesource.gerrit.plugins.its.base.its.SingleItsServer;
-import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -32,27 +36,30 @@
   private static final Logger LOG = LoggerFactory.getLogger(JiraModule.class);
 
   private final String pluginName;
-  private final Config gerritConfig;
   private final PluginConfigFactory pluginCfgFactory;
 
   @Inject
-  public JiraModule(
-      @PluginName String pluginName,
-      @GerritServerConfig Config config,
-      PluginConfigFactory pluginCfgFactory) {
+  public JiraModule(@PluginName String pluginName, PluginConfigFactory pluginCfgFactory) {
     this.pluginName = pluginName;
-    this.gerritConfig = config;
     this.pluginCfgFactory = pluginCfgFactory;
   }
 
   @Override
   protected void configure() {
-    if (gerritConfig.getString(pluginName, null, "url") != null) {
-      LOG.info("JIRA is configured as ITS");
-      bind(ItsFacade.class).to(JiraItsFacade.class).asEagerSingleton();
-      bind(ItsFacadeFactory.class).to(SingleItsServer.class);
-
-      install(new ItsHookModule(pluginName, pluginCfgFactory));
-    }
+    bind(ItsFacade.class).to(JiraItsFacade.class);
+    bind(ItsFacadeFactory.class).to(JiraItsServer.class).asEagerSingleton();
+    bind(ProjectConfigEntry.class)
+        .annotatedWith(Exports.named(PROJECT_CONFIG_URL_KEY))
+        .toInstance(new JiraUrlProjectConfigEntry("Server URL"));
+    bind(ProjectConfigEntry.class)
+        .annotatedWith(Exports.named(PROJECT_CONFIG_USERNAME_KEY))
+        .toInstance(new ProjectConfigEntry("JIRA username", ""));
+    bind(ProjectConfigEntry.class)
+        .annotatedWith(Exports.named(PROJECT_CONFIG_PASSWORD_KEY))
+        .toInstance(new ProjectConfigEntry("JIRA password", ""));
+    bind(ItsConfig.class);
+    install(new ItsHookModule(pluginName, pluginCfgFactory));
+    install(JiraItsServerCacheImpl.module());
+    LOG.info("JIRA is configured as ITS");
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraUrlProjectConfigEntry.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraUrlProjectConfigEntry.java
new file mode 100644
index 0000000..40c248c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraUrlProjectConfigEntry.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2018 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.jira;
+
+import com.google.gerrit.extensions.api.projects.ConfigValue;
+import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.googlesource.gerrit.plugins.its.jira.restapi.JiraURL;
+import java.net.MalformedURLException;
+
+/** A {@link ProjectConfigEntry} for the Jira url. */
+class JiraUrlProjectConfigEntry extends ProjectConfigEntry {
+
+  public static final String INVALID_URL_MSG = "******* Invalid URL *******";
+
+  /**
+   * Builds a @{link ProjectConfigEntry}.
+   *
+   * @param displayName the display name
+   */
+  JiraUrlProjectConfigEntry(String displayName) {
+    super(displayName, "");
+  }
+
+  @Override
+  public ConfigValue preUpdate(ConfigValue configValue) {
+    if (configValue.value != null && !configValue.value.isEmpty()) {
+      try {
+        new JiraURL(configValue.value);
+      } catch (MalformedURLException e) {
+        configValue.value = INVALID_URL_MSG;
+      }
+    }
+    return configValue;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiProvider.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiProvider.java
index 7ca8434..17809ab 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiProvider.java
@@ -14,36 +14,30 @@
 
 package com.googlesource.gerrit.plugins.its.jira.restapi;
 
-import com.google.inject.Inject;
-import com.googlesource.gerrit.plugins.its.jira.JiraConfig;
+import com.googlesource.gerrit.plugins.its.jira.JiraItsServerInfo;
 
 public class JiraRestApiProvider {
-  private JiraConfig jiraConfig;
 
-  @Inject
-  public JiraRestApiProvider(JiraConfig jiraConfig) {
-    this.jiraConfig = jiraConfig;
-  }
-
-  public <T> JiraRestApi<T> get(Class<T> classOfT, String classPrefix) {
+  public <T> JiraRestApi<T> get(
+      JiraItsServerInfo serverInfo, Class<T> classOfT, String classPrefix) {
     return new JiraRestApi<>(
-        jiraConfig.getJiraUrl(),
-        jiraConfig.getUsername(),
-        jiraConfig.getPassword(),
+        serverInfo.getUrl(),
+        serverInfo.getUsername(),
+        serverInfo.getPassword(),
         classOfT,
         classPrefix);
   }
 
-  public JiraRestApi<JiraIssue> getIssue() {
-    return get(JiraIssue.class, "/issue");
+  public JiraRestApi<JiraIssue> getIssue(JiraItsServerInfo serverInfo) {
+    return get(serverInfo, JiraIssue.class, "/issue");
   }
 
-  public JiraRestApi<JiraServerInfo> getServerInfo() {
-    return get(JiraServerInfo.class, "/serverInfo");
+  public JiraRestApi<JiraServerInfo> getServerInfo(JiraItsServerInfo server) {
+    return get(server, JiraServerInfo.class, "/serverInfo");
   }
 
-  public JiraRestApi<JiraProject[]> getProjects() {
-    return get(JiraProject[].class, "/project");
+  public JiraRestApi<JiraProject[]> getProjects(JiraItsServerInfo serverInfo) {
+    return get(serverInfo, JiraProject[].class, "/project");
   }
 
   public JiraRestApi<JiraVersion[]> getVersions() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraURL.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraURL.java
index 03935a5..ed0cac6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraURL.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraURL.java
@@ -22,13 +22,14 @@
 import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.URL;
-import java.util.Arrays;
 import java.util.Objects;
 import org.eclipse.jgit.util.HttpSupport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class JiraURL {
+  /** Suffix to create a comment link based on this URL */
+  public static final String URL_SUFFIX = "/browse/$1";
 
   private static final Logger log = LoggerFactory.getLogger(JiraURL.class);
 
@@ -43,7 +44,7 @@
   }
 
   public JiraURL resolveUrl(String... paths) {
-    String relativePath = String.join("", Arrays.asList(paths));
+    String relativePath = String.join("", paths);
     try {
       return new JiraURL(new URL(url, relativePath));
     } catch (MalformedURLException e) {
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 3f50930..db66b9b 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -134,6 +134,17 @@
            optional
     Issue-id enforced in commit message [MANDATORY/?]: suggested
 
+The connectivity of its-jira plugin with Jira server happens on-request. When an
+action is requested, a connection is established based on any of the two
+configuration's availability, i.e., global config extracted from gerrit.config or
+project level config from project.config.
+
+The way a Jira issue and its corresponding gerrit change are annotated can be
+configured by specifying rules in a separate config file. Global rules, applied
+by all configured ITS plugins, can be defined in the file
+`review_site/etc/its/actions.config`. Rules specific to @PLUGIN@ are defined in
+the file `review_site/etc/its/actions-@PLUGIN@.config`.
+
 **Sample actions-@Plugin@.config:**
 
     [rule "open"]
@@ -168,3 +179,64 @@
         event-type = patchset-created
         action = add-soy-comment Change ${its.formatLink($changeUrl)} is created.
         action = In Progress
+
+Multiple Jira servers integration
+---------------------------------
+
+```
+Please note that this feature is considered EXPERIMENTAL and should be used with
+caution, as it could expose sensitive information.
+```
+
+In corporate environments, it is not unusual to have multiple Jira servers
+and it is a common requirement to integrate Gerrit projects with those.
+
+This plugin offers the possibility of configuring integrations with multiple Jira
+servers at the Gerrit project level, i.e., a Gerrit project can be associated with
+a particular Jira instance. This is done by specifying the Jira server URL,
+username and password in the project configuration using the GUI controls
+this plugin adds to the project's General page. In this case, the *commentlink*
+section is automatically added by the plugin. It is also possible to add the
+configuration entries by manually editing the *project.config* file in the
+*refs/meta/config* branch.
+
+A typical Jira server configuration in the *project.config* file will look like:
+
+    [plugin "its-jira"]
+         enabled = true
+         instanceUrl = http://jiraserver:8075/
+         jiraUsername = *user*
+         password = *pass*
+
+    [commentlink "its-jira"]
+         match = ([A-Z]+-[0-9]+)
+         link = http://jiraserver:8075/browse/$1
+
+A different project could define its own Jira server in its *project.config*
+file:
+
+    [plugin "its-jira"]
+         enabled = true
+         instanceUrl = http://other_jiraserver:7171/
+         jiraUsername = *another_user*
+         password = *another_pass*
+
+    [commentlink "its-jira"]
+         match = (JIRA-ISSUE:[0-9]+)
+         link = http://other_jiraserver:7171/browse/$1
+
+In case its-jira plugin is enabled for a project but no Jira server is configured
+for the project, i.e., it is not specified in the *project.config* file, the
+default configuration will be the one defined in *gerrit.config*.
+
+If no Jira server information is defined in *gerrit.config* either, an error is
+logged and the Jira integration is disabled for the project.
+
+The credentials mentioned at the project level, i.e., in the *project.config* file,
+will take precedence over the global credentials mentioned in *secure.config*.
+It is important to notice that __the credentials at the project level are stored as
+clear text and will be visible to anyone having access to the
+*refs/meta/config branch* like project owners and site administrators__. This is a
+limitation and the reason why this feature is marked as experimental, i.e., not
+production ready. Additional work is needed in order to offer a secure level of
+encryption for this information.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraConfigTest.java
index e25d1ef..66e151e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraConfigTest.java
@@ -15,14 +15,17 @@
 package com.googlesource.gerrit.plugins.its.jira;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.googlesource.gerrit.plugins.its.jira.JiraConfig.GERRIT_CONFIG_PASSWORD;
-import static com.googlesource.gerrit.plugins.its.jira.JiraConfig.GERRIT_CONFIG_URL;
-import static com.googlesource.gerrit.plugins.its.jira.JiraConfig.GERRIT_CONFIG_USERNAME;
 import static org.mockito.Mockito.when;
 
-import com.googlesource.gerrit.plugins.its.jira.restapi.JiraURL;
-import java.net.MalformedURLException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -32,39 +35,31 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class JiraConfigTest {
-
-  private static final String PASS = "pass";
-  private static final JiraURL TEST_URL = newUrl("http://jira_example.com/");
-  private static final String USER = "user";
   private static final String PLUGIN_NAME = "its-jira";
 
   @Rule public ExpectedException thrown = ExpectedException.none();
+
   @Mock private Config cfg;
+  @Mock private PluginConfigFactory cfgFactory;
+  @Mock private PersonIdent serverUser;
+  @Mock private ProjectCache projectCache;
+  @Mock private GitRepositoryManager repoManager;
 
   private JiraConfig jiraConfig;
 
-  @Test
-  public void gerritConfigContainsSaneValues() throws Exception {
-    when(cfg.getString(PLUGIN_NAME, null, GERRIT_CONFIG_URL)).thenReturn(TEST_URL.toString());
-    when(cfg.getString(PLUGIN_NAME, null, GERRIT_CONFIG_USERNAME)).thenReturn(USER);
-    when(cfg.getString(PLUGIN_NAME, null, GERRIT_CONFIG_PASSWORD)).thenReturn(PASS);
-    jiraConfig = new JiraConfig(cfg, PLUGIN_NAME);
-    assertThat(jiraConfig.getUsername()).isEqualTo(USER);
-    assertThat(jiraConfig.getPassword()).isEqualTo(PASS);
-    assertThat(jiraConfig.getJiraUrl()).isEqualTo(TEST_URL);
+  @Before
+  public void createJiraConfig() {
+    jiraConfig =
+        new JiraConfig(cfg, PLUGIN_NAME, cfgFactory, serverUser, projectCache, repoManager);
   }
 
   @Test
-  public void gerritConfigContainsNullValues() throws Exception {
-    thrown.expect(RuntimeException.class);
-    jiraConfig = new JiraConfig(cfg, PLUGIN_NAME);
-  }
-
-  private static JiraURL newUrl(String url) {
-    try {
-      return new JiraURL(url);
-    } catch (MalformedURLException e) {
-      throw new RuntimeException(e);
-    }
+  public void testGetPluginConfigFor() throws NoSuchProjectException {
+    Project.NameKey project = new Project.NameKey("$project");
+    PluginConfig pluginCfg = new PluginConfig(PLUGIN_NAME, new Config());
+    when(cfgFactory.getFromProjectConfigWithInheritance(project, PLUGIN_NAME))
+        .thenReturn(pluginCfg);
+    jiraConfig.getPluginConfigFor(project.get());
+    assertThat(pluginCfg).isNotNull();
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraItsFacadeTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraItsFacadeTest.java
index b5bae41..56dfb09 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraItsFacadeTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraItsFacadeTest.java
@@ -14,17 +14,12 @@
 
 package com.googlesource.gerrit.plugins.its.jira;
 
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import com.googlesource.gerrit.plugins.its.base.its.InvalidTransitionException;
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacade.Check;
-import com.googlesource.gerrit.plugins.its.jira.restapi.JiraProject;
-import com.googlesource.gerrit.plugins.its.jira.restapi.JiraServerInfo;
 import java.io.IOException;
 import java.net.URL;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -39,54 +34,42 @@
   private static final String PROJECT_KEY = "projectKey";
 
   @Mock private JiraClient jiraClient;
-
+  private JiraItsServerInfo server;
   private JiraItsFacade jiraFacade;
 
-  @Before
-  public void setUp() throws Exception {
-    JiraServerInfo jiraServerInfo = mock(JiraServerInfo.class);
-    when(jiraServerInfo.getBaseUri()).thenReturn("http://jira-server.com");
-    when(jiraServerInfo.getVersion()).thenReturn("v1");
-    when(jiraClient.sysInfo()).thenReturn(jiraServerInfo);
-    JiraProject jiraProject = mock(JiraProject.class);
-    when(jiraProject.getKey()).thenReturn("key1");
-    when(jiraProject.getName()).thenReturn("testProject");
-    when(jiraClient.getProjects()).thenReturn(new JiraProject[] {jiraProject});
-  }
-
   @Test
   public void healthCheckAccess() throws IOException {
     jiraFacade = new JiraItsFacade(jiraClient);
     jiraFacade.healthCheck(Check.ACCESS);
-    verify(jiraClient).healthCheckAccess();
+    verify(jiraClient).healthCheckAccess(server);
   }
 
   @Test
   public void healthCheckSysInfo() throws IOException {
     jiraFacade = new JiraItsFacade(jiraClient);
     jiraFacade.healthCheck(Check.SYSINFO);
-    verify(jiraClient).healthCheckSysinfo();
+    verify(jiraClient).healthCheckSysinfo(server);
   }
 
   @Test
   public void addComment() throws IOException {
     jiraFacade = new JiraItsFacade(jiraClient);
     jiraFacade.addComment(ISSUE_KEY, COMMENT);
-    verify(jiraClient).addComment(ISSUE_KEY, COMMENT);
+    verify(jiraClient).addComment(server, ISSUE_KEY, COMMENT);
   }
 
   @Test
   public void addRelatedLink() throws IOException {
     jiraFacade = new JiraItsFacade(jiraClient);
     jiraFacade.addRelatedLink(ISSUE_KEY, new URL("http://jira.com"), "description");
-    verify(jiraClient).addComment(ISSUE_KEY, "Related URL: [description|http://jira.com]");
+    verify(jiraClient).addComment(server, ISSUE_KEY, "Related URL: [description|http://jira.com]");
   }
 
   @Test
   public void performAction() throws IOException, InvalidTransitionException {
     jiraFacade = new JiraItsFacade(jiraClient);
     jiraFacade.performAction(ISSUE_KEY, ACTION);
-    verify(jiraClient).doTransition(ISSUE_KEY, ACTION);
+    verify(jiraClient).doTransition(server, ISSUE_KEY, ACTION);
   }
 
   @Test
@@ -100,6 +83,6 @@
   public void exists() throws IOException {
     jiraFacade = new JiraItsFacade(jiraClient);
     jiraFacade.exists(ISSUE_KEY);
-    verify(jiraClient).issueExists(ISSUE_KEY);
+    verify(jiraClient).issueExists(server, ISSUE_KEY);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerTest.java
new file mode 100644
index 0000000..06daa15
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraItsServerTest.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2018 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.jira;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.reviewdb.client.Project;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JiraItsServerTest {
+  private static final Project.NameKey PROJECT_NAMEKEY = new Project.NameKey("project");
+
+  @Mock private JiraConfig jiraConfig;
+  @Mock private JiraItsFacade itsFacade;
+  @Mock private JiraItsServerCache serverCache;
+  @Mock private JiraItsServerInfo jiraItsServerInfo;
+
+  @Rule public ExpectedException expectedException = ExpectedException.none();
+
+  private JiraItsServer jiraItsServer;
+
+  @Test
+  public void testValidServerInfoIsreturnedFromTheCache() throws Exception {
+    when(jiraItsServerInfo.isValid()).thenReturn(true);
+    when(serverCache.get(PROJECT_NAMEKEY.get())).thenReturn(jiraItsServerInfo);
+    jiraItsServer = new JiraItsServer(jiraConfig, itsFacade, serverCache);
+    jiraItsServer.getFacade(PROJECT_NAMEKEY);
+    verify(jiraConfig).addCommentLinksSection(PROJECT_NAMEKEY, jiraItsServerInfo);
+    verify(itsFacade).setJiraServerInstance(jiraItsServerInfo);
+  }
+
+  @Test
+  public void testGetDefaultServerInfo() throws Exception {
+    when(jiraItsServerInfo.isValid()).thenReturn(false).thenReturn(true);
+    when(serverCache.get(PROJECT_NAMEKEY.get())).thenReturn(jiraItsServerInfo);
+    when(jiraConfig.getDefaultServerInfo()).thenReturn(jiraItsServerInfo);
+    jiraItsServer = new JiraItsServer(jiraConfig, itsFacade, serverCache);
+    jiraItsServer.getFacade(PROJECT_NAMEKEY);
+    verify(jiraConfig, never()).addCommentLinksSection(PROJECT_NAMEKEY, jiraItsServerInfo);
+    verify(itsFacade).setJiraServerInstance(jiraItsServerInfo);
+  }
+
+  @Test
+  public void testNoConfiguredServerInfo() throws Exception {
+    when(serverCache.get(PROJECT_NAMEKEY.get())).thenReturn(jiraItsServerInfo);
+    when(jiraItsServerInfo.isValid()).thenReturn(false).thenReturn(false);
+    when(jiraConfig.getDefaultServerInfo()).thenReturn(jiraItsServerInfo);
+    jiraItsServer = new JiraItsServer(jiraConfig, itsFacade, serverCache);
+    expectedException.expect(RuntimeException.class);
+    jiraItsServer.getFacade(PROJECT_NAMEKEY);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiTest.java
index 1ee2ab2..1fa7f6a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiTest.java
@@ -32,10 +32,10 @@
 public class JiraRestApiTest {
   private static final String ISSUE_CLASS_PREFIX = "/issue/";
   private static final String JSON_PAYLOAD = "{}";
+  private static final String USERNAME = "user";
+  private static final String PASSWORD = "pass";
 
   private JiraURL url;
-  private String user = "user";
-  private String password = "pass";
   private JiraRestApi restApi;
 
   private void setURL(String jiraUrl) throws MalformedURLException {
@@ -45,7 +45,7 @@
   @Test
   public void testJiraServerInfoForNonRootJiraUrl() throws Exception {
     setURL("http://jira.mycompany.com/myroot/");
-    restApi = new JiraRestApi(url, user, password, JiraIssue.class, ISSUE_CLASS_PREFIX);
+    restApi = new JiraRestApi(url, USERNAME, PASSWORD, JiraIssue.class, ISSUE_CLASS_PREFIX);
     String jiraApiUrl = restApi.getBaseUrl().toString();
     assertThat(jiraApiUrl).startsWith(url.toString());
   }
@@ -53,15 +53,15 @@
   @Test
   public void testJiraServerInfoForNonRootJiraUrlNotEndingWithSlash() throws Exception {
     setURL("http://jira.mycompany.com/myroot");
-    restApi = new JiraRestApi(url, user, password, JiraIssue.class, ISSUE_CLASS_PREFIX);
+    restApi = new JiraRestApi(url, USERNAME, PASSWORD, JiraIssue.class, ISSUE_CLASS_PREFIX);
     String jiraApiUrl = restApi.getBaseUrl().toString();
     assertThat(jiraApiUrl).startsWith(url.toString());
   }
 
   @Test
   public void testJiraServerInfoForRootJiraUrl() throws Exception {
-    setURL("http://jira.mycompany.com");
-    restApi = new JiraRestApi(url, user, password, JiraIssue.class, ISSUE_CLASS_PREFIX);
+    setURL("http://jira.mycompany.com/myroot");
+    restApi = new JiraRestApi(url, USERNAME, PASSWORD, JiraIssue.class, ISSUE_CLASS_PREFIX);
     String jiraApiUrl = restApi.getBaseUrl().toString();
     assertThat(jiraApiUrl).startsWith(url.toString());
   }
@@ -78,7 +78,7 @@
     when(connection.getOutputStream()).thenReturn(connectionOutputStream);
     when(connection.getResponseCode()).thenReturn(HTTP_NO_CONTENT);
 
-    restApi = new JiraRestApi(url, user, password, JiraIssue.class, ISSUE_CLASS_PREFIX);
+    restApi = new JiraRestApi(url, USERNAME, PASSWORD, JiraIssue.class, ISSUE_CLASS_PREFIX);
     boolean pass = restApi.doPut(ISSUE_CLASS_PREFIX, JSON_PAYLOAD, HTTP_NO_CONTENT);
 
     verify(connection).setRequestMethod("PUT");