]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17497 Add web service to reindex issues
authoralain <108417558+alain-kermis-sonarsource@users.noreply.github.com>
Thu, 27 Oct 2022 12:08:20 +0000 (14:08 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 27 Oct 2022 20:03:02 +0000 (20:03 +0000)
13 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/issue/index/NoAsyncIssueIndexing.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexing.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexer.java
server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexingImpl.java
server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexingImplTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ReindexAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ReindexActionTest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java

index 6ade7075c0545290196ef45f12e0517344fb9ca7..762ae11f31bc8af863bc7fe3bf65e8559ff6ace9 100644 (file)
@@ -28,4 +28,9 @@ public class NoAsyncIssueIndexing implements AsyncIssueIndexing {
   public void triggerOnIndexCreation() {
     throw new IllegalStateException("Async issue indexing should not be triggered in Compute Engine");
   }
+
+  @Override
+  public void triggerForProject(String projectUuid) {
+    throw new IllegalStateException("Async issue indexing should not be triggered in Compute Engine");
+  }
 }
index f655c175cfd0bd2bd7edb34f73bf260c1aefd3dd..39242c80ac6158e0bf6c435b051f8fe4fdb72e04 100644 (file)
@@ -148,10 +148,18 @@ public class BranchDao implements Dao {
     return mapper(dbSession).selectBranchNeedingIssueSync();
   }
 
+  public List<BranchDto> selectBranchNeedingIssueSyncForProject(DbSession dbSession, String projectUuid) {
+    return mapper(dbSession).selectBranchNeedingIssueSyncForProject(projectUuid);
+  }
+
   public long updateAllNeedIssueSync(DbSession dbSession) {
     return mapper(dbSession).updateAllNeedIssueSync(system2.now());
   }
 
+  public long updateAllNeedIssueSyncForProject(DbSession dbSession, String projectUuid) {
+    return mapper(dbSession).updateAllNeedIssueSyncForProject(projectUuid, system2.now());
+  }
+
   public long updateNeedIssueSync(DbSession dbSession, String branchUuid, boolean needIssueSync) {
     long now = system2.now();
     return mapper(dbSession).updateNeedIssueSync(branchUuid, needIssueSync, now);
index 9f19580941c27b45acf9e73edf441b7859fb0c10..691ef746cbde3afe88333446742690002a03bb57 100644 (file)
@@ -65,8 +65,12 @@ public interface BranchMapper {
 
   List<BranchDto> selectBranchNeedingIssueSync();
 
+  List<BranchDto> selectBranchNeedingIssueSyncForProject(@Param("projectUuid") String projectUuid);
+
   long updateAllNeedIssueSync(@Param("now") long now);
 
+  long updateAllNeedIssueSyncForProject(@Param("projectUuid") String projectUuid, @Param("now") long now);
+
   long updateNeedIssueSync(@Param("uuid") String uuid, @Param("needIssueSync")boolean needIssueSync,@Param("now") long now);
 
   short doAnyOfComponentsNeedIssueSync(@Param("componentKeys") List<String> components);
index 246dc8bbf43f838aea74225b900da326b668e049..fcfae3b120a3e7cdae81b6fc11a20efd31c30197 100644 (file)
     order by pb.updated_at desc, uuid
   </select>
 
+  <select id="selectBranchNeedingIssueSyncForProject" resultType="org.sonar.db.component.BranchDto">
+    select
+    <include refid="columns"/>
+    from project_branches pb
+    where need_issue_sync = ${_true} and project_uuid = #{projectUuid, jdbcType=VARCHAR}
+    order by pb.updated_at desc, uuid
+  </select>
+
   <update id="updateAllNeedIssueSync">
     update project_branches
     set
       updated_at = #{now, jdbcType=BIGINT}
   </update>
 
+  <update id="updateAllNeedIssueSyncForProject">
+    update project_branches
+    set
+      need_issue_sync = ${_true},
+      updated_at = #{now, jdbcType=BIGINT}
+    where
+      project_uuid = #{projectUuid, jdbcType=VARCHAR}
+  </update>
+
   <update id="updateNeedIssueSync">
     update project_branches
     set
index 5de47d10d246017228cdc818b0e98c8a756bb4c0..1c6589f3190506101156f8426f5b7ed7479a9957 100644 (file)
@@ -237,7 +237,7 @@ public class BranchDaoTest {
     assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch);
     assertThat(loadedPullRequestData.getTitle()).isEqualTo(title);
     assertThat(loadedPullRequestData.getUrl()).isEqualTo(url);
-    assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue);
+    assertThat(loadedPullRequestData.getAttributesMap()).containsEntry(tokenAttributeName, tokenAttributeValue);
   }
 
   @Test
@@ -303,7 +303,7 @@ public class BranchDaoTest {
     assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch);
     assertThat(loadedPullRequestData.getTitle()).isEqualTo(title);
     assertThat(loadedPullRequestData.getUrl()).isEqualTo(url);
-    assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue);
+    assertThat(loadedPullRequestData.getAttributesMap()).containsEntry(tokenAttributeName, tokenAttributeValue);
   }
 
   @Test
@@ -356,7 +356,7 @@ public class BranchDaoTest {
     assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch);
     assertThat(loadedPullRequestData.getTitle()).isEqualTo(title);
     assertThat(loadedPullRequestData.getUrl()).isEqualTo(url);
-    assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue);
+    assertThat(loadedPullRequestData.getAttributesMap()).containsEntry(tokenAttributeName, tokenAttributeValue);
   }
 
   @Test
@@ -679,6 +679,17 @@ public class BranchDaoTest {
       .containsExactly(uuid);
   }
 
+  @Test
+  public void selectBranchNeedingIssueSyncForProject() {
+    ComponentDto project = db.components().insertPrivateProject();
+    String uuid = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(true)).uuid();
+    db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(false));
+
+    assertThat(underTest.selectBranchNeedingIssueSyncForProject(dbSession, project.uuid()))
+      .extracting(BranchDto::getUuid)
+      .containsExactly(uuid);
+  }
+
   @Test
   public void updateAllNeedIssueSync() {
     ComponentDto project = db.components().insertPrivateProject();
@@ -696,6 +707,23 @@ public class BranchDaoTest {
     assertThat(project2.get().isNeedIssueSync()).isTrue();
   }
 
+  @Test
+  public void updateAllNeedIssueSyncForProject() {
+    ComponentDto project = db.components().insertPrivateProject();
+    String uuid1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(true)).uuid();
+    String uuid2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(false)).uuid();
+
+    underTest.updateAllNeedIssueSyncForProject(dbSession, project.uuid());
+
+    Optional<BranchDto> project1 = underTest.selectByUuid(dbSession, uuid1);
+    assertThat(project1).isPresent();
+    assertThat(project1.get().isNeedIssueSync()).isTrue();
+
+    Optional<BranchDto> project2 = underTest.selectByUuid(dbSession, uuid2);
+    assertThat(project2).isPresent();
+    assertThat(project2.get().isNeedIssueSync()).isTrue();
+  }
+
   @Test
   public void updateNeedIssueSync() {
     ComponentDto project = db.components().insertPrivateProject();
index 35f5de4f29d37d10d751b8f2d2a9e6cf4211ea99..cb0221e44f49105e220ba5a840048b14b5099aef 100644 (file)
@@ -21,4 +21,5 @@ package org.sonar.server.issue.index;
 
 public interface AsyncIssueIndexing {
   void triggerOnIndexCreation();
+  void triggerForProject(String projectUuid);
 }
index 308f6a21d970308451081812ebf74224756a7066..72246142b91662bbf992ae187581258a20b872d0 100644 (file)
@@ -116,6 +116,10 @@ public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer {
     }
   }
 
+  public void indexProject(String projectUuid) {
+    asyncIssueIndexing.triggerForProject(projectUuid);
+  }
+
   @Override
   public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) {
     switch (cause) {
index 322d91037ca8bf51abb612c4cf1a46ddc43b0403..bf92288a2a7aeb0f2faa0b3a4c9722ca18bed7c5 100644 (file)
@@ -96,6 +96,27 @@ public class AsyncIssueIndexingImpl implements AsyncIssueIndexing {
     }
   }
 
+  @Override
+  public void triggerForProject(String projectUuid) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+
+      // remove already existing indexation task, if any
+      removeExistingIndexationTasksForProject(dbSession, projectUuid);
+
+      dbClient.branchDao().updateAllNeedIssueSyncForProject(dbSession, projectUuid);
+      List<BranchDto> branchInNeedOfIssueSync = dbClient.branchDao().selectBranchNeedingIssueSyncForProject(dbSession, projectUuid);
+      LOG.info("{} branch(es) found in need of issue sync for project.", branchInNeedOfIssueSync.size());
+
+      List<CeTaskSubmit> tasks = new ArrayList<>();
+      for (BranchDto branch : branchInNeedOfIssueSync) {
+        tasks.add(buildTaskSubmit(branch));
+      }
+
+      ceQueue.massSubmit(tasks);
+      dbSession.commit();
+    }
+  }
+
   private void sortProjectUuids(DbSession dbSession, List<String> projectUuids) {
     Map<String, SnapshotDto> snapshotByProjectUuid = dbClient.snapshotDao()
       .selectLastAnalysesByRootComponentUuids(dbSession, projectUuids).stream()
@@ -122,10 +143,19 @@ public class AsyncIssueIndexingImpl implements AsyncIssueIndexing {
   }
 
   private void removeExistingIndexationTasks(DbSession dbSession) {
-    List<String> uuids = dbClient.ceQueueDao().selectAllInAscOrder(dbSession).stream()
+    removeIndexationTasks(dbSession, dbClient.ceQueueDao().selectAllInAscOrder(dbSession));
+  }
+
+  private void removeExistingIndexationTasksForProject(DbSession dbSession, String projectUuid) {
+    removeIndexationTasks(dbSession, dbClient.ceQueueDao().selectByMainComponentUuid(dbSession, projectUuid));
+  }
+
+  private void removeIndexationTasks(DbSession dbSession, List<CeQueueDto> ceQueueDtos) {
+    List<String> uuids = ceQueueDtos.stream()
       .filter(p -> p.getTaskType().equals(BRANCH_ISSUE_SYNC))
       .map(CeQueueDto::getUuid)
       .collect(Collectors.toList());
+
     LOG.info(String.format("%s pending indexation task found to be deleted...", uuids.size()));
     for (String uuid : uuids) {
       dbClient.ceQueueDao().deleteByUuid(dbSession, uuid);
index adc62e4b41b3390c99ae87e70e99bdb99f1bb141..a97fea02e85d1880054d5342ca0d2aa7d6f49e64 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.server.issue.index;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -46,6 +45,7 @@ import org.sonar.db.ce.CeQueueDto;
 import org.sonar.db.ce.CeTaskCharacteristicDto;
 import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.project.ProjectDto;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
@@ -69,9 +69,9 @@ public class AsyncIssueIndexingImplTest {
   @Rule
   public LogTester logTester = new LogTester();
 
-  private DbClient dbClient = dbTester.getDbClient();
-  private CeQueue ceQueue = mock(CeQueue.class);
-  private UuidFactory uuidFactory = new SequenceUuidFactory();
+  private final DbClient dbClient = dbTester.getDbClient();
+  private final CeQueue ceQueue = mock(CeQueue.class);
+  private final UuidFactory uuidFactory = new SequenceUuidFactory();
 
   private final AsyncIssueIndexingImpl underTest = new AsyncIssueIndexingImpl(ceQueue, dbClient);
 
@@ -101,6 +101,27 @@ public class AsyncIssueIndexingImplTest {
       .contains("1 branch found in need of issue sync.");
   }
 
+  @Test
+  public void triggerForProject() {
+    ProjectDto projectDto = dbTester.components().insertPrivateProjectDto();
+    BranchDto dto = new BranchDto()
+      .setBranchType(BRANCH)
+      .setKey("branchName")
+      .setUuid("branch_uuid")
+      .setProjectUuid(projectDto.getUuid());
+    dbTester.components().insertProjectBranch(projectDto, dto);
+
+    underTest.triggerForProject(projectDto.getUuid());
+
+    Optional<BranchDto> branch = dbClient.branchDao().selectByUuid(dbTester.getSession(), "branch_uuid");
+    assertThat(branch).isPresent();
+    assertThat(branch.get().isNeedIssueSync()).isTrue();
+    verify(ceQueue, times(2)).prepareSubmit();
+    verify(ceQueue, times(1)).massSubmit(anyCollection());
+    assertThat(logTester.logs(LoggerLevel.INFO))
+      .contains("2 branch(es) found in need of issue sync for project.");
+  }
+
   @Test
   public void triggerOnIndexCreation_no_branch() {
     underTest.triggerOnIndexCreation();
@@ -108,37 +129,28 @@ public class AsyncIssueIndexingImplTest {
     assertThat(logTester.logs(LoggerLevel.INFO)).contains("0 branch found in need of issue sync.");
   }
 
+  @Test
+  public void triggerForProject_no_branch() {
+    underTest.triggerForProject("some-random-uuid");
+    assertThat(logTester.logs(LoggerLevel.INFO)).contains("0 branch(es) found in need of issue sync for project.");
+  }
+
   @Test
   public void remove_existing_indexation_task() {
-    CeQueueDto reportTask = new CeQueueDto();
-    reportTask.setUuid("uuid_1");
-    reportTask.setTaskType(REPORT);
-    dbClient.ceQueueDao().insert(dbTester.getSession(), reportTask);
+    String reportTaskUuid = persistReportTasks();
 
-    CeActivityDto reportActivity = new CeActivityDto(reportTask);
-    reportActivity.setStatus(Status.SUCCESS);
-    dbClient.ceActivityDao().insert(dbTester.getSession(), reportActivity);
     CeQueueDto task = new CeQueueDto();
     task.setUuid("uuid_2");
     task.setTaskType(BRANCH_ISSUE_SYNC);
     dbClient.ceQueueDao().insert(dbTester.getSession(), task);
-
     CeActivityDto activityDto = new CeActivityDto(task);
     activityDto.setStatus(Status.SUCCESS);
     dbClient.ceActivityDao().insert(dbTester.getSession(), activityDto);
-
     dbTester.commit();
 
     underTest.triggerOnIndexCreation();
 
-    assertThat(dbClient.ceQueueDao().selectAllInAscOrder(dbTester.getSession())).extracting("uuid")
-      .containsExactly(reportTask.getUuid());
-    assertThat(dbClient.ceActivityDao().selectByTaskType(dbTester.getSession(), BRANCH_ISSUE_SYNC)).isEmpty();
-
-    assertThat(dbClient.ceActivityDao().selectByTaskType(dbTester.getSession(), REPORT)).hasSize(1);
-
-    assertThat(dbClient.ceTaskCharacteristicsDao().selectByTaskUuids(dbTester.getSession(), new HashSet<>(Arrays.asList("uuid_2")))).isEmpty();
-
+    assertCeTasks(reportTaskUuid);
     assertThat(logTester.logs(LoggerLevel.INFO))
       .contains(
         "1 pending indexation task found to be deleted...",
@@ -148,6 +160,35 @@ public class AsyncIssueIndexingImplTest {
         "Tasks characteristics deletion complete.");
   }
 
+  @Test
+  public void remove_existing_indexation_for_project_task() {
+    String reportTaskUuid = persistReportTasks();
+
+    ProjectDto projectDto = dbTester.components().insertPrivateProjectDto();
+    String branchUuid = "branch_uuid";
+    dbTester.components().insertProjectBranch(projectDto, b -> b.setBranchType(BRANCH).setUuid(branchUuid));
+    CeQueueDto mainBranchTask = new CeQueueDto().setUuid("uuid_2").setTaskType(BRANCH_ISSUE_SYNC)
+      .setMainComponentUuid(projectDto.getUuid()).setComponentUuid(projectDto.getUuid());
+    CeQueueDto branchTask = new CeQueueDto().setUuid("uuid_3").setTaskType(BRANCH_ISSUE_SYNC)
+      .setMainComponentUuid(projectDto.getUuid()).setComponentUuid(branchUuid);
+    dbClient.ceQueueDao().insert(dbTester.getSession(), mainBranchTask);
+    dbClient.ceQueueDao().insert(dbTester.getSession(), branchTask);
+    dbTester.commit();
+
+    underTest.triggerForProject(projectDto.getUuid());
+
+    assertCeTasks(reportTaskUuid);
+    assertThat(logTester.logs(LoggerLevel.INFO))
+      .contains(
+        "2 pending indexation task found to be deleted...",
+        "2 completed indexation task found to be deleted...",
+        "Indexation task deletion complete.",
+        "Deleting tasks characteristics...",
+        "Tasks characteristics deletion complete.",
+        "Tasks characteristics deletion complete.",
+        "2 branch(es) found in need of issue sync for project.");
+  }
+
   @Test
   public void order_by_last_analysis_date() {
     BranchDto dto = new BranchDto()
@@ -274,4 +315,24 @@ public class AsyncIssueIndexingImplTest {
     return snapshot;
   }
 
+  private String persistReportTasks() {
+    CeQueueDto reportTask = new CeQueueDto();
+    reportTask.setUuid("uuid_1");
+    reportTask.setTaskType(REPORT);
+    dbClient.ceQueueDao().insert(dbTester.getSession(), reportTask);
+
+    CeActivityDto reportActivity = new CeActivityDto(reportTask);
+    reportActivity.setStatus(Status.SUCCESS);
+    dbClient.ceActivityDao().insert(dbTester.getSession(), reportActivity);
+    return reportTask.getUuid();
+  }
+
+  private void assertCeTasks(String reportTaskUuid) {
+    assertThat(dbClient.ceQueueDao().selectAllInAscOrder(dbTester.getSession())).extracting("uuid")
+      .containsExactly(reportTaskUuid);
+    assertThat(dbClient.ceActivityDao().selectByTaskType(dbTester.getSession(), BRANCH_ISSUE_SYNC)).isEmpty();
+    assertThat(dbClient.ceActivityDao().selectByTaskType(dbTester.getSession(), REPORT)).hasSize(1);
+    assertThat(dbClient.ceTaskCharacteristicsDao().selectByTaskUuids(dbTester.getSession(), new HashSet<>(List.of("uuid_2")))).isEmpty();
+  }
+
 }
index 37b3a53adbb3a5a0e3cb3c0a03678c5b9886c57f..6bf37943b81e66baac37865a578f3af3a4d004c4 100644 (file)
@@ -67,6 +67,7 @@ public class IssueWsModule extends Module {
       SetTagsAction.class,
       SetTypeAction.class,
       ComponentTagsAction.class,
+      ReindexAction.class,
       AuthorsAction.class,
       ChangelogAction.class,
       BulkChangeAction.class,
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ReindexAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ReindexAction.java
new file mode 100644 (file)
index 0000000..2d2a261
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.issue.ws;
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.user.UserSession;
+
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT;
+
+/**
+ * Implementation of the {@code reindex} action for the Issues WebService.
+ */
+public class ReindexAction implements IssuesWsAction {
+
+  private static final String ACTION = "reindex";
+  private final DbClient dbClient;
+  private final IssueIndexer issueIndexer;
+  private final UserSession userSession;
+
+  public ReindexAction(DbClient dbClient, IssueIndexer indexer, UserSession userSession) {
+    this.dbClient = dbClient;
+    this.issueIndexer = indexer;
+    this.userSession = userSession;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context
+      .createAction(ACTION)
+      .setPost(true)
+      .setDescription("Reindex issues for a project.<br> " +
+        "Requires one of the following permissions: " +
+        "<ul>" +
+        "<li>'Administer System'</li>" +
+        "<li>'Administer' rights on the specified project</li>" +
+        "</ul>")
+      .setSince("9.8")
+      .setHandler(this);
+
+    action
+      .createParam(PARAM_PROJECT)
+      .setDescription("Project key")
+      .setRequired(true)
+      .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    String projectKey = request.mandatoryParam(PARAM_PROJECT);
+
+    ProjectDto projectDto;
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      projectDto = dbClient.projectDao().selectProjectByKey(dbSession, projectKey).orElseThrow(() -> new NotFoundException("project not found"));
+      userSession.checkProjectPermission(UserRole.ADMIN, projectDto);
+    }
+
+    issueIndexer.indexProject(projectDto.getUuid());
+    response.noContent();
+  }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ReindexActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ReindexActionTest.java
new file mode 100644 (file)
index 0000000..fb18d38
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.issue.ws;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbTester;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.index.AsyncIssueIndexing;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class ReindexActionTest {
+
+  @Rule
+  public EsTester es = EsTester.create();
+  @Rule
+  public DbTester db = DbTester.create();
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  private final AsyncIssueIndexing mock = mock(AsyncIssueIndexing.class);
+  private final IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()), mock);
+  private final ReindexAction underTest = new ReindexAction(db.getDbClient(), issueIndexer, userSession);
+  private final WsActionTester tester = new WsActionTester(underTest);
+
+  @Test
+  public void test_definition() {
+    WebService.Action action = tester.getDef();
+
+    assertThat(action.key()).isEqualTo("reindex");
+    assertThat(action.isPost()).isTrue();
+    assertThat(action.isInternal()).isFalse();
+    assertThat(action.params()).extracting(WebService.Param::key).containsExactly("project");
+  }
+
+  @Test
+  public void reindex_project() {
+    ProjectDto project = db.components().insertPrivateProjectDto();
+    userSession.logIn().setSystemAdministrator();
+    userSession.addProjectPermission(UserRole.ADMIN, project);
+
+    TestResponse response = tester.newRequest()
+      .setParam("project", project.getKey())
+      .execute();
+
+    assertThat(response.getStatus()).isEqualTo(204);
+    verify(mock, times(1)).triggerForProject(project.getUuid());
+  }
+
+  @Test
+  public void fail_if_project_does_not_exist() {
+    userSession.logIn().setSystemAdministrator();
+
+    TestRequest testRequest = tester.newRequest().setParam("project", "some-key");
+    assertThatThrownBy(testRequest::execute)
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("project not found");
+  }
+
+  @Test
+  public void fail_if_parameter_not_present() {
+    userSession.anonymous();
+    TestRequest testRequest = tester.newRequest();
+    assertThatThrownBy(testRequest::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("The 'project' parameter is missing");
+  }
+
+  @Test
+  public void fail_if_not_authorized() {
+    ProjectDto project = db.components().insertPrivateProjectDto();
+    userSession.addProjectPermission(UserRole.USER, project);
+
+    TestRequest testRequest = tester.newRequest().setParam("project", project.getKey());
+    assertThatThrownBy(testRequest::execute)
+      .isInstanceOf(ForbiddenException.class)
+      .hasMessage("Insufficient privileges");
+  }
+
+}
index bd8295956c55318fcaa471b33985f846c151bdf4..a607c6c8c1885c5e8e6301f6c24d27325a919987 100644 (file)
@@ -192,6 +192,19 @@ public class IssuesService extends BaseService {
         .setMediaType(MediaTypes.JSON)).content();
   }
 
+  /**
+   *
+   * This is part of the internal API.
+   * This is a POST request.
+   * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/issues/reindex">Further information about this action online (including a response example)</a>
+   * @since 9.8
+   */
+  public void reindex() {
+    call(
+      new PostRequest(path("reindex"))
+        .setMediaType(MediaTypes.JSON)).content();
+  }
+
   /**
    *
    * This is part of the internal API.