Merge "Allow Jira REST API to be located outside of the server root"
diff --git a/BUILD b/BUILD
index 8762bf6..c22e753 100644
--- a/BUILD
+++ b/BUILD
@@ -1,9 +1,14 @@
-load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "gerrit_plugin",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+)
 
 gerrit_plugin(
     name = "its-jira",
     srcs = glob(["src/main/java/**/*.java"]),
-    resources = glob(["src/main/resources/**/*"]),
     manifest_entries = [
         "Gerrit-PluginName: its-jira",
         "Gerrit-Module: com.googlesource.gerrit.plugins.its.jira.JiraModule",
@@ -12,8 +17,31 @@
         "Implementation-Title: Jira ITS Plugin",
         "Implementation-URL: http://www.gerritforge.com",
     ],
+    resources = glob(["src/main/resources/**/*"]),
     deps = [
         "//plugins/its-base",
     ],
 )
 
+junit_tests(
+    name = "its_jira_tests",
+    testonly = 1,
+    srcs = glob(
+        ["src/test/java/**/*.java"],
+    ),
+    tags = ["its-jira"],
+    deps = [
+        "its-jira__plugin_test_deps",
+    ],
+)
+
+java_library(
+    name = "its-jira__plugin_test_deps",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":its-jira__plugin",
+        "//plugins/its-base",
+        "@mockito//jar",
+    ],
+)
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..2709df3
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,25 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+  maven_jar(
+    name = "mockito",
+    artifact = "org.mockito:mockito-core:2.13.0",
+    sha1 = "8e372943974e4a121fb8617baced8ebfe46d54f0",
+    deps = [
+      '@byte-buddy//jar',
+      '@objenesis//jar',
+    ],
+  )
+
+  maven_jar(
+    name = "byte-buddy",
+    artifact = "net.bytebuddy:byte-buddy:1.7.9",
+    sha1 = "51218a01a882c04d0aba8c028179cce488bbcb58",
+  )
+
+  maven_jar(
+    name = "objenesis",
+    artifact = "org.objenesis:objenesis:2.6",
+    sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
+  )
+
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 ab1ca48..72c5acc 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
@@ -84,7 +84,7 @@
   public List<JiraTransition.Item> getTransitions(String issueKey) throws IOException {
 
     JiraRestApi<JiraTransition> api = apiBuilder.get(JiraTransition.class, "/issue");
-    return Arrays.asList(api.doGet("/" + issueKey + "/transitions", HTTP_OK).transitions);
+    return Arrays.asList(api.doGet("/" + issueKey + "/transitions", HTTP_OK).getTransitions());
   }
 
   /**
@@ -144,4 +144,25 @@
     }
     return null;
   }
+
+  public String healthCheckAccess() throws IOException {
+    sysInfo();
+    String result = "{\"status\"=\"ok\"}";
+    log.debug("Health check on access result: {}", result);
+    return result;
+  }
+
+  public String healthCheckSysinfo() throws IOException {
+    JiraServerInfo info = sysInfo();
+    String result =
+        "{\"status\"=\"ok\",\"system\"=\"Jira\",\"version\"=\""
+            + info.getVersion()
+            + "\",\"url\"=\""
+            + info.getBaseUri()
+            + "\",\"build\"=\""
+            + info.getBuildNumber()
+            + "\"}";
+    log.debug("Health check on sysinfo result: {}", result);
+    return result;
+  }
 }
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
new file mode 100644
index 0000000..b757b65
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraConfig.java
@@ -0,0 +1,79 @@
+// 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 static java.lang.String.format;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+/** The JIRA plugin configuration as read from Gerrit config. */
+@Singleton
+public class JiraConfig {
+  static final String ERROR_MSG = "Unable to load plugin %s. Cause: Wrong configuration ";
+  static final String GERRIT_CONFIG_URL = "url";
+  static final String GERRIT_CONFIG_USERNAME = "username";
+  static final String GERRIT_CONFIG_PASSWORD = "password";
+
+  private final String jiraUrl;
+  private final String jiraUsername;
+  private final String jiraPassword;
+
+  /**
+   * Builds an JiraConfig.
+   *
+   * @param config the gerrit server config
+   * @param pluginName the name of this very plugin
+   */
+  @Inject
+  JiraConfig(@GerritServerConfig Config config, @PluginName String pluginName) {
+    jiraUrl = config.getString(pluginName, null, GERRIT_CONFIG_URL);
+    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));
+    }
+  }
+
+  /**
+   * The Jira url to connect to.
+   *
+   * @return the jira url
+   */
+  public String getJiraUrl() {
+    return jiraUrl;
+  }
+
+  /**
+   * 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;
+  }
+}
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 cb20458..790cefd 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
@@ -14,8 +14,6 @@
 
 package com.googlesource.gerrit.plugins.its.jira;
 
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.its.base.its.InvalidTransitionException;
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
@@ -25,30 +23,23 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.concurrent.Callable;
-import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class JiraItsFacade implements ItsFacade {
 
-  private static final String GERRIT_CONFIG_USERNAME = "username";
-  private static final String GERRIT_CONFIG_PASSWORD = "password";
-  private static final String GERRIT_CONFIG_URL = "url";
-
   private static final int MAX_ATTEMPTS = 3;
 
   private Logger log = LoggerFactory.getLogger(JiraItsFacade.class);
 
-  private final String pluginName;
-  private Config gerritConfig;
+  private final JiraConfig jiraConfig;
 
   private JiraClient client;
 
   @Inject
-  public JiraItsFacade(@PluginName String pluginName, @GerritServerConfig Config cfg) {
-    this.pluginName = pluginName;
+  public JiraItsFacade(JiraConfig jiraConfig) {
+    this.jiraConfig = jiraConfig;
     try {
-      this.gerritConfig = cfg;
       JiraServerInfo info = client().sysInfo();
       log.info(
           "Connected to JIRA at {}, reported version is {}", info.getBaseUri(), info.getVersion());
@@ -61,17 +52,19 @@
   }
 
   @Override
-  public String healthCheck(final Check check) throws IOException {
+  public String healthCheck(Check check) throws IOException {
 
     return execute(
         () -> {
-          if (check.equals(Check.ACCESS)) return healthCheckAccess();
-          return healthCheckSysinfo();
+          if (check.equals(Check.ACCESS)) {
+            return client().healthCheckAccess();
+          }
+          return client().healthCheckSysinfo();
         });
   }
 
   @Override
-  public void addComment(final String issueKey, final String comment) throws IOException {
+  public void addComment(String issueKey, String comment) throws IOException {
 
     execute(
         () -> {
@@ -83,14 +76,14 @@
   }
 
   @Override
-  public void addRelatedLink(final String issueKey, final URL relatedUrl, String description)
+  public void addRelatedLink(String issueKey, URL relatedUrl, String description)
       throws IOException {
     addComment(
         issueKey, "Related URL: " + createLinkForWebui(relatedUrl.toExternalForm(), description));
   }
 
   @Override
-  public void performAction(final String issueKey, final String actionName) throws IOException {
+  public void performAction(String issueKey, String actionName) throws IOException {
 
     execute(
         () -> {
@@ -100,7 +93,7 @@
         });
   }
 
-  private void doPerformAction(final String issueKey, final String actionName)
+  private void doPerformAction(String issueKey, String actionName)
       throws IOException, InvalidTransitionException {
     log.debug("Trying to perform action: {} on issue {}", actionName, issueKey);
     boolean ret = client().doTransition(issueKey, actionName);
@@ -112,15 +105,17 @@
   }
 
   @Override
-  public boolean exists(final String issueKey) throws IOException {
+  public boolean exists(String issueKey) throws IOException {
     return execute(() -> client().issueExists(issueKey));
   }
 
   private JiraClient client() throws MalformedURLException {
     if (client == null) {
-      log.debug("Connecting to jira at {}", getUrl());
-      client = new JiraClient(getUrl(), getUsername(), getPassword());
-      log.debug("Authenticating as User {}", getUsername());
+      log.debug("Connecting to jira at {}", jiraConfig.getJiraUrl());
+      client =
+          new JiraClient(
+              jiraConfig.getJiraUrl(), jiraConfig.getUsername(), jiraConfig.getPassword());
+      log.debug("Authenticating as User {}", jiraConfig.getUsername());
     }
     return client;
   }
@@ -146,41 +141,8 @@
     return className.startsWith("java.net");
   }
 
-  private String getPassword() {
-    return gerritConfig.getString(pluginName, null, GERRIT_CONFIG_PASSWORD);
-  }
-
-  private String getUsername() {
-    return gerritConfig.getString(pluginName, null, GERRIT_CONFIG_USERNAME);
-  }
-
-  private String getUrl() {
-    return gerritConfig.getString(pluginName, null, GERRIT_CONFIG_URL);
-  }
-
   @Override
   public String createLinkForWebui(String url, String text) {
     return "[" + text + "|" + url + "]";
   }
-
-  private String healthCheckAccess() throws IOException {
-    client().sysInfo();
-    final String result = "{\"status\"=\"ok\",\"username\"=\"" + getUsername() + "\"}";
-    log.debug("Health check on access result: {}", result);
-    return result;
-  }
-
-  private String healthCheckSysinfo() throws IOException {
-    JiraServerInfo info = client().sysInfo();
-    final String result =
-        "{\"status\"=\"ok\",\"system\"=\"Jira\",\"version\"=\""
-            + info.getVersion()
-            + "\",\"url\"=\""
-            + getUrl()
-            + "\",\"build\"=\""
-            + info.getBuildNumber()
-            + "\"}";
-    log.debug("Health check on sysinfo result: {}", result);
-    return result;
-  }
 }
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 2b1119e..ac3889a 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
@@ -47,7 +47,7 @@
   protected void configure() {
     if (gerritConfig.getString(pluginName, null, "url") != null) {
       LOG.info("JIRA is configured as ITS");
-      bind(ItsFacade.class).toInstance(new JiraItsFacade(pluginName, gerritConfig));
+      bind(ItsFacade.class).to(JiraItsFacade.class).asEagerSingleton();
 
       install(new ItsHookModule(pluginName, pluginCfgFactory));
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraComment.java
index e3e2f0e..eb2755d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraComment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraComment.java
@@ -16,9 +16,13 @@
 
 public class JiraComment {
 
-  final String body;
+  private final String body;
 
   public JiraComment(String body) {
     this.body = body;
   }
+
+  public String getBody() {
+    return body;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraIssue.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraIssue.java
index 8a22098..0eab61b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraIssue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraIssue.java
@@ -15,6 +15,19 @@
 package com.googlesource.gerrit.plugins.its.jira.restapi;
 
 public class JiraIssue {
-  String id;
-  String key;
+  private final String id;
+  private final String key;
+
+  public JiraIssue(String id, String key) {
+    this.id = id;
+    this.key = key;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public String getKey() {
+    return key;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraProject.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraProject.java
index d244cb4..fb4b3be 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraProject.java
@@ -16,9 +16,15 @@
 
 public class JiraProject {
 
-  String id;
-  String key;
-  String name;
+  private final String id;
+  private final String key;
+  private final String name;
+
+  public JiraProject(String id, String key, String name) {
+    this.id = id;
+    this.key = key;
+    this.name = name;
+  }
 
   public String getId() {
     return id;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraTransition.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraTransition.java
index 4bbad2c..b541217 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraTransition.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraTransition.java
@@ -17,18 +17,35 @@
 public class JiraTransition {
 
   // 'Get Transactions' require a list of items
-  public Item[] transitions;
+  private Item[] transitions;
 
   // 'Do Transaction' require a single item
-  Item transition;
+  private Item transition;
+
+  public JiraTransition(Item[] transitions) {
+    this.transitions = transitions;
+  }
 
   public JiraTransition(Item transition) {
     this.transition = transition;
   }
 
+  public Item[] getTransitions() {
+    return transitions;
+  }
+
+  public Item getTransition() {
+    return transition;
+  }
+
   public static class Item {
-    String name;
-    String id;
+    private final String name;
+    private final String id;
+
+    public Item(String name, String id) {
+      this.name = name;
+      this.id = id;
+    }
 
     public String getName() {
       return name;
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 1235321..4e44633 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -7,6 +7,14 @@
 [plugins/its-base](https://gerrit-review.googlesource.com/#/admin/projects/plugins/its-base)
 to the `plugins` directory of Gerrit's source tree.
 
+Put the external dependency Bazel build file into the Gerrit plugins directory,
+replacing the existing empty one.
+
+```
+  cd gerrit/plugins
+  ln -sf @PLUGIN@/external_plugin_deps.bzl .
+```
+
 Then issue
 
 ```
@@ -22,13 +30,20 @@
 ```
 
 This project can be imported into the Eclipse IDE.
-Add the plugin name to the `CUSTOM_PLUGINS` set in
-Gerrit core in `tools/bzl/plugins.bzl`, and execute:
+Add the plugin name to the `CUSTOM_PLUGINS` and
+`CUSTOM_PLUGINS_TEST_DEPS` sets in the file
+`<gerrit_source_code>/tools/bzl/plugins.bzl` and execute:
 
 ```
   ./tools/eclipse/project.py
 ```
 
+To execute the tests run:
+
+```
+  bazel test plugins/@PLUGIN@:its_jira_tests
+```
+
 [Back to @PLUGIN@ documentation index][index]
 
 [index]: index.html
\ No newline at end of file
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 3eff578..5864b75 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -134,3 +134,37 @@
            optional
     Issue-id enforced in commit message [MANDATORY/?]: suggested
 
+**Sample actions-@Plugin@.config:**
+
+    [rule "open"]
+        event-type = patchset-created
+        action = add-velocity-comment inline Change ${its.formatLink($changeUrl)} is created.
+        action = In Progress
+    [rule "resolve"]
+        event-type = comment-added
+        approval-Code-Review = 2
+        action = add-velocity-comment inline Change ${its.formatLink($changeUrl)} is verified.
+        action = In Review
+    [rule "merged"]
+        event-type = change-merged
+        action = add-velocity-comment inline Change ${its.formatLink($changeUrl)} is merged.
+        action = Done
+    [rule "abandoned"]
+        event-type = change-abandoned'
+        action = add-velocity-comment inline Change ${its.formatLink($changeUrl)} is abandoned.
+        action = To Do
+
+The first rule triggers an action which adds a comment and a hyperlink to the change created
+in gerrit. The comment will appear in an Jira issue's `Comment` section whenever a patchset-created event
+is triggered. The second action item in the first rule transitions the state of the issue
+in Jira to `In Progress`. The title of the action `In Progress` should match the workflow actions
+used by the JIRA server as different versions of JIRA can have different workflow actions.
+
+**Note:** Velocity comments were deprecated in Gerrit 2.14 and will be removed in Gerrit 2.16/3.0;
+the `actions-@Plugin@.config` needs to be changed accordingly. For example, to use Soy comments instead
+of velocity comments:
+
+    [rule "open"]
+        event-type = patchset-created
+        action = add-soy-comment Change ${its.formatLink($changeUrl)} is created.
+        action = In Progress
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
new file mode 100644
index 0000000..f7f5a40
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/jira/JiraConfigTest.java
@@ -0,0 +1,63 @@
+// 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 com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.its.jira.JiraConfig.ERROR_MSG;
+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 java.lang.String.format;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.jgit.lib.Config;
+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 JiraConfigTest {
+
+  private static final String PASS = "pass";
+  private static final String URL = "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;
+
+  private JiraConfig jiraConfig;
+
+  @Test
+  public void gerritConfigContainsSaneValues() throws Exception {
+    when(cfg.getString(PLUGIN_NAME, null, GERRIT_CONFIG_URL)).thenReturn(URL);
+    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(URL);
+  }
+
+  @Test
+  public void gerritConfigContainsNullValues() throws Exception {
+    thrown.expect(RuntimeException.class);
+    thrown.expectMessage(format(ERROR_MSG, PLUGIN_NAME));
+    jiraConfig = new JiraConfig(cfg, PLUGIN_NAME);
+  }
+}