Support multiple Jira instances

Previously, its-jira only supported connecting to a single Jira server.
In corporate environments, it is not unusual to have multiple Jira
servers and it is a common requirement to integrate Gerrit with those.

This change enables its-jira to integrate 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. The plugin adds
a "commentlink" section accordingly.

A typical Jira server configuration in project.config will look like:

   [plugin "its-jira"]
     enabled = true
     instanceUrl = http://localhost:8075/
     jiraUsername = admin
     password = admin

   [commentlink "its-jira"]
     match = ([A-Z]+-[0-9]+)
     link = http://localhost:8075/browse/$1

In case its-jira plugin is enabled for a project but no Jira server is
configured for this project, the default configuration will be the one
defined in gerrit.config. If no Jira server information is defined in
gerrit.config, an error is logged and the Jira integration is disabled
for the project.

Please note that this functionality is considered EXPERIMENTAL as the
credentials are stored in clear text at the project level and sensitive
information (the password of the Jira instance) could be exposed. The
work to add encryption is ongoing [1] and once a solution is found to
the raised issues, it will be leveraged by this plugin.

[1] https://gerrit-review.googlesource.com/c/gerrit/+/177390

Change-Id: I88f7298055ec1a136348758d4ec6fab67668e1ca
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 4a77ca3..c17affd 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
@@ -52,9 +52,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) {
@@ -77,9 +76,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());
   }
 
@@ -88,12 +87,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 {
@@ -106,33 +106,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;
       }
@@ -140,15 +140,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 c14f2e6..866a664 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 {
@@ -103,7 +91,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 d09441a..2e31d1b 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,35 +14,29 @@
 
 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");
   }
 }
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 6d645c8..9241d36 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;
@@ -38,60 +33,48 @@
   private static final String ISSUE_KEY = "issueKey";
 
   @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
   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");