Refactoring to allow additional commands

Factor out common operations than can be reused by additional commands.
Makes it clearer how the various modules are structured.

Change-Id: I8e0d1f012bfa810b62fcd5a20e0fec948157cf88
diff --git a/README.md b/README.md
index f360e7b..1097fd7 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
 REST Example:
 
 ```
-   $ curl http://gerrit.mycompany.com/project/myproyject/analytics~contributors
+   $ curl http://gerrit.mycompany.com/projects/myproject/analytics~contributors
 
    {"name":"John Doe","email":"john.doe@mycompany.com","num_commits":1,"commits":[{"sha1":"6a1f73738071e299f600017d99f7252d41b96b4b","date":"Apr 28, 2011 5:13:14 AM","merge":false}]}
    {"name":"Matt Smith","email":"matt.smith@mycompany.com","num_commits":1,"commits":[{"sha1":"54527e7e3086758a23e3b069f183db6415aca304","date":"Sep 8, 2015 3:11:23 AM","merge":true}]}
@@ -70,7 +70,7 @@
 SSH Example:
 
 ```
-   $ ssh -p 29418 admin@gerrit.mycompany.com analytics contributors
+   $ ssh -p 29418 admin@gerrit.mycompany.com analytics contributors myproject
 
    {"name":"John Doe","email":"john.doe@mycompany.com","num_commits":1,"commits":[{"sha1":"6a1f73738071e299f600017d99f7252d41b96b4b","date":"Apr 28, 2011 5:13:14 AM","merge":false}]}
    {"name":"Matt Smith","email":"matt.smith@mycompany.com","num_commits":1,"commits":[{"sha1":"54527e7e3086758a23e3b069f183db6415aca304","date":"Sep 8, 2015 3:11:23 AM","merge":true}]}
diff --git a/build.sbt b/build.sbt
index dc579ab..211b55b 100644
--- a/build.sbt
+++ b/build.sbt
@@ -13,7 +13,6 @@
     libraryDependencies ++= Seq(
       "io.fabric8" % "gitective-core" % "0.9.19"
         exclude("org.eclipse.jgit", "org.eclipse.jgit"),
-
       "com.google.inject" % "guice" % "3.0" % Provided,
       "com.google.gerrit" % "gerrit-plugin-api" % gerritApiVersion % Provided withSources(),
       "com.google.code.gson" % "gson" % "2.7" % Provided,
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/CommitInfo.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/CommitInfo.scala
deleted file mode 100644
index d0ac124..0000000
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/CommitInfo.scala
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (C) 2016 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.analytics
-
-case class CommitInfo(val sha1: String, val date: Long, val merge: Boolean)
\ No newline at end of file
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
new file mode 100644
index 0000000..df3bbe9
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
@@ -0,0 +1,71 @@
+// Copyright (C) 2016 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.analytics
+
+import com.google.gerrit.extensions.restapi.{Response, RestReadView}
+import com.google.gerrit.server.git.GitRepositoryManager
+import com.google.gerrit.server.project.{ProjectResource, ProjectsCollection}
+import com.google.gerrit.sshd.{CommandMetaData, SshCommand}
+import com.google.inject.Inject
+import com.googlesource.gerrit.plugins.analytics.common._
+import org.eclipse.jgit.lib.ObjectId
+import org.gitective.core.stat.{AuthorHistogramFilter, UserCommitActivity}
+
+
+@CommandMetaData(name = "contributors", description = "Extracts the list of contributors to a project")
+class ContributorsCommand @Inject()(val executor: ContributorsService,
+                                    val projects: ProjectsCollection,
+                                    val gsonFmt: GsonFormatter)
+  extends SshCommand with ProjectResourceParser {
+
+  override protected def run = gsonFmt.format(executor.get(projectRes), stdout)
+}
+
+class ContributorsResource @Inject()(val executor: ContributorsService,
+                                     val gson: GsonFormatter)
+  extends RestReadView[ProjectResource] {
+
+  override def apply(projectRes: ProjectResource) = Response.ok(
+    new GsonStreamedResult[UserActivitySummary](gson, executor.get(projectRes)))
+}
+
+class ContributorsService @Inject()(repoManager: GitRepositoryManager,
+                                    histogram: UserActivityHistogram,
+                                    gsonFmt: GsonFormatter) {
+
+  def get(projectRes: ProjectResource): TraversableOnce[UserActivitySummary] =
+    ManagedResource.use(repoManager.openRepository(projectRes.getNameKey)) {
+      histogram.get(_, new AuthorHistogramFilter)
+        .par
+        .map(UserActivitySummary.apply).toStream
+    }
+}
+
+case class CommitInfo(val sha1: String, val date: Long, val merge: Boolean)
+
+case class UserActivitySummary(name: String, email: String, numCommits: Int,
+                               commits: Array[CommitInfo], lastCommitDate: Long)
+
+object UserActivitySummary {
+  def apply(uca: UserCommitActivity): UserActivitySummary =
+    UserActivitySummary(uca.getName, uca.getEmail, uca.getCount,
+      getCommits(uca.getIds, uca.getTimes, uca.getMerges), uca.getLatest)
+
+  private def getCommits(ids: Array[ObjectId], times: Array[Long], merges: Array[Boolean]):
+  Array[CommitInfo] = {
+    (ids, times, merges).zipped.map((id, time, merge) => CommitInfo(id.name, time, merge))
+  }
+}
+
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/ContributorsCommand.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/ContributorsCommand.scala
deleted file mode 100644
index a53c632..0000000
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/ContributorsCommand.scala
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2016 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.analytics
-
-import java.io.IOException
-
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException
-import com.google.gerrit.server.project.{ProjectResource, ProjectsCollection}
-import com.google.gerrit.sshd.{CommandMetaData, SshCommand}
-import com.google.inject.Inject
-import org.kohsuke.args4j.Argument
-
-@CommandMetaData(name = "contributors", description = "Extracts the list of contributors to a project")
-class ContributorsCommand @Inject() (
-  val projects: ProjectsCollection,
-  val contributors: ContributorsResource,
-  val gsonFmt: GsonFormatter) extends SshCommand {
-
-  @Argument(usage = "project name", metaVar = "PROJECT", required = true)
-  def setProject(project: String): Unit = {
-    try {
-      this.projectRes = projects.parse(project)
-    } catch {
-      case e: UnprocessableEntityException =>
-        throw new IllegalArgumentException(e.getLocalizedMessage, e)
-      case e: IOException =>
-        throw new IllegalArgumentException("I/O Error while trying to access project " + project, e)
-    }
-  }
-
-  private var projectRes: ProjectResource = null
-  override protected def run(): Unit = {
-    gsonFmt.format(contributors.get(projectRes), stdout)
-  }
-}
\ No newline at end of file
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/ContributorsResource.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/ContributorsResource.scala
deleted file mode 100644
index 3d43a18..0000000
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/ContributorsResource.scala
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2016 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.analytics
-
-import java.io.{OutputStream, PrintWriter}
-
-import com.google.gerrit.extensions.restapi.{BinaryResult, Response, RestReadView}
-import com.google.gerrit.server.git.GitRepositoryManager
-import com.google.gerrit.server.project.ProjectResource
-import com.google.inject.Inject
-import com.googlesource.gerrit.plugins.analytics.ManagedResource.use
-
-class ContributorsResource @Inject()(val repoManager: GitRepositoryManager,
-                                     val userSummary: UserSummaryExport,
-                                     val gsonFmt: GsonFormatter) extends RestReadView[ProjectResource] {
-
-  private[analytics] class JsonStreamedResult[T](val committers: TraversableOnce[T]) extends BinaryResult {
-    override def writeTo(os: OutputStream) {
-      use(new PrintWriter(os)) { gsonFmt.format(committers, _) }
-    }
-  }
-
-  override def apply(projectRes: ProjectResource): Response[BinaryResult] =
-    Response.ok(new JsonStreamedResult[UserActivitySummary](get(projectRes)))
-
-  def get(projectRes: ProjectResource): TraversableOnce[UserActivitySummary] =
-    use(repoManager.openRepository(projectRes.getNameKey)) { userSummary.getCommitters }
-}
\ No newline at end of file
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/Module.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Module.scala
index 1c804ad..df65ae4 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/Module.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Module.scala
@@ -21,9 +21,7 @@
 class Module extends AbstractModule {
 
   override protected def configure() {
-
     install(new RestApiModule() {
-
       override protected def configure() = {
         get(PROJECT_KIND, "contributors").to(classOf[ContributorsResource])
       }
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/SshModule.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/SshModule.scala
index 5995de5..6a2b076 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/SshModule.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/SshModule.scala
@@ -17,6 +17,7 @@
 import com.google.gerrit.sshd.PluginCommandModule
 
 class SshModule extends PluginCommandModule {
+
   override protected def configureCommands {
     command(classOf[ContributorsCommand])
   }
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/UserActivitySummary.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/UserActivitySummary.scala
deleted file mode 100644
index bde6731..0000000
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/UserActivitySummary.scala
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2016 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.analytics
-
-import org.eclipse.jgit.lib.ObjectId
-import org.gitective.core.stat.UserCommitActivity
-
-object UserActivitySummary {
-  def fromUserActivity(uca: UserCommitActivity) =
-    UserActivitySummary(uca.getName, uca.getEmail, uca.getCount,
-      getCommits(uca.getIds, uca.getTimes, uca.getMerges), uca.getLatest)
-
-  private def getCommits(ids: Array[ObjectId], times: Array[Long], merges: Array[Boolean]):
-  Array[CommitInfo] = {
-    (ids, times, merges).zipped.map((id, time, merge) => CommitInfo(id.name, time, merge))
-  }
-}
-
-case class UserActivitySummary(name: String, email: String, numCommits: Int,
-                               commits: Array[CommitInfo], lastCommitDate: Long)
\ No newline at end of file
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/GsonFormatter.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/GsonFormatter.scala
similarity index 93%
rename from src/main/scala/com/googlesource/gerrit/plugins/analytics/GsonFormatter.scala
rename to src/main/scala/com/googlesource/gerrit/plugins/analytics/common/GsonFormatter.scala
index d400ad8..f9ab7a9 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/GsonFormatter.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/GsonFormatter.scala
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.googlesource.gerrit.plugins.analytics
+package com.googlesource.gerrit.plugins.analytics.common
 
 import java.io.PrintWriter
 
@@ -28,7 +28,7 @@
     val gson: Gson = gsonBuilder.create
     for (value <- values) {
       gson.toJson(value, out)
-      out.println()
+      out.println
     }
   }
-}
+}
\ No newline at end of file
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/JsonStreamedResultBuilder.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/JsonStreamedResultBuilder.scala
new file mode 100644
index 0000000..991be59
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/JsonStreamedResultBuilder.scala
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 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.analytics.common
+
+import java.io.{OutputStream, PrintWriter}
+
+import com.google.gerrit.extensions.restapi.BinaryResult
+
+class GsonStreamedResult[T](val jsonFmt: GsonFormatter,
+                            val committers: TraversableOnce[T]) extends BinaryResult {
+  override def writeTo(os: OutputStream) =
+    ManagedResource.use(new PrintWriter(os))(jsonFmt.format(committers, _))
+}
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/ManagedResources.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/ManagedResources.scala
similarity index 92%
rename from src/main/scala/com/googlesource/gerrit/plugins/analytics/ManagedResources.scala
rename to src/main/scala/com/googlesource/gerrit/plugins/analytics/common/ManagedResources.scala
index 741f91c..83da961 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/ManagedResources.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/ManagedResources.scala
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.googlesource.gerrit.plugins.analytics
+package com.googlesource.gerrit.plugins.analytics.common
 
 object ManagedResource {
   def use[A <: { def close(): Unit }, B](resource: A)(code: A ⇒ B): B =
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/ProjectResourceParser.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/ProjectResourceParser.scala
new file mode 100644
index 0000000..a375ff4
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/ProjectResourceParser.scala
@@ -0,0 +1,23 @@
+package com.googlesource.gerrit.plugins.analytics.common
+
+import java.io.IOException
+
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException
+import com.google.gerrit.server.project.{ProjectResource, ProjectsCollection}
+import org.kohsuke.args4j.Argument
+
+trait ProjectResourceParser {
+  def projects: ProjectsCollection
+
+  var projectRes: ProjectResource = null
+
+  @Argument(usage = "project name", metaVar = "PROJECT", required = true)
+  def setProject(project: String): Unit = {
+    try {
+      this.projectRes = projects.parse(project)
+    } catch {
+      case e: Exception =>
+        throw new IllegalArgumentException("Error while trying to access project " + project, e)
+    }
+  }
+}
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/UserSummaryExport.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/UserActivityHistogram.scala
similarity index 69%
rename from src/main/scala/com/googlesource/gerrit/plugins/analytics/UserSummaryExport.scala
rename to src/main/scala/com/googlesource/gerrit/plugins/analytics/common/UserActivityHistogram.scala
index d4a5142..cebc84e 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/UserSummaryExport.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/UserActivityHistogram.scala
@@ -12,21 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.googlesource.gerrit.plugins.analytics
+package com.googlesource.gerrit.plugins.analytics.common
 
 import com.google.inject.Singleton
 import org.eclipse.jgit.lib.Repository
 import org.gitective.core.CommitFinder
-import org.gitective.core.stat.AuthorHistogramFilter
+import org.gitective.core.stat.CommitHistogramFilter
 
 @Singleton
-class UserSummaryExport {
-  def getCommitters(repo: Repository): TraversableOnce[UserActivitySummary] = {
+class UserActivityHistogram {
+  def get(repo: Repository, filter: CommitHistogramFilter) = {
     val finder = new CommitFinder(repo)
-    val filter = new AuthorHistogramFilter
     finder.setFilter(filter).find
     val histogram = filter.getHistogram
-    val authorActivity = histogram.getUserActivity
-    authorActivity.par.map(UserActivitySummary.fromUserActivity).toStream
+    histogram.getUserActivity
   }
 }
\ No newline at end of file