Add support for the Storyboard issue tracking system integration
This implements the ability to add comments to storybaord task tracking
system[1][2]. Updating of story status will be implemented in a follow
on change.
[1] https://wiki.openstack.org/wiki/StoryBoard
[2] http://git.openstack.org/cgit/openstack-infra/storyboard
Change-Id: If25ec21d4b89ef82dfef3d5f1f2dec1cc8294005
diff --git a/BUCK b/BUCK
index 7853f51..0a40036 100644
--- a/BUCK
+++ b/BUCK
@@ -4,13 +4,100 @@
resources = glob(['src/main/resources/**/*']),
manifest_entries = [
'Gerrit-PluginName: its-storyboard',
- 'Gerrit-Module: com.googlesource.gerrit.plugins.hooks.sb.InitStoryboard',
- 'Gerrit-InitStep: com.googlesource.gerrit.plugins.hooks.sb.StoryboardModule',
+ 'Gerrit-Module: com.googlesource.gerrit.plugins.its.storyboard.StoryboardModule',
'Gerrit-ReloadMode: reload',
- 'Implementation-Title: Plugin its-storyboard',
- 'Implementation-URL: https://www.openstack.org',
+ 'Gerrit-ApiType: plugin',
+ 'Implementation-Vendor: Hewlett Packard',
+ 'Implementation-Title: its-storyboard plugin',
+ 'Implementation-URL: https://gerrit.googlesource.com/plugins/its-storyboard',
],
deps = [
- '//plugins/its-base:its-base__plugin',
+ ':its-base_stripped',
+ '//lib/httpcomponents:httpcore',
+ '//lib/httpcomponents:httpclient',
+ '//lib:gson',
+ ],
+)
+
+
+def strip_jar(
+ name,
+ src,
+ excludes = [],
+ visibility = [],
+ ):
+ name_zip = name + '.zip'
+ genrule(
+ name = name_zip,
+ cmd = 'cp $SRCS $OUT && zip -qd $OUT ' + ' '.join(excludes),
+ srcs = [ src ],
+ deps = [ src ],
+ out = name_zip,
+ visibility = visibility,
+ )
+ prebuilt_jar(
+ name = name,
+ binary_jar = ':' + name_zip,
+ visibility = visibility,
+ )
+
+strip_jar(
+ name = 'its-base_stripped',
+ src = '//plugins/its-base:its-base',
+ excludes = [
+ 'Documentation/about.md',
+ 'Documentation/build.md',
+ 'Documentation/config-connectivity.md',
+ 'Documentation/config-rulebase-plugin-actions.md',
+ ]
+)
+
+TEST_UTIL_SRC = glob(['src/test/java/com/googlesource/gerrit/plugins/its/testutil/**/*.java'])
+
+java_library(
+ name = 'its-storyboard_tests-utils',
+ srcs = TEST_UTIL_SRC,
+ deps = [
+ '//lib:guava',
+ '//plugins/its-storyboard/lib:easymock',
+ '//lib/log:impl_log4j',
+ '//lib/log:log4j',
+ '//lib:junit',
+ '//plugins/its-storyboard/lib:powermock-api-easymock',
+ '//lib/powermock:powermock-api-support',
+ '//lib/powermock:powermock-core',
+ '//lib/powermock:powermock-module-junit4',
+ '//lib/powermock:powermock-module-junit4-common',
+ ],
+)
+
+java_test(
+ name = 'its-storyboard_tests',
+ srcs = glob(
+ ['src/test/java/**/*.java'],
+ excludes = TEST_UTIL_SRC
+ ),
+ labels = ['its-storyboard'],
+ source_under_test = [':its-storyboard__plugin'],
+ deps = [
+ ':its-storyboard__plugin',
+ ':its-storyboard_tests-utils',
+ '//gerrit-plugin-api:lib',
+ '//plugins/its-storyboard/lib:easymock',
+ '//lib:guava',
+ '//lib/guice:guice',
+ '//lib/jgit:jgit',
+ '//lib:junit',
+ '//lib/log:api',
+ '//lib/log:impl_log4j',
+ '//lib/log:log4j',
+ '//plugins/its-storyboard/lib:powermock-api-easymock',
+ '//lib/powermock:powermock-api-support',
+ '//lib/powermock:powermock-core',
+ '//lib/powermock:powermock-module-junit4',
+ '//lib/powermock:powermock-module-junit4-common',
+ '//lib/powermock:powermock-reflect',
+ '//lib/httpcomponents:httpclient',
+ '//lib:gson',
],
)
diff --git a/lib/BUCK b/lib/BUCK
index 77e73bc..fb07316 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -1,35 +1,38 @@
include_defs('//lib/maven.defs')
+# Upstream is at 3.2, which does not work with PowerMock 1.5's expectNew.
+# So we force EasyMock 3.1.
maven_jar(
- name = 'xmlrpc-client',
- id = 'org.apache.xmlrpc:xmlrpc-client:3.1.3',
- sha1 = 'e486ad917028b52265610206fb5a1e2b5914b94b',
- license = 'Apache2.0',
- deps = [':xmlrpc-common'],
- visibility = [],
+ name = 'easymock',
+ id = 'org.easymock:easymock:3.1',
+ sha1 = '3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e',
+ license = 'DO_NOT_DISTRIBUTE',
+ deps = [
+ '//lib/easymock:cglib-2_2',
+ ':objenesis',
+ ],
)
+# Duplicate upstream's objenesis, which would not be
+# visible otherwise.
maven_jar(
- name = 'xmlrpc-common',
- id = 'org.apache.xmlrpc:xmlrpc-common:3.1.3',
- sha1 = '415daf1f1473a947452588906dc9f5b3575fb44d',
- license = 'Apache2.0',
- deps = [':ws-commons-util'],
- visibility = [],
+ name = 'objenesis',
+ id = 'org.objenesis:objenesis:1.2',
+ sha1 = 'bfcb0539a071a4c5a30690388903ac48c0667f2a',
+ license = 'DO_NOT_DISTRIBUTE',
+ visibility = ['//lib/powermock:powermock-reflect'],
+ attach_source = False,
)
+# Upstream's powermock-api-easymock would pull in upstream's
+# EasyMock 3.2, so we hard-wire dependency to the plugin's EasyMock.
maven_jar(
- name = 'ws-commons-util',
- id = 'org.apache.ws.commons.util:ws-commons-util:1.0.2',
- sha1 = '3f478e6def772c19d1053f61198fa1f6a6119238',
- license = 'Apache2.0',
- deps = [':xml-apis'],
- visibility = [],
-)
-
-maven_jar(
- name = 'xml-apis',
- id = 'xml-apis:xml-apis:1.0.b2',
- sha1 = '3136ca936f64c9d68529f048c2618bd356bf85c9',
- license = 'Apache2.0',
-)
+ name = 'powermock-api-easymock',
+ id = 'org.powermock:powermock-api-easymock:1.5',
+ sha1 = 'a485b570b9debb46b53459a8e866a40343b2cfe2',
+ license = 'DO_NOT_DISTRIBUTE',
+ deps = [
+ '//lib/powermock:powermock-api-support',
+ ':easymock',
+ ],
+)
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index a58907e..64c785a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,14 +27,19 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <Gerrit-PluginName>${project.artifactId}</Gerrit-PluginName>
<Gerrit-ApiType>plugin</Gerrit-ApiType>
<Gerrit-ReloadMode>reload</Gerrit-ReloadMode>
- <Gerrit-PluginName>its-storyboard</Gerrit-PluginName>
- <Gerrit-InitStep>com.googlesource.gerrit.plugins.hooks.sb.InitStoryboard</Gerrit-InitStep>
- <Gerrit-Module>com.googlesource.gerrit.plugins.hooks.sb.StoryboardModule</Gerrit-Module>
- <easymockVersion>3.0</easymockVersion>
+ <Gerrit-Module>com.googlesource.gerrit.plugins.its.storyboard.StoryboardModule</Gerrit-Module>
+ <Implementation-Vendor>Hewlett Packard Corp.</Implementation-Vendor>
+ <Implementation-Url>https://gerrit.googlesource.com/plugins/its-storyboard</Implementation-Url>
+ <Implementation-Title>${project.artifactId} plugin</Implementation-Title>
+ <easymockVersion>3.1</easymockVersion>
<powermockVersion>1.5</powermockVersion>
<slf4jVersion>1.6.2</slf4jVersion>
+ <powermockVersion>1.5</powermockVersion>
+ <httpclientVersion>4.3.4</httpclientVersion>
+ <gsonVersion>2.1</gsonVersion>
</properties>
<build>
@@ -68,15 +73,14 @@
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
- <Implementation-Vendor>Openstack</Implementation-Vendor>
- <Implementation-URL>https://www.openstack.org/</Implementation-URL>
- <Implementation-Title>Plugin ${project.artifactId}</Implementation-Title>
+ <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
+ <Implementation-URL>${Implementation-Url}</Implementation-URL>
+ <Implementation-Title>${Implementation-Title}</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
<Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
<Gerrit-ApiVersion>${project.version}</Gerrit-ApiVersion>
<Gerrit-ReloadMode>${Gerrit-ReloadMode}</Gerrit-ReloadMode>
<Gerrit-PluginName>${Gerrit-PluginName}</Gerrit-PluginName>
- <Gerrit-InitStep>${Gerrit-InitStep}</Gerrit-InitStep>
<Gerrit-Module>${Gerrit-Module}</Gerrit-Module>
</manifestEntries>
</transformer>
@@ -100,6 +104,40 @@
<artifactId>its-base</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>${httpclientVersion}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>${gsonVersion}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <version>${slf4jVersion}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.easymock</groupId>
+ <artifactId>easymock</artifactId>
+ <version>${easymockVersion}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.powermock</groupId>
+ <artifactId>powermock-module-junit4</artifactId>
+ <version>${powermockVersion}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.powermock</groupId>
+ <artifactId>powermock-api-easymock</artifactId>
+ <version>${powermockVersion}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<repositories>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java
new file mode 100644
index 0000000..b117517
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2014 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.storyboard;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class StoryboardClient {
+
+ private static final Logger log = LoggerFactory.getLogger(
+ StoryboardClient.class);
+
+ public static final String STORIES_ENDPOINT = "/api/v1/stories";
+ public static final String SYS_INFO_ENDPOINT = "/api/v1/systeminfo";
+
+ private final String baseUrl;
+ private final String username;
+ private final String password;
+
+ public StoryboardClient(final String baseUrl, final String username,
+ String password) {
+ this.baseUrl = baseUrl;
+ this.username = username;
+ this.password = password;
+ }
+
+ // generic method to get data from a REST endpoint
+ public String getData(final String url) throws IOException {
+ CloseableHttpClient client = HttpClients.createDefault();
+ String responseJson = null;
+
+ try {
+ HttpGet httpget = new HttpGet(url);
+ log.debug("Making request for " + httpget.getRequestLine());
+ CloseableHttpResponse response = client.execute(httpget);
+ try {
+ StatusLine sl = response.getStatusLine();
+ int responseCode = sl.getStatusCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ log.debug("Retreiving data from response " + httpget.getRequestLine());
+ InputStream inputStream = response.getEntity().getContent();
+ Reader reader = new InputStreamReader(inputStream);
+ int contentLength = (int) response.getEntity().getContentLength();
+ char[] charArray = new char[contentLength];
+ reader.read(charArray);
+ responseJson = new String(charArray);
+ log.debug("Data retreived: " + responseJson);
+ } else {
+ log.error("Failed request: " + httpget.getRequestLine() +
+ " with response: " + responseCode);
+ }
+ } finally {
+ response.close();
+ }
+ } finally {
+ client.close();
+ }
+ return responseJson;
+ }
+
+ // generic method to POST data with a REST endpoint
+ public void postData(final String url, final String data)
+ throws IOException {
+ CloseableHttpClient httpclient = HttpClients.createDefault();
+
+ try {
+ HttpPost httpPost = new HttpPost(url);
+ httpPost.addHeader("Authorization", "Bearer " + password);
+ httpPost.addHeader("Content-Type", "application/json; charset=utf-8");
+ httpPost.setEntity(new StringEntity(data, "utf-8"));
+
+ log.debug("Executing request " + httpPost.getRequestLine());
+ CloseableHttpResponse response = httpclient.execute(httpPost);
+ try {
+ int responseCode = response.getStatusLine().getStatusCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ log.info("Updated " + url + " with " + data);
+ } else {
+ log.error("Failed to add comment, response: " + responseCode +
+ " (" + response.getStatusLine().getReasonPhrase() + ")");
+ }
+ } finally {
+ response.close();
+ }
+ } finally {
+ httpclient.close();
+ }
+ }
+
+ public String getSysInfo() throws IOException {
+ return getData(this.baseUrl + SYS_INFO_ENDPOINT);
+ }
+
+ public String getStory(final String issueId) throws IOException {
+ return getData(this.baseUrl + STORIES_ENDPOINT + "/" + issueId);
+ }
+
+ public void addComment(final String issueId, final String comment)
+ throws IOException {
+ log.debug("Posting comment with data: ({},{})", issueId, comment);
+ final String url = baseUrl+STORIES_ENDPOINT+"/"+issueId+"/comments";
+ final String escapedComment = comment.replace("\n", "\\n");
+ final String json =
+ "{\"story_id\":\"" + issueId + "\",\"content\":\"" +
+ escapedComment + "\"}";
+
+ postData(url, json);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java
new file mode 100644
index 0000000..ba74f7c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2014 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.storyboard;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+
+public class StoryboardItsFacade implements ItsFacade {
+ private final Logger log = LoggerFactory.getLogger(StoryboardItsFacade.class);
+
+ 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 final StoryboardClient client;
+
+ @Inject
+ public StoryboardItsFacade(@PluginName String pluginName,
+ @GerritServerConfig Config cfg) {
+ final String url = cfg.getString(pluginName, null, GERRIT_CONFIG_URL);
+ final String username = cfg.getString(pluginName, null,
+ GERRIT_CONFIG_USERNAME);
+ final String password = cfg.getString(pluginName, null,
+ GERRIT_CONFIG_PASSWORD);
+
+ this.client = new StoryboardClient(url, username, password);
+ }
+
+ @Override
+ public String healthCheck(final Check check) throws IOException {
+ // This method is not used, so there is no need to implement it.
+ return "unknown";
+ }
+
+ @Override
+ public void addComment(final String issueId, final String comment) {
+
+ if (!exists(issueId)) {
+ log.warn("Story " + issueId + " does not exist, nothing to update");
+ return;
+ }
+
+ try {
+ client.addComment(issueId, comment);
+ } catch (IOException e) {
+ log.error("Error: could not add comment to issue " + issueId);
+ }
+ log.info("Updated " + issueId + "with comment: " + comment);
+ }
+
+ @Override
+ public void addRelatedLink(final String issueId, final URL relatedUrl,
+ String description) throws IOException {
+ addComment(issueId, "Related URL: " + createLinkForWebui(
+ relatedUrl.toExternalForm(), description));
+ }
+
+ @Override
+ public void performAction(final String issueId, final String actionString) {
+ // No custom actions at this point.
+ //
+ // Note that you can use hashtag names in comments to associate a task
+ // with a new project.
+ }
+
+ @Override
+ public boolean exists(final String issudeId) {
+ String info = null;
+ try {
+ info = client.getStory(issudeId);
+ } catch (IOException e) {
+ log.error("Error: Storyboard is not accessible");
+ }
+ if (info != null) {
+ log.debug("Story exists, info: " + info);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String createLinkForWebui(String url, String text) {
+ String ret = "[" + url;
+ if (text != null && !text.isEmpty() && !text.equals(url)) {
+ ret += "|" + text;
+ }
+ ret += "]";
+ return ret;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardModule.java
new file mode 100644
index 0000000..4c30ce8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardModule.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2014 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.storyboard;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.hooks.ItsHookModule;
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+
+public class StoryboardModule extends AbstractModule {
+
+ private static final Logger log = LoggerFactory.getLogger(
+ StoryboardModule.class);
+
+ private final String pluginName;
+ private final Config gerritConfig;
+ private final PluginConfigFactory pluginCfgFactory;
+
+ @Inject
+ public StoryboardModule(@PluginName final String pluginName,
+ @GerritServerConfig final Config config,
+ PluginConfigFactory pluginCfgFactory) {
+ this.pluginName = pluginName;
+ this.gerritConfig = config;
+ this.pluginCfgFactory = pluginCfgFactory;
+ }
+
+ @Override
+ protected void configure() {
+ if (gerritConfig.getString(pluginName, null, "url") != null) {
+ log.info("Storyboard is configured as ITS");
+ bind(ItsFacade.class).toInstance(new StoryboardItsFacade(
+ pluginName, gerritConfig));
+ install(new ItsHookModule(pluginName, pluginCfgFactory));
+ }
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..5c0d532
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,15 @@
+This @PLUGIN@ plugin is an [`its-base`][its-base] based plugin that integrates
+Gerrit and [`Storyboard`][storyboard]. The @PLUGIN@ allows users
+to configure how a storyboard story or task should be updated relative
+to Gerrit change updates. For example, it can be configured to
+automatically add comments to a story when a comment is entered to
+an associated Gerrit change. It can also be configured to update a
+story's status upon an update to an associated Gerrit change.
+
+For details on how to install this plugin start with the
+[Quick Install Guide][quick].
+
+[quick]: quick-install-guide.html
+[its-base]: https://gerrit-review.googlesource.com/#/admin/projects/plugins/its-base
+[storyboard]: http://git.openstack.org/cgit/openstack-infra/storyboard
+
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 3079426..d2f8c4d 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -42,3 +42,4 @@
How to build the Gerrit Plugin API is described in the [Gerrit
documentation](../../../Documentation/dev-buck.html#_extension_and_plugin_api_jar_files).
+
diff --git a/src/main/resources/Documentation/config-connectivity.md b/src/main/resources/Documentation/config-connectivity.md
new file mode 100644
index 0000000..a138c82
--- /dev/null
+++ b/src/main/resources/Documentation/config-connectivity.md
@@ -0,0 +1,10 @@
+Configuring connectivity for @PLUGIN@
+=====================================
+
+Please refer to the [quick install guide][quick] for info on how to
+configure a connection.
+
+[quick]: quick-install-guide.html
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
deleted file mode 100644
index bf49e3b..0000000
--- a/src/main/resources/Documentation/config.md
+++ /dev/null
@@ -1,6 +0,0 @@
-Plugin @PLUGIN@
-===============
-
-This plugin allows to associate Storyboard issues to Git commits thanks to
-the Gerrit listener interface.
-
diff --git a/src/main/resources/Documentation/quick-install-guide.md b/src/main/resources/Documentation/quick-install-guide.md
new file mode 100644
index 0000000..b749a0e
--- /dev/null
+++ b/src/main/resources/Documentation/quick-install-guide.md
@@ -0,0 +1,125 @@
+Quick Install Guide
+===================
+
+For general instructions on how to enable and configure an its plugin
+please refer to the general [configuration documentation][config-doc]
+Instructions in this document are specific to the @PLUGIN@ plugin.
+
+Install Steps:
+
+1. Verify Storyboard [REST endpoint][rest-enabled].
+2. [Configure the connection][its-connection].
+3. [Associate a changes with stories][its-associate-change].
+4. [Configure the actions][its-actions] that the plugin will take on a Gerrit change.
+5. [Enable the @PLUGIN@ plugin][its-enable] for the Gerrit project.
+6. [Install the plugin][its-install]
+7. Restart Gerrit
+
+[rest-enabled]: #rest-enabled
+<a name="rest-enabled">Checking REST API availability</a>
+---------------------------------------------------------
+
+This plugin will connect to Storyboard via it's REST endpoints.
+Make sure that the Storyboard REST API is up and running.
+
+Assuming the Storyboard instance you want to connect to is at
+`http://my_storyboard_instance.com/`, open
+
+```
+http://my_storyboard_instance.com/api/v1/systeminfo
+```
+
+in your browser. If you get a xml response page without errors, the REST
+interface is enabled.
+
+If you get an error page then you'll need to enable the Storyboard REST API.
+
+[its-connection]: #its-connection
+<a name="its-connection">Connection Configuration</a>
+-----------------------------------------------------
+
+In order for @PLUGIN@ to connect to the REST service of your
+Storyboard instance, the url and credentials are required in
+your site's `etc/gerrit.config` or `etc/secure.config` under
+the `@PLUGIN@` section.
+
+Example:
+
+```
+[@PLUGIN@]
+ url=https://my_storyboard_instance.com
+ username=USERNAME_TO_CONNECT_TO_STORYBOARD
+ password=AUTH_TOKEN_FOR_ABOVE_USERNAME
+```
+
+[its-associate-change]: #its-associate-change
+<a name="its-associate-change">Associating Gerrit Changes</a>
+-------------------------------------------------------------
+
+In order for @PLUGIN@ to associate a Gerrit change with
+a Storyboard story, a Gerrit commentlink needs to be
+defined in `etc/gerrit.config`
+
+Example:
+
+```
+[commentLink "@PLUGIN@"]
+ match = [Ss][Tt][Oo][Rr][Yy][ ]*([1-9][0-9]*)
+ html = "<a href=\"https://my_storyboard_instance.com/#!/story/$1\">story $1</a>"
+```
+
+[its-actions]: #its-actions
+<a name="its-actions">Configure its actions</a>
+-----------------------------------------------
+
+The @PLUGIN@ plugin can take action when there are updates
+to Gerrit changes. Users can define what events will trigger
+which actions. To configure this a `etc/its/actions.config`
+file is required.
+
+Example of actions.config:
+
+```
+# Add a custom comment when a comment has been added to the associated Gerrit change.
+[rule "update-comment"]
+ event-type = comment-added
+ action = add-velocity-comment inline $commenter-name commented on change ${its.formatLink($change-url, $subject)}
+# add a comment only when a user leaves a -2 or a -1 vote on the Code-Review label on the associated Gerrit change.
+[rule "comment-on-negative-vote"]
+ event-type = comment-added
+ approval-Code-Review = -2,-1
+ action = add-comment Boo-hoo, go away!
+# add a standard comment when there is a status update to the associated Gerrit change.
+[rule "comment-on-status-update"]
+ event-type = patchset-created,change-abandoned,change-restored,change-merged
+ action = add-standard-comment
+```
+
+More detailed information on actions is found the [rules documentation][rules-doc]
+
+[its-enable]: #its-enable
+<a name="its-enable">Enable the Plugin</a>
+-------------------------------------------------------
+
+In order to enable the @PLUGIN@ plugin, an entry must be
+added to the project.config file in refs/meta/config.
+To enable the plugin for all projects a single entry can
+be added to project.config in All-Projects.
+
+Example:
+
+```
+[plugin "@PLUGIN@"]
+ enabled = true
+```
+
+[its-install]: #its-install
+<a name="its-install">Install the Plugin</a>
+-------------------------------------------------------
+
+In order to install the @PLUGIN@ plugin simply copy the built jar
+file into the `plugins` folder.
+
+[config-common-doc]: config-common.html
+[config-doc]: config.html
+[rules-doc]: config-rulebase-common.html
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java
new file mode 100644
index 0000000..e7619ad
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2014 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.storyboard;
+
+import static org.easymock.EasyMock.expect;
+
+import org.eclipse.jgit.lib.Config;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.testutil.LoggingMockingTestCase;
+
+public class StoryboardItsFacadeTest extends LoggingMockingTestCase {
+ private Injector injector;
+ private Config serverConfig;
+
+ public void testCreateLinkForWebUiDifferentUrlAndText() {
+ mockUnconnectableStoryboard();
+
+ replayMocks();
+
+ StoryboardItsFacade itsFacade = createStoryboardItsFacade();
+ String actual = itsFacade.createLinkForWebui("Test-Url", "Test-Text");
+
+ assertEquals("[Test-Url|Test-Text]", actual);
+ }
+
+ public void testCreateLinkForWebUiSameUrlAndText() {
+ mockUnconnectableStoryboard();
+
+ replayMocks();
+
+ StoryboardItsFacade itsFacade = createStoryboardItsFacade();
+ String actual = itsFacade.createLinkForWebui("Test-Url", "Test-Url");
+
+ assertEquals("[Test-Url]", actual);
+ }
+
+ public void testCreateLinkForWebUiNullText() {
+ mockUnconnectableStoryboard();
+
+ replayMocks();
+
+ StoryboardItsFacade itsFacade = createStoryboardItsFacade();
+ String actual = itsFacade.createLinkForWebui("Test-Url", null);
+
+ assertEquals("[Test-Url]", actual);
+ }
+
+ public void testCreateLinkForWebUiEmptyText() {
+ mockUnconnectableStoryboard();
+
+ replayMocks();
+
+ StoryboardItsFacade itsFacade = createStoryboardItsFacade();
+ String actual = itsFacade.createLinkForWebui("Test-Url", "");
+
+ assertEquals("[Test-Url]", actual);
+ }
+
+ private StoryboardItsFacade createStoryboardItsFacade() {
+ return injector.getInstance(StoryboardItsFacade.class);
+ }
+
+ private void mockUnconnectableStoryboard() {
+ expect(serverConfig.getString("its-storyboard", null, "url"))
+ .andReturn("<no-url>").anyTimes();
+ expect(serverConfig.getString("its-storyboard", null, "username"))
+ .andReturn("none").anyTimes();
+ expect(serverConfig.getString("its-storyboard", null, "password"))
+ .andReturn("none").anyTimes();
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ injector = Guice.createInjector(new TestModule());
+ }
+
+ private class TestModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ serverConfig = createMock(Config.class);
+ bind(Config.class).annotatedWith(GerritServerConfig.class)
+ .toInstance(serverConfig);
+ bind(String.class).annotatedWith(PluginName.class)
+ .toInstance("its-storyboard");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/testutil/LoggingMockingTestCase.java b/src/test/java/com/googlesource/gerrit/plugins/its/testutil/LoggingMockingTestCase.java
new file mode 100644
index 0000000..a2d4d00
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/testutil/LoggingMockingTestCase.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.testutil;
+
+import com.google.common.collect.Lists;
+import com.googlesource.gerrit.plugins.its.testutil.MockingTestCase;
+import com.googlesource.gerrit.plugins.its.testutil.log.LogUtil;
+
+import org.apache.log4j.spi.LoggingEvent;
+import org.junit.After;
+
+import java.util.Iterator;
+import org.apache.log4j.Level;
+
+public abstract class LoggingMockingTestCase extends MockingTestCase {
+
+ private java.util.Collection<LoggingEvent> loggedEvents;
+
+ protected final void assertLogMessageContains(String needle, Level level) {
+ LoggingEvent hit = null;
+ Iterator<LoggingEvent> iter = loggedEvents.iterator();
+ while (hit == null && iter.hasNext()) {
+ LoggingEvent event = iter.next();
+ if (event.getRenderedMessage().contains(needle)) {
+ if (level == null || level.equals(event.getLevel())) {
+ hit = event;
+ }
+ }
+ }
+ assertNotNull("Could not find log message containing '" + needle + "'",
+ hit);
+ assertTrue("Could not remove log message containing '" + needle + "'",
+ loggedEvents.remove(hit));
+ }
+
+ protected final void assertLogMessageContains(String needle) {
+ assertLogMessageContains(needle, null);
+ }
+
+ protected final void assertLogThrowableMessageContains(String needle) {
+ LoggingEvent hit = null;
+ Iterator<LoggingEvent> iter = loggedEvents.iterator();
+ while (hit == null && iter.hasNext()) {
+ LoggingEvent event = iter.next();
+
+ if (event.getThrowableInformation().getThrowable().toString()
+ .contains(needle)) {
+ hit = event;
+ }
+ }
+ assertNotNull("Could not find log message with a Throwable containing '"
+ + needle + "'", hit);
+ assertTrue("Could not remove log message with a Throwable containing '"
+ + needle + "'", loggedEvents.remove(hit));
+ }
+
+ // As the PowerMock runner does not pass through runTest, we inject log
+ // verification through @After
+ @After
+ public final void assertNoUnassertedLogEvents() {
+ if (loggedEvents.size() > 0) {
+ LoggingEvent event = loggedEvents.iterator().next();
+ String msg = "Found untreated logged events. First one is:\n";
+ msg += event.getRenderedMessage();
+ if (event.getThrowableInformation() != null) {
+ msg += "\n" + event.getThrowableInformation().getThrowable();
+ }
+ fail(msg);
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ loggedEvents = Lists.newArrayList();
+
+ // The logger we're interested is class name without the trailing "Test".
+ // While this is not the most general approach it is sufficient for now,
+ // and we can improve later to allow tests to specify which loggers are
+ // to check.
+ String logName = this.getClass().getCanonicalName();
+ logName = logName.substring(0, logName.length()-4);
+ LogUtil.logToCollection(logName, loggedEvents);
+ }
+
+ @Override
+ protected void runTest() throws Throwable {
+ super.runTest();
+ // Plain JUnit runner does not pick up @After, so we add it here
+ // explicitly. Note, that we cannot put this into tearDown, as failure
+ // to verify mocks would bail out and might leave open resources from
+ // subclasses open.
+ assertNoUnassertedLogEvents();
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/testutil/MockingTestCase.java b/src/test/java/com/googlesource/gerrit/plugins/its/testutil/MockingTestCase.java
new file mode 100644
index 0000000..060269b
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/testutil/MockingTestCase.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.testutil;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.After;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Test case with some support for automatically verifying mocks.
+ */
+public abstract class MockingTestCase extends TestCase {
+ private Collection<Object> mocks;
+ private Collection<IMocksControl> mockControls;
+ private boolean mocksReplayed;
+ private boolean usePowerMock;
+
+ /**
+ * Create and register a mock control.
+ *
+ * @return The mock control instance.
+ */
+ protected final IMocksControl createMockControl() {
+ IMocksControl mockControl = EasyMock.createControl();
+ assertTrue("Adding mock control failed", mockControls.add(mockControl));
+ return mockControl;
+ }
+
+ /**
+ * Create and register a mock.
+ *
+ * Creates a mock and registers it in the list of created mocks, so it gets
+ * treated automatically upon {@code replay} and {@code verify};
+ * @param toMock The class to create a mock for.
+ * @return The mock instance.
+ */
+ protected final <T> T createMock(Class<T> toMock) {
+ return createMock(toMock, null);
+ }
+
+ /**
+ * Create a mock for a mock control and register a mock.
+ *
+ * Creates a mock and registers it in the list of created mocks, so it gets
+ * treated automatically upon {@code replay} and {@code verify};
+ * @param toMock The class to create a mock for.
+ * @param control The mock control to create the mock on. If null, do not use
+ * a specific control.
+ * @return The mock instance.
+ */
+ protected final <T> T createMock(Class<T> toMock, IMocksControl control) {
+ assertFalse("Mocks have already been set to replay", mocksReplayed);
+ final T mock;
+ if (control == null) {
+ if (usePowerMock) {
+ mock = PowerMock.createMock(toMock);
+ } else {
+ mock = EasyMock.createMock(toMock);
+ }
+ assertTrue("Adding " + toMock.getName() + " mock failed",
+ mocks.add(mock));
+ } else {
+ mock = control.createMock(toMock);
+ }
+ return mock;
+ }
+
+ /**
+ * Set all registered mocks to replay
+ */
+ protected final void replayMocks() {
+ assertFalse("Mocks have already been set to replay", mocksReplayed);
+ if (usePowerMock) {
+ PowerMock.replayAll();
+ } else {
+ EasyMock.replay(mocks.toArray());
+ }
+ for (IMocksControl mockControl : mockControls) {
+ mockControl.replay();
+ }
+ mocksReplayed = true;
+ }
+
+ /**
+ * Verify all registered mocks
+ *
+ * This method is called automatically at the end of a test. Nevertheless,
+ * it is safe to also call it beforehand, if this better meets the
+ * verification part of a test.
+ */
+ // As the PowerMock runner does not pass through runTest, we inject mock
+ // verification through @After
+ @After
+ public final void verifyMocks() {
+ if (!mocks.isEmpty() || !mockControls.isEmpty()) {
+ assertTrue("Created mocks have not been set to replay. Call replayMocks "
+ + "within the test", mocksReplayed);
+ if (usePowerMock) {
+ PowerMock.verifyAll();
+ } else {
+ EasyMock.verify(mocks.toArray());
+ }
+ for (IMocksControl mockControl : mockControls) {
+ mockControl.verify();
+ }
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ usePowerMock = false;
+ RunWith runWith = this.getClass().getAnnotation(RunWith.class);
+ if (runWith != null) {
+ usePowerMock = PowerMockRunner.class.isAssignableFrom(runWith.value());
+ }
+
+ mocks = new ArrayList<Object>();
+ mockControls = new ArrayList<IMocksControl>();
+ mocksReplayed = false;
+ }
+
+ @Override
+ protected void runTest() throws Throwable {
+ super.runTest();
+ // Plain JUnit runner does not pick up @After, so we add it here
+ // explicitly. Note, that we cannot put this into tearDown, as failure
+ // to verify mocks would bail out and might leave open resources from
+ // subclasses open.
+ verifyMocks();
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/testutil/log/CollectionAppender.java b/src/test/java/com/googlesource/gerrit/plugins/its/testutil/log/CollectionAppender.java
new file mode 100644
index 0000000..8f21bde
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/testutil/log/CollectionAppender.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.testutil.log;
+
+import com.google.common.collect.Lists;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * Log4j appender that logs into a list
+ */
+public class CollectionAppender extends AppenderSkeleton {
+ private Collection<LoggingEvent> events;
+
+ public CollectionAppender() {
+ events = new LinkedList<LoggingEvent>();
+ }
+
+ public CollectionAppender(Collection<LoggingEvent> events) {
+ this.events = events;
+ }
+
+ @Override
+ public boolean requiresLayout() {
+ return false;
+ }
+
+ @Override
+ protected void append(LoggingEvent event) {
+ if (! events.add(event)) {
+ throw new RuntimeException("Could not append event " + event);
+ }
+ }
+
+ @Override
+ public void close() {
+ }
+
+ public Collection<LoggingEvent> getLoggedEvents() {
+ return Lists.newLinkedList(events);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/testutil/log/LogUtil.java b/src/test/java/com/googlesource/gerrit/plugins/its/testutil/log/LogUtil.java
new file mode 100644
index 0000000..4fee52f
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/testutil/log/LogUtil.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.testutil.log;
+
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+
+import com.googlesource.gerrit.plugins.its.testutil.log.CollectionAppender;
+
+
+import java.util.Collection;
+
+public class LogUtil {
+ public static CollectionAppender logToCollection(String logName,
+ Collection<LoggingEvent> collection) {
+ Logger log = LogManager.getLogger(logName);
+ CollectionAppender listAppender = new CollectionAppender(collection);
+ log.removeAllAppenders();
+ log.addAppender(listAppender);
+ return listAppender;
+ }
+}