URL Encode project name

Project name need to be URL encoded to allow Gerrit REST API
to find the resource.

Furthermore Docker compose doesn't like container names with '/',
hence we replace it with '-' and add a timestamp to make them unique.

Bug: Issue 9067
Change-Id: I4fe990ff20946a43e08a2f1505676d36b1514ffd
diff --git a/src/main/resources/static/js/analyticswizard.js b/src/main/resources/static/js/analyticswizard.js
index 0b0cc79..579285e 100644
--- a/src/main/resources/static/js/analyticswizard.js
+++ b/src/main/resources/static/js/analyticswizard.js
@@ -24,7 +24,7 @@
 
 
 function submitDetailsForm() {
-    var projectName = $("#input-project-name").val();
+    var projectName = encodeURIComponent($("#input-project-name").val());
     $.ajax({
       type : "PUT",
       url : `/a/projects/${projectName}/analytics-wizard~stack`,
@@ -54,7 +54,7 @@
 };
 
 function dashboardService(command) {
-    var projectName = $("#input-project-name").val();
+    var projectName = encodeURIComponent($("#input-project-name").val());
     $.ajax({
       type : "POST",
       url : `/a/projects/${projectName}/analytics-wizard~server`,
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/wizard/AnalyticDashboardSetup.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/wizard/AnalyticDashboardSetup.scala
index 1c07435..f288fab 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/wizard/AnalyticDashboardSetup.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/wizard/AnalyticDashboardSetup.scala
@@ -13,8 +13,7 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.analytics.wizard
 
-import java.io.PrintWriter
-import java.nio.charset.{Charset, StandardCharsets}
+import java.nio.charset.StandardCharsets
 import java.nio.file.{Files, Path}
 
 trait ConfigWriter {
@@ -30,7 +29,13 @@
 case class AnalyticDashboardSetup(name: String, dockerComposeYamlPath: Path)(
     implicit val writer: ConfigWriter) {
 
-  private val dockerComposeTemplate = { (name: String) =>
+  // Docker doesn't like container names with '/', hence the replace with '-'
+  // Furthermore timestamp has been added to avoid conflicts among container names, i.e.:
+  // A project named 'foo/bar' would be encoded as 'foo-bar' and thus its container
+  // would be potentially in conflict with another 'foo-bar' project's one
+  private val sanitisedName =
+    s"${name.replace("/", "-")}-${System.currentTimeMillis}"
+  private val dockerComposeTemplate = {
     s"""
        |version: '3'
        |services:
@@ -45,7 +50,7 @@
        |
        |  kibana:
        |    image: gerritforge/analytics-kibana:latest
-       |    container_name: "kibana-for-${name}-project"
+       |    container_name: "kibana-for-${sanitisedName}-project"
        |    networks:
        |      - ek
        |    depends_on:
@@ -55,7 +60,7 @@
        |
        |  elasticsearch:
        |    image: gerritforge/analytics-elasticsearch:latest
-       |    container_name: "es-for-${name}-project"
+       |    container_name: "es-for-${sanitisedName}-project"
        |    networks:
        |      - ek
        |    environment:
@@ -69,7 +74,7 @@
   }
 
   def createDashboardSetupFile(): Unit = {
-    writer.write(dockerComposeYamlPath, dockerComposeTemplate(name))
+    writer.write(dockerComposeYamlPath, dockerComposeTemplate)
   }
 
 }
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/wizard/AnalyticsWizardActions.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/wizard/AnalyticsWizardActions.scala
index f63447c..6763e3a 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/wizard/AnalyticsWizardActions.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/wizard/AnalyticsWizardActions.scala
@@ -13,10 +13,12 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.analytics.wizard
 
+import java.net.URLEncoder
 import java.nio.charset.StandardCharsets.UTF_8
 import java.nio.file.Path
 
 import com.google.common.io.ByteStreams
+import com.google.gerrit.extensions.annotations.PluginData
 import com.google.gerrit.extensions.restapi.{
   Response,
   RestApiException,
@@ -25,12 +27,10 @@
 }
 import com.google.gerrit.server.project.ProjectResource
 import com.google.inject.Inject
-import AnalyticDashboardSetup.writer
-import com.google.gerrit.extensions.annotations.PluginData
+import com.googlesource.gerrit.plugins.analytics.wizard.AnalyticDashboardSetup.writer
 
-import scala.io.Source
-
-class GetAnalyticsStack @Inject()(@PluginData val dataPath: Path) extends RestReadView[ProjectResource] {
+class GetAnalyticsStack @Inject()(@PluginData val dataPath: Path)
+    extends RestReadView[ProjectResource] {
   override def apply(
       resource: ProjectResource): Response[AnalyticDashboardSetup] = {
 
@@ -50,8 +50,15 @@
                      input: Input): Response[String] = {
 
     val projectName = resource.getControl.getProject.getName
-    AnalyticDashboardSetup(projectName, dataPath.resolve(s"docker-compose.${projectName}.yaml")).createDashboardSetupFile()
-    Response.created(s"Dashboard configuration created for $projectName!")
+    val encodedName = AnalyticsWizardActions
+      .encodedName(projectName)
+
+    AnalyticDashboardSetup(
+      projectName,
+      dataPath.resolve(s"docker-compose.${encodedName}.yaml"))
+      .createDashboardSetupFile()
+    Response.created(s"Dashboard configuration created for $encodedName!")
+
   }
 }
 
@@ -62,16 +69,20 @@
                      input: DockerComposeCommand): Response[String] = {
 
     val projectName = resource.getControl.getProject.getName
+    val encodedName = AnalyticsWizardActions
+      .encodedName(projectName)
+
     val pb = new ProcessBuilder(
       "docker-compose",
       "-f",
-      s"${dataPath.toFile.getAbsolutePath}/docker-compose.${projectName}.yaml",
+      s"${dataPath.toFile.getAbsolutePath}/docker-compose.${encodedName}.yaml",
       input.action.toLowerCase)
     pb.redirectErrorStream(true)
 
     val ps: Process = pb.start
     ps.getOutputStream.close
-    val output = new String(ByteStreams.toByteArray(ps.getInputStream), UTF_8)
+    val output =
+      new String(ByteStreams.toByteArray(ps.getInputStream), UTF_8)
     ps.waitFor
 
     ps.exitValue match {
@@ -83,3 +94,14 @@
 
   }
 }
+
+object AnalyticsWizardActions {
+  // URLEncoder could potentially throw UnsupportedEncodingException,
+  // but UTF-8 will *always* be resolved, otherwise, Gerrit wouldn't work at all
+  def encodedName(name: String) =
+    try {
+      URLEncoder.encode(name, "UTF-8")
+    } catch {
+      case e: Throwable => throw new RuntimeException(e)
+    }
+}