]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21819 Add POST /dop-translation/bound-projects endpoint.
authorWojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com>
Wed, 20 Mar 2024 08:43:54 +0000 (09:43 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 28 Mar 2024 20:02:50 +0000 (20:02 +0000)
23 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java
server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportProjectRequest.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportProjectService.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportedProject.java [new file with mode: 0644]
server/sonar-webserver-common/src/test/java/org/sonar/server/common/project/ImportProjectServiceTest.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/build.gradle
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/BoundProjectsController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/DefaultBoundProjectsController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/BoundProjectCreateRestRequest.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/response/BoundProjectCreateRestResponse.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/response/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projects/controller/DefaultBoundProjectsControllerTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/package-info.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java

index a05bcb5582b5d840096a4d1de11d0986a7082984..b10d241fa180a31c910918db5db91ed9721cd1f7 100644 (file)
@@ -70,7 +70,7 @@ public class ProjectAlmSettingDto {
   private long updatedAt;
   private long createdAt;
 
-  String getUuid() {
+  public String getUuid() {
     return uuid;
   }
 
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportProjectRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportProjectRequest.java
new file mode 100644 (file)
index 0000000..017ed88
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.common.project;
+
+import javax.annotation.Nullable;
+
+public record ImportProjectRequest(
+  @Nullable
+  String projectKey,
+
+  @Nullable
+  String projectName,
+  String almSettingId,
+  String repositoryIdentifier,
+  @Nullable
+  String newCodeDefinitionType,
+  @Nullable
+  String newCodeDefinitionValue,
+  Boolean monorepo) {
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportProjectService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportProjectService.java
new file mode 100644 (file)
index 0000000..3015020
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.common.project;
+
+import java.util.Optional;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.project.CreationMethod;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreatorFactory;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.component.ComponentUpdater;
+import org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.user.UserSession;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.db.project.CreationMethod.getCreationMethod;
+import static org.sonar.db.project.CreationMethod.Category.ALM_IMPORT;
+import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.checkNewCodeDefinitionParam;
+
+@ServerSide
+public class ImportProjectService {
+  private final DbClient dbClient;
+  private final DevOpsProjectCreatorFactory devOpsProjectCreatorFactory;
+  private final UserSession userSession;
+  private final ComponentUpdater componentUpdater;
+  private final NewCodeDefinitionResolver newCodeDefinitionResolver;
+
+  public ImportProjectService(DbClient dbClient, DevOpsProjectCreatorFactory devOpsProjectCreatorFactory, UserSession userSession, ComponentUpdater componentUpdater,
+    NewCodeDefinitionResolver newCodeDefinitionResolver) {
+    this.dbClient = dbClient;
+    this.devOpsProjectCreatorFactory = devOpsProjectCreatorFactory;
+    this.userSession = userSession;
+    this.componentUpdater = componentUpdater;
+    this.newCodeDefinitionResolver = newCodeDefinitionResolver;
+  }
+
+  public ImportedProject importProject(ImportProjectRequest request) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      checkNewCodeDefinitionParam(request.newCodeDefinitionType(), request.newCodeDefinitionValue());
+      AlmSettingDto almSetting = dbClient.almSettingDao().selectByUuid(dbSession, request.almSettingId()).orElseThrow(() -> new IllegalArgumentException("ALM setting not found"));
+      String almUrl = requireNonNull(almSetting.getUrl());
+      DevOpsProjectDescriptor projectDescriptor = new DevOpsProjectDescriptor(almSetting.getAlm(), almUrl, request.repositoryIdentifier());
+
+      DevOpsProjectCreator projectCreator = devOpsProjectCreatorFactory.getDevOpsProjectCreator(almSetting, projectDescriptor)
+        .orElseThrow(() -> new IllegalArgumentException(format("Platform %s not supported", almSetting.getAlm().name())));
+
+      CreationMethod creationMethod = getCreationMethod(ALM_IMPORT, userSession.isAuthenticatedBrowserSession());
+      ComponentCreationData componentCreationData = projectCreator.createProjectAndBindToDevOpsPlatform(
+        dbSession,
+        creationMethod,
+        request.monorepo(),
+        request.projectKey(),
+        request.projectName());
+
+      ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
+      BranchDto mainBranchDto = Optional.ofNullable(componentCreationData.mainBranchDto()).orElseThrow();
+
+      if (request.newCodeDefinitionType() != null) {
+        newCodeDefinitionResolver.createNewCodeDefinition(dbSession, projectDto.getUuid(), mainBranchDto.getUuid(),
+          mainBranchDto.getKey(), request.newCodeDefinitionType(), request.newCodeDefinitionValue());
+      }
+      componentUpdater.commitAndIndex(dbSession, componentCreationData);
+      ProjectAlmSettingDto projectAlmSettingDto = dbClient.projectAlmSettingDao().selectByProject(dbSession, projectDto)
+        .orElseThrow(() -> new IllegalStateException("Project ALM setting was not created"));
+      dbSession.commit();
+      return new ImportedProject(projectDto, projectAlmSettingDto);
+    }
+  }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportedProject.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ImportedProject.java
new file mode 100644 (file)
index 0000000..20f1dc5
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.common.project;
+
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.project.ProjectDto;
+
+public record ImportedProject(ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto){
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/project/ImportProjectServiceTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/project/ImportProjectServiceTest.java
new file mode 100644 (file)
index 0000000..6fe1068
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.common.project;
+
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.jupiter.api.Test;
+import org.mockito.Answers;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreatorFactory;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.component.ComponentUpdater;
+import org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.tester.UserSessionRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+
+class ImportProjectServiceTest {
+
+  private static final String API_URL = "https://api.com";
+  private static final String PROJECT_UUID = "project-uuid";
+  private static final String REPOSITORY_ID = "repository-id";
+  private static final String PROJECT_KEY = "project-key";
+  private static final String PROJECT_NAME = "project-name";
+  private static final String MAIN_BRANCH_UUID = "main-branch-uuid";
+  private static final String MAIN_BRANCH_KEY = "main-branch-key";
+  private static final String ALM_SETTING_ID = "alm-setting-id";
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  private final DevOpsProjectCreatorFactory devOpsProjectCreatorFactory = mock();
+
+  private final DbClient dbClient = mock(Answers.RETURNS_DEEP_STUBS);
+  private final NewCodeDefinitionResolver newCodeDefinitionResolver = mock();
+  private final ComponentUpdater componentUpdater = mock();
+
+  private final ImportProjectService importProjectService = new ImportProjectService(dbClient, devOpsProjectCreatorFactory, userSession, componentUpdater,
+    newCodeDefinitionResolver);;
+
+  @Test
+  void createdImportedProject_whenAlmSettingDoesntExist_throws() {
+    userSession.logIn().addPermission(PROVISION_PROJECTS);
+    DbSession dbSession = mockDbSession();
+    when(dbClient.almSettingDao().selectByUuid(dbSession, ALM_SETTING_ID)).thenReturn(Optional.empty());
+
+    ImportProjectRequest request = new ImportProjectRequest(PROJECT_KEY, PROJECT_NAME, ALM_SETTING_ID, REPOSITORY_ID, null, null, true);
+
+    assertThatThrownBy(() -> importProjectService.importProject(request))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("ALM setting not found");
+
+  }
+
+  @Test
+  void createImportedProject_whenAlmIsNotSupported_throws() {
+    userSession.logIn().addPermission(PROVISION_PROJECTS);
+
+    DbSession dbSession = mockDbSession();
+    AlmSettingDto almSetting = mockAlmSetting(dbSession);
+
+    when(devOpsProjectCreatorFactory.getDevOpsProjectCreator(eq(almSetting), any()))
+      .thenReturn(Optional.empty());
+
+    ImportProjectRequest request = new ImportProjectRequest(PROJECT_KEY, PROJECT_NAME, ALM_SETTING_ID, REPOSITORY_ID, null, null, true);
+
+    assertThatThrownBy(() -> importProjectService.importProject(request))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Platform GITHUB not supported");
+  }
+
+  @Test
+  void createImportedProject_whenAlmIsSupportedAndNoNewCodeDefinitionDefined_shouldCreateProject() {
+    userSession.logIn().addPermission(PROVISION_PROJECTS);
+    DbSession dbSession = mockDbSession();
+    AlmSettingDto almSetting = mockAlmSetting(dbSession);
+
+    DevOpsProjectCreator devOpsProjectCreator = mockDevOpsProjectCreator(almSetting);
+
+    ComponentCreationData componentCreationData = mockProjectCreation(devOpsProjectCreator, dbSession);
+
+    ProjectDto projectDto = mockProjectDto(componentCreationData);
+    when(componentCreationData.mainBranchDto()).thenReturn(mock(BranchDto.class));
+
+    ProjectAlmSettingDto projectAlmSettingDto = mockProjectAlmSetting(dbSession, projectDto);
+
+    ImportProjectRequest request = new ImportProjectRequest(PROJECT_KEY, PROJECT_NAME, ALM_SETTING_ID, REPOSITORY_ID, null, null, true);
+
+    ImportedProject importedProject = importProjectService.importProject(request);
+
+    assertThat(importedProject.projectDto()).isEqualTo(projectDto);
+    assertThat(importedProject.projectAlmSettingDto()).isEqualTo(projectAlmSettingDto);
+
+    verify(componentUpdater).commitAndIndex(dbSession, componentCreationData);
+  }
+
+  @Test
+  void createImportedProject_whenAlmIsSupportedAndNewCodeDefinitionDefined_shouldCreateProjectAndNewCodeDefinition() {
+    userSession.logIn().addPermission(PROVISION_PROJECTS);
+    DbSession dbSession = mockDbSession();
+    AlmSettingDto almSetting = mockAlmSetting(dbSession);
+
+    DevOpsProjectCreator devOpsProjectCreator = mockDevOpsProjectCreator(almSetting);
+
+    ComponentCreationData componentCreationData = mockProjectCreation(devOpsProjectCreator, dbSession);
+
+    ProjectDto projectDto = mockProjectDto(componentCreationData);
+    mockBranchDto(componentCreationData);
+
+    ProjectAlmSettingDto projectAlmSettingDto = mockProjectAlmSetting(dbSession, projectDto);
+
+    ImportProjectRequest request = new ImportProjectRequest(PROJECT_KEY, PROJECT_NAME, ALM_SETTING_ID, REPOSITORY_ID, "NUMBER_OF_DAYS", "10", true);
+
+    ImportedProject importedProject = importProjectService.importProject(request);
+
+    assertThat(importedProject.projectDto()).isEqualTo(projectDto);
+    assertThat(importedProject.projectAlmSettingDto()).isEqualTo(projectAlmSettingDto);
+
+    verify(newCodeDefinitionResolver).createNewCodeDefinition(
+      dbSession,
+      PROJECT_UUID,
+      MAIN_BRANCH_UUID,
+      MAIN_BRANCH_KEY,
+      "NUMBER_OF_DAYS",
+      "10");
+    verify(componentUpdater).commitAndIndex(dbSession, componentCreationData);
+  }
+
+  private DbSession mockDbSession() {
+    DbSession dbSession = mock(DbSession.class);
+    when(dbClient.openSession(false)).thenReturn(dbSession);
+    return dbSession;
+  }
+
+  private AlmSettingDto mockAlmSetting(DbSession dbSession) {
+    AlmSettingDto almSetting = mock(AlmSettingDto.class);
+    when(almSetting.getAlm()).thenReturn(ALM.GITHUB);
+    when(almSetting.getUrl()).thenReturn(API_URL);
+    when(dbClient.almSettingDao().selectByUuid(dbSession, ALM_SETTING_ID)).thenReturn(Optional.of(almSetting));
+    return almSetting;
+  }
+
+  private DevOpsProjectCreator mockDevOpsProjectCreator(AlmSettingDto almSetting) {
+    DevOpsProjectCreator devOpsProjectCreator = mock(DevOpsProjectCreator.class);
+    DevOpsProjectDescriptor projectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, API_URL, REPOSITORY_ID);
+    when(devOpsProjectCreatorFactory.getDevOpsProjectCreator(almSetting, projectDescriptor))
+      .thenReturn(Optional.of(devOpsProjectCreator));
+    return devOpsProjectCreator;
+  }
+
+  private static ComponentCreationData mockProjectCreation(DevOpsProjectCreator devOpsProjectCreator, DbSession dbSession) {
+    ComponentCreationData componentCreationData = mock(ComponentCreationData.class);
+    when(devOpsProjectCreator.createProjectAndBindToDevOpsPlatform(eq(dbSession), any(), eq(true), eq(PROJECT_KEY), eq(PROJECT_NAME)))
+      .thenReturn(componentCreationData);
+    return componentCreationData;
+  }
+
+  private static ProjectDto mockProjectDto(ComponentCreationData componentCreationData) {
+    ProjectDto projectDto = mock(ProjectDto.class);
+    lenient().when(projectDto.getUuid()).thenReturn(PROJECT_UUID);
+    when(componentCreationData.projectDto()).thenReturn(projectDto);
+    return projectDto;
+  }
+
+  private static void mockBranchDto(ComponentCreationData componentCreationData) {
+    BranchDto mainBrainDto = mock(BranchDto.class);
+    when(mainBrainDto.getUuid()).thenReturn(MAIN_BRANCH_UUID);
+    when(mainBrainDto.getKey()).thenReturn(MAIN_BRANCH_KEY);
+    when(componentCreationData.mainBranchDto()).thenReturn(mainBrainDto);
+  }
+
+  private ProjectAlmSettingDto mockProjectAlmSetting(DbSession dbSession, ProjectDto projectDto) {
+    ProjectAlmSettingDto projectAlmSetting = mock(ProjectAlmSettingDto.class);
+    when(dbClient.projectAlmSettingDao().selectByProject(dbSession, projectDto))
+      .thenReturn(Optional.of(projectAlmSetting));
+    return projectAlmSetting;
+  }
+
+}
index 5fcc13f99a85d37b3cf9e885d06ca6a7f5d04c68..760f4a2519853b45991593f06ddf364822e33166 100644 (file)
@@ -18,6 +18,7 @@ dependencies {
     testImplementation 'javax.servlet:javax.servlet-api'
     testImplementation 'org.junit.jupiter:junit-jupiter-api'
     testImplementation 'org.mockito:mockito-core'
+    testImplementation 'org.mockito:mockito-junit-jupiter'
     testImplementation 'org.skyscreamer:jsonassert:1.5.1'
     testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
     testImplementation 'com.tngtech.java:junit-dataprovider'
index 5e90478e87e057317a106a22052580c4bb7d1acf..201c625b04aad9a04639d0ddc08f442ed2e2029c 100644 (file)
@@ -38,6 +38,8 @@ public class WebApiEndpoints {
 
   public static final String GITLAB_CONFIGURATION_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/gitlab-configurations";
 
+  public static final String BOUND_PROJECTS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/bound-projects";
+
   public static final String INTERNAL = "internal";
 
   private WebApiEndpoints() {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/BoundProjectsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/BoundProjectsController.java
new file mode 100644 (file)
index 0000000..2a54c02
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.v2.api.projects.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import javax.validation.Valid;
+import org.sonar.server.v2.api.projects.request.BoundProjectCreateRestRequest;
+import org.sonar.server.v2.api.projects.response.BoundProjectCreateRestResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.sonar.server.v2.WebApiEndpoints.BOUND_PROJECTS_ENDPOINT;
+
+@RequestMapping(BOUND_PROJECTS_ENDPOINT)
+@RestController
+public interface BoundProjectsController {
+
+
+  @PostMapping
+  @Operation(summary = "Create a SonarQube project with the information from the provided DevOps platform project.", description = """
+    Create a SonarQube project with the information from the provided DevOps platform project.
+    Autoconfigure Pull-Request decoration mechanism.
+    Requires the 'Create Projects' permission
+    """)
+  @ResponseStatus(HttpStatus.CREATED)
+  BoundProjectCreateRestResponse createBoundProject(@Valid @RequestBody BoundProjectCreateRestRequest request);
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/DefaultBoundProjectsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/DefaultBoundProjectsController.java
new file mode 100644 (file)
index 0000000..1ca3578
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.v2.api.projects.controller;
+
+import org.sonar.server.common.project.ImportProjectRequest;
+import org.sonar.server.common.project.ImportProjectService;
+import org.sonar.server.common.project.ImportedProject;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.api.projects.request.BoundProjectCreateRestRequest;
+import org.sonar.server.v2.api.projects.response.BoundProjectCreateRestResponse;
+
+import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+
+public class DefaultBoundProjectsController implements BoundProjectsController {
+
+  private final UserSession userSession;
+
+  private final ImportProjectService importProjectService;
+
+  public DefaultBoundProjectsController(UserSession userSession, ImportProjectService importProjectService) {
+    this.userSession = userSession;
+    this.importProjectService = importProjectService;
+  }
+
+  @Override
+  public BoundProjectCreateRestResponse createBoundProject(BoundProjectCreateRestRequest request) {
+    userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS);
+    ImportedProject importedProject = importProjectService.importProject(restRequestToServiceRequest(request));
+    return toRestResponse(importedProject);
+
+  }
+
+  private static ImportProjectRequest restRequestToServiceRequest(BoundProjectCreateRestRequest request) {
+    return new ImportProjectRequest(request.projectKey(), request.projectName(), request.devOpsPlatformSettingId(), request.repositoryIdentifier(),
+      request.newCodeDefinitionType(), request.newCodeDefinitionValue(), request.monorepo());
+  }
+
+  private static BoundProjectCreateRestResponse toRestResponse(ImportedProject importedProject) {
+    return new BoundProjectCreateRestResponse(importedProject.projectDto().getUuid(), importedProject.projectAlmSettingDto().getUuid());
+  }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/controller/package-info.java
new file mode 100644 (file)
index 0000000..f125e37
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.projects.controller;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/BoundProjectCreateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/BoundProjectCreateRestRequest.java
new file mode 100644 (file)
index 0000000..bee9a99
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.v2.api.projects.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.annotation.Nullable;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+public record BoundProjectCreateRestRequest(
+
+  @NotEmpty
+  @Schema(description = "Key of the project to create")
+  String projectKey,
+
+  @NotEmpty
+  @Schema(description = "Name of the project to create")
+  String projectName,
+
+  @NotEmpty
+  @Schema(description = "Identifier of DevOps platform configuration to use.")
+  String devOpsPlatformSettingId,
+
+  @NotEmpty
+  @Schema(description = "Identifier of the DevOps platform repository to import. Repository slug for GitHub, repository id for GitLab.")
+  String repositoryIdentifier,
+
+  @Nullable
+  @Schema(description = """
+    Project New Code Definition Type
+    New code definitions of the following types are allowed:
+      - PREVIOUS_VERSION
+      - NUMBER_OF_DAYS
+      - REFERENCE_BRANCH - will default to the main branch.
+  """)
+  String newCodeDefinitionType,
+
+  @Nullable
+  @Schema(description = """
+    Project New Code Definition Value
+    For each new code definition type, a different value is expected:
+    - no value, when the new code definition type is PREVIOUS_VERSION and REFERENCE_BRANCH
+    - a number between 1 and 90, when the new code definition type is NUMBER_OF_DAYS
+  """)
+  String newCodeDefinitionValue,
+
+  @NotNull
+  @Schema(description = "True if project is part of a mono repo.")
+  Boolean monorepo
+) {
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/package-info.java
new file mode 100644 (file)
index 0000000..8f659af
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.projects.request;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/response/BoundProjectCreateRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/response/BoundProjectCreateRestResponse.java
new file mode 100644 (file)
index 0000000..2d423f2
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.v2.api.projects.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public record BoundProjectCreateRestResponse(
+
+  @Schema(description = "The identifier of the created project")
+  String projectId,
+
+  @Schema(description = "The identifier of the binding between the created project and the DevOps platform project")
+  String bindingId) {
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/response/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/response/package-info.java
new file mode 100644 (file)
index 0000000..cc98c9d
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.projects.response;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 9125aeb23ae8e549cc6b5e376bb92bb030e72e9d..af59c51d9ed228cacc9a7683d447b35c6a23e17e 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.server.common.health.WebServerStatusNodeCheck;
 import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.common.platform.LivenessChecker;
 import org.sonar.server.common.platform.LivenessCheckerImpl;
+import org.sonar.server.common.project.ImportProjectService;
 import org.sonar.server.common.rule.service.RuleService;
 import org.sonar.server.common.text.MacroInterpreter;
 import org.sonar.server.common.user.service.UserService;
@@ -46,6 +47,8 @@ import org.sonar.server.v2.api.group.controller.DefaultGroupController;
 import org.sonar.server.v2.api.group.controller.GroupController;
 import org.sonar.server.v2.api.membership.controller.DefaultGroupMembershipController;
 import org.sonar.server.v2.api.membership.controller.GroupMembershipController;
+import org.sonar.server.v2.api.projects.controller.DefaultBoundProjectsController;
+import org.sonar.server.v2.api.projects.controller.BoundProjectsController;
 import org.sonar.server.v2.api.rule.controller.DefaultRuleController;
 import org.sonar.server.v2.api.rule.controller.RuleController;
 import org.sonar.server.v2.api.rule.converter.RuleRestResponseGenerator;
@@ -100,7 +103,6 @@ public class PlatformLevel4WebConfig {
     return new DefaultGroupController(userSession, dbClient, groupService, managedInstanceChecker);
   }
 
-
   @Bean
   public GroupMembershipController groupMembershipsController(UserSession userSession,
     GroupMembershipService groupMembershipService, ManagedInstanceChecker managedInstanceChecker) {
@@ -129,4 +131,10 @@ public class PlatformLevel4WebConfig {
     return new DefaultGitlabConfigurationController(userSession, gitlabConfigurationService);
   }
 
+  @Bean
+  public BoundProjectsController importedProjectsController(UserSession userSession, ImportProjectService importProjectService) {
+    return new DefaultBoundProjectsController(
+      userSession, importProjectService);
+  }
+
 }
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projects/controller/DefaultBoundProjectsControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projects/controller/DefaultBoundProjectsControllerTest.java
new file mode 100644 (file)
index 0000000..58af5d0
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.v2.api.projects.controller;
+
+import org.junit.Rule;
+import org.junit.jupiter.api.Test;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.common.project.ImportProjectRequest;
+import org.sonar.server.common.project.ImportProjectService;
+import org.sonar.server.common.project.ImportedProject;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.v2.api.ControllerTester;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonar.server.v2.WebApiEndpoints.BOUND_PROJECTS_ENDPOINT;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class DefaultBoundProjectsControllerTest {
+
+  private static final String PROJECT_UUID = "project-uuid";
+  private static final String PROJECT_ALM_SETTING_UUID = "project-alm-setting-uuid";
+  private static final String REPOSITORY_ID = "repository-id";
+  private static final String PROJECT_KEY = "project-key";
+  private static final String PROJECT_NAME = "project-name";
+  private static final String ALM_SETTING_ID = "alm-setting-id";
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  private final ImportProjectService importProjectService = mock();
+  private final MockMvc mockMvc = ControllerTester.getMockMvc(
+    new DefaultBoundProjectsController(
+      userSession, importProjectService));
+
+  @Test
+  void createBoundProject_whenUserDoesntHaveCreateProjectPermission_returnsForbidden() throws Exception {
+    userSession.logIn();
+    mockMvc.perform(
+      post(BOUND_PROJECTS_ENDPOINT)
+        .contentType(MediaType.APPLICATION_JSON)
+        .content("""
+                {
+                  "projectKey": "project-key",
+                  "projectName": "project-name",
+                  "devOpsPlatformSettingId": "alm-setting-id",
+                  "repositoryIdentifier": "repository-id",
+                  "monorepo": true
+                }
+          """)
+
+    )
+      .andExpect(status().isForbidden());
+  }
+
+  @Test
+  void createdBoundProject_whenImportProjectServiceThrowsIllegalArgumentExceptions_returnsBadRequest() throws Exception {
+    userSession.logIn().addPermission(PROVISION_PROJECTS);
+    when(importProjectService.importProject(any()))
+      .thenThrow(new IllegalArgumentException("Error message"));
+    mockMvc.perform(
+      post(BOUND_PROJECTS_ENDPOINT)
+        .contentType(MediaType.APPLICATION_JSON)
+        .content("""
+                {
+                  "projectKey": "project-key",
+                  "projectName": "project-name",
+                  "devOpsPlatformSettingId": "alm-setting-id",
+                  "repositoryIdentifier": "repository-id",
+                  "monorepo": true
+                }
+          """))
+      .andExpectAll(
+        status().isBadRequest(),
+        content().json("""
+          {
+            "message": "Error message"
+          }
+          """));
+  }
+
+  @Test
+  void createBoundProject_whenProjectIsCreatedSuccessfully_returnResponse() throws Exception {
+    userSession.logIn().addPermission(PROVISION_PROJECTS);
+
+    ProjectDto projectDto = mock(ProjectDto.class);
+    when(projectDto.getUuid()).thenReturn(PROJECT_UUID);
+
+    ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class);
+    when(projectAlmSettingDto.getUuid()).thenReturn(PROJECT_ALM_SETTING_UUID);
+
+    when(importProjectService.importProject(new ImportProjectRequest(
+      PROJECT_KEY,
+      PROJECT_NAME,
+      ALM_SETTING_ID,
+      REPOSITORY_ID,
+      "NUMBER_OF_DAYS",
+      "10",
+      true)))
+      .thenReturn(new ImportedProject(
+        projectDto,
+        projectAlmSettingDto));
+
+    mockMvc.perform(
+      post(BOUND_PROJECTS_ENDPOINT)
+        .contentType(MediaType.APPLICATION_JSON)
+        .content("""
+                {
+                  "projectKey": "project-key",
+                  "projectName": "project-name",
+                  "devOpsPlatformSettingId": "alm-setting-id",
+                  "repositoryIdentifier": "repository-id",
+                  "newCodeDefinitionType": "NUMBER_OF_DAYS",
+                  "newCodeDefinitionValue": "10",
+                  "monorepo": true
+                }
+          """))
+      .andExpectAll(
+        status().isCreated(),
+        content().json("""
+          {
+            "projectId": "project-uuid",
+            "bindingId": "project-alm-setting-uuid"
+          }
+          """));
+  }
+}
index 0bd1deaa6a1debda83fb6289d7d7b33a53c8497a..a5a8085809495100395ac047ad0ce3fbf8aabcbd 100644 (file)
@@ -54,6 +54,7 @@ import org.sonar.server.almintegration.ws.ImportHelper;
 import org.sonar.server.common.almintegration.ProjectKeyGenerator;
 import org.sonar.server.common.almsettings.github.GithubProjectCreatorFactory;
 import org.sonar.server.common.component.ComponentUpdater;
+import org.sonar.server.common.project.ImportProjectService;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.es.IndexersImpl;
 import org.sonar.server.es.TestIndexers;
@@ -144,8 +145,10 @@ public class ImportGithubProjectActionIT {
   private final GithubProjectCreatorFactory gitHubProjectCreatorFactory = new GithubProjectCreatorFactory(db.getDbClient(),
     null, appClient, projectKeyGenerator, userSession, projectCreator, gitHubSettings, githubPermissionConverter, userPermissionUpdater, permissionService,
     managedProjectService);
-  private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(db.getDbClient(), userSession,
-    componentUpdater, importHelper, newCodeDefinitionResolver, defaultBranchNameResolver, gitHubProjectCreatorFactory));
+
+  private final ImportProjectService importProjectService = new ImportProjectService(db.getDbClient(), gitHubProjectCreatorFactory, userSession, componentUpdater,
+    newCodeDefinitionResolver);
+  private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(importProjectService, importHelper));
 
   @Before
   public void before() {
index a9914d3c810c5b3cf93a71c98de89f635a6360fa..6133ac34d0b335021544acea1f4e22beec6233e2 100644 (file)
@@ -44,6 +44,7 @@ import org.sonar.server.almintegration.ws.ImportHelper;
 import org.sonar.server.common.almintegration.ProjectKeyGenerator;
 import org.sonar.server.common.almsettings.gitlab.GitlabProjectCreatorFactory;
 import org.sonar.server.common.component.ComponentUpdater;
+import org.sonar.server.common.project.ImportProjectService;
 import org.sonar.server.es.TestIndexers;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.favorite.FavoriteUpdater;
@@ -105,8 +106,10 @@ public class ImportGitLabProjectActionIT {
   private final ProjectCreator projectCreator = new ProjectCreator(userSession, projectDefaultVisibility, componentUpdater);
   private final GitlabProjectCreatorFactory gitlabProjectCreatorFactory = new GitlabProjectCreatorFactory(db.getDbClient(), projectKeyGenerator, projectCreator,
     gitlabApplicationClient, userSession);
-  private final ImportGitLabProjectAction importGitLabProjectAction = new ImportGitLabProjectAction(
-    db.getDbClient(), userSession, componentUpdater, importHelper, newCodeDefinitionResolver, gitlabProjectCreatorFactory);
+
+  private final ImportProjectService importProjectService = new ImportProjectService(db.getDbClient(), gitlabProjectCreatorFactory, userSession, componentUpdater,
+    newCodeDefinitionResolver);
+  private final ImportGitLabProjectAction importGitLabProjectAction = new ImportGitLabProjectAction(importProjectService, importHelper);
   private final WsActionTester ws = new WsActionTester(importGitLabProjectAction);
 
   @Before
index 130078e23287face93558e5eaf322985b340a9af..e5eb07f77d219d1b3845c69711308edc39ce44c5 100644 (file)
  */
 package org.sonar.server.almintegration.ws.github;
 
-import java.util.Optional;
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 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.db.DbClient;
-import org.sonar.db.DbSession;
 import org.sonar.db.alm.setting.ALM;
 import org.sonar.db.alm.setting.AlmSettingDto;
-import org.sonar.db.component.BranchDto;
-import org.sonar.db.project.CreationMethod;
-import org.sonar.db.project.ProjectDto;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
 import org.sonar.server.almintegration.ws.ImportHelper;
-import org.sonar.server.common.almsettings.DevOpsProjectCreator;
-import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
-import org.sonar.server.common.almsettings.github.GithubProjectCreatorFactory;
-import org.sonar.server.component.ComponentCreationData;
-import org.sonar.server.common.component.ComponentUpdater;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver;
-import org.sonar.server.project.DefaultBranchNameResolver;
-import org.sonar.server.user.UserSession;
+import org.sonar.server.common.project.ImportProjectRequest;
+import org.sonar.server.common.project.ImportProjectService;
+import org.sonar.server.common.project.ImportedProject;
 import org.sonarqube.ws.Projects;
 
-import static java.util.Objects.requireNonNull;
-import static org.sonar.db.project.CreationMethod.getCreationMethod;
-import static org.sonar.db.project.CreationMethod.Category.ALM_IMPORT;
 import static org.sonar.server.almintegration.ws.ImportHelper.PARAM_ALM_SETTING;
-import static org.sonar.server.almintegration.ws.ImportHelper.toCreateResponse;
 import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION;
 import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION;
-import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.checkNewCodeDefinitionParam;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 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;
@@ -60,30 +44,13 @@ import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_COD
 public class ImportGithubProjectAction implements AlmIntegrationsWsAction {
   public static final String PARAM_REPOSITORY_KEY = "repositoryKey";
 
-  private final DbClient dbClient;
-
-  private final UserSession userSession;
-  private final ComponentUpdater componentUpdater;
+  private final ImportProjectService importProjectService;
   private final ImportHelper importHelper;
 
-  private final NewCodeDefinitionResolver newCodeDefinitionResolver;
-
-  private final DefaultBranchNameResolver defaultBranchNameResolver;
-
-  private final GithubProjectCreatorFactory githubProjectCreatorFactory;
-
   @Inject
-  public ImportGithubProjectAction(DbClient dbClient, UserSession userSession,
-    ComponentUpdater componentUpdater, ImportHelper importHelper,
-    NewCodeDefinitionResolver newCodeDefinitionResolver,
-    DefaultBranchNameResolver defaultBranchNameResolver, GithubProjectCreatorFactory githubProjectCreatorFactory) {
-    this.dbClient = dbClient;
-    this.userSession = userSession;
-    this.componentUpdater = componentUpdater;
+  public ImportGithubProjectAction(ImportProjectService importProjectService, ImportHelper importHelper) {
+    this.importProjectService = importProjectService;
     this.importHelper = importHelper;
-    this.newCodeDefinitionResolver = newCodeDefinitionResolver;
-    this.defaultBranchNameResolver = defaultBranchNameResolver;
-    this.githubProjectCreatorFactory = githubProjectCreatorFactory;
   }
 
   @Override
@@ -128,34 +95,18 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction {
   private Projects.CreateWsResponse doHandle(Request request) {
     importHelper.checkProvisionProjectPermission();
     AlmSettingDto almSettingDto = importHelper.getAlmSettingDtoForAlm(request, ALM.GITHUB);
-
+    String repositoryKey = request.mandatoryParam(PARAM_REPOSITORY_KEY);
     String newCodeDefinitionType = request.param(PARAM_NEW_CODE_DEFINITION_TYPE);
     String newCodeDefinitionValue = request.param(PARAM_NEW_CODE_DEFINITION_VALUE);
-    try (DbSession dbSession = dbClient.openSession(false)) {
 
-      String repositoryKey = request.mandatoryParam(PARAM_REPOSITORY_KEY);
+    ImportedProject importedProject = importProjectService.importProject(toServiceRequest(almSettingDto, repositoryKey, newCodeDefinitionType, newCodeDefinitionValue));
 
-      String url = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null");
-      DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, url, repositoryKey);
+    return ImportHelper.toCreateResponse(importedProject.projectDto());
 
-      DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(almSettingDto, devOpsProjectDescriptor)
-        .orElseThrow(() -> BadRequestException.create("GitHub DevOps platform configuration not found."));
-      CreationMethod creationMethod = getCreationMethod(ALM_IMPORT, userSession.isAuthenticatedBrowserSession());
-      ComponentCreationData componentCreationData = devOpsProjectCreator.createProjectAndBindToDevOpsPlatform(dbSession, creationMethod, false, null, null);
-
-      checkNewCodeDefinitionParam(newCodeDefinitionType, newCodeDefinitionValue);
-
-      ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
-      BranchDto mainBranchDto = Optional.ofNullable(componentCreationData.mainBranchDto()).orElseThrow();
-
-      if (newCodeDefinitionType != null) {
-        newCodeDefinitionResolver.createNewCodeDefinition(dbSession, projectDto.getUuid(), mainBranchDto.getUuid(),
-          Optional.ofNullable(mainBranchDto.getKey()).orElse(defaultBranchNameResolver.getEffectiveMainBranchName()),
-          newCodeDefinitionType, newCodeDefinitionValue);
-      }
+  }
 
-      componentUpdater.commitAndIndex(dbSession, componentCreationData);
-      return toCreateResponse(projectDto);
-    }
+  private static ImportProjectRequest toServiceRequest(AlmSettingDto almSettingDto, String githubRepositoryKey, @Nullable String newCodeDefinitionType,
+    @Nullable String newCodeDefinitionValue) {
+    return new ImportProjectRequest(null, null, almSettingDto.getUuid(), githubRepositoryKey, newCodeDefinitionType, newCodeDefinitionValue, false);
   }
 }
index dc5e0ca7e0ace3c25a2b44b15b7b1bda434a87e9..5bacb525b84f157042ce7716f68dbbcaca79843b 100644 (file)
  */
 package org.sonar.server.almintegration.ws.gitlab;
 
-import java.util.Optional;
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 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.db.DbClient;
-import org.sonar.db.DbSession;
 import org.sonar.db.alm.setting.ALM;
 import org.sonar.db.alm.setting.AlmSettingDto;
-import org.sonar.db.component.BranchDto;
-import org.sonar.db.project.ProjectDto;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
 import org.sonar.server.almintegration.ws.ImportHelper;
-import org.sonar.server.common.almsettings.DevOpsProjectCreator;
-import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
-import org.sonar.server.common.almsettings.gitlab.GitlabProjectCreatorFactory;
-import org.sonar.server.component.ComponentCreationData;
-import org.sonar.server.common.component.ComponentUpdater;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver;
-import org.sonar.server.user.UserSession;
+import org.sonar.server.common.project.ImportProjectRequest;
+import org.sonar.server.common.project.ImportProjectService;
+import org.sonar.server.common.project.ImportedProject;
 import org.sonarqube.ws.Projects.CreateWsResponse;
 
-import static java.util.Objects.requireNonNull;
-import static org.sonar.db.project.CreationMethod.getCreationMethod;
-import static org.sonar.db.project.CreationMethod.Category.ALM_IMPORT;
 import static org.sonar.server.almintegration.ws.ImportHelper.PARAM_ALM_SETTING;
 import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION;
 import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION;
-import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.checkNewCodeDefinitionParam;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 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;
@@ -58,23 +45,13 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
 
   public static final String PARAM_GITLAB_PROJECT_ID = "gitlabProjectId";
 
-  private final DbClient dbClient;
-  private final UserSession userSession;
-  private final ComponentUpdater componentUpdater;
+  private final ImportProjectService importProjectService;
   private final ImportHelper importHelper;
-  private final NewCodeDefinitionResolver newCodeDefinitionResolver;
-  private final GitlabProjectCreatorFactory projectCreatorFactory;
 
   @Inject
-  public ImportGitLabProjectAction(DbClient dbClient, UserSession userSession,
-    ComponentUpdater componentUpdater, ImportHelper importHelper, NewCodeDefinitionResolver newCodeDefinitionResolver,
-    GitlabProjectCreatorFactory projectCreatorFactory) {
-    this.dbClient = dbClient;
-    this.userSession = userSession;
-    this.componentUpdater = componentUpdater;
+  public ImportGitLabProjectAction(ImportProjectService importProjectService, ImportHelper importHelper) {
+    this.importProjectService = importProjectService;
     this.importHelper = importHelper;
-    this.newCodeDefinitionResolver = newCodeDefinitionResolver;
-    this.projectCreatorFactory = projectCreatorFactory;
   }
 
   @Override
@@ -112,33 +89,17 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
 
   private CreateWsResponse doHandle(Request request) {
     importHelper.checkProvisionProjectPermission();
-
+    AlmSettingDto almSettingDto = importHelper.getAlmSettingDtoForAlm(request, ALM.GITLAB);
+    String gitlabProjectId = request.mandatoryParam(PARAM_GITLAB_PROJECT_ID);
     String newCodeDefinitionType = request.param(PARAM_NEW_CODE_DEFINITION_TYPE);
     String newCodeDefinitionValue = request.param(PARAM_NEW_CODE_DEFINITION_VALUE);
+    ImportedProject importedProject = importProjectService.importProject(toServiceRequest(almSettingDto, gitlabProjectId, newCodeDefinitionType, newCodeDefinitionValue));
 
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      AlmSettingDto almSettingDto = importHelper.getAlmSettingDtoForAlm(request, ALM.GITLAB);
-
-      String gitlabProjectId = request.mandatoryParam(PARAM_GITLAB_PROJECT_ID);
-      String gitlabUrl = requireNonNull(almSettingDto.getUrl(), "DevOps Platform gitlabUrl cannot be null");
-
-      DevOpsProjectDescriptor projectDescriptor = new DevOpsProjectDescriptor(ALM.GITLAB, gitlabUrl, gitlabProjectId);
-      DevOpsProjectCreator projectCreator = projectCreatorFactory.getDevOpsProjectCreator(almSettingDto, projectDescriptor)
-        .orElseThrow(() -> BadRequestException.create("Gitlab DevOps platform configuration not found"));
-      ComponentCreationData componentCreationData = projectCreator.createProjectAndBindToDevOpsPlatform(dbSession,
-        getCreationMethod(ALM_IMPORT, userSession.isAuthenticatedBrowserSession()), false, null, null);
-
-      ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
-      BranchDto mainBranchDto = Optional.ofNullable(componentCreationData.mainBranchDto()).orElseThrow();
-
-      checkNewCodeDefinitionParam(newCodeDefinitionType, newCodeDefinitionValue);
-      if (newCodeDefinitionType != null) {
-        newCodeDefinitionResolver.createNewCodeDefinition(dbSession, projectDto.getUuid(), mainBranchDto.getUuid(),
-          mainBranchDto.getKey(), newCodeDefinitionType, newCodeDefinitionValue);
-      }
+    return ImportHelper.toCreateResponse(importedProject.projectDto());
+  }
 
-      componentUpdater.commitAndIndex(dbSession, componentCreationData);
-      return ImportHelper.toCreateResponse(projectDto);
-    }
+  private static ImportProjectRequest toServiceRequest(AlmSettingDto almSettingDto, String gitlabProjectId, @Nullable String newCodeDefinitionType,
+    @Nullable String newCodeDefinitionValue) {
+    return new ImportProjectRequest(null, null, almSettingDto.getUuid(), gitlabProjectId, newCodeDefinitionType, newCodeDefinitionValue, false);
   }
 }
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/package-info.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/package-info.java
deleted file mode 100644 (file)
index 281ee26..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.server.newcodeperiod;
-
-import javax.annotation.ParametersAreNonnullByDefault;
index 70ac999aee8e85dc940841afb2be9d8b9793c1ed..5dbd55f6f9804be295026ab56d36b41e5aca65c1 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.project.ws;
 
 import org.sonar.core.platform.Module;
+import org.sonar.server.common.project.ImportProjectService;
 import org.sonar.server.common.project.ProjectCreator;
 import org.sonar.server.project.ProjectDefaultVisibility;
 import org.sonar.server.project.ProjectLifeCycleListenersImpl;
@@ -33,6 +34,7 @@ public class ProjectsWsModule extends Module {
   @Override
   protected void configureModule() {
     add(
+      ImportProjectService.class,
       ProjectDefaultVisibility.class,
       ProjectFinder.class,
       ProjectLifeCycleListenersImpl.class,
index 7b9ad92433594b5966c72ad771e7921fbe3e95c8..f3eb02a0b35ea03efa6157ba201efbcaabe89169 100644 (file)
@@ -29,6 +29,6 @@ public class ProjectsWsModuleTest {
   public void verify_count_of_added_components_on_SonarQube() {
     ListContainer container = new ListContainer();
     new ProjectsWsModule().configure(container);
-    assertThat(container.getAddedObjects()).hasSize(14);
+    assertThat(container.getAddedObjects()).hasSize(15);
   }
 }