Merge "log the configured storyboard url on failure"
diff --git a/BUCK b/BUCK
index d9f1b97..d3cde83 100644
--- a/BUCK
+++ b/BUCK
@@ -1,3 +1,5 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+
 gerrit_plugin(
   name = 'its-storyboard',
   srcs = glob(['src/main/java/**/*.java']),
@@ -55,16 +57,13 @@
   name = 'its-storyboard_tests',
   srcs = glob(['src/test/java/**/*.java']),
   labels = ['its-storyboard'],
-  source_under_test = [':its-storyboard__plugin'],
-  deps = [
+  deps = GERRIT_PLUGIN_API + GERRIT_TESTS + [
     ':its-storyboard__plugin',
     '//plugins/its-base:its-base_tests-utils',
     ':its-base_stripped',
-    '//gerrit-plugin-api:lib',
     '//lib/easymock:easymock',
     '//lib:guava',
     '//lib/guice:guice',
-    '//lib/jgit:jgit',
     '//lib:junit',
     '//lib/log:api',
     '//lib/log:impl_log4j',
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
index b117517..d28013d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java
@@ -14,101 +14,108 @@
 
 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 com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
 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.client.methods.HttpPut;
 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;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+
 
 public class StoryboardClient {
 
-  private static final Logger log = LoggerFactory.getLogger(
-      StoryboardClient.class);
+  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";
+  public static final String TASKS_ENDPOINT = "/api/v1/tasks";
 
   private final String baseUrl;
-  private final String username;
   private final String password;
 
-  public StoryboardClient(final String baseUrl, final String username,
-      String password) {
+  public StoryboardClient(final String baseUrl, 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);
+    HttpGet httpget = new HttpGet(url);
+    try (CloseableHttpClient client = HttpClients.createDefault();
+        CloseableHttpResponse response = client.execute(httpget)) {
       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();
+      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);
+        String responseJson = new String(charArray);
+        log.debug("Data retreived: " + responseJson);
+        return responseJson;
+      } else {
+        log.error("Failed request: " + httpget.getRequestLine()
+            + " with response: " + responseCode);
       }
-    } finally {
-      client.close();
     }
-    return responseJson;
+    return null;
   }
 
   // generic method to POST data with a REST endpoint
-  public void postData(final String url, final String data)
-      throws IOException {
-    CloseableHttpClient httpclient = HttpClients.createDefault();
+  public void postData(final String url, final String data) throws IOException {
 
-    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"));
-
+    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"));
+    try (CloseableHttpClient httpclient = HttpClients.createDefault();
+        CloseableHttpResponse response = httpclient.execute(httpPost)) {
       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();
+      int responseCode = response.getStatusLine().getStatusCode();
+      if (responseCode == HttpURLConnection.HTTP_OK) {
+        log.info("Updated " + url + " with " + data);
+      } else {
+        log.error("Failed to post, response: " + responseCode + " ("
+            + response.getStatusLine().getReasonPhrase() + ")");
       }
-    } finally {
-      httpclient.close();
+    }
+  }
+
+  // generic method to PUT data with a REST endpoint
+  public void putData(final String url, final String data) throws IOException {
+
+    HttpPut HttpPut = new HttpPut(url);
+    HttpPut.addHeader("Authorization", "Bearer " + password);
+    HttpPut.addHeader("Content-Type", "application/json; charset=utf-8");
+    HttpPut.setEntity(new StringEntity(data, "utf-8"));
+    try (CloseableHttpClient httpclient = HttpClients.createDefault();
+        CloseableHttpResponse response = httpclient.execute(HttpPut)) {
+      log.debug("Executing request " + HttpPut.getRequestLine());
+      int responseCode = response.getStatusLine().getStatusCode();
+      if (responseCode == HttpURLConnection.HTTP_OK) {
+        log.info("Updated " + url + " with " + data);
+      } else {
+        log.error("Failed to post, response: " + responseCode + " ("
+            + response.getStatusLine().getReasonPhrase() + ")");
+      }
     }
   }
 
@@ -116,18 +123,51 @@
     return getData(this.baseUrl + SYS_INFO_ENDPOINT);
   }
 
-  public String getStory(final String issueId) throws IOException {
-    return getData(this.baseUrl + STORIES_ENDPOINT + "/" + issueId);
+  public String getStory(final String id) throws IOException {
+    return getData(this.baseUrl + STORIES_ENDPOINT + "/" + getStoryId(id));
+  }
+
+  public int getStoryId(final String issueId) throws IOException {
+    String taskJson = getTask(issueId);
+    JsonObject jobj = new Gson().fromJson(taskJson, JsonObject.class);
+    return jobj.get("story_id").getAsInt();
+  }
+
+  public String getTask(final String issueId) throws IOException {
+    return getData(this.baseUrl + TASKS_ENDPOINT + "/" + issueId);
+  }
+
+  public void setStatus(final String issueId, final String status)
+      throws IOException {
+    log.debug("PUT task with data: ({},{})", issueId, status);
+    final String url = baseUrl + TASKS_ENDPOINT + "/" + issueId;
+    final String json =
+        "{\"task_id\":\"" + issueId + "\",\"status\":\"" + status + "\"}";
+
+    putData(url, json);
+  }
+
+  public String getTaskStatus(final String issueId) throws IOException {
+    String taskJson = getTask(issueId);
+    JsonObject jobj = new Gson().fromJson(taskJson, JsonObject.class);
+    return jobj.get("status").getAsString();
+  }
+
+  public String getTaskNotes(final String issueId) throws IOException {
+    String taskJson = getTask(issueId);
+    JsonObject jobj = new Gson().fromJson(taskJson, JsonObject.class);
+    return jobj.get("link").getAsString();
   }
 
   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";
+    int story_id = getStoryId(issueId);
+    log.debug("Posting comment with data: ({},{})", story_id, comment);
+    final String url =
+        baseUrl + STORIES_ENDPOINT + "/" + story_id + "/comments";
     final String escapedComment = comment.replace("\n", "\\n");
-    final String json =
-        "{\"story_id\":\"" + issueId + "\",\"content\":\"" +
-        escapedComment + "\"}";
+    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
index 28ff6c8..2313c03 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java
@@ -30,7 +30,6 @@
 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";
 
@@ -40,12 +39,10 @@
   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);
+    final String password =
+        cfg.getString(pluginName, null, GERRIT_CONFIG_PASSWORD);
 
-    this.client = new StoryboardClient(url, username, password);
+    this.client = new StoryboardClient(url, password);
   }
 
   @Override
@@ -73,16 +70,29 @@
   @Override
   public void addRelatedLink(final String issueId, final URL relatedUrl,
       String description) throws IOException {
-    addComment(issueId, "Related URL: " + createLinkForWebui(
-        relatedUrl.toExternalForm(), description));
+    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.
+
+    try {
+      String actionName =
+          actionString.substring(0, actionString.indexOf(" ")).toLowerCase();
+      String actionValue =
+          actionString.substring(actionString.indexOf(" ") + 1).toLowerCase();
+      if (actionName.equals("set-status")) {
+        if (!client.getTaskStatus(issueId).toLowerCase().equals(actionValue)) {
+          log.info("Updating task " + issueId + " with status: " + actionValue);
+          client.setStatus(issueId, actionValue);
+        }
+      }
+    } catch (StringIndexOutOfBoundsException e) {
+      log.error("Error: Invalid action: " + actionString);
+    } catch (IOException e) {
+      log.error("Error: Failed to peform action: " + actionString);
+    }
   }
 
   @Override
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 5c0d532..389b896 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,15 +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.
+This @PLUGIN@ plugin is an [`its-base`][its-base] based plugin that
+integrates Gerrit and [`Storyboard`][storyboard].  The @PLUGIN@ plugin
+allows users to configure how a storyboard story and 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 status on a task when an associated Gerrit change is updated.
 
-For details on how to install this plugin start with the
+For details on how to install and configure this plugin start with the
 [Quick Install Guide][quick].
 
-[quick]: quick-install-guide.html
+[quick]: quick-install-guide.md
 [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/config-connectivity.md b/src/main/resources/Documentation/config-connectivity.md
index a138c82..36ff372 100644
--- a/src/main/resources/Documentation/config-connectivity.md
+++ b/src/main/resources/Documentation/config-connectivity.md
@@ -4,7 +4,7 @@
 Please refer to the [quick install guide][quick] for info on how to
 configure a connection.
 
-[quick]: quick-install-guide.html
+[quick]: quick-install-guide.md
 [Back to @PLUGIN@ documentation index][index]
 
 [index]: index.html
diff --git a/src/main/resources/Documentation/quick-install-guide.md b/src/main/resources/Documentation/quick-install-guide.md
index b749a0e..97327b6 100644
--- a/src/main/resources/Documentation/quick-install-guide.md
+++ b/src/main/resources/Documentation/quick-install-guide.md
@@ -2,18 +2,19 @@
 ===================
 
 For general instructions on how to enable and configure an its plugin
-please refer to the general [configuration documentation][config-doc]
+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.
+1. [Check Storyboard REST API availability][rest-enabled]
+2. [Connection Configuration][its-connection].
+3. [Verify access to Storyboard][access-enabled]
+4. [Associate Gerrit changes with Storyboard stories and tasks][its-associate-change].
+5. [Configure the actions][its-actions] that the plugin will take on a Gerrit change update.
 6. [Install the plugin][its-install]
-7. Restart Gerrit
+7. [Enable the @PLUGIN@ plugin][its-enable] for the Gerrit project.
+8. [Testing][testing]
 
 [rest-enabled]: #rest-enabled
 <a name="rest-enabled">Checking REST API availability</a>
@@ -38,9 +39,9 @@
 <a name="its-connection">Connection Configuration</a>
 -----------------------------------------------------
 
-In order for @PLUGIN@ to connect to the REST service of your
+In order for the @PLUGIN@ 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
+your Gerrit site's `etc/gerrit.config` or `etc/secure.config` under
 the `@PLUGIN@` section.
 
 Example:
@@ -48,31 +49,47 @@
 ```
 [@PLUGIN@]
   url=https://my_storyboard_instance.com
-  username=USERNAME_TO_CONNECT_TO_STORYBOARD
-  password=AUTH_TOKEN_FOR_ABOVE_USERNAME
+  password=STORYBOARD_USER_AUTH_TOKEN
 ```
 
+[access-enabled]: #access-enabled
+<a name="access-enabled">Check Accessibility</a>
+---------------------------------------------------------
+
+This plugin uses the Storyboard REST endpoints to POST updates.  Make sure
+that the STORYBOARD_USER_AUTH_TOKEN has access to update Storyboard stories
+and tasks. To verify this use the [Storyboard story API] from the Gerrit sever
+to post an update. If it fails to update the story you'll need to make the
+necessary changes to allow access between the Gerrit and Storyboard
+servers.
+
+
 [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
+In order for the @PLUGIN@ plugin to associate a Gerrit change with
+a Storyboard story and task, a Gerrit commentlink needs to be
 defined in `etc/gerrit.config`
 
 Example:
 
 ```
+[commentlink "story"]
+    match = "\\b[Ss]tory:? #?(\\d+)"
+    link = "http://my_storyboard_instance.com/#!/story/$1"
+    html = ""
 [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>"
+    match = "\\b[Tt]ask:? #?(\\d+)"
+    link = "task: $1"
+    html = ""
 ```
 
 [its-actions]: #its-actions
 <a name="its-actions">Configure its actions</a>
 -----------------------------------------------
 
-The @PLUGIN@ plugin can take action when there are updates
+The @PLUGIN@ plugin can take actions 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.
@@ -84,18 +101,34 @@
 [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]
+# set storyboard task status to 'review' when a patch is uploaded or when a change is restored
+[rule "change-in-progress"]
+    event-type = patchset-created,change-restored
+    action = set-status REVIEW
+```
+*_NOTE_: A Gerrit restart is required to update these settings.
+
+### <a id="task-status"></a>TaskStatus
+Valid task status: TODO, REVIEW, INPROGRESS, MERGED, and INVALID
+
+[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.
 
 [its-enable]: #its-enable
 <a name="its-enable">Enable the Plugin</a>
@@ -113,13 +146,31 @@
   enabled = true
 ```
 
-[its-install]: #its-install
-<a name="its-install">Install the Plugin</a>
+[testing]: #testing
+<a name="testing">Testing the Plugin</a>
 -------------------------------------------------------
 
-In order to install the @PLUGIN@ plugin simply copy the built jar
-file into the `plugins` folder.
+Create a new Gerrit change with a commit message that contains a reference
+to the Storyboard story and task.
 
-[config-common-doc]: config-common.html
-[config-doc]: config.html
-[rules-doc]: config-rulebase-common.html
+Example:
+
+```
+My change to test integration
+
+This is an example change to test storyboard integration.
+Story: 123
+Task: 1000
+Change-Id: I3912f42c371023eb8bd048a5b17b776801b405e2
+```
+
+Make an update to the Gerrit change (abandone, restore, submit, etc..),
+the @PLUGIN@ plugin should automatically update the corresponding story
+and task in Storyboard.
+
+SEE ALSO
+--------
+* More detailed information on actions is found in the [rules documentation][rules-doc]
+
+
+[Storyboard story API]: http://docs.openstack.org/infra/storyboard/webapi/v1.html#put--v1-stories
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
index 9364ad3..63caf65 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java
@@ -80,8 +80,6 @@
   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();
   }