Merge branch 'stable-2.16' into stable-3.0

* stable-2.16:
  Make year, month, day, hour extraction more resilient
  Move extract-issues to analytics.config
  Revert "Move extract-issues to analytics.config"
  Move extract-issues to analytics.config

Change-Id: Ic54c97f744ce67fd953e16ec06619ca42bf84cf1
diff --git a/README.md b/README.md
index 0f9f244..8338bcd 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,6 @@
 - --until -e Ending timestamp (excluded) to consider
 - --aggregate -granularity -g one of email, email_year, email_month, email_day, email_hour defaulting to aggregation by email
 - --extract-branches -r enables splitting of aggregation by branch name and expose branch name in the payload
-- --extract-issues -i enables the extraction of issues from commentLink
 
 NOTE: Timestamp format is consistent with Gerrit's query syntax, see /Documentation/user-search.html for details.
 
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 89a6806..6c08a06 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -22,4 +22,16 @@
   ```
 
   Keep in mind that plugin configurations are written in [git-config style syntax](https://git-scm.com/docs/git-config#_syntax),
-  so you should be escaping regular expressions accordingly.
\ No newline at end of file
+  so you should be escaping regular expressions accordingly.
+
+- `contributors.extract-issues`
+
+  when set to true, enables the extraction of issues from commentLink
+
+  Default: false
+
+  example:
+  ```ini
+  [contributors]
+    extract-issues = true
+  ```
\ No newline at end of file
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/AnalyticsConfig.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/AnalyticsConfig.scala
index 8035a43..fd31626 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/AnalyticsConfig.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/AnalyticsConfig.scala
@@ -16,17 +16,22 @@
 
 import com.google.gerrit.extensions.annotations.PluginName
 import com.google.gerrit.server.config.PluginConfigFactory
-import com.google.inject.Inject
+import com.google.inject.{ImplementedBy, Inject}
 import org.eclipse.jgit.lib.Config
 
-class AnalyticsConfig @Inject() (val pluginConfigFactory: PluginConfigFactory, @PluginName val pluginName: String) {
+@ImplementedBy(classOf[AnalyticsConfigImpl])
+trait AnalyticsConfig {
+  def botlikeFilenameRegexps: List[String]
+  def isExtractIssues: Boolean
+}
 
-  lazy val pluginConfig: Config = pluginConfigFactory.getGlobalPluginConfig(pluginName)
-
-  val Contributors = "contributors"
-  val BotlikeFilenameRegexp = "botlike-filename-regexp"
-
+class AnalyticsConfigImpl @Inject() (val pluginConfigFactory: PluginConfigFactory, @PluginName val pluginName: String) extends AnalyticsConfig{
   lazy val botlikeFilenameRegexps: List[String] = pluginConfigBotLikeFilenameRegexp
+  lazy val isExtractIssues: Boolean = pluginConfig.getBoolean(Contributors, null, ExtractIssues, false)
 
+  private lazy val pluginConfig: Config = pluginConfigFactory.getGlobalPluginConfig(pluginName)
+  private val Contributors = "contributors"
+  private val BotlikeFilenameRegexp = "botlike-filename-regexp"
+  private val ExtractIssues = "extract-issues"
   private lazy val pluginConfigBotLikeFilenameRegexp = pluginConfig.getStringList(Contributors, null, BotlikeFilenameRegexp).toList
 }
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
index 8e048e2..f634725 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
@@ -14,21 +14,17 @@
 
 package com.googlesource.gerrit.plugins.analytics
 
-import com.google.common.cache.{Cache, LoadingCache}
-import com.google.gerrit.extensions.api.projects.CommentLinkInfo
 import com.google.gerrit.extensions.restapi.{BadRequestException, Response, RestReadView}
 import com.google.gerrit.server.git.GitRepositoryManager
 import com.google.gerrit.server.project.{ProjectCache, ProjectResource}
 import com.google.gerrit.server.restapi.project.ProjectsCollection
 import com.google.gerrit.sshd.{CommandMetaData, SshCommand}
 import com.google.inject.Inject
-import com.google.inject.name.Named
 import com.googlesource.gerrit.plugins.analytics.common.DateConversions._
 import com.googlesource.gerrit.plugins.analytics.common._
-import com.googlesource.gerrit.plugins.analytics.common.CommitsStatisticsCache.COMMITS_STATISTICS_CACHE
-import org.eclipse.jgit.lib.ObjectId
 import org.kohsuke.args4j.{Option => ArgOption}
 
+import scala.util.Try
 
 @CommandMetaData(name = "contributors", description = "Extracts the list of contributors to a project")
 class ContributorsCommand @Inject()(val executor: ContributorsService,
@@ -76,13 +72,9 @@
     }
   }
 
-  @ArgOption(name = "--extract-issues", aliases = Array("-i"),
-    usage = "Extract a list of issues and links using the Gerrit's commentLink configuration")
-  private var extractIssues: Boolean = false
-
   override protected def run =
     gsonFmt.format(executor.get(projectRes, beginDate, endDate,
-      granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches, extractIssues), stdout)
+      granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches), stdout)
 
 }
 
@@ -130,15 +122,11 @@
     usage = "Do extra parsing to extract a list of all branches for each line")
   private var extractBranches: Boolean = false
 
-  @ArgOption(name = "--extract-issues", aliases = Array("-i"),
-    usage = "Extract a list of issues and links using the Gerrit's commentLink configuration")
-  private var extractIssues: Boolean = false
-
   override def apply(projectRes: ProjectResource) =
     Response.ok(
       new GsonStreamedResult[UserActivitySummary](gson,
         executor.get(projectRes, beginDate, endDate,
-          granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches, extractIssues)))
+          granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches)))
 }
 
 class ContributorsService @Inject()(repoManager: GitRepositoryManager,
@@ -148,15 +136,9 @@
                                     commitsStatisticsCache: CommitsStatisticsCache) {
   import RichBoolean._
 
-  import scala.collection.JavaConverters._
-
   def get(projectRes: ProjectResource, startDate: Option[Long], stopDate: Option[Long],
-          aggregationStrategy: AggregationStrategy, extractBranches: Boolean, extractIssues: Boolean)
+          aggregationStrategy: AggregationStrategy, extractBranches: Boolean)
   : TraversableOnce[UserActivitySummary] = {
-    val nameKey = projectRes.getNameKey
-    val commentLinks: List[CommentLinkInfo] = extractIssues.option {
-      projectCache.get(nameKey).getCommentLinks.asScala
-    }.toList.flatten
 
     ManagedResource.use(repoManager.openRepository(projectRes.getNameKey)) { repo =>
       val stats  = new Statistics(projectRes.getNameKey, commitsStatisticsCache)
@@ -197,7 +179,7 @@
   def apply(statisticsHandler: Statistics)(uca: AggregatedUserCommitActivity)
   : Iterable[UserActivitySummary] = {
 
-    implicit def stringToIntOrNull(x: String): Integer = if (x.isEmpty) null else new Integer(x)
+    def stringToIntOrNull(x: String): Integer = Try(new Integer(x)).getOrElse(null)
 
     uca.key.split("/", AggregationStrategy.MAX_MAPPING_TOKENS) match {
       case Array(email, year, month, day, hour, branch) =>
@@ -206,7 +188,8 @@
             Option(branch).filter(_.nonEmpty).map(b => Array(b)).getOrElse(Array.empty)
 
           UserActivitySummary(
-            year, month, day, hour, uca.getName, email, stat.commits.size,
+            stringToIntOrNull(year), stringToIntOrNull(month), stringToIntOrNull(day), stringToIntOrNull(hour),
+            uca.getName, email, stat.commits.size,
             stat.numFiles, stat.numDistinctFiles, stat.addedLines, stat.deletedLines,
             stat.commits.toArray, maybeBranches, stat.issues.map(_.code)
               .toArray, stat.issues.map(_.link).toArray, uca.getLatest, stat
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsStatisticsLoader.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsStatisticsLoader.scala
index 36a175b..1144901 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsStatisticsLoader.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsStatisticsLoader.scala
@@ -20,7 +20,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager
 import com.google.gerrit.server.project.ProjectCache
 import com.google.inject.Inject
-import com.googlesource.gerrit.plugins.analytics.{CommitInfo, IssueInfo}
+import com.googlesource.gerrit.plugins.analytics.{AnalyticsConfig, CommitInfo, IssueInfo}
 import com.googlesource.gerrit.plugins.analytics.common.ManagedResource.use
 import org.eclipse.jgit.diff.{DiffFormatter, RawTextComparator}
 import org.eclipse.jgit.revwalk.RevWalk
@@ -30,14 +30,20 @@
 import scala.collection.JavaConverters._
 import scala.util.matching.Regex
 
-class CommitsStatisticsLoader @Inject() (gitRepositoryManager: GitRepositoryManager, projectCache: ProjectCache, botLikeExtractor: BotLikeExtractor) extends CacheLoader[CommitsStatisticsCacheKey, CommitsStatistics] {
+class CommitsStatisticsLoader @Inject() (
+  gitRepositoryManager: GitRepositoryManager,
+  projectCache: ProjectCache,
+  botLikeExtractor: BotLikeExtractor,
+  config: AnalyticsConfig
+) extends CacheLoader[CommitsStatisticsCacheKey, CommitsStatistics] {
 
   override def load(cacheKey: CommitsStatisticsCacheKey): CommitsStatistics = {
     import RevisionBrowsingSupport._
 
     val objectId = cacheKey.commitId
     val nameKey = new Project.NameKey(cacheKey.projectName)
-    val commentInfoList: Seq[CommentLinkInfo] = projectCache.get(nameKey).getCommentLinks.asScala
+    val commentInfoList: Seq[CommentLinkInfo] =
+      if(config.isExtractIssues) projectCache.get(nameKey).getCommentLinks.asScala else Seq.empty
     val replacers = commentInfoList.map(info =>
       Replacer(
         info.`match`.r,
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractorImplSpec.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractorImplSpec.scala
index e9f9c7a..5bb73c5 100644
--- a/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractorImplSpec.scala
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractorImplSpec.scala
@@ -46,7 +46,8 @@
     extractor.isBotLike(Set("some/path/anyFile")).shouldBe(false)
   }
 
-  private def newBotLikeExtractorImpl(botLikeRegexps: List[String]) = new BotLikeExtractorImpl(new AnalyticsConfig(null, null) {
+  private def newBotLikeExtractorImpl(botLikeRegexps: List[String]) = new BotLikeExtractorImpl(new AnalyticsConfig {
     override lazy val botlikeFilenameRegexps = botLikeRegexps
+    override lazy val isExtractIssues: Boolean = false
   })
 }
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/GerritTestDaemon.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/GerritTestDaemon.scala
index 5fe8a57..0024e1e 100644
--- a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/GerritTestDaemon.scala
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/GerritTestDaemon.scala
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.client.SubmitType
 import com.google.gerrit.reviewdb.client.Project
 import com.google.inject.{AbstractModule, Module}
+import com.googlesource.gerrit.plugins.analytics.AnalyticsConfig
 import org.eclipse.jgit.api.MergeCommand.FastForwardMode
 import org.eclipse.jgit.api.{Git, MergeResult}
 import org.eclipse.jgit.internal.storage.file.FileRepository
@@ -154,6 +155,10 @@
 
   override def createModule(): Module = new AbstractModule {
     override def configure(): Unit = {
+      bind(classOf[AnalyticsConfig]).toInstance(new AnalyticsConfig {
+        override def botlikeFilenameRegexps: List[String] = List.empty
+        override def isExtractIssues: Boolean = true
+      })
       bind(classOf[String]).annotatedWith(classOf[PluginName]).toInstance("analytics")
     }
   }