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;
+  }
+}