Introduce an optional configuration to define a regex for naming rules

Before this change, this plugin enforced a rule that a project cannot
contain spaces in its name. Therefore, projects could have a name with
any set of characters while not including a space in their names.
However, due to IRI (not URI) and backward compatibility, the non-ASCII
characters may be represented in a way that can cause issues [1].

This change introduces a configurable parameter that can be set to
define a regex that is then enforced upon project creation to ensure
backward compatibility. If not defined, the default behavior, to check
that projects don't contain spaces, applies.

If the regex is defined, it will apply if it accepts the character / and
doesn't accept spaces, else it will get ignored to not affect the
functionality of the plugin. In case the regex gets ignored, a warning
will be registered in the log file to flag that the regex is invalid.

[1] https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier

Bug: Issue 14297
Change-Id: Ia21c9834f41c4d5289f56aacd357c5de09f4521f
diff --git a/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/Configuration.java
new file mode 100644
index 0000000..6a3c40f
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/Configuration.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2021 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.ericsson.gerrit.plugins.projectgroupstructure;
+
+import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class Configuration {
+  private static final Logger log = LoggerFactory.getLogger(Configuration.class);
+
+  private static final String NAME_REGEX = "nameRegex";
+  private static final String DEFAULT_NAME_REGEX_VALUE = ".+";
+  private static final String DEFAULT_NAME_REGEX_MESSAGE = "The value of the regex is invalid.";
+
+  static final String SEE_DOCUMENTATION_MSG = "\n\nSee documentation for more info: %s";
+  static final String DOCUMENTATION_PATH = "Documentation/index.html";
+
+  private String regexNameFilter;
+
+  @Inject
+  Configuration(
+      PluginConfigFactory pluginConfigFactory,
+      @PluginName String pluginName,
+      @PluginCanonicalWebUrl String url) {
+    PluginConfig config = pluginConfigFactory.getFromGerritConfig(pluginName);
+    regexNameFilter = config.getString(NAME_REGEX, DEFAULT_NAME_REGEX_VALUE);
+    if (!"/".matches(regexNameFilter) || " ".matches(regexNameFilter)) {
+      log.warn(
+          String.format(
+              DEFAULT_NAME_REGEX_MESSAGE + SEE_DOCUMENTATION_MSG, url + DOCUMENTATION_PATH));
+      regexNameFilter = DEFAULT_NAME_REGEX_VALUE;
+    }
+  }
+
+  public String getRegexNameFilter() {
+    return regexNameFilter;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidator.java b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidator.java
index 331c968..089ca92 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidator.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidator.java
@@ -14,6 +14,8 @@
 
 package com.ericsson.gerrit.plugins.projectgroupstructure;
 
+import static com.ericsson.gerrit.plugins.projectgroupstructure.Configuration.SEE_DOCUMENTATION_MSG;
+
 import com.google.common.base.Charsets;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.data.GroupReference;
@@ -51,14 +53,12 @@
   private static final String AN_ERROR_OCCURRED_MSG =
       "An error occurred while creating project, please contact Gerrit support";
 
-  private static final String SEE_DOCUMENTATION_MSG = "\n\nSee documentation for more info: %s";
-
   private static final String MUST_BE_OWNER_TO_CREATE_PROJECT_MSG =
       "You must be owner of the parent project \"%s\" to create a nested project."
           + SEE_DOCUMENTATION_MSG;
 
   private static final String PROJECT_CANNOT_CONTAINS_SPACES_MSG =
-      "Project name cannot contains spaces." + SEE_DOCUMENTATION_MSG;
+      "Project name cannot contain spaces." + SEE_DOCUMENTATION_MSG;
 
   private static final String ROOT_PROJECT_CANNOT_CONTAINS_SLASHES_MSG =
       "Since the \"Rights Inherit From\" field is empty, "
@@ -80,6 +80,9 @@
   private static final String PROJECT_MUST_START_WITH_PARENT_NAME_MSG =
       "Project name must start with parent project name, e.g. %s." + SEE_DOCUMENTATION_MSG;
 
+  private static final String PROJECT_SHOULD_MATCH_REGEX_MSG =
+      "Project name should match the regex: %s." + SEE_DOCUMENTATION_MSG;
+
   static final String DELEGATE_PROJECT_CREATION_TO = "delegateProjectCreationTo";
 
   static final String DISABLE_GRANTING_PROJECT_OWNERSHIP = "disableGrantingProjectOwnership";
@@ -91,6 +94,7 @@
   private final PermissionBackend permissionBackend;
   private final PluginConfigFactory cfg;
   private final String pluginName;
+  private final Configuration config;
 
   @Inject
   public ProjectCreationValidator(
@@ -100,20 +104,27 @@
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
       PluginConfigFactory cfg,
+      Configuration config,
       @PluginName String pluginName) {
     this.groups = groups;
-    this.documentationUrl = url + "Documentation/index.html";
+    this.documentationUrl = url + Configuration.DOCUMENTATION_PATH;
     this.allProjectsName = allProjectsName;
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.cfg = cfg;
     this.pluginName = pluginName;
+    this.config = config;
   }
 
   @Override
   public void validateNewProject(CreateProjectArgs args) throws ValidationException {
     String name = args.getProjectName();
     log.debug("validating creation of {}", name);
+    String regex = config.getRegexNameFilter();
+    if (!(name.matches(regex))) {
+      throw new ValidationException(
+          String.format(PROJECT_SHOULD_MATCH_REGEX_MSG, regex, documentationUrl));
+    }
     if (name.contains(" ")) {
       throw new ValidationException(
           String.format(PROJECT_CANNOT_CONTAINS_SPACES_MSG, documentationUrl));
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index ac9a12e..64af68d 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -23,4 +23,17 @@
   label-Code-Review = -2..+2 group ${owner}
   submit = group ${owner}
 
-```
\ No newline at end of file
+```
+Also, this plugin offers a way to restrict the new names of the projects to match an optionally
+configured regex in the gerrit.config. For example:
+
+```
+[plugin "@PLUGIN@"]
+  nameRegex = [a-z0-9/]+
+
+```
+
+In this example, the regex will limit the project created to non-capital letters, numbers
+and slashes. The regex must accept slash (/) to not disturb the functionality of the plugin.
+If the regex doesn't accept / or accepts spaces, it will be ignored and replaced with a default
+non-empty wildcard (.+) regex.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java
index b190c59..24cebcf 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java
@@ -21,9 +21,11 @@
 import com.google.common.base.Charsets;
 import com.google.common.collect.Lists;
 import com.google.common.hash.Hashing;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
@@ -46,6 +48,10 @@
   @Inject private ProjectOperations projectOperations;
 
   private static final String PLUGIN_NAME = "project-group-structure";
+  private static final String REGEX_INCLUDING_SLASH = "[a-z_/]+";
+  private static final String REGEX_NOT_INCLUDING_SLASH = "[a-z_]+";
+  private static final String REGEX_INCLUDING_SPACE = "[a-z_/ ]+";
+  private static final String PROJECT_WITH_SPACE = "project with space";
 
   @Override
   @Before
@@ -69,9 +75,44 @@
   public void shouldProjectWithASpaceInTheirName() throws Exception {
     ProjectInput in = new ProjectInput();
     in.permissionsOnly = true;
-    RestResponse r = userRestSession.put("/projects/" + Url.encode("project with space"), in);
+    RestResponse r = userRestSession.put("/projects/" + Url.encode(PROJECT_WITH_SPACE), in);
     r.assertConflict();
-    assertThat(r.getEntityContent()).contains("Project name cannot contains spaces");
+    assertThat(r.getEntityContent()).contains("Project name cannot contain spaces");
+  }
+
+  @Test
+  @UseLocalDisk
+  @GerritConfig(name = "project-group-structure.nameRegex", value = REGEX_INCLUDING_SPACE)
+  public void shouldRejectProjectWithSpaceInItsNameEvenWithRegex() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.permissionsOnly = true;
+    RestResponse r = userRestSession.put("/projects/" + Url.encode(PROJECT_WITH_SPACE), in);
+    r.assertConflict();
+    assertThat(r.getEntityContent()).contains("Project name cannot contain spaces");
+  }
+
+  @Test
+  @UseLocalDisk
+  @GerritConfig(name = "plugin.project-group-structure.nameRegex", value = REGEX_INCLUDING_SLASH)
+  public void shouldMatchProjectNameIfRegexContainsSlash() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.permissionsOnly = true;
+    RestResponse r = userRestSession.put("/projects/" + Url.encode("project1"), in);
+    userRestSession.put("/projects/" + Url.encode("project"), in).assertCreated();
+    r.assertConflict();
+    assertThat(r.getEntityContent())
+        .contains(String.format("Project name should match the regex: %s", REGEX_INCLUDING_SLASH));
+  }
+
+  @Test
+  @UseLocalDisk
+  @GerritConfig(
+      name = "plugin.project-group-structure.nameRegex",
+      value = REGEX_NOT_INCLUDING_SLASH)
+  public void shouldNotMatchProjectNameIfRegexMissesSlash() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.permissionsOnly = true;
+    userRestSession.put("/projects/" + Url.encode("PROJECT1"), in).assertCreated();
   }
 
   @Test