Collect gerritVersion in the support bundle zip

Collect Gerrit version into a single JsonString

Example:
curl -v -H "Content-Type: application/json" -d '{"gerritVersion":"true"}' http://localhost:8080/plugins/gerrit-support/collect

> POST /plugins/gerrit-support/collect HTTP/1.1

< HTTP/1.1 201 Created
< Location: /plugins/gerrit-support/collect/20170319-175459-collect-ab2093a4-139f-4310-872f-6d00bfa313e5.zip

The resulting zip file can be then downloaded using an HTTP GET.
Example:

curl -v http://localhost:8080/plugins/gerrit-support/collect/20170319-175459-collect-ab2093a4-139f-4310-872f-6d00bfa313e5.zip

> GET /plugins/gerrit-support/collect/20170319-175459-collect-ab2093a4-139f-4310-872f-6d00bfa313e5.zip

< HTTP/1.1 200 OK
< Content-Type: multipart/x-zip;charset=utf-8

The archive bundle contains one entry named 'version.json' with a
single JSON String containing the Gerrit version.

Example:

unzip 20170319-175459-collect-ab2093a4-139f-4310-872f-6d00bfa313e5.zip
  inflating: version.json
cat version.json
"2.13.6-3240-g1c96d0a"

Change-Id: Ifb2a50c4269212c5adf68ff3edd5e6871e8f5b48
diff --git a/.gitignore b/.gitignore
index c58d83b..34c435d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,11 @@
 # Scala-IDE specific
 .scala_dependencies
 .worksheet
+/bin/
+
+
+# Intellij
+.idea/
+.settings/
+.classpath
+.project
diff --git a/README.md b/README.md
index 4ef8047..5145daf 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,19 @@
-# gerrit-support
-Collect information on Gerrit Code Review to request support
+# Gerrit-Support Plugin
+
+Collect information on Gerrit Code Review in order to have enough
+elements to request support.
+
+## How to build
+
+Gerrit-Support plugins is written in Scala language and built with Sbt.
+To build the plugin you need to have SBT 0.13.13 installed and then
+run the ``assembly`` target.
+
+Example:
+
+```
+   $ sbt assembly
+   [...]
+   [info] Packaging /Users/lucamilanesio/gerrithub/gerrit-support/target/scala-2.11/gerrit-support.jar ...
+```
+
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..f259ef9
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,41 @@
+val pluginName = "gerrit-support"
+
+name := pluginName
+
+version := "1.0-SNAPSHOT"
+
+scalaVersion := "2.11.8"
+
+val guiceV = "3.0"
+val gerritV = "2.11"
+val scalatraV = "2.5.+"
+val scalatestV = "3.0.1"
+val gsonV = "2.7" // same version provided by Gerrit
+val jodaV = "2.9.4" // same version provided by Gerrit
+val scalaGuiceV = "4.1.0"
+
+
+libraryDependencies ++= Seq(
+  // provided by gerrit
+  "com.google.inject"     %   "guice"             % guiceV      % Provided,
+  "com.google.gerrit"     %   "gerrit-plugin-api" % gerritV     % Provided,
+  "com.google.code.gson"  %   "gson"              % gsonV       % Provided,
+  "joda-time"             %   "joda-time"         % jodaV       % Provided,
+
+  // added to assembly
+  "org.scalatra"          %%  "scalatra"          % scalatraV,
+
+
+  // test dependencies
+  "org.scalatra"          %%  "scalatra-scalatest"% scalatraV   % Test,
+  "org.scalatest"         %%  "scalatest"         % scalatestV  % Test,
+  "net.codingwell"        %%  "scala-guice"       % scalaGuiceV % Test
+  )
+
+assemblyJarName in assembly := s"$pluginName.jar"
+
+packageOptions in (Compile, packageBin) +=  {
+  Package.ManifestAttributes(
+    "Gerrit-ApiType" -> "plugin",
+    "Gerrit-PluginName" -> pluginName)
+}
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..e0cbc71
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version = 0.13.13
\ No newline at end of file
diff --git a/project/plugins.sbt b/project/plugins.sbt
new file mode 100644
index 0000000..e43b477
--- /dev/null
+++ b/project/plugins.sbt
@@ -0,0 +1,4 @@
+logLevel := Level.Warn
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.4")
+addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.1.0")
+
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..6db2133
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,2 @@
+Collect information on Gerrit Code Review in order to have enough
+elements to request support.
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/support/GerritSupportCommands.scala b/src/main/scala/com/googlesource/gerrit/plugins/support/GerritSupportCommands.scala
new file mode 100644
index 0000000..5f53b9c
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/support/GerritSupportCommands.scala
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 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.support
+
+import com.google.gerrit.common.Version
+import com.google.gson.{JsonElement, JsonPrimitive}
+import com.google.inject._
+
+case class CommandResult(entryName: String, content: JsonElement)
+
+trait GerritSupportCommand {
+  def execute: CommandResult
+}
+
+@Singleton
+class GerritSupportCommandFactory @Inject()(val injector: Injector) {
+  def apply(name: String): GerritSupportCommand =
+    injector.getInstance(
+      Class.forName(s"com.googlesource.gerrit.plugins.support.${name.capitalize}Command")
+        .asInstanceOf[Class[_ <: GerritSupportCommand]])
+}
+
+class GerritVersionCommand extends GerritSupportCommand {
+  def execute = CommandResult("version.json", new JsonPrimitive(Version.getVersion))
+}
\ No newline at end of file
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/support/GerritSupportServlet.scala b/src/main/scala/com/googlesource/gerrit/plugins/support/GerritSupportServlet.scala
new file mode 100644
index 0000000..019d84c
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/support/GerritSupportServlet.scala
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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.support
+
+import java.io.{ File, FileNotFoundException }
+
+import com.google.gerrit.extensions.annotations._
+import com.google.inject.{ Inject, Singleton }
+import eu.medsea.mimeutil.detector.ExtensionMimeDetector
+import org.scalatra._
+import org.scalatra.util.Mimes
+
+import scala.collection.JavaConversions._
+import scala.util.{ Failure, Success, Try }
+
+@Singleton
+@Export("/collect*")
+class GerritSupportServlet @Inject() (processor: RequestProcessor, bundleFactory: SupportBundleFile, mimeDetector: ExtensionMimeDetector)
+    extends ScalatraServlet with Mimes {
+
+  post("/") {
+    processor.processRequest(request.body) match {
+      case Success(zipped) =>
+        Created("OK", Map(
+          "Location" -> s"${request.getRequestURI}/${zipped.filename}"))
+      case Failure(e) =>
+        InternalServerError(reason = e.getLocalizedMessage)
+    }
+  }
+
+  get("/:filename") {
+    val bundleFilename = params.getOrElse("filename", halt(BadRequest("Missing or invalid bundle name")))
+
+    bundleFactory(bundleFilename) match {
+      case Success(bundleFile: File) =>
+        Ok(bundleFile, Map("Content-Type" -> mimeType(bundleFilename)))
+
+      case Failure(e: FileNotFoundException) => NotFound("Bundle not found")
+
+      case Failure(e: IllegalArgumentException) => BadRequest("Invalid bundle name")
+
+    }
+  }
+
+  private def mimeType(filename: String) = mimeDetector.getMimeTypes(filename)
+    .map(_.toString)
+    .headOption.getOrElse("application/octect-stream")
+}
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/support/RequestProcessor.scala b/src/main/scala/com/googlesource/gerrit/plugins/support/RequestProcessor.scala
new file mode 100644
index 0000000..2278970
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/support/RequestProcessor.scala
@@ -0,0 +1,48 @@
+// Copyright (C) 2017 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.support
+
+import com.google.gson.{Gson, JsonObject}
+import com.google.inject.{Inject, Injector, Singleton}
+
+import scala.collection.JavaConverters._
+import scala.util.Try
+
+@Singleton
+class RequestProcessor @Inject()(injector: Injector,
+                                 gson: Gson,
+                                 commandFactory: GerritSupportCommandFactory) {
+
+  def processRequest(body: String): Try[SupportBundleBuilder] = {
+    Try {
+      val requestJson = gson.fromJson(body, classOf[JsonObject])
+      val zipped: SupportBundleBuilder = injector.getInstance(classOf[SupportBundleBuilder])
+
+      try {
+        requestJson
+          .entrySet().asScala
+          .filter(_.getValue.getAsBoolean)
+          .map(_.getKey)
+          .map(commandFactory.apply)
+          .map(_.execute)
+          .foreach(zipped.write)
+        zipped
+
+      } finally {
+        zipped.build
+      }
+    }
+  }
+}
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/support/SupportBundle.scala b/src/main/scala/com/googlesource/gerrit/plugins/support/SupportBundle.scala
new file mode 100644
index 0000000..89aea04
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/support/SupportBundle.scala
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 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.support
+
+import java.io.{File, FileNotFoundException, FileOutputStream}
+import java.nio.charset.Charset
+import java.nio.file.Path
+import java.util.UUID
+import java.util.zip.{ZipEntry, ZipOutputStream}
+
+import com.google.gerrit.extensions.annotations.PluginData
+import com.google.gson.Gson
+import com.google.inject.Inject
+import org.joda.time.DateTime
+import org.joda.time.format.DateTimeFormat
+
+import scala.util.{Failure, Success, Try}
+
+class SupportBundleFile @Inject()(@PluginData val pluginData: Path) {
+
+  import SupportBundle._
+
+  def apply(bundleFilename: String): Try[File] = {
+    val bundleMatched = bundleFilename match {
+      case BundleName(filename) => Success(filename)
+      case _ => Failure(new IllegalArgumentException(s"Invalid bundle name '$bundleFilename'"))
+    }
+
+    bundleMatched.map(pluginData.resolve).map(_.toFile).flatMap {
+      case f if f.exists => Success(f)
+      case f => Failure(new FileNotFoundException(s"Bundle file $f does not exists"))
+    }
+  }
+}
+
+class SupportBundleBuilder @Inject()(@PluginData val directory: Path, gson: Gson) {
+
+  import SupportBundle._
+
+  lazy val timestamp = DateTimeFormat.forPattern("yyyyMMdd-HHmmss").print(new DateTime)
+
+  lazy val filename = BundleName(timestamp, UUID.randomUUID)
+
+  lazy val file = directory.resolve(filename).toFile
+
+  lazy val zip = new ZipOutputStream(new FileOutputStream(file))
+
+  val UTF8 = Charset.forName("UTF-8")
+
+  def write(file: CommandResult): SupportBundleBuilder = {
+    zip.putNextEntry(new ZipEntry(file.entryName))
+    zip.write(gson.toJson(file.content).getBytes(UTF8))
+    this
+  }
+
+  def build() = {
+    zip.close
+    file
+  }
+}
+
+object SupportBundle {
+
+  val BundleName = "([0-9\\-]+-collect-[a-z0-9\\-]+.zip)".r
+
+  def BundleName(timestamp: String, uuid: UUID) = s"$timestamp-collect-$uuid.zip"
+}
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/support/FileMatchers.scala b/src/test/scala/com/googlesource/gerrit/plugins/support/FileMatchers.scala
new file mode 100644
index 0000000..78035e6
--- /dev/null
+++ b/src/test/scala/com/googlesource/gerrit/plugins/support/FileMatchers.scala
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 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.support
+
+import org.scalatest._
+import matchers._
+
+trait FileMatchers {
+
+  class FileEndsWithExtensionMatcher(expectedExtension: String) extends Matcher[java.io.File] {
+
+    def apply(left: java.io.File) = {
+      val name = left.getName
+      MatchResult(
+        name.endsWith(expectedExtension),
+        s"""File $name did not end with extension "$expectedExtension"""",
+        s"""File $name ended with extension "$expectedExtension""""
+      )
+    }
+  }
+
+  def endWithExtension(expectedExtension: String) = new FileEndsWithExtensionMatcher(expectedExtension)
+}
+
+// Make them easy to import with:
+// import CustomMatchers._
+object FileMatchers extends FileMatchers
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/support/GerritSupportTest.scala b/src/test/scala/com/googlesource/gerrit/plugins/support/GerritSupportTest.scala
new file mode 100644
index 0000000..f4b8eb0
--- /dev/null
+++ b/src/test/scala/com/googlesource/gerrit/plugins/support/GerritSupportTest.scala
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 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.support
+
+import java.io.File
+import java.util.zip.{ZipEntry, ZipFile}
+
+import com.google.gson.{Gson, JsonPrimitive}
+import org.scalatest.{FlatSpec, Matchers}
+import FileMatchers._
+import scala.collection.JavaConverters._
+
+class GerritSupportTest extends FlatSpec with Matchers {
+  val tmpPath = File.createTempFile(classOf[GerritSupportTest].getName, "").getParentFile
+
+  def tmpFile = {
+    val file = File.createTempFile("file.txt", System.currentTimeMillis.toString, tmpPath)
+    file.deleteOnExit
+    file
+  }
+
+  "version command" should "return a non-empty version string" in {
+    val version = new GerritVersionCommand().execute.content
+
+    version.getAsString should not be null
+  }
+
+  "Bundle builder" should "create an output zip file" in {
+    val zipFile = new SupportBundleBuilder(tmpPath.toPath, new Gson).build
+
+    zipFile should (be a 'file
+      and endWithExtension("zip"))
+  }
+
+  it should "create a one entry in the output zip file" in {
+    val file = new SupportBundleBuilder(tmpPath.toPath, new Gson)
+      .write(CommandResult("foo", new JsonPrimitive("bar")))
+      .build
+
+    val zipEntries = new ZipFile(file).entries.asScala
+    zipEntries should have size (1)
+  }
+
+  it should "add the Json primitive into the zip entry" in {
+    val file = new SupportBundleBuilder(tmpPath.toPath, new Gson)
+      .write(CommandResult("entry-name", new JsonPrimitive("foo")))
+      .build
+
+    val zipEntries = new ZipFile(file).entries.asScala.toSeq
+    zipEntries.map(_.getName) should contain("entry-name")
+  }
+
+}
+