]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13160 Fix display of Portfolio Admin page when project contains UTF-8 characters
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 4 Mar 2020 17:06:32 +0000 (18:06 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 12 Mar 2020 20:04:29 +0000 (20:04 +0000)
14 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ValidateProjectStep.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/ValidateProjectStepTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentKeyUpdaterDao.java
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentKeyUpdaterDaoTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentService.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentUpdater.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ComponentUpdaterTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/project/ws/BulkUpdateKeyActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/project/ws/CreateActionTest.java
sonar-core/src/main/java/org/sonar/core/component/ComponentKeys.java
sonar-core/src/test/java/org/sonar/core/component/ComponentKeysTest.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectReactorValidator.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectReactorValidatorTest.java

index ddee476f0089a5cc0ef1c3a38d3c64b815895f62..47e752333344dfc2f52f396fe47751f41643b3fc 100644 (file)
@@ -27,6 +27,8 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.System2;
+import org.sonar.ce.task.log.CeTaskMessages;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
 import org.sonar.ce.task.projectanalysis.component.Component;
 import org.sonar.ce.task.projectanalysis.component.ComponentVisitor;
@@ -45,17 +47,9 @@ import org.sonar.db.component.SnapshotDto;
 import static com.google.common.base.Preconditions.checkState;
 import static java.lang.String.format;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.core.component.ComponentKeys.ALLOWED_CHARACTERS_MESSAGE;
+import static org.sonar.core.component.ComponentKeys.isValidProjectKey;
 
-/**
- * Validate project and modules. It will fail in the following cases :
- * <ol>
- * <li>module key is not valid</li>
- * <li>module key already exists in another project (same module key cannot exists in different projects)</li>
- * <li>module key is already used as a project key</li>
- * <li>date of the analysis is before last analysis</li>
- * <li>PR targets a branch that still contains modules</li>
- * </ol>
- */
 public class ValidateProjectStep implements ComputationStep {
 
   private static final Joiner MESSAGES_JOINER = Joiner.on("\n  o ");
@@ -63,11 +57,15 @@ public class ValidateProjectStep implements ComputationStep {
   private final DbClient dbClient;
   private final TreeRootHolder treeRootHolder;
   private final AnalysisMetadataHolder analysisMetadataHolder;
+  private final CeTaskMessages taskMessages;
+  private final System2 system2;
 
-  public ValidateProjectStep(DbClient dbClient, TreeRootHolder treeRootHolder, AnalysisMetadataHolder analysisMetadataHolder) {
+  public ValidateProjectStep(DbClient dbClient, TreeRootHolder treeRootHolder, AnalysisMetadataHolder analysisMetadataHolder, CeTaskMessages taskMessages, System2 system2) {
     this.dbClient = dbClient;
     this.treeRootHolder = treeRootHolder;
     this.analysisMetadataHolder = analysisMetadataHolder;
+    this.taskMessages = taskMessages;
+    this.system2 = system2;
   }
 
   @Override
@@ -75,6 +73,7 @@ public class ValidateProjectStep implements ComputationStep {
     try (DbSession dbSession = dbClient.openSession(false)) {
       validateTargetBranch(dbSession);
       Component root = treeRootHolder.getRoot();
+      // FIXME if module have really be dropped, no more need to load them
       List<ComponentDto> baseModules = dbClient.componentDao().selectEnabledModulesFromProjectKey(dbSession, root.getDbKey());
       Map<String, ComponentDto> baseModulesByKey = baseModules.stream().collect(Collectors.toMap(ComponentDto::getDbKey, x -> x));
       ValidateProjectsVisitor visitor = new ValidateProjectsVisitor(dbSession, dbClient.componentDao(), baseModulesByKey);
@@ -121,8 +120,21 @@ public class ValidateProjectStep implements ComputationStep {
     @Override
     public void visitProject(Component rawProject) {
       String rawProjectKey = rawProject.getDbKey();
-      Optional<ComponentDto> baseProject = loadBaseComponent(rawProjectKey);
-      validateAnalysisDate(baseProject);
+      Optional<ComponentDto> baseProjectOpt = loadBaseComponent(rawProjectKey);
+      validateAnalysisDate(baseProjectOpt);
+      if (!baseProjectOpt.isPresent()) {
+        return;
+      }
+      if (!isValidProjectKey(baseProjectOpt.get().getKey())) {
+        ComponentDto baseProject = baseProjectOpt.get();
+        // As it was possible in the past to use project key with a format that is no more compatible, we need to display a warning to the user in
+        // order for him to update his project key.
+        // SONAR-13191 This warning should be removed in 9.0, and instead the analysis should fail
+        taskMessages.add(new CeTaskMessages.Message(
+          format("The project key ‘%s’ contains invalid characters. %s. You should update the project key with the expected format.", baseProject.getKey(),
+            ALLOWED_CHARACTERS_MESSAGE),
+          system2.now()));
+      }
     }
 
     private void validateAnalysisDate(Optional<ComponentDto> baseProject) {
@@ -132,7 +144,7 @@ public class ValidateProjectStep implements ComputationStep {
         Long lastAnalysisDate = snapshotDto.map(SnapshotDto::getCreatedAt).orElse(null);
         if (lastAnalysisDate != null && currentAnalysisDate <= lastAnalysisDate) {
           validationMessages.add(format("Date of analysis cannot be older than the date of the last known analysis on this project. Value: \"%s\". " +
-              "Latest analysis: \"%s\". It's only possible to rebuild the past in a chronological order.",
+            "Latest analysis: \"%s\". It's only possible to rebuild the past in a chronological order.",
             formatDateTime(new Date(currentAnalysisDate)), formatDateTime(new Date(lastAnalysisDate))));
         }
       }
index 42a52d6d3a58e77c88f04bbc985a52bcc0e9d0b4..0a35b16968e507b4310493a0ce84e898794ea13a 100644 (file)
@@ -24,9 +24,11 @@ import javax.annotation.Nullable;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.sonar.api.impl.utils.TestSystem2;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.MessageException;
 import org.sonar.api.utils.System2;
+import org.sonar.ce.task.log.CeTaskMessages;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
 import org.sonar.ce.task.projectanalysis.analysis.Branch;
 import org.sonar.ce.task.projectanalysis.component.Component;
@@ -42,78 +44,78 @@ import org.sonar.db.component.ComponentTesting;
 import org.sonar.db.component.SnapshotTesting;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 public class ValidateProjectStepTest {
 
-  static long DEFAULT_ANALYSIS_TIME = 1433131200000L; // 2015-06-01
+  static long PAST_ANALYSIS_TIME = 1_420_088_400_000L; // 2015-01-01
+  static long DEFAULT_ANALYSIS_TIME = 1_433_131_200_000L; // 2015-06-01
+  static long NOW = 1_500_000_000_000L;
+
   static final String PROJECT_KEY = "PROJECT_KEY";
   static final Branch DEFAULT_BRANCH = new DefaultBranchImpl();
 
   @Rule
-  public DbTester dbTester = DbTester.create(System2.INSTANCE);
-
+  public DbTester db = DbTester.create(System2.INSTANCE);
   @Rule
   public ExpectedException thrown = ExpectedException.none();
-
   @Rule
   public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
-
   @Rule
   public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule()
     .setAnalysisDate(new Date(DEFAULT_ANALYSIS_TIME))
     .setBranch(DEFAULT_BRANCH);
+  private System2 system2 = new TestSystem2().setNow(NOW);
+
+  private CeTaskMessages taskMessages = mock(CeTaskMessages.class);
+  private DbClient dbClient = db.getDbClient();
 
-  private DbClient dbClient = dbTester.getDbClient();
-  private ValidateProjectStep underTest = new ValidateProjectStep(dbClient, treeRootHolder, analysisMetadataHolder);
+  private ValidateProjectStep underTest = new ValidateProjectStep(dbClient, treeRootHolder, analysisMetadataHolder, taskMessages, system2);
 
   @Test
-  public void fail_if_pr_is_targeting_branch_with_modules() {
-    ComponentDto masterProject = dbTester.components().insertPublicProject();
-    ComponentDto mergeBranch = dbTester.components().insertProjectBranch(masterProject, b -> b.setKey("mergeBranch"));
-    dbClient.componentDao().insert(dbTester.getSession(), ComponentTesting.newModuleDto(mergeBranch));
-    setBranch(BranchType.PULL_REQUEST, mergeBranch.uuid());
-    dbTester.getSession().commit();
+  public void dont_fail_for_long_forked_from_master_with_modules() {
+    ComponentDto masterProject = db.components().insertPublicProject();
+    dbClient.componentDao().insert(db.getSession(), ComponentTesting.newModuleDto(masterProject));
+    setBranch(BranchType.BRANCH, masterProject.uuid());
+    db.getSession().commit();
 
     treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid("DEFG")
       .setKey("branch")
       .build());
 
-    thrown.expect(MessageException.class);
-    thrown.expectMessage("Due to an upgrade, you need first to re-analyze the target branch 'mergeBranch' before analyzing this pull request.");
     underTest.execute(new TestComputationStepContext());
+    verifyNoInteractions(taskMessages);
   }
 
   @Test
-  public void dont_fail_for_long_forked_from_master_with_modules() {
-    ComponentDto masterProject = dbTester.components().insertPublicProject();
-    dbClient.componentDao().insert(dbTester.getSession(), ComponentTesting.newModuleDto(masterProject));
-    setBranch(BranchType.BRANCH, masterProject.uuid());
-    dbTester.getSession().commit();
+  public void not_fail_if_analysis_date_is_after_last_analysis() {
+    ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert(), "ABCD").setDbKey(PROJECT_KEY);
+    dbClient.componentDao().insert(db.getSession(), project);
+    dbClient.snapshotDao().insert(db.getSession(), SnapshotTesting.newAnalysis(project).setCreatedAt(PAST_ANALYSIS_TIME));
+    db.getSession().commit();
 
-    treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid("DEFG")
-      .setKey("branch")
-      .build());
+    treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid("ABCD").setKey(PROJECT_KEY).build());
 
     underTest.execute(new TestComputationStepContext());
   }
 
-  private void setBranch(BranchType type, @Nullable String mergeBranchUuid) {
-    Branch branch = mock(Branch.class);
-    when(branch.getType()).thenReturn(type);
-    when(branch.getReferenceBranchUuid()).thenReturn(mergeBranchUuid);
-    analysisMetadataHolder.setBranch(branch);
-  }
-
   @Test
-  public void not_fail_if_analysis_date_is_after_last_analysis() {
-    ComponentDto project = ComponentTesting.newPrivateProjectDto(dbTester.organizations().insert(), "ABCD").setDbKey(PROJECT_KEY);
-    dbClient.componentDao().insert(dbTester.getSession(), project);
-    dbClient.snapshotDao().insert(dbTester.getSession(), SnapshotTesting.newAnalysis(project).setCreatedAt(1420088400000L)); // 2015-01-01
-    dbTester.getSession().commit();
+  public void fail_if_pr_is_targeting_branch_with_modules() {
+    ComponentDto masterProject = db.components().insertPublicProject();
+    ComponentDto mergeBranch = db.components().insertProjectBranch(masterProject, b -> b.setKey("mergeBranch"));
+    dbClient.componentDao().insert(db.getSession(), ComponentTesting.newModuleDto(mergeBranch));
+    setBranch(BranchType.PULL_REQUEST, mergeBranch.uuid());
+    db.getSession().commit();
 
-    treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid("ABCD").setKey(PROJECT_KEY).build());
+    treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid("DEFG")
+      .setKey("branch")
+      .build());
 
+    thrown.expect(MessageException.class);
+    thrown.expectMessage("Due to an upgrade, you need first to re-analyze the target branch 'mergeBranch' before analyzing this pull request.");
     underTest.execute(new TestComputationStepContext());
   }
 
@@ -121,10 +123,10 @@ public class ValidateProjectStepTest {
   public void fail_if_analysis_date_is_before_last_analysis() {
     analysisMetadataHolder.setAnalysisDate(DateUtils.parseDate("2015-01-01"));
 
-    ComponentDto project = ComponentTesting.newPrivateProjectDto(dbTester.organizations().insert(), "ABCD").setDbKey(PROJECT_KEY);
-    dbClient.componentDao().insert(dbTester.getSession(), project);
-    dbClient.snapshotDao().insert(dbTester.getSession(), SnapshotTesting.newAnalysis(project).setCreatedAt(1433131200000L)); // 2015-06-01
-    dbTester.getSession().commit();
+    ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert(), "ABCD").setDbKey(PROJECT_KEY);
+    dbClient.componentDao().insert(db.getSession(), project);
+    dbClient.snapshotDao().insert(db.getSession(), SnapshotTesting.newAnalysis(project).setCreatedAt(1433131200000L)); // 2015-06-01
+    db.getSession().commit();
 
     treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid("ABCD").setKey(PROJECT_KEY).build());
 
@@ -136,4 +138,28 @@ public class ValidateProjectStepTest {
     underTest.execute(new TestComputationStepContext());
   }
 
+  @Test
+  public void add_warning_when_project_key_is_invalid() {
+    ComponentDto project = db.components().insertPrivateProject(p -> p.setDbKey("inv$lid!"));
+    db.components().insertSnapshot(project, a -> a.setCreatedAt(PAST_ANALYSIS_TIME));
+    treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1)
+      .setUuid(project.uuid())
+      .setKey(project.getKey())
+      .build());
+
+    underTest.execute(new TestComputationStepContext());
+
+    verify(taskMessages, times(1))
+      .add(new CeTaskMessages.Message(
+        "The project key ‘inv$lid!’ contains invalid characters. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit. " +
+          "You should update the project key with the expected format.",
+        NOW));
+  }
+
+  private void setBranch(BranchType type, @Nullable String mergeBranchUuid) {
+    Branch branch = mock(Branch.class);
+    when(branch.getType()).thenReturn(type);
+    when(branch.getReferenceBranchUuid()).thenReturn(mergeBranchUuid);
+    analysisMetadataHolder.setBranch(branch);
+  }
 }
index 61dcb2242b63cbe13cb56bc86d5f062655f1e8e8..902f569766d85fd677c3c181bf1b298832dfab22 100644 (file)
@@ -47,6 +47,7 @@ import static org.sonar.core.component.ComponentKeys.checkProjectKey;
  * @since 3.2
  */
 public class ComponentKeyUpdaterDao implements Dao {
+
   public void updateKey(DbSession dbSession, String projectUuid, String newKey) {
     ComponentKeyUpdaterMapper mapper = dbSession.getMapper(ComponentKeyUpdaterMapper.class);
     if (mapper.countResourceByKey(newKey) > 0) {
index 085f07a8139cfe4aa040157bedcdb3ee439f0ab2..c37af6ba61cf6bd5685d8e2d103187808b177e71 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.db.component;
 
 import com.google.common.base.Strings;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -368,9 +367,9 @@ public class ComponentKeyUpdaterDaoTest {
     ComponentDto project = db.components().insertPrivateProject();
 
     thrown.expect(IllegalArgumentException.class);
-    thrown.expectMessage("Malformed key for '  '. Project key cannot be empty nor contain whitespaces.");
+    thrown.expectMessage("Malformed key for 'my?project?key'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
 
-    underTest.bulkUpdateKey(dbSession, project.uuid(), project.getDbKey(), "  ", doNotReturnAnyRekeyedResource());
+    underTest.bulkUpdateKey(dbSession, project.uuid(), project.getDbKey(), "my?project?key", doNotReturnAnyRekeyedResource());
   }
 
   @Test
@@ -431,7 +430,7 @@ public class ComponentKeyUpdaterDaoTest {
 
     thrown.expect(IllegalArgumentException.class);
 
-    underTest.simulateBulkUpdateKey(dbSession, "A", "project", "  ");
+    underTest.simulateBulkUpdateKey(dbSession, "A", "project", "project?");
   }
 
   @Test
index 064d94f81a2f93de92cf5ca0408ab59d1935e659..7a32281a2f560b9b4bec2966d0c7b3b0b15498b9 100644 (file)
@@ -41,8 +41,7 @@ import org.sonar.server.user.UserSession;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
-import static org.sonar.core.component.ComponentKeys.isValidProjectKey;
-import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+import static org.sonar.core.component.ComponentKeys.checkProjectKey;
 
 @ServerSide
 public class ComponentService {
@@ -60,7 +59,7 @@ public class ComponentService {
 
   public void updateKey(DbSession dbSession, ProjectDto project, String newKey) {
     userSession.checkProjectPermission(UserRole.ADMIN, project);
-    checkProjectKeyFormat(newKey);
+    checkProjectKey(newKey);
     dbClient.componentKeyUpdaterDao().updateKey(dbSession, project.getUuid(), newKey);
     projectIndexers.commitAndIndexProjects(dbSession, singletonList(project), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
     Project newProject = new Project(project.getUuid(), newKey, project.getName(), project.getDescription(), project.getTags());
@@ -94,8 +93,4 @@ public class ComponentService {
     return new RekeyedProject(project, rekeyedResource.getOldKey());
   }
 
-  private static void checkProjectKeyFormat(String key) {
-    checkRequest(isValidProjectKey(key), "Malformed key for '%s'. It cannot be empty nor contain whitespaces.", key);
-  }
-
 }
index 5bba07896c83b6860eb10854eda21aef5f554650..0ae333ec5eeb6ef6a4131cda8177478b3b35dcb3 100644 (file)
@@ -42,8 +42,8 @@ import org.sonar.server.favorite.FavoriteUpdater;
 import org.sonar.server.permission.PermissionTemplateService;
 
 import static java.util.Collections.singletonList;
-import static org.sonar.api.resources.Qualifiers.APP;
 import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.core.component.ComponentKeys.ALLOWED_CHARACTERS_MESSAGE;
 import static org.sonar.core.component.ComponentKeys.isValidProjectKey;
 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
 
@@ -170,7 +170,7 @@ public class ComponentUpdater {
   }
 
   private void checkKeyFormat(String qualifier, String key) {
-    checkRequest(isValidProjectKey(key), "Malformed key for %s: '%s'. It cannot be empty nor contain whitespaces.", getQualifierToDisplay(qualifier), key);
+    checkRequest(isValidProjectKey(key), "Malformed key for %s: '%s'. %s.", getQualifierToDisplay(qualifier), key, ALLOWED_CHARACTERS_MESSAGE);
   }
 
   private String getQualifierToDisplay(String qualifier) {
index 18772de9c9c1c9bf0474a8d3ee8a62a4575ac915..43b0450498d124695ca3be904f66b07c7f953f7c 100644 (file)
@@ -33,7 +33,6 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
 import org.sonar.server.es.ProjectIndexer;
 import org.sonar.server.es.TestProjectIndexers;
-import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.project.Project;
 import org.sonar.server.project.ProjectLifeCycleListeners;
@@ -132,21 +131,21 @@ public class ComponentServiceUpdateKeyTest {
     ComponentDto project = insertSampleProject();
     logInAsProjectAdministrator(project);
 
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Malformed key for ''. It cannot be empty nor contain whitespaces.");
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Malformed key for ''. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
 
     underTest.updateKey(dbSession, componentDb.getProjectDto(project), "");
   }
 
   @Test
-  public void fail_if_new_key_is_invalid() {
+  public void fail_if_new_key_is_not_formatted_correctly() {
     ComponentDto project = insertSampleProject();
     logInAsProjectAdministrator(project);
 
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Malformed key for 'sample root'. It cannot be empty nor contain whitespaces.");
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Malformed key for 'sample?root'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
 
-    underTest.updateKey(dbSession, componentDb.getProjectDto(project), "sample root");
+    underTest.updateKey(dbSession, componentDb.getProjectDto(project), "sample?root");
   }
 
   @Test
index c592065c9aaf297d54a868cb93fb5ddec3d1d3dd..bb2ea365fff18a1f81066c7fe219fd58811ee6e1 100644 (file)
@@ -290,11 +290,11 @@ public class ComponentUpdaterTest {
   @Test
   public void fail_when_key_has_bad_format() {
     expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Malformed key for Project: '  '");
+    expectedException.expectMessage("Malformed key for Project: '1234'");
 
     underTest.create(db.getSession(),
       NewComponent.newComponentBuilder()
-        .setKey("  ")
+        .setKey("1234")
         .setName(DEFAULT_PROJECT_NAME)
         .setOrganizationUuid(db.getDefaultOrganization().getUuid())
         .build(),
@@ -302,13 +302,13 @@ public class ComponentUpdaterTest {
   }
 
   @Test
-  public void properly_fail_when_key_contains_percent_character() {
+  public void fail_when_key_contains_percent_character() {
     expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Malformed key for Project: '  '");
+    expectedException.expectMessage("Malformed key for Project: 'roject%Key'");
 
     underTest.create(db.getSession(),
       NewComponent.newComponentBuilder()
-        .setKey("  ")
+        .setKey("roject%Key")
         .setName(DEFAULT_PROJECT_NAME)
         .setOrganizationUuid(db.getDefaultOrganization().getUuid())
         .build(),
index a85345e71cee50c9c1a76b3f9cadafb9daf6280b..638c71b45d126becefeb361398acb53c4dc74cdf 100644 (file)
@@ -60,6 +60,7 @@ import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_TO;
 
 public class BulkUpdateKeyActionTest {
+
   private static final String MY_PROJECT_KEY = "my_project";
   private static final String FROM = "my_";
   private static final String TO = "your_";
@@ -169,9 +170,9 @@ public class BulkUpdateKeyActionTest {
     insertMyProject();
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("Malformed key for 'my aproject'. Project key cannot be empty nor contain whitespaces.");
+    expectedException.expectMessage("Malformed key for 'my?project'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
 
-    callByKey(MY_PROJECT_KEY, FROM, "my a");
+    callByKey(MY_PROJECT_KEY, FROM, "my?");
   }
 
   @Test
@@ -179,9 +180,9 @@ public class BulkUpdateKeyActionTest {
     insertMyProject();
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("Malformed key for 'my aproject'. Project key cannot be empty nor contain whitespaces.");
+    expectedException.expectMessage("Malformed key for 'my?project'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
 
-    callDryRunByKey(MY_PROJECT_KEY, FROM, "my a");
+    callDryRunByKey(MY_PROJECT_KEY, FROM, "my?");
   }
 
   @Test
index def3cc53b025bb1e98bd3715d2924966074a08d9..4b23370bc14219456eeaa612cb8fe24df683d251 100644 (file)
@@ -273,14 +273,14 @@ public class CreateActionTest {
   }
 
   @Test
-  public void properly_fail_when_invalid_project_key() {
+  public void fail_when_invalid_project_key() {
     userSession.addPermission(PROVISION_PROJECTS, db.getDefaultOrganization());
 
     expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Malformed key for Project: 'project Key'. It cannot be empty nor contain whitespaces.");
+    expectedException.expectMessage("Malformed key for Project: 'project%Key'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
 
     call(CreateRequest.builder()
-      .setProjectKey("project Key")
+      .setProjectKey("project%Key")
       .setName(DEFAULT_PROJECT_NAME)
       .build());
   }
index 8378a30bab161325dbced0850bb1ef4910b17344..5eb95b145340a2df689d9a550421512ea9e51988 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.core.component;
 
+import java.util.regex.Pattern;
 import javax.annotation.Nullable;
 import org.apache.commons.lang.StringUtils;
 
@@ -28,15 +29,14 @@ public final class ComponentKeys {
 
   public static final int MAX_COMPONENT_KEY_LENGTH = 400;
 
-  /*
-   * Must not be blank or empty
-   */
-  private static final String VALID_PROJECT_KEY_REGEXP = "[^\\p{javaWhitespace}]+";
+  public static final String ALLOWED_CHARACTERS_MESSAGE = "Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit";
+
+  public static final String MALFORMED_KEY_MESSAGE = "Malformed key for '%s'. %s.";
 
-  /*
-   * Allowed characters are alphanumeric, '-', '_', '.' and '/'
+  /**
+   * Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit
    */
-  private static final String VALID_BRANCH_REGEXP = "[\\p{Alnum}\\-_./]*";
+  private static final Pattern VALID_PROJECT_KEY_REGEXP = Pattern.compile("[\\p{Alnum}\\-_.:]*[\\p{Alpha}\\-_.:]+[\\p{Alnum}\\-_.:]*");
 
   private static final String KEY_WITH_BRANCH_FORMAT = "%s:%s";
 
@@ -53,13 +53,8 @@ public final class ComponentKeys {
     return sb.toString();
   }
 
-  /**
-   * Test if given parameter is valid for a project. A key is valid if it doesn't contain whitespaces.
-   *
-   * @return <code>true</code> if <code>keyCandidate</code> can be used for a project
-   */
   public static boolean isValidProjectKey(String keyCandidate) {
-    return keyCandidate.matches(VALID_PROJECT_KEY_REGEXP);
+    return VALID_PROJECT_KEY_REGEXP.matcher(keyCandidate).matches();
   }
 
   /**
@@ -68,7 +63,7 @@ public final class ComponentKeys {
    * @throws IllegalArgumentException if the format is incorrect
    */
   public static void checkProjectKey(String keyCandidate) {
-    checkArgument(isValidProjectKey(keyCandidate), "Malformed key for '%s'. %s", keyCandidate, "Project key cannot be empty nor contain whitespaces.");
+    checkArgument(isValidProjectKey(keyCandidate), MALFORMED_KEY_MESSAGE, keyCandidate, ALLOWED_CHARACTERS_MESSAGE);
   }
 
   /**
index 05848ac1c569087a47f4d32e8cd93739c5fce324..eef16fac229cb4096a735005459e867e6e48675b 100644 (file)
@@ -37,12 +37,18 @@ public class ComponentKeysTest {
   }
 
   @Test
-  public void isValidProjectKey() {
+  public void valid_project_key() {
     assertThat(ComponentKeys.isValidProjectKey("abc")).isTrue();
-    assertThat(ComponentKeys.isValidProjectKey("0123")).isTrue();
     assertThat(ComponentKeys.isValidProjectKey("ab_12")).isTrue();
-    assertThat(ComponentKeys.isValidProjectKey("ab/12")).isTrue();
-    assertThat(ComponentKeys.isValidProjectKey("코드품질")).isTrue();
+  }
+
+  @Test
+  public void invalid_project_key() {
+    assertThat(ComponentKeys.isValidProjectKey("0123")).isFalse();
+
+    assertThat(ComponentKeys.isValidProjectKey("ab/12")).isFalse();
+    assertThat(ComponentKeys.isValidProjectKey("코드품질")).isFalse();
+
     assertThat(ComponentKeys.isValidProjectKey("")).isFalse();
     assertThat(ComponentKeys.isValidProjectKey(" ")).isFalse();
     assertThat(ComponentKeys.isValidProjectKey("ab 12")).isFalse();
@@ -69,4 +75,19 @@ public class ComponentKeysTest {
 
     ComponentKeys.checkProjectKey("ab 12");
   }
+
+  @Test
+  public void checkProjectKey_fail_if_only_digit() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Malformed key for '0123'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
+
+    ComponentKeys.checkProjectKey("0123");
+  }
+
+  @Test
+  public void checkProjectKey_fail_if_special_characters_not_allowed() {
+    expectedException.expect(IllegalArgumentException.class);
+
+    ComponentKeys.checkProjectKey("ab/12");
+  }
 }
index 319d5eabf6347ef09f3be67613a16971886eef2a..ccb7173b572ebf5bcb92a87f316e6f53541e8398 100644 (file)
 package org.sonar.scanner.scan;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.sonar.api.batch.bootstrap.ProjectDefinition;
 import org.sonar.api.batch.bootstrap.ProjectReactor;
 import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.component.ComponentKeys;
 import org.sonar.scanner.bootstrap.GlobalConfiguration;
 import org.sonar.scanner.scan.branch.BranchParamsValidator;
 
 import static java.lang.String.format;
+import static java.util.Collections.singletonList;
 import static java.util.Objects.nonNull;
 import static org.apache.commons.lang.StringUtils.isNotEmpty;
+import static org.sonar.core.component.ComponentKeys.ALLOWED_CHARACTERS_MESSAGE;
 import static org.sonar.core.config.ScannerProperties.BRANCHES_DOC_LINK;
 import static org.sonar.core.config.ScannerProperties.BRANCH_NAME;
 import static org.sonar.core.config.ScannerProperties.PULL_REQUEST_BASE;
@@ -46,6 +49,9 @@ import static org.sonar.core.config.ScannerProperties.PULL_REQUEST_KEY;
  * @since 3.6
  */
 public class ProjectReactorValidator {
+
+  private static final Logger LOG = Loggers.get(ProjectReactorValidator.class);
+
   private final GlobalConfiguration settings;
 
   // null = branch plugin is not available
@@ -65,7 +71,7 @@ public class ProjectReactorValidator {
     List<String> validationMessages = new ArrayList<>();
 
     for (ProjectDefinition moduleDef : reactor.getProjects()) {
-      validateModule(moduleDef, validationMessages);
+      validateModule(moduleDef);
     }
 
     if (isBranchFeatureAvailable()) {
@@ -82,7 +88,7 @@ public class ProjectReactorValidator {
   }
 
   private void validateBranchParamsWhenPluginAbsent(List<String> validationMessages) {
-    for (String param : Arrays.asList(BRANCH_NAME)) {
+    for (String param : singletonList(BRANCH_NAME)) {
       if (isNotEmpty(settings.get(param).orElse(null))) {
         validationMessages.add(format("To use the property \"%s\" and analyze branches, Developer Edition or above is required. "
           + "See %s for more information.", param, BRANCHES_DOC_LINK));
@@ -97,9 +103,12 @@ public class ProjectReactorValidator {
         + "See %s for more information.", param, BRANCHES_DOC_LINK)));
   }
 
-  private static void validateModule(ProjectDefinition moduleDef, List<String> validationMessages) {
+  private static void validateModule(ProjectDefinition moduleDef) {
     if (!ComponentKeys.isValidProjectKey(moduleDef.getKey())) {
-      validationMessages.add(format("\"%s\" is not a valid project or module key. It cannot be empty nor contain whitespaces.", moduleDef.getKey()));
+      // As it was possible in the past to use project key with a format that is no more compatible, we need to display a warning to the user in
+      // order for him to update his project key.
+      // SONAR-13191 This warning should be removed in 9.0
+      LOG.warn("\"{}\" is not a valid project or module key. {}.", moduleDef.getKey(), ALLOWED_CHARACTERS_MESSAGE);
     }
   }
 
index 91993c340c57b2ade6b94093cebf96efd60e161e..f74dd8df5bd3e74b94676da74a9b205329e9c255 100644 (file)
@@ -34,11 +34,15 @@ import org.sonar.api.CoreProperties;
 import org.sonar.api.batch.bootstrap.ProjectDefinition;
 import org.sonar.api.batch.bootstrap.ProjectReactor;
 import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.LogAndArguments;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.core.config.ScannerProperties;
 import org.sonar.scanner.ProjectInfo;
 import org.sonar.scanner.bootstrap.GlobalConfiguration;
 
 import static org.apache.commons.lang.RandomStringUtils.randomAscii;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -50,6 +54,9 @@ public class ProjectReactorValidatorTest {
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  @Rule
+  public LogTester logTester = new LogTester();
+
   private GlobalConfiguration settings = mock(GlobalConfiguration.class);
   private ProjectInfo projectInfo = mock(ProjectInfo.class);
   private ProjectReactorValidator underTest = new ProjectReactorValidator(settings);
@@ -86,12 +93,36 @@ public class ProjectReactorValidatorTest {
   }
 
   @Test
-  public void fail_with_invalid_key() {
-    ProjectReactor reactor = createProjectReactor("  ");
+  public void log_warning_when_invalid_key() {
+    ProjectReactor reactor = createProjectReactor("foo$bar");
+
+    underTest.validate(reactor);
+
+    assertThat(logTester.getLogs(LoggerLevel.WARN))
+      .extracting(LogAndArguments::getFormattedMsg)
+      .containsOnly("\"foo$bar\" is not a valid project or module key. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
+  }
+
+  @Test
+  public void log_warning_when_only_digits() {
+    ProjectReactor reactor = createProjectReactor("12345");
+
+    underTest.validate(reactor);
+
+    assertThat(logTester.getLogs(LoggerLevel.WARN))
+      .extracting(LogAndArguments::getFormattedMsg)
+      .containsOnly("\"12345\" is not a valid project or module key. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
+  }
+
+  @Test
+  public void log_warning_when_backslash_in_key() {
+    ProjectReactor reactor = createProjectReactor("foo\\bar");
 
-    thrown.expect(MessageException.class);
-    thrown.expectMessage("\"  \" is not a valid project or module key");
     underTest.validate(reactor);
+
+    assertThat(logTester.getLogs(LoggerLevel.WARN))
+      .extracting(LogAndArguments::getFormattedMsg)
+      .containsOnly("\"foo\\bar\" is not a valid project or module key. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
   }
 
   @Test