]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19453 New code definition is made part of project creation (#8470)
authorNolwenn Cadic <98824442+Nolwenn-cadic-sonarsource@users.noreply.github.com>
Thu, 8 Jun 2023 14:20:36 +0000 (16:20 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 14 Jun 2023 09:51:05 +0000 (09:51 +0000)
Co-authored-by: Zipeng WU <zipeng.wu@sonarsource.com>
server/sonar-webserver-webapi/src/it/java/org/sonar/server/newcodeperiod/NewCodePeriodUtilsTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/CreateActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/NewCodePeriodUtils.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/ws/SetAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/CreateAction.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java

diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/newcodeperiod/NewCodePeriodUtilsTest.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/newcodeperiod/NewCodePeriodUtilsTest.java
new file mode 100644 (file)
index 0000000..088b987
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.newcodeperiod;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.project.ProjectDto;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.getNewCodeDefinitionValue;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.getNewCodeDefinitionValueProjectCreation;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.validateType;
+
+public class NewCodePeriodUtilsTest {
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE, true);
+
+  private static final String MAIN_BRANCH = "main";
+  private ComponentDbTester componentDb = new ComponentDbTester(db);
+
+  private DbSession dbSession = db.getSession();
+  private DbClient dbClient = db.getDbClient();
+  @Test
+  public void validateType_throw_IAE_if_no_valid_type() {
+    assertThatThrownBy(() -> validateType("nonValid", false, false))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Invalid type: nonValid");
+  }
+
+  @Test
+  public void validateType_throw_IAE_if_type_is_invalid_for_global() {
+    assertThatThrownBy(() -> validateType("SPECIFIC_ANALYSIS", true, false))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Invalid type 'SPECIFIC_ANALYSIS'. Overall setting can only be set with types: [PREVIOUS_VERSION, NUMBER_OF_DAYS]");
+  }
+
+  @Test
+  public void validateType_throw_IAE_if_type_is_invalid_for_project() {
+    assertThatThrownBy(() -> validateType("SPECIFIC_ANALYSIS", false, false))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Invalid type 'SPECIFIC_ANALYSIS'. Projects can only be set with types: [PREVIOUS_VERSION, NUMBER_OF_DAYS, REFERENCE_BRANCH]");
+  }
+
+  @Test
+  public void validateType_return_type_for_branch() {
+    assertThat(validateType("REFERENCE_BRANCH", false, true)).isEqualTo(REFERENCE_BRANCH);
+  }
+
+  @Test
+  public void validateType_return_type_for_project() {
+    assertThat(validateType("REFERENCE_BRANCH", false, false)).isEqualTo(REFERENCE_BRANCH);
+  }
+
+  @Test
+  public void validateType_return_type_for_overall() {
+    assertThat(validateType("PREVIOUS_VERSION", true, false)).isEqualTo(PREVIOUS_VERSION);
+  }
+
+  @Test
+  public void getNCDValue_throw_IAE_if_no_value_for_days() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, NUMBER_OF_DAYS, null, null, null))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("New code definition type 'NUMBER_OF_DAYS' requires a value");
+  }
+
+  @Test
+  public void getNCDValue_throw_IAE_if_no_value_for_reference_branch() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, REFERENCE_BRANCH, null, null, null))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("New code definition type 'REFERENCE_BRANCH' requires a value");
+  }
+
+  @Test
+  public void getNCDValue_throw_IAE_if_no_value_for_analysis() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, SPECIFIC_ANALYSIS, null, null, null))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("New code definition type 'SPECIFIC_ANALYSIS' requires a value");
+  }
+
+  @Test
+  public void getNCDValue_throw_IAE_if_days_is_invalid() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, NUMBER_OF_DAYS, null, null, "unknown"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Failed to parse number of days: unknown");
+  }
+
+  @Test
+  public void getNCDValue_throw_IAE_if_previous_version_type_and_value_provided() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, PREVIOUS_VERSION, null, null, "someValue"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Unexpected value for type 'PREVIOUS_VERSION'");
+  }
+
+  @Test
+  public void getNCDValue_return_empty_for_previous_version_type() {
+    assertThat(getNewCodeDefinitionValue(dbSession, dbClient, PREVIOUS_VERSION, null, null, null)).isEmpty();
+  }
+
+  @Test
+  public void getNCDValue_return_days_value_for_number_of_days_type() {
+    String numberOfDays = "30";
+
+    assertThat(getNewCodeDefinitionValue(dbSession, dbClient, NUMBER_OF_DAYS, null, null, numberOfDays))
+      .isPresent()
+      .get()
+      .isEqualTo(numberOfDays);
+  }
+
+  @Test
+  public void getNCDValue_return_specific_analysis_uuid_for_specific_analysis_type() {
+    ComponentDto project = componentDb.insertPublicProject().getMainBranchComponent();
+    SnapshotDto analysisMaster = db.components().insertSnapshot(project);
+    ProjectDto projectDto = new ProjectDto().setUuid(project.uuid());
+    BranchDto branchDto = new BranchDto().setUuid(project.uuid());
+    String numberOfDays = "30";
+
+    assertThat(getNewCodeDefinitionValue(dbSession, dbClient, SPECIFIC_ANALYSIS, projectDto, branchDto, analysisMaster.getUuid()))
+      .isPresent()
+      .get()
+      .isEqualTo(analysisMaster.getUuid());
+  }
+
+  @Test
+  public void getNCDValue_return_branch_value_for_reference_branch_type() {
+    String branchKey = "main";
+
+    assertThat(getNewCodeDefinitionValue(dbSession, dbClient, REFERENCE_BRANCH, null, null, branchKey))
+      .isPresent()
+      .get()
+      .isEqualTo(branchKey);
+  }
+
+  @Test
+  public void getNCDValueProjectCreation_throw_IAE_if_no_value_for_days() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValueProjectCreation(NUMBER_OF_DAYS, null, MAIN_BRANCH))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("New code definition type 'NUMBER_OF_DAYS' requires a value");
+  }
+
+  @Test
+  public void getNCDValueProjectCreation_throw_IAE_if_days_is_invalid() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValueProjectCreation(NUMBER_OF_DAYS, "unknown", MAIN_BRANCH))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Failed to parse number of days: unknown");
+  }
+
+  @Test
+  public void getNCDValueProjectCreation_throw_IAE_if_previous_version_type_and_value_provided() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValueProjectCreation(PREVIOUS_VERSION, "someValue", MAIN_BRANCH))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Unexpected value for type 'PREVIOUS_VERSION'");
+  }
+
+  @Test
+  public void getNCDValueProjectCreation_return_empty_for_previous_version_type() {
+    assertThat(getNewCodeDefinitionValueProjectCreation(PREVIOUS_VERSION,  null, MAIN_BRANCH)).isEmpty();
+  }
+
+  @Test
+  public void getNCDValueProjectCreation_return_days_value_for_number_of_days_type() {
+    String numberOfDays = "30";
+
+    assertThat(getNewCodeDefinitionValueProjectCreation(NUMBER_OF_DAYS, numberOfDays, MAIN_BRANCH))
+      .isPresent()
+      .get()
+      .isEqualTo(numberOfDays);
+  }
+
+  @Test
+  public void getNCDValueProjectCreation_return_branch_value_for_reference_branch_type() {
+    String branchKey = "main";
+
+    assertThat(getNewCodeDefinitionValueProjectCreation(REFERENCE_BRANCH, null, branchKey))
+      .isPresent()
+      .get()
+      .isEqualTo(branchKey);
+  }
+
+  @Test
+  public void getNCDValueProjectCreation_throw_IAE_fiif_reference_branch_type_and_value_provided() {
+    assertThatThrownBy(() -> getNewCodeDefinitionValueProjectCreation(REFERENCE_BRANCH, "someValue", MAIN_BRANCH))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Unexpected value for type 'REFERENCE_BRANCH'");
+  }
+
+}
index cfc0f3d3d1db0931971d0c5d4461be2d4f08c974..dcfa88bdd86a17a0434c36efca4137dfd79a2228 100644 (file)
 package org.sonar.server.project.ws;
 
 import com.google.common.base.Strings;
+import java.util.Optional;
 import javax.annotation.Nullable;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.System2;
+import org.sonar.core.platform.EditionProvider;
+import org.sonar.core.platform.PlatformEditionProvider;
 import org.sonar.core.util.SequenceUuidFactory;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.component.ComponentUpdater;
@@ -59,12 +63,18 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.component.BranchDto.DEFAULT_MAIN_BRANCH_NAME;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
 import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION;
 import static org.sonar.server.project.Visibility.PRIVATE;
 import static org.sonar.test.JsonAssert.assertJson;
 import static org.sonarqube.ws.client.WsRequest.Method.POST;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_MAIN_BRANCH;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NAME;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_TYPE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_VALUE;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
 
@@ -87,12 +97,14 @@ public class CreateActionIT {
   private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
   private final TestProjectIndexers projectIndexers = new TestProjectIndexers();
   private final PermissionTemplateService permissionTemplateService = mock(PermissionTemplateService.class);
+
+  private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
   private final WsActionTester ws = new WsActionTester(
     new CreateAction(
       db.getDbClient(), userSession,
       new ComponentUpdater(db.getDbClient(), i18n, system2, permissionTemplateService, new FavoriteUpdater(db.getDbClient()),
         projectIndexers, new SequenceUuidFactory(), defaultBranchNameResolver, true),
-      projectDefaultVisibility));
+      projectDefaultVisibility, editionProvider, defaultBranchNameResolver));
 
   @Before
   public void before() {
@@ -208,7 +220,8 @@ public class CreateActionIT {
   public void do_not_add_project_to_user_favorites_if_project_creator_is_defined_in_permission_template_and_already_100_favorites() {
     UserDto user = db.users().insertUser();
     when(permissionTemplateService.hasDefaultTemplateWithPermissionOnProjectCreator(any(DbSession.class), any(ComponentDto.class))).thenReturn(true);
-    rangeClosed(1, 100).forEach(i -> db.favorites().add(db.components().insertPrivateProject().getProjectDto(), user.getUuid(), user.getLogin()));
+    rangeClosed(1, 100).forEach(i -> db.favorites().add(db.components().insertPrivateProject().getProjectDto(), user.getUuid(),
+      user.getLogin()));
     userSession.logIn(user).addPermission(PROVISION_PROJECTS);
 
     ws.newRequest()
@@ -243,14 +256,15 @@ public class CreateActionIT {
       .build();
     assertThatThrownBy(() -> call(request))
       .isInstanceOf(BadRequestException.class)
-      .hasMessage("Malformed key for Project: 'project%Key'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
+      .hasMessage("Malformed key for Project: 'project%Key'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least " +
+        "one non-digit.");
   }
 
   @Test
   public void fail_when_missing_project_parameter() {
     userSession.addPermission(PROVISION_PROJECTS);
 
-    assertThatThrownBy(() -> call(null, DEFAULT_PROJECT_NAME, null))
+    assertThatThrownBy(() -> call(null, DEFAULT_PROJECT_NAME, null, null, null))
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("The 'project' parameter is missing");
   }
@@ -259,7 +273,7 @@ public class CreateActionIT {
   public void fail_when_missing_name_parameter() {
     userSession.addPermission(PROVISION_PROJECTS);
 
-    assertThatThrownBy(() -> call(DEFAULT_PROJECT_KEY, null, null))
+    assertThatThrownBy(() -> call(DEFAULT_PROJECT_KEY, null, null, null, null))
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("The 'name' parameter is missing");
   }
@@ -296,7 +310,10 @@ public class CreateActionIT {
     assertThat(definition.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder(
       PARAM_VISIBILITY,
       PARAM_NAME,
-      PARAM_PROJECT, PARAM_MAIN_BRANCH);
+      PARAM_PROJECT,
+      PARAM_MAIN_BRANCH,
+      PARAM_NEW_CODE_DEFINITION_TYPE,
+      PARAM_NEW_CODE_DEFINITION_VALUE);
 
     WebService.Param visibilityParam = definition.param(PARAM_VISIBILITY);
     assertThat(visibilityParam.description()).isNotEmpty();
@@ -312,6 +329,15 @@ public class CreateActionIT {
     WebService.Param name = definition.param(PARAM_NAME);
     assertThat(name.isRequired()).isTrue();
     assertThat(name.description()).isEqualTo("Name of the project. If name is longer than 500, it is abbreviated.");
+
+    WebService.Param ncdType = definition.param(PARAM_NEW_CODE_DEFINITION_TYPE);
+    assertThat(ncdType.isRequired()).isFalse();
+    assertThat(ncdType.description()).isEqualTo(NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION);
+
+    WebService.Param ncdValue = definition.param(PARAM_NEW_CODE_DEFINITION_VALUE);
+    assertThat(ncdValue.isRequired()).isFalse();
+    assertThat(ncdValue.description()).isEqualTo(NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION);
+
   }
 
   @Test
@@ -349,16 +375,207 @@ public class CreateActionIT {
       .isInstanceOf(NullPointerException.class);
   }
 
+  @Test
+  public void set_default_branch_name_for_reference_branch_NCD_when_no_main_branch_provided() {
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    String otherBranchName = "otherBranchName";
+
+    when(defaultBranchNameResolver.getEffectiveMainBranchName()).thenReturn(otherBranchName);
+
+    call(CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setNewCodeDefinitionType(REFERENCE_BRANCH.name())
+      .build());
+
+    ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get();
+
+    assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid()))
+      .isPresent()
+      .get()
+      .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue)
+      .containsExactly(REFERENCE_BRANCH, otherBranchName);
+  }
+
+  @Test
+  public void set_main_branch_name_for_reference_branch_NCD() {
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    call(CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setMainBranchKey(MAIN_BRANCH)
+      .setNewCodeDefinitionType(REFERENCE_BRANCH.name())
+      .build());
+
+    ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get();
+
+    assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid()))
+      .isPresent()
+      .get()
+      .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue)
+      .containsExactly(REFERENCE_BRANCH, MAIN_BRANCH);
+  }
+  @Test
+  public void set_new_code_definition_on_project_creation() {
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    call(CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setMainBranchKey(MAIN_BRANCH)
+      .setNewCodeDefinitionType(NUMBER_OF_DAYS.name())
+      .setNewCodeDefinitionValue("30")
+      .build());
+
+    ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get();
+
+    assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid()))
+      .isPresent()
+      .get()
+      .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue)
+      .containsExactly(NUMBER_OF_DAYS, "30");
+  }
+
+  @Test
+  public void set_new_code_definition_branch_for_community_edition() {
+    when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.COMMUNITY));
+
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    call(CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setMainBranchKey(MAIN_BRANCH)
+      .setNewCodeDefinitionType(NUMBER_OF_DAYS.name())
+      .setNewCodeDefinitionValue("30")
+      .build());
+
+    ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get();
+
+    assertThat(db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), component.uuid(), component.uuid()))
+      .isPresent()
+      .get()
+      .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue, NewCodePeriodDto::getBranchUuid)
+      .containsExactly(NUMBER_OF_DAYS, "30", component.uuid());
+  }
+
+  @Test
+  public void throw_IAE_if_setting_is_not_cayc_compliant() {
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    CreateRequest request = CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setMainBranchKey(MAIN_BRANCH)
+      .setNewCodeDefinitionType(NUMBER_OF_DAYS.name())
+      .setNewCodeDefinitionValue("99")
+      .build();
+
+    assertThatThrownBy(() -> call(request))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("Failed to set the New Code Definition. The given value is not compatible with the Clean as You Code methodology. "
+        + "Please refer to the documentation for compliant options.");
+    assertThat(db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY))
+      .isEmpty();
+  }
+
+  @Test
+  public void throw_IAE_if_setting_is_new_code_definition_value_provided_without_type() {
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    CreateRequest request = CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setMainBranchKey(MAIN_BRANCH)
+      .setNewCodeDefinitionValue("99")
+      .build();
+
+    assertThatThrownBy(() -> call(request))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("New code definition type is required when new code definition value is provided");
+    assertThat(db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY))
+      .isEmpty();
+    assertThat(db.getDbClient().newCodePeriodDao().selectAll(db.getSession())).isEmpty();
+  }
+
+  @Test
+  public void set_new_code_definition_for_project_for_developer_edition() {
+    when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER));
+
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    call(CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setMainBranchKey(MAIN_BRANCH)
+      .setNewCodeDefinitionType(NUMBER_OF_DAYS.name())
+      .setNewCodeDefinitionValue("30")
+      .build());
+
+    ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get();
+
+    assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid()))
+      .isPresent()
+      .get()
+      .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue, NewCodePeriodDto::getBranchUuid)
+      .containsExactly(NUMBER_OF_DAYS, "30", null);
+  }
+
+
+  @Test
+  public void do_not_create_project_when_ncdType_invalid() {
+    when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER));
+
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    CreateRequest request = CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setMainBranchKey(MAIN_BRANCH)
+      .setNewCodeDefinitionType("InvalidType")
+      .build();
+
+    assertThatThrownBy(() -> call(request))
+      .isInstanceOf(IllegalArgumentException.class);
+
+    assertThat(db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY))
+      .isEmpty();
+
+  }
+
+  @Test
+  public void do_not_set_new_code_definition_when_ncdType_not_provided() {
+    when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER));
+
+    userSession.addPermission(PROVISION_PROJECTS);
+
+    call(CreateRequest.builder()
+      .setProjectKey(DEFAULT_PROJECT_KEY)
+      .setName(DEFAULT_PROJECT_NAME)
+      .setMainBranchKey(MAIN_BRANCH)
+      .build());
+
+    ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get();
+
+    assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid()))
+      .isEmpty();
+  }
+
   private CreateWsResponse call(CreateRequest request) {
-    return call(request.getProjectKey(), request.getName(), request.getMainBranchKey());
+    return call(request.getProjectKey(), request.getName(), request.getMainBranchKey(), request.getNewCodeDefinitionType(), request.getNewCodeDefinitionValue());
   }
 
-  private CreateWsResponse call(@Nullable String projectKey, @Nullable String projectName, @Nullable String mainBranch) {
+  private CreateWsResponse call(@Nullable String projectKey, @Nullable String projectName, @Nullable String mainBranch,
+    @Nullable String newCodeDefinitionType, @Nullable String newCodeDefinitionValue) {
     TestRequest httpRequest = ws.newRequest()
       .setMethod(POST.name());
     ofNullable(projectKey).ifPresent(key -> httpRequest.setParam("project", key));
     ofNullable(projectName).ifPresent(name -> httpRequest.setParam("name", name));
     ofNullable(mainBranch).ifPresent(name -> httpRequest.setParam("mainBranch", mainBranch));
+    ofNullable(newCodeDefinitionType).ifPresent(type -> httpRequest.setParam(PARAM_NEW_CODE_DEFINITION_TYPE, type));
+    ofNullable(newCodeDefinitionValue).ifPresent(value -> httpRequest.setParam(PARAM_NEW_CODE_DEFINITION_VALUE, value));
     return httpRequest.executeProtobuf(CreateWsResponse.class);
   }
 
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/NewCodePeriodUtils.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/NewCodePeriodUtils.java
new file mode 100644 (file)
index 0000000..0b57038
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.newcodeperiod;
+
+import com.google.common.base.Preconditions;
+import java.util.EnumSet;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodParser;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.exceptions.NotFoundException;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS;
+
+public interface NewCodePeriodUtils {
+  String BEGIN_LIST = "<ul>";
+
+  String END_LIST = "</ul>";
+  String BEGIN_ITEM_LIST = "<li>";
+  String END_ITEM_LIST = "</li>";
+  String NEW_CODE_PERIOD_TYPE_DESCRIPTION = "Type<br/>" +
+    "New code definitions of the following types are allowed:" +
+    BEGIN_LIST +
+    BEGIN_ITEM_LIST + SPECIFIC_ANALYSIS.name() + " - can be set at branch level only" + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + PREVIOUS_VERSION.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + NUMBER_OF_DAYS.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + REFERENCE_BRANCH.name() + " - can only be set for projects and branches" + END_ITEM_LIST +
+    END_LIST;
+
+  String NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION = "Type<br/>" +
+    "New code definitions of the following types are allowed:" +
+    BEGIN_LIST +
+    BEGIN_ITEM_LIST + PREVIOUS_VERSION.name() + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + NUMBER_OF_DAYS.name() + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + REFERENCE_BRANCH.name() + " - will default to the main branch. A new code definition should be set for the branch itself. " +
+    "Until you do so, this branch will use itself as a reference branch and no code will be considered new for this branch" + END_ITEM_LIST +
+    END_LIST;
+
+  String NEW_CODE_PERIOD_VALUE_DESCRIPTION = "Value<br/>" +
+    "For each type, a different value is expected:" +
+    BEGIN_LIST +
+    BEGIN_ITEM_LIST + "the uuid of an analysis, when type is " + SPECIFIC_ANALYSIS.name() + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + "no value, when type is " + PREVIOUS_VERSION.name() + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + "a number between 1 and 90, when type is " + NUMBER_OF_DAYS.name() + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + "a string, when type is " + REFERENCE_BRANCH.name() + END_ITEM_LIST +
+    END_LIST;
+  String NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION  = "Value<br/>" +
+    "For each type, a different value is expected:" +
+    BEGIN_LIST +
+    BEGIN_ITEM_LIST + "no value, when type is " + PREVIOUS_VERSION.name() + " and " + REFERENCE_BRANCH.name() + END_ITEM_LIST +
+    BEGIN_ITEM_LIST + "a number between 1 and 90, when type is " + NUMBER_OF_DAYS.name() + END_ITEM_LIST +
+    END_LIST;
+
+  String UNEXPECTED_VALUE_ERROR_MESSAGE = "Unexpected value for type '%s'";
+
+  static Optional<String> getNewCodeDefinitionValue(DbSession dbSession, DbClient dbClient, NewCodePeriodType type, @Nullable ProjectDto project,
+    @Nullable BranchDto branch, @Nullable String value) {
+    switch (type) {
+      case PREVIOUS_VERSION:
+        Preconditions.checkArgument(value == null, UNEXPECTED_VALUE_ERROR_MESSAGE, type);
+        return Optional.empty();
+      case NUMBER_OF_DAYS:
+        requireValue(type, value);
+        return Optional.of(parseDays(value));
+      case SPECIFIC_ANALYSIS:
+        requireValue(type, value);
+        requireBranch(type, branch);
+        SnapshotDto analysis = getAnalysis(dbSession, value, project, branch, dbClient);
+        return Optional.of(analysis.getUuid());
+      case REFERENCE_BRANCH:
+        requireValue(type, value);
+        return Optional.of(value);
+      default:
+        throw new IllegalStateException("Unexpected type: " + type);
+    }
+  }
+
+  static Optional<String> getNewCodeDefinitionValueProjectCreation(NewCodePeriodType type, @Nullable String value, String defaultBranchName) {
+    switch (type) {
+      case PREVIOUS_VERSION:
+        Preconditions.checkArgument(value == null, UNEXPECTED_VALUE_ERROR_MESSAGE, type);
+        return Optional.empty();
+      case NUMBER_OF_DAYS:
+        requireValue(type, value);
+        return Optional.of(parseDays(value));
+      case REFERENCE_BRANCH:
+        Preconditions.checkArgument(value == null, UNEXPECTED_VALUE_ERROR_MESSAGE, type);
+        return Optional.of(defaultBranchName);
+      default:
+        throw new IllegalStateException("Unexpected type: " + type);
+    }
+  }
+
+  private static String parseDays(String value) {
+    try {
+      return Integer.toString(NewCodePeriodParser.parseDays(value));
+    } catch (Exception e) {
+      throw new IllegalArgumentException("Failed to parse number of days: " + value);
+    }
+  }
+
+  private static void requireValue(NewCodePeriodType type, @Nullable String value) {
+    Preconditions.checkArgument(value != null, "New code definition type '%s' requires a value", type);
+  }
+
+  private static void requireBranch(NewCodePeriodType type, @Nullable BranchDto branch) {
+    Preconditions.checkArgument(branch != null, "New code definition type '%s' requires a branch", type);
+  }
+  private static Set<NewCodePeriodType> getInstanceTypes() {
+    return EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS);
+  }
+
+  private static Set<NewCodePeriodType> getProjectTypes() {
+    return EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS, REFERENCE_BRANCH);
+  }
+
+  private static Set<NewCodePeriodType> getBranchTypes() {
+    return EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS, SPECIFIC_ANALYSIS, REFERENCE_BRANCH);
+  }
+
+  static NewCodePeriodType validateType(String typeStr, boolean isOverall, boolean isBranch) {
+    NewCodePeriodType type;
+    try {
+      type = NewCodePeriodType.valueOf(typeStr.toUpperCase(Locale.US));
+    } catch (IllegalArgumentException e) {
+      throw new IllegalArgumentException("Invalid type: " + typeStr);
+    }
+
+    if (isOverall) {
+      checkType("Overall setting", getInstanceTypes(), type);
+    } else if (isBranch) {
+      checkType("Branches", getBranchTypes(), type);
+    } else {
+      checkType("Projects", getProjectTypes(), type);
+    }
+    return type;
+  }
+
+  private static SnapshotDto getAnalysis(DbSession dbSession, String analysisUuid, ProjectDto project, BranchDto branch, DbClient dbClient) {
+    SnapshotDto snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid)
+      .orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", analysisUuid)));
+    checkAnalysis(dbSession, project, branch, snapshotDto, dbClient);
+    return snapshotDto;
+  }
+
+  private static void checkAnalysis(DbSession dbSession, ProjectDto project, BranchDto branch, SnapshotDto analysis, DbClient dbClient) {
+    BranchDto analysisBranch = dbClient.branchDao().selectByUuid(dbSession, analysis.getComponentUuid()).orElse(null);
+    boolean analysisMatchesProjectBranch = analysisBranch != null && analysisBranch.getUuid().equals(branch.getUuid());
+
+    checkArgument(analysisMatchesProjectBranch,
+      "Analysis '%s' does not belong to branch '%s' of project '%s'",
+      analysis.getUuid(), branch.getKey(), project.getKey());
+  }
+
+  private static void checkType(String name, Set<NewCodePeriodType> validTypes, NewCodePeriodType type) {
+    if (!validTypes.contains(type)) {
+      throw new IllegalArgumentException(String.format("Invalid type '%s'. %s can only be set with types: %s", type, name, validTypes));
+    }
+  }
+
+}
index c3a29aecd35aeed5b19783f5710926f2f14b1003..899398d765186a93d9d68497d2167b41845a258a 100644 (file)
  */
 package org.sonar.server.newcodeperiod.ws;
 
-import com.google.common.base.Preconditions;
 import java.util.EnumSet;
-import java.util.Locale;
 import java.util.Set;
-import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
@@ -34,10 +31,8 @@ import org.sonar.core.platform.PlatformEditionProvider;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.BranchDto;
-import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.newcodeperiod.NewCodePeriodDao;
 import org.sonar.db.newcodeperiod.NewCodePeriodDto;
-import org.sonar.db.newcodeperiod.NewCodePeriodParser;
 import org.sonar.db.newcodeperiod.NewCodePeriodType;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.server.component.ComponentFinder;
@@ -45,12 +40,15 @@ import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.newcodeperiod.CaycUtils;
 import org.sonar.server.user.UserSession;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static java.lang.String.format;
 import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
 import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
 import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
 import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_TYPE_DESCRIPTION;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_VALUE_DESCRIPTION;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.getNewCodeDefinitionValue;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.validateType;
 import static org.sonar.server.ws.WsUtils.createHtmlExternalLink;
 
 public class SetAction implements NewCodePeriodsWsAction {
@@ -111,25 +109,9 @@ public class SetAction implements NewCodePeriodsWsAction {
       .setDescription("Branch key");
     action.createParam(PARAM_TYPE)
       .setRequired(true)
-      .setDescription("Type<br/>" +
-        "New code definitions of the following types are allowed:" +
-        BEGIN_LIST +
-        BEGIN_ITEM_LIST + SPECIFIC_ANALYSIS.name() + " - can be set at branch level only" + END_ITEM_LIST +
-        BEGIN_ITEM_LIST + PREVIOUS_VERSION.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST +
-        BEGIN_ITEM_LIST + NUMBER_OF_DAYS.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST +
-        BEGIN_ITEM_LIST + REFERENCE_BRANCH.name() + " - can only be set for projects and branches" + END_ITEM_LIST +
-        END_LIST
-      );
+      .setDescription(NEW_CODE_PERIOD_VALUE_DESCRIPTION);
     action.createParam(PARAM_VALUE)
-      .setDescription("Value<br/>" +
-        "For each type, a different value is expected:" +
-        BEGIN_LIST +
-        BEGIN_ITEM_LIST + "the uuid of an analysis, when type is " + SPECIFIC_ANALYSIS.name() + END_ITEM_LIST +
-        BEGIN_ITEM_LIST + "no value, when type is " + PREVIOUS_VERSION.name() + END_ITEM_LIST +
-        BEGIN_ITEM_LIST + "a number between 1 and 90, when type is " + NUMBER_OF_DAYS.name() + END_ITEM_LIST +
-        BEGIN_ITEM_LIST + "a string, when type is " + REFERENCE_BRANCH.name() + END_ITEM_LIST +
-        END_LIST
-      );
+      .setDescription(NEW_CODE_PERIOD_TYPE_DESCRIPTION);
   }
 
   @Override
@@ -172,7 +154,7 @@ public class SetAction implements NewCodePeriodsWsAction {
         userSession.checkIsSystemAdministrator();
       }
 
-      setValue(dbSession, dto, type, project, branch, valueStr);
+      getNewCodeDefinitionValue(dbSession, dbClient, type, project, branch, valueStr).ifPresent(dto::setValue);
 
       if (!CaycUtils.isNewCodePeriodCompliant(dto.getType(), dto.getValue())) {
         throw new IllegalArgumentException("Failed to set the New Code Definition. The given value is not compatible with the Clean as You Code methodology. "
@@ -184,47 +166,6 @@ public class SetAction implements NewCodePeriodsWsAction {
     }
   }
 
-  private void setValue(DbSession dbSession, NewCodePeriodDto dto, NewCodePeriodType type, @Nullable ProjectDto project,
-    @Nullable BranchDto branch, @Nullable String value) {
-    switch (type) {
-      case PREVIOUS_VERSION:
-        Preconditions.checkArgument(value == null, "Unexpected value for type '%s'", type);
-        break;
-      case NUMBER_OF_DAYS:
-        requireValue(type, value);
-        dto.setValue(parseDays(value));
-        break;
-      case SPECIFIC_ANALYSIS:
-        requireValue(type, value);
-        requireBranch(type, branch);
-        SnapshotDto analysis = getAnalysis(dbSession, value, project, branch);
-        dto.setValue(analysis.getUuid());
-        break;
-      case REFERENCE_BRANCH:
-        requireValue(type, value);
-        dto.setValue(value);
-        break;
-      default:
-        throw new IllegalStateException("Unexpected type: " + type);
-    }
-  }
-
-  private static String parseDays(String value) {
-    try {
-      return Integer.toString(NewCodePeriodParser.parseDays(value));
-    } catch (Exception e) {
-      throw new IllegalArgumentException("Failed to parse number of days: " + value);
-    }
-  }
-
-  private static void requireValue(NewCodePeriodType type, @Nullable String value) {
-    Preconditions.checkArgument(value != null, "New code definition type '%s' requires a value", type);
-  }
-
-  private static void requireBranch(NewCodePeriodType type, @Nullable BranchDto branch) {
-    Preconditions.checkArgument(branch != null, "New code definition type '%s' requires a branch", type);
-  }
-
   private BranchDto getBranch(DbSession dbSession, ProjectDto project, String branchKey) {
     return dbClient.branchDao().selectByBranchKey(dbSession, project.getUuid(), branchKey)
       .orElseThrow(() -> new NotFoundException(format("Branch '%s' in project '%s' not found", branchKey, project.getKey())));
@@ -240,44 +181,4 @@ public class SetAction implements NewCodePeriodsWsAction {
       .findFirst()
       .orElseThrow(() -> new NotFoundException(format("Main branch in project '%s' is not found", project.getKey())));
   }
-
-  private static NewCodePeriodType validateType(String typeStr, boolean isOverall, boolean isBranch) {
-    NewCodePeriodType type;
-    try {
-      type = NewCodePeriodType.valueOf(typeStr.toUpperCase(Locale.US));
-    } catch (IllegalArgumentException e) {
-      throw new IllegalArgumentException("Invalid type: " + typeStr);
-    }
-
-    if (isOverall) {
-      checkType("Overall setting", OVERALL_TYPES, type);
-    } else if (isBranch) {
-      checkType("Branches", BRANCH_TYPES, type);
-    } else {
-      checkType("Projects", PROJECT_TYPES, type);
-    }
-    return type;
-  }
-
-  private SnapshotDto getAnalysis(DbSession dbSession, String analysisUuid, ProjectDto project, BranchDto branch) {
-    SnapshotDto snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid)
-      .orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", analysisUuid)));
-    checkAnalysis(dbSession, project, branch, snapshotDto);
-    return snapshotDto;
-  }
-
-  private void checkAnalysis(DbSession dbSession, ProjectDto project, BranchDto branch, SnapshotDto analysis) {
-    BranchDto analysisBranch = dbClient.branchDao().selectByUuid(dbSession, analysis.getComponentUuid()).orElse(null);
-    boolean analysisMatchesProjectBranch = analysisBranch != null && analysisBranch.getUuid().equals(branch.getUuid());
-
-    checkArgument(analysisMatchesProjectBranch,
-      "Analysis '%s' does not belong to branch '%s' of project '%s'",
-      analysis.getUuid(), branch.getKey(), project.getKey());
-  }
-
-  private static void checkType(String name, Set<NewCodePeriodType> validTypes, NewCodePeriodType type) {
-    if (!validTypes.contains(type)) {
-      throw new IllegalArgumentException(String.format("Invalid type '%s'. %s can only be set with types: %s", type, name, validTypes));
-    }
-  }
 }
index 69a440fe38b5fdb58c90ff871506bc8bdb129b52..06278fd680d72a113d21d7c2ce0a968fdf0306a0 100644 (file)
  */
 package org.sonar.server.project.ws;
 
+import java.util.Optional;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
+import org.sonar.core.platform.EditionProvider;
+import org.sonar.core.platform.PlatformEditionProvider;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
 import org.sonar.server.component.ComponentUpdater;
+import org.sonar.server.newcodeperiod.CaycUtils;
+import org.sonar.server.project.DefaultBranchNameResolver;
 import org.sonar.server.project.ProjectDefaultVisibility;
 import org.sonar.server.project.Visibility;
 import org.sonar.server.user.UserSession;
@@ -41,11 +48,17 @@ import static org.sonar.core.component.ComponentKeys.MAX_COMPONENT_KEY_LENGTH;
 import static org.sonar.db.component.ComponentValidator.MAX_COMPONENT_NAME_LENGTH;
 import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
 import static org.sonar.server.component.NewComponent.newComponentBuilder;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.getNewCodeDefinitionValueProjectCreation;
+import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.validateType;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_CREATE;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_MAIN_BRANCH;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NAME;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_TYPE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_VALUE;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
 
@@ -55,13 +68,18 @@ public class CreateAction implements ProjectsWsAction {
   private final UserSession userSession;
   private final ComponentUpdater componentUpdater;
   private final ProjectDefaultVisibility projectDefaultVisibility;
+  private final PlatformEditionProvider editionProvider;
+  private final DefaultBranchNameResolver defaultBranchNameResolver;
 
   public CreateAction(DbClient dbClient, UserSession userSession, ComponentUpdater componentUpdater,
-    ProjectDefaultVisibility projectDefaultVisibility) {
+    ProjectDefaultVisibility projectDefaultVisibility, PlatformEditionProvider editionProvider,
+    DefaultBranchNameResolver defaultBranchNameResolver) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.componentUpdater = componentUpdater;
     this.projectDefaultVisibility = projectDefaultVisibility;
+    this.editionProvider = editionProvider;
+    this.defaultBranchNameResolver = defaultBranchNameResolver;
   }
 
   @Override
@@ -91,17 +109,23 @@ public class CreateAction implements ProjectsWsAction {
 
     action.createParam(PARAM_MAIN_BRANCH)
       .setDescription("Key of the main branch of the project. If not provided, the default main branch key will be used.")
-      .setRequired(false)
       .setSince("9.8")
       .setExampleValue("develop");
 
     action.createParam(PARAM_VISIBILITY)
       .setDescription("Whether the created project should be visible to everyone, or only specific user/groups.<br/>" +
         "If no visibility is specified, the default project visibility will be used.")
-      .setRequired(false)
       .setSince("6.4")
       .setPossibleValues(Visibility.getLabels());
 
+    action.createParam(PARAM_NEW_CODE_DEFINITION_TYPE)
+      .setDescription(NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION)
+      .setSince("10.1");
+
+    action.createParam(PARAM_NEW_CODE_DEFINITION_VALUE)
+      .setDescription(NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION)
+      .setSince("10.1");
+
   }
 
   @Override
@@ -113,28 +137,69 @@ public class CreateAction implements ProjectsWsAction {
   private CreateWsResponse doHandle(CreateRequest request) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       userSession.checkPermission(PROVISION_PROJECTS);
-      String visibility = request.getVisibility();
-      boolean changeToPrivate = visibility == null ? projectDefaultVisibility.get(dbSession).isPrivate() : "private".equals(visibility);
-
-      ComponentDto componentDto = componentUpdater.create(dbSession, newComponentBuilder()
-          .setKey(request.getProjectKey())
-          .setName(request.getName())
-          .setPrivate(changeToPrivate)
-          .setQualifier(PROJECT)
-          .build(),
-        userSession.isLoggedIn() ? userSession.getUuid() : null,
-        userSession.isLoggedIn() ? userSession.getLogin() : null,
-        request.getMainBranchKey()).mainBranchComponent();
+      checkNewCodeDefinitionParam(request);
+      ComponentDto componentDto = createProject(request, dbSession);
+      if(request.getNewCodeDefinitionType() != null) {
+        createNewCodeDefinition(dbSession, request, componentDto.uuid());
+      }
+      componentUpdater.commitAndIndex(dbSession, componentDto);
       return toCreateResponse(componentDto);
     }
   }
 
+  private static void checkNewCodeDefinitionParam(CreateRequest request) {
+    if (request.getNewCodeDefinitionType() == null && request.getNewCodeDefinitionValue() != null) {
+      throw new IllegalArgumentException("New code definition type is required when new code definition value is provided");
+    }
+  }
+  private ComponentDto createProject(CreateRequest request, DbSession dbSession) {
+    String visibility = request.getVisibility();
+    boolean changeToPrivate = visibility == null ? projectDefaultVisibility.get(dbSession).isPrivate() : "private".equals(visibility);
+
+    return componentUpdater.createWithoutCommit(dbSession, newComponentBuilder()
+        .setKey(request.getProjectKey())
+        .setName(request.getName())
+        .setPrivate(changeToPrivate)
+        .setQualifier(PROJECT)
+        .build(),
+      userSession.isLoggedIn() ? userSession.getUuid() : null,
+      userSession.isLoggedIn() ? userSession.getLogin() : null,
+      request.getMainBranchKey(), s -> {}).mainBranchComponent();
+
+  }
+  private void createNewCodeDefinition(DbSession dbSession, CreateRequest request, String projectUuid) {
+
+    boolean isCommunityEdition = editionProvider.get().filter(EditionProvider.Edition.COMMUNITY::equals).isPresent();
+    NewCodePeriodType newCodePeriodType = validateType(request.getNewCodeDefinitionType(), false, isCommunityEdition);
+    String newCodePeriodValue = request.getNewCodeDefinitionValue();
+    String defaultBranchName = Optional.ofNullable(request.getMainBranchKey()).orElse(defaultBranchNameResolver.getEffectiveMainBranchName());
+
+    NewCodePeriodDto dto = new NewCodePeriodDto();
+    dto.setType(newCodePeriodType);
+    dto.setProjectUuid(projectUuid);
+
+    if (isCommunityEdition) {
+      dto.setBranchUuid(projectUuid);
+    }
+
+    getNewCodeDefinitionValueProjectCreation(newCodePeriodType, newCodePeriodValue, defaultBranchName).ifPresent(dto::setValue);
+
+    if (!CaycUtils.isNewCodePeriodCompliant(dto.getType(), dto.getValue())) {
+      throw new IllegalArgumentException("Failed to set the New Code Definition. The given value is not compatible with the Clean as You Code methodology. "
+        + "Please refer to the documentation for compliant options.");
+    }
+
+    dbClient.newCodePeriodDao().insert(dbSession, dto);
+  }
+
   private static CreateRequest toCreateRequest(Request request) {
     return CreateRequest.builder()
       .setProjectKey(request.mandatoryParam(PARAM_PROJECT))
       .setName(abbreviate(request.mandatoryParam(PARAM_NAME), MAX_COMPONENT_NAME_LENGTH))
       .setVisibility(request.param(PARAM_VISIBILITY))
       .setMainBranchKey(request.param(PARAM_MAIN_BRANCH))
+      .setNewCodeDefinitionType(request.param(PARAM_NEW_CODE_DEFINITION_TYPE))
+      .setNewCodeDefinitionValue(request.param(PARAM_NEW_CODE_DEFINITION_VALUE))
       .build();
   }
 
@@ -155,11 +220,19 @@ public class CreateAction implements ProjectsWsAction {
     @CheckForNull
     private final String visibility;
 
+    @CheckForNull
+    private final String newCodeDefinitionType;
+
+    @CheckForNull
+    private final String newCodeDefinitionValue;
+
     private CreateRequest(Builder builder) {
       this.projectKey = builder.projectKey;
       this.name = builder.name;
       this.visibility = builder.visibility;
       this.mainBranchKey = builder.mainBranchKey;
+      this.newCodeDefinitionType = builder.newCodeDefinitionType;
+      this.newCodeDefinitionValue = builder.newCodeDefinitionValue;
     }
 
     public String getProjectKey() {
@@ -179,6 +252,16 @@ public class CreateAction implements ProjectsWsAction {
       return mainBranchKey;
     }
 
+    @CheckForNull
+    public String getNewCodeDefinitionType() {
+      return newCodeDefinitionType;
+    }
+
+    @CheckForNull
+    public String getNewCodeDefinitionValue() {
+      return newCodeDefinitionValue;
+    }
+
     public static Builder builder() {
       return new Builder();
     }
@@ -191,6 +274,11 @@ public class CreateAction implements ProjectsWsAction {
 
     @CheckForNull
     private String visibility;
+    @CheckForNull
+    private String newCodeDefinitionType;
+
+    @CheckForNull
+    private String newCodeDefinitionValue;
 
     private Builder() {
     }
@@ -217,6 +305,16 @@ public class CreateAction implements ProjectsWsAction {
       return this;
     }
 
+    public Builder setNewCodeDefinitionType(@Nullable String newCodeDefinitionType) {
+      this.newCodeDefinitionType = newCodeDefinitionType;
+      return this;
+    }
+
+    public Builder setNewCodeDefinitionValue(@Nullable String newCodeDefinitionValue) {
+      this.newCodeDefinitionValue = newCodeDefinitionValue;
+      return this;
+    }
+
     public CreateRequest build() {
       requireNonNull(projectKey);
       requireNonNull(name);
index 93c6f3aef7edaeb104507c616e13bb19ac608beb..74801f4c935540c55b4e42d58d5f4229538d97ae 100644 (file)
@@ -46,11 +46,14 @@ public class ProjectsWsParameters {
   public static final String PARAM_PROJECTS = "projects";
   public static final String PARAM_ALM_ID = "almId";
   public static final String PARAM_ALM_REPOSITORY_ID = "almRepoId";
+  public static final String PARAM_NEW_CODE_DEFINITION_VALUE = "newCodeDefinitionValue";
+  public static final String PARAM_NEW_CODE_DEFINITION_TYPE = "newCodeDefinitionType";
 
   public static final String FILTER_LANGUAGES = "languages";
   public static final String FILTER_TAGS = "tags";
   public static final String FILTER_QUALIFIER = "qualifier";
 
+
   private ProjectsWsParameters() {
     // static utils only
   }