]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19728 add `api/hotspots/list` endpoint
authorJacek <jacek.poreda@sonarsource.com>
Mon, 10 Jul 2023 12:52:20 +0000 (14:52 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Jul 2023 20:03:05 +0000 (20:03 +0000)
17 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ListActionIT.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/SearchActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ListAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/NewCodePeriodResolver.java [new file with mode: 0644]
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/ListAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java
sonar-ws/src/main/protobuf/ws-hotspots.proto
sonar-ws/src/main/protobuf/ws-issues.proto

index f4b4f8e0337907bc3c5acf0a1d9144822300ffa8..217f047ab6c1bdb8835a43b3bbb71fe6e03802cb 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.db.issue;
 
 import java.util.Collection;
 import java.util.Collections;
+import javax.annotation.Nullable;
 
 import static java.util.Collections.emptyList;
 import static java.util.Optional.ofNullable;
@@ -121,12 +122,12 @@ public class IssueListQuery {
       return this;
     }
 
-    public IssueListQueryBuilder branch(String branch) {
+    public IssueListQueryBuilder branch(@Nullable String branch) {
       this.branch = branch;
       return this;
     }
 
-    public IssueListQueryBuilder pullRequest(String pullRequest) {
+    public IssueListQueryBuilder pullRequest(@Nullable String pullRequest) {
       this.pullRequest = pullRequest;
       return this;
     }
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ListActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ListActionIT.java
new file mode 100644 (file)
index 0000000..ccc6134
--- /dev/null
@@ -0,0 +1,520 @@
+/*
+ * 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.hotspot.ws;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.time.Clock;
+import java.util.List;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.RuleType;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ProjectData;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.component.TestComponentFinder;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.issue.NewCodePeriodResolver;
+import org.sonar.server.issue.TextRangeResponseFormatter;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.MessageFormattingUtils;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Hotspots;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.tuple;
+import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
+import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
+import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
+import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
+import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
+import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
+import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
+import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.api.utils.DateUtils.parseDate;
+import static org.sonar.api.utils.DateUtils.parseDateTime;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+import static org.sonar.db.protobuf.DbIssues.MessageFormattingType.CODE;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
+import static org.sonar.db.rule.RuleTesting.XOO_X1;
+import static org.sonar.db.rule.RuleTesting.XOO_X2;
+import static org.sonar.db.rule.RuleTesting.newRule;
+import static org.sonar.server.tester.UserSessionRule.standalone;
+
+@RunWith(DataProviderRunner.class)
+public class ListActionIT {
+
+  public static final DbIssues.MessageFormatting MESSAGE_FORMATTING = DbIssues.MessageFormatting.newBuilder()
+    .setStart(0).setEnd(11).setType(CODE).build();
+  private final UuidFactoryFast uuidFactory = UuidFactoryFast.getInstance();
+  @Rule
+  public UserSessionRule userSession = standalone();
+  @Rule
+  public DbTester db = DbTester.create();
+  private final DbClient dbClient = db.getDbClient();
+  private final TextRangeResponseFormatter textRangeResponseFormatter = new TextRangeResponseFormatter();
+  private final HotspotWsResponseFormatter hotspotWsResponseFormatter = new HotspotWsResponseFormatter(textRangeResponseFormatter);
+  private final ComponentFinder componentFinder = TestComponentFinder.from(db);
+  private final WsActionTester ws = new WsActionTester(
+    new ListAction(dbClient, userSession, hotspotWsResponseFormatter, new NewCodePeriodResolver(dbClient, Clock.systemUTC()), componentFinder));
+
+  @Test
+  public void whenNoProjectProvided_shouldFailWithMessage() {
+    TestRequest request = ws.newRequest();
+    assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("The 'project' parameter is missing");
+  }
+
+  @Test
+  public void whenBranchAndPullRequestProvided_shouldFailWithMessage() {
+    TestRequest request = ws.newRequest()
+      .setParam("project", "some-project")
+      .setParam("branch", "some-branch")
+      .setParam("pullRequest", "some-pr");
+    assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Only one of parameters 'branch' and 'pullRequest' can be provided");
+  }
+
+  @Test
+  public void whenAnonymousUser_shouldFailIfInsufficientPrivileges() {
+    UserDto user = db.users().insertUser();
+
+    ProjectData projectData = db.components().insertPrivateProject();
+    ComponentDto project = projectData.getMainBranchComponent();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    UserDto simon = db.users().insertUser();
+    RuleDto rule = newHotspotRule();
+    db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_RESOLVED)
+      .setResolution(RESOLUTION_FIXED)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04"))
+      .setCodeVariants(List.of("variant1", "variant2")));
+
+    userSession
+      .logIn(user)
+      .registerProjects(projectData.getProjectDto());
+
+    TestRequest request = ws.newRequest()
+      .setParam("project", projectData.projectKey())
+      .setParam("branch", projectData.getMainBranchDto().getKey());
+    assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class))
+      .isInstanceOf(ForbiddenException.class)
+      .hasMessage("Insufficient privileges");
+  }
+
+  @Test
+  public void whenListHotspotsByProject_shouldReturnAllFields() {
+    UserDto user = db.users().insertUser();
+
+    ProjectData projectData = db.components().insertPublicProject();
+    ComponentDto project = projectData.getMainBranchComponent();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    UserDto simon = db.users().insertUser();
+    RuleDto rule = newHotspotRule();
+    IssueDto hotspot = db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_TO_REVIEW)
+      .setResolution(null)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04")));
+
+    ComponentDto anotherBranch = db.components().insertProjectBranch(project, b -> b.setKey("branch1"));
+
+    ComponentDto fileFromAnotherBranch = db.components().insertComponent(newFileDto(anotherBranch));
+    db.issues().insertHotspot(rule, anotherBranch, fileFromAnotherBranch, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_REVIEWED)
+      .setResolution(RESOLUTION_FIXED)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04")));
+
+    userSession
+      .logIn(user)
+      .registerProjects(projectData.getProjectDto());
+
+    Hotspots.ListWsResponse response = ws.newRequest()
+      .setParam("project", projectData.projectKey())
+      .executeProtobuf(Hotspots.ListWsResponse.class);
+
+    assertThat(response.getHotspotsList())
+      .extracting(
+        Hotspots.SearchWsResponse.Hotspot::getKey, Hotspots.SearchWsResponse.Hotspot::getRuleKey, Hotspots.SearchWsResponse.Hotspot::getSecurityCategory,
+        Hotspots.SearchWsResponse.Hotspot::getComponent, Hotspots.SearchWsResponse.Hotspot::getResolution, Hotspots.SearchWsResponse.Hotspot::getStatus,
+        Hotspots.SearchWsResponse.Hotspot::getMessage, Hotspots.SearchWsResponse.Hotspot::getMessageFormattingsList,
+        Hotspots.SearchWsResponse.Hotspot::getAssignee, Hotspots.SearchWsResponse.Hotspot::getAuthor, Hotspots.SearchWsResponse.Hotspot::getLine,
+        Hotspots.SearchWsResponse.Hotspot::getCreationDate, Hotspots.SearchWsResponse.Hotspot::getUpdateDate)
+      .containsExactlyInAnyOrder(
+        tuple(hotspot.getKey(), rule.getKey().toString(), "others", file.getKey(), "", STATUS_TO_REVIEW, "the message",
+          MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), simon.getUuid(), "John", 42,
+          formatDateTime(hotspot.getIssueCreationDate()), formatDateTime(hotspot.getIssueUpdateDate())));
+  }
+
+  @Test
+  public void whenListHotspotsByResolution_shouldReturnValidHotspots() {
+    UserDto user = db.users().insertUser();
+
+    ProjectData projectData = db.components().insertPublicProject();
+    ComponentDto project = projectData.getMainBranchComponent();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    UserDto simon = db.users().insertUser();
+    RuleDto rule = newHotspotRule();
+    IssueDto hotspot1 = db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_REVIEWED)
+      .setResolution(RESOLUTION_FIXED)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04")));
+
+    IssueDto hotspot2 = db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_REVIEWED)
+      .setResolution(RESOLUTION_FIXED)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04")));
+
+    IssueDto hotspot3 = db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_REVIEWED)
+      .setResolution(RESOLUTION_SAFE)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04")));
+
+    RuleDto vulnerabilityRule = newIssueRule(XOO_X2, RuleType.VULNERABILITY);
+    IssueDto vulnerabilityIssue = db.issues().insertIssue(vulnerabilityRule, project, file, i -> i
+      .setType(RuleType.VULNERABILITY)
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_RESOLVED)
+      .setResolution(RESOLUTION_WONT_FIX)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04"))
+      .setCodeVariants(List.of("variant1", "variant2")));
+
+    userSession
+      .logIn(user)
+      .registerProjects(projectData.getProjectDto());
+
+    Hotspots.ListWsResponse response = ws.newRequest()
+      .setParam("project", projectData.getProjectDto().getKey())
+      .setParam("branch", projectData.getMainBranchDto().getKey())
+      .setParam("resolution", RESOLUTION_FIXED)
+      .executeProtobuf(Hotspots.ListWsResponse.class);
+
+    assertThat(response.getHotspotsList())
+      .extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
+      .containsExactlyInAnyOrder(hotspot1.getKey(), hotspot2.getKey())
+      .doesNotContain(hotspot3.getKey(), vulnerabilityIssue.getKey());
+
+    response = ws.newRequest()
+      .setParam("project", projectData.getProjectDto().getKey())
+      .setParam("branch", projectData.getMainBranchDto().getKey())
+      .setParam("resolution", RESOLUTION_SAFE)
+      .executeProtobuf(Hotspots.ListWsResponse.class);
+
+    assertThat(response.getHotspotsList())
+      .extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
+      .containsExactlyInAnyOrder(hotspot3.getKey())
+      .doesNotContain(hotspot1.getKey(), hotspot2.getKey(), vulnerabilityIssue.getKey());
+
+    response = ws.newRequest()
+      .setParam("project", projectData.getProjectDto().getKey())
+      .setParam("branch", projectData.getMainBranchDto().getKey())
+      .setParam("resolution", RESOLUTION_ACKNOWLEDGED)
+      .executeProtobuf(Hotspots.ListWsResponse.class);
+
+    assertThat(response.getHotspotsList()).isEmpty();
+  }
+
+  @Test
+  public void whenListHotspotsByNewCodePeriodDate_shouldReturnHotspots() {
+    UserDto user = db.users().insertUser();
+
+    ProjectData projectData = db.components().insertPublicProject();
+    ComponentDto project = projectData.getMainBranchComponent();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    UserDto simon = db.users().insertUser();
+    RuleDto rule = newHotspotRule();
+
+    db.components().insertSnapshot(project, s -> s.setLast(true).setPeriodDate(parseDateTime("2014-09-05T00:00:00+0100").getTime()));
+
+    List<String> beforeNewCodePeriod = IntStream.range(0, 10).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_TO_REVIEW)
+      .setResolution(null)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04"))
+      .setCodeVariants(List.of("variant1", "variant2"))))
+      .map(IssueDto::getKey)
+      .toList();
+
+    List<String> afterNewCodePeriod = IntStream.range(0, 5).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_TO_REVIEW)
+      .setResolution(null)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2015-01-02"))
+      .setIssueUpdateDate(parseDate("2017-12-04"))
+      .setCodeVariants(List.of("variant1", "variant2"))))
+      .map(IssueDto::getKey)
+      .toList();
+
+    userSession
+      .logIn(user)
+      .registerProjects(projectData.getProjectDto());
+
+    Hotspots.ListWsResponse response = ws.newRequest()
+      .setParam("project", projectData.projectKey())
+      .setParam("branch", projectData.getMainBranchDto().getKey())
+      .setParam("inNewCodePeriod", "true")
+      .executeProtobuf(Hotspots.ListWsResponse.class);
+
+    assertThat(response.getHotspotsList())
+      .extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
+      .containsExactlyInAnyOrderElementsOf(afterNewCodePeriod)
+      .doesNotContainAnyElementsOf(beforeNewCodePeriod);
+  }
+
+  @Test
+  public void whenListHotspotsByNewCodePeriodReferenceBranch_shouldReturnHotspots() {
+    UserDto user = db.users().insertUser();
+
+    ProjectData projectData = db.components().insertPublicProject();
+    ComponentDto project = projectData.getMainBranchComponent();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    UserDto simon = db.users().insertUser();
+    RuleDto rule = newHotspotRule();
+
+    db.components().insertSnapshot(project, s -> s.setLast(true).setPeriodMode(REFERENCE_BRANCH.name()));
+    MetricDto metric = db.measures().insertMetric(metricDto -> metricDto.setKey(ANALYSIS_FROM_SONARQUBE_9_4_KEY));
+    db.measures().insertLiveMeasure(project, metric);
+
+    List<String> beforeNewCodePeriod = IntStream.range(0, 10).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_TO_REVIEW)
+      .setResolution(null)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04"))
+      .setCodeVariants(List.of("variant1", "variant2"))))
+      .map(IssueDto::getKey)
+      .toList();
+
+    List<String> afterNewCodePeriod = IntStream.range(0, 5).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_TO_REVIEW)
+      .setResolution(null)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2015-01-02"))
+      .setIssueUpdateDate(parseDate("2017-12-04"))
+      .setCodeVariants(List.of("variant1", "variant2"))))
+      .peek(issueDto -> db.issues().insertNewCodeReferenceIssue(issueDto))
+      .map(IssueDto::getKey)
+      .toList();
+
+    userSession
+      .logIn(user)
+      .registerProjects(projectData.getProjectDto());
+
+    Hotspots.ListWsResponse response = ws.newRequest()
+      .setParam("project", projectData.projectKey())
+      .setParam("branch", projectData.getMainBranchDto().getKey())
+      .setParam("inNewCodePeriod", "true")
+      .executeProtobuf(Hotspots.ListWsResponse.class);
+
+    assertThat(response.getHotspotsList())
+      .extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
+      .containsExactlyInAnyOrderElementsOf(afterNewCodePeriod)
+      .doesNotContainAnyElementsOf(beforeNewCodePeriod);
+  }
+
+  @Test
+  @UseDataProvider("pages")
+  public void whenUsingPagination_shouldReturnPaginatedResults(String page, int expectedNumberOfIssues) {
+    UserDto user = db.users().insertUser();
+
+    ProjectData projectData = db.components().insertPublicProject();
+    ComponentDto project = projectData.getMainBranchComponent();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    UserDto simon = db.users().insertUser();
+    RuleDto rule = newHotspotRule();
+    IntStream.range(0, 10).forEach(number -> db.issues().insertHotspot(rule, project, file, i -> i
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
+      .setStatus(STATUS_TO_REVIEW)
+      .setResolution(null)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setAssigneeUuid(simon.getUuid())
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(parseDate("2014-09-03"))
+      .setIssueUpdateDate(parseDate("2017-12-04"))
+      .setCodeVariants(List.of("variant1", "variant2"))));
+
+    userSession
+      .logIn(user)
+      .registerProjects(projectData.getProjectDto());
+
+    Hotspots.ListWsResponse response = ws.newRequest()
+      .setParam("project", projectData.projectKey())
+      .setParam("branch", projectData.getMainBranchDto().getKey())
+      .setParam("p", page)
+      .setParam("ps", "3")
+      .executeProtobuf(Hotspots.ListWsResponse.class);
+
+    assertThat(response.getHotspotsList()).hasSize(expectedNumberOfIssues);
+    assertThat(response.getPaging())
+      .extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal)
+      .containsExactly(Integer.parseInt(page), expectedNumberOfIssues, 0);
+  }
+
+  private RuleDto newHotspotRule() {
+    return newIssueRule(XOO_X1, RuleType.SECURITY_HOTSPOT);
+  }
+
+  private RuleDto newIssueRule(RuleKey ruleKey, RuleType ruleType) {
+    RuleDto rule = newRule(ruleKey, createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule desc"))
+      .setLanguage("xoo")
+      .setName("Rule name")
+      .setType(ruleType)
+      .setStatus(RuleStatus.READY);
+    db.rules().insert(rule);
+    return rule;
+  }
+
+  @DataProvider
+  public static Object[][] pages() {
+    return new Object[][] {
+      {"1", 3},
+      {"2", 3},
+      {"3", 3},
+      {"4", 1},
+    };
+  }
+}
index 66551da50c418a4d9f03b284accf3a32c7de43d9..85dc8b69cb7d3a2a805a663b18b2b0cb110b9bfd 100644 (file)
@@ -155,11 +155,11 @@ public class SearchActionIT {
   private final IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), mock(AsyncIssueIndexing.class));
   private final ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client());
   private final PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
-  private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter();
+  private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(new TextRangeResponseFormatter());
   private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
   private final ComponentFinder componentFinder = TestComponentFinder.from(dbTester);
   private final SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex,
-    issueIndexSyncProgressChecker, responseFormatter, new TextRangeResponseFormatter(), system2, componentFinder);
+    issueIndexSyncProgressChecker, responseFormatter, system2, componentFinder);
   private final WsActionTester actionTester = new WsActionTester(underTest);
 
   @Test
@@ -252,8 +252,8 @@ public class SearchActionIT {
   @DataProvider
   public static Object[][] badStatuses() {
     return Stream.concat(
-        Issue.STATUSES.stream(),
-        Stream.of(randomAlphabetic(3)))
+      Issue.STATUSES.stream(),
+      Stream.of(randomAlphabetic(3)))
       .filter(t -> !STATUS_REVIEWED.equals(t))
       .filter(t -> !STATUS_TO_REVIEW.equals(t))
       .map(t -> new Object[] {t})
@@ -288,9 +288,9 @@ public class SearchActionIT {
   @DataProvider
   public static Object[][] badResolutions() {
     return Stream.of(
-        Issue.RESOLUTIONS.stream(),
-        Issue.SECURITY_HOTSPOT_RESOLUTIONS.stream(),
-        Stream.of(randomAlphabetic(4)))
+      Issue.RESOLUTIONS.stream(),
+      Issue.SECURITY_HOTSPOT_RESOLUTIONS.stream(),
+      Stream.of(randomAlphabetic(4)))
       .flatMap(t -> t)
       .filter(t -> !RESOLUTION_TYPES.contains(t))
       .map(t -> new Object[] {t})
@@ -1260,14 +1260,14 @@ public class SearchActionIT {
     ComponentDto file3 = dbTester.components().insertComponent(newFileDto(project).setPath("a/a/d"));
     RuleDto rule = newRule(SECURITY_HOTSPOT);
     List<IssueDto> hotspots = Stream.of(
-        newHotspot(rule, project, file3).setLine(8),
-        newHotspot(rule, project, file3).setLine(10),
-        newHotspot(rule, project, file1).setLine(null),
-        newHotspot(rule, project, file1).setLine(9),
-        newHotspot(rule, project, file1).setLine(11).setKee("a"),
-        newHotspot(rule, project, file1).setLine(11).setKee("b"),
-        newHotspot(rule, project, file2).setLine(null),
-        newHotspot(rule, project, file2).setLine(2))
+      newHotspot(rule, project, file3).setLine(8),
+      newHotspot(rule, project, file3).setLine(10),
+      newHotspot(rule, project, file1).setLine(null),
+      newHotspot(rule, project, file1).setLine(9),
+      newHotspot(rule, project, file1).setLine(11).setKee("a"),
+      newHotspot(rule, project, file1).setLine(11).setKee("b"),
+      newHotspot(rule, project, file2).setLine(null),
+      newHotspot(rule, project, file2).setLine(2))
       .collect(toList());
     String[] expectedHotspotKeys = hotspots.stream().map(IssueDto::getKey).toArray(String[]::new);
     // insert hotspots in random order
@@ -1294,11 +1294,11 @@ public class SearchActionIT {
     ComponentDto anotherFile = dbTester.components().insertComponent(newFileDto(project));
 
     List<DbIssues.Location> hotspotLocations = Stream.of(
-        newHotspotLocation(file.uuid(), "security hotspot flow message 0", 1, 1, 0, 12),
-        newHotspotLocation(file.uuid(), "security hotspot flow message 1", 3, 3, 0, 10),
-        newHotspotLocation(anotherFile.uuid(), "security hotspot flow message 2", 5, 5, 0, 15),
-        newHotspotLocation(anotherFile.uuid(), "security hotspot flow message 3", 7, 7, 0, 18),
-        newHotspotLocation(null, "security hotspot flow message 4", 12, 12, 2, 8))
+      newHotspotLocation(file.uuid(), "security hotspot flow message 0", 1, 1, 0, 12),
+      newHotspotLocation(file.uuid(), "security hotspot flow message 1", 3, 3, 0, 10),
+      newHotspotLocation(anotherFile.uuid(), "security hotspot flow message 2", 5, 5, 0, 15),
+      newHotspotLocation(anotherFile.uuid(), "security hotspot flow message 3", 7, 7, 0, 18),
+      newHotspotLocation(null, "security hotspot flow message 4", 12, 12, 2, 8))
       .collect(toList());
 
     DbIssues.Locations.Builder locations = DbIssues.Locations.newBuilder().addFlow(DbIssues.Flow.newBuilder().addAllLocation(hotspotLocations));
@@ -1781,9 +1781,9 @@ public class SearchActionIT {
     assertThat(responseAll.getHotspotsList())
       .extracting(SearchWsResponse.Hotspot::getKey)
       .containsExactlyInAnyOrder(Stream.of(
-          hotspotsInLeakPeriod.stream(),
-          atLeakPeriod.stream(),
-          hotspotsBefore.stream())
+        hotspotsInLeakPeriod.stream(),
+        atLeakPeriod.stream(),
+        hotspotsBefore.stream())
         .flatMap(t -> t)
         .map(IssueDto::getKey)
         .toArray(String[]::new));
@@ -1794,8 +1794,8 @@ public class SearchActionIT {
     assertThat(responseOnLeak.getHotspotsList())
       .extracting(SearchWsResponse.Hotspot::getKey)
       .containsExactlyInAnyOrder(Stream.concat(
-          hotspotsInLeakPeriod.stream(),
-          atLeakPeriod.stream())
+        hotspotsInLeakPeriod.stream(),
+        atLeakPeriod.stream())
         .map(IssueDto::getKey)
         .toArray(String[]::new));
   }
@@ -1826,8 +1826,8 @@ public class SearchActionIT {
     assertThat(responseAll.getHotspotsList())
       .extracting(SearchWsResponse.Hotspot::getKey)
       .containsExactlyInAnyOrder(Stream.of(
-          hotspotsInLeakPeriod.stream(),
-          hotspotsNotInLeakPeriod.stream())
+        hotspotsInLeakPeriod.stream(),
+        hotspotsNotInLeakPeriod.stream())
         .flatMap(t -> t)
         .map(IssueDto::getKey)
         .toArray(String[]::new));
index 7dda20b840fbfbc2bfd26fd5d99cb721cf49faef..bf7e5ee10c953923dc32094e236ce9afaac73786 100644 (file)
@@ -123,12 +123,12 @@ public class ShowActionIT {
 
   private final DbClient dbClient = dbTester.getDbClient();
   private final AvatarResolver avatarResolver = new AvatarResolverImpl();
-  private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter();
+  private final TextRangeResponseFormatter textRangeFormatter = new TextRangeResponseFormatter();
+  private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(textRangeFormatter);
   private final IssueChangeWSSupport issueChangeSupport = Mockito.mock(IssueChangeWSSupport.class);
   private final HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, System2.INSTANCE);
   private final UserResponseFormatter userFormatter = new UserResponseFormatter(new AvatarResolverImpl());
-  private final TextRangeResponseFormatter textRangeFormatter = new TextRangeResponseFormatter();
-  private final ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, textRangeFormatter, userFormatter, issueChangeSupport);
+  private final ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, userFormatter, issueChangeSupport);
   private final WsActionTester actionTester = new WsActionTester(underTest);
   private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();
 
index 12a603d00d8c40f8cc25b28873aa1c898168bd92..31e866372516113ee8c853e22290be8f6fbef201 100644 (file)
@@ -50,6 +50,7 @@ import org.sonar.server.component.TestComponentFinder;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.issue.AvatarResolverImpl;
 import org.sonar.server.issue.IssueFieldsSetter;
+import org.sonar.server.issue.NewCodePeriodResolver;
 import org.sonar.server.issue.TextRangeResponseFormatter;
 import org.sonar.server.issue.TransitionService;
 import org.sonar.server.issue.workflow.FunctionExecutor;
@@ -58,6 +59,7 @@ import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.MessageFormattingUtils;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Common.Severity;
 import org.sonarqube.ws.Issues;
 import org.sonarqube.ws.Issues.Issue;
@@ -104,7 +106,7 @@ public class ListActionIT {
   private final SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new TextRangeResponseFormatter(), userFormatter);
   private final ComponentFinder componentFinder = TestComponentFinder.from(db);
   private final WsActionTester ws = new WsActionTester(
-    new ListAction(userSession, dbClient, Clock.systemUTC(), searchResponseLoader, searchResponseFormat, componentFinder));
+    new ListAction(userSession, dbClient, new NewCodePeriodResolver(dbClient, Clock.systemUTC()), searchResponseLoader, searchResponseFormat, componentFinder));
 
   @Before
   public void setUp() {
@@ -220,6 +222,13 @@ public class ListActionIT {
           MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), "10min",
           simon.getLogin(), "John", 42, "a227e508d6646b55a086ee11d63b21e9", asList("bug", "owasp"), formatDateTime(issue.getIssueCreationDate()),
           formatDateTime(issue.getIssueUpdateDate()), false, List.of("variant1", "variant2")));
+
+    assertThat(response.getComponentsList())
+      .extracting(
+        Issues.Component::getKey, Issues.Component::getName, Issues.Component::getQualifier, Issues.Component::getLongName, Issues.Component::getPath)
+      .containsExactlyInAnyOrder(
+        tuple(project.getKey(), project.name(), project.qualifier(), project.longName(), ""),
+        tuple(file.getKey(), file.name(), file.qualifier(), file.longName(), file.path()));
   }
 
   @Test
@@ -723,6 +732,9 @@ public class ListActionIT {
       .executeProtobuf(Issues.ListWsResponse.class);
 
     assertThat(response.getIssuesList()).hasSize(expectedNumberOfIssues);
+    assertThat(response.getPaging())
+      .extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal)
+      .containsExactly(Integer.parseInt(page), expectedNumberOfIssues, 0);
   }
 
   private RuleDto newIssueRule() {
index f1b2b4957a3e4beb480d503d7dee5768dc457c8a..fecd5efd5fdc289913f4bd96c98638cfa1928452 100644 (file)
  */
 package org.sonar.server.hotspot.ws;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
 import javax.annotation.Nullable;
+import org.sonar.api.utils.Paging;
 import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
 import org.sonar.db.project.ProjectDto;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.server.issue.TextRangeResponseFormatter;
+import org.sonar.server.security.SecurityStandards;
+import org.sonar.server.ws.MessageFormattingUtils;
+import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Hotspots;
 
+import static java.util.Collections.emptyList;
 import static java.util.Optional.ofNullable;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
+import static org.sonarqube.ws.WsUtils.nullToEmpty;
 
 public class HotspotWsResponseFormatter {
 
-  public HotspotWsResponseFormatter() {
-    // nothing to do here
+  private final TextRangeResponseFormatter textRangeFormatter;
+
+  public HotspotWsResponseFormatter(TextRangeResponseFormatter textRangeFormatter) {
+    this.textRangeFormatter = textRangeFormatter;
   }
 
   Hotspots.Component formatProject(Hotspots.Component.Builder builder, ProjectDto project, @Nullable String branch, @Nullable String pullRequest) {
@@ -58,6 +77,58 @@ public class HotspotWsResponseFormatter {
     return builder.build();
   }
 
+  void formatHotspots(SearchResponseData searchResponseData, Hotspots.ListWsResponse.Builder responseBuilder) {
+    responseBuilder.addAllHotspots(mapHotspots(searchResponseData));
+  }
+
+  void formatHotspots(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
+    responseBuilder.addAllHotspots(mapHotspots(searchResponseData));
+  }
+
+  private List<Hotspots.SearchWsResponse.Hotspot> mapHotspots(SearchResponseData searchResponseData) {
+    List<IssueDto> hotspots = searchResponseData.getHotspots();
+    if (hotspots.isEmpty()) {
+      return emptyList();
+    }
+
+    Hotspots.SearchWsResponse.Hotspot.Builder builder = Hotspots.SearchWsResponse.Hotspot.newBuilder();
+    List<Hotspots.SearchWsResponse.Hotspot> hotspotsList = new ArrayList<>(hotspots.size());
+    for (IssueDto hotspot : hotspots) {
+      SecurityStandards.SQCategory sqCategory = fromSecurityStandards(hotspot.getSecurityStandards()).getSqCategory();
+      builder
+        .clear()
+        .setKey(hotspot.getKey())
+        .setComponent(hotspot.getComponentKey())
+        .setProject(hotspot.getProjectKey())
+        .setSecurityCategory(sqCategory.getKey())
+        .setVulnerabilityProbability(sqCategory.getVulnerability().name())
+        .setRuleKey(hotspot.getRuleKey().toString());
+      ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus);
+      ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution);
+      ofNullable(hotspot.getLine()).ifPresent(builder::setLine);
+      builder.setMessage(nullToEmpty(hotspot.getMessage()));
+      builder.addAllMessageFormattings(MessageFormattingUtils.dbMessageFormattingToWs(hotspot.parseMessageFormattings()));
+      ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee);
+      builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin()));
+      builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate()));
+      builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));
+      completeHotspotLocations(hotspot, builder, searchResponseData);
+      hotspotsList.add(builder.build());
+    }
+    return hotspotsList;
+  }
+
+  void completeHotspotLocations(IssueDto hotspot, Hotspots.SearchWsResponse.Hotspot.Builder hotspotBuilder, SearchResponseData data) {
+    DbIssues.Locations locations = hotspot.parseLocations();
+
+    if (locations == null) {
+      return;
+    }
+
+    textRangeFormatter.formatTextRange(locations, hotspotBuilder::setTextRange);
+    hotspotBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, hotspotBuilder.getComponent(), data.getComponentsByUuid()));
+  }
+
   Hotspots.Component formatComponent(Hotspots.Component.Builder builder, ComponentDto component, @Nullable BranchDto branchDto) {
     if (branchDto == null || branchDto.isMain()) {
       return formatComponent(builder, component, null, null);
@@ -65,4 +136,60 @@ public class HotspotWsResponseFormatter {
     return formatComponent(builder, component, branchDto.getBranchKey(), branchDto.getPullRequestKey());
   }
 
+  void formatTextRange(IssueDto dto, Consumer<Common.TextRange> rangeConsumer) {
+    textRangeFormatter.formatTextRange(dto, rangeConsumer);
+  }
+
+  List<Common.Flow> formatFlows(DbIssues.Locations locations, String issueComponent, Map<String, ComponentDto> componentsByUuid) {
+    return textRangeFormatter.formatFlows(locations, issueComponent, componentsByUuid);
+  }
+
+  static final class SearchResponseData {
+    private final Paging paging;
+    private final List<IssueDto> hotspots;
+    private final Map<String, ComponentDto> componentsByUuid = new HashMap<>();
+    private final Map<String, BranchDto> branchesByBranchUuid = new HashMap<>();
+
+    SearchResponseData(Paging paging, List<IssueDto> hotspots) {
+      this.paging = paging;
+      this.hotspots = hotspots;
+    }
+
+    boolean isPresent() {
+      return !hotspots.isEmpty();
+    }
+
+    public Paging getPaging() {
+      return paging;
+    }
+
+    List<IssueDto> getHotspots() {
+      return hotspots;
+    }
+
+    void addComponents(Collection<ComponentDto> components) {
+      for (ComponentDto component : components) {
+        componentsByUuid.put(component.uuid(), component);
+      }
+    }
+
+    public void addBranches(List<BranchDto> branchDtos) {
+      for (BranchDto branch : branchDtos) {
+        branchesByBranchUuid.put(branch.getUuid(), branch);
+      }
+    }
+
+    public BranchDto getBranch(String branchUuid) {
+      return branchesByBranchUuid.get(branchUuid);
+    }
+
+    Collection<ComponentDto> getComponents() {
+      return componentsByUuid.values();
+    }
+
+    public Map<String, ComponentDto> getComponentsByUuid() {
+      return componentsByUuid;
+    }
+  }
+
 }
index 888410bb4cf9fb9f45d22e55cd621c8d404c81c0..3edc0e5aaf8e87fba31e3fc3d8108fbef433b120 100644 (file)
@@ -29,6 +29,7 @@ public class HotspotsWsModule extends Module {
       HotspotWsSupport.class,
       AssignAction.class,
       SearchAction.class,
+      ListAction.class,
       ShowAction.class,
       ChangeStatusAction.class,
       AddCommentAction.class,
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ListAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ListAction.java
new file mode 100644 (file)
index 0000000..1f44254
--- /dev/null
@@ -0,0 +1,303 @@
+/*
+ * 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.hotspot.ws;
+
+import com.google.common.base.Preconditions;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.sonar.api.rules.RuleType;
+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.utils.Paging;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.Pagination;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueListQuery;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.component.ComponentFinder.ProjectAndBranch;
+import org.sonar.server.hotspot.ws.HotspotWsResponseFormatter.SearchResponseData;
+import org.sonar.server.issue.NewCodePeriodResolver;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Hotspots;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.lang.String.format;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
+import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
+import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
+import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
+import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
+import static org.sonar.api.server.ws.WebService.Param.PAGE;
+import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
+import static org.sonar.api.utils.Paging.forPageIndex;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE;
+import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+
+public class ListAction implements HotspotsWsAction {
+  private static final String PARAM_PROJECT = "project";
+  private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_PULL_REQUEST = "pullRequest";
+  private static final String PARAM_STATUS = "status";
+  private static final String PARAM_RESOLUTION = "resolution";
+  private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod";
+  private static final List<String> STATUSES = List.of(STATUS_TO_REVIEW, STATUS_REVIEWED);
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final HotspotWsResponseFormatter responseFormatter;
+  private final NewCodePeriodResolver newCodePeriodResolver;
+  private final ComponentFinder componentFinder;
+
+  public ListAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter,
+    NewCodePeriodResolver newCodePeriodResolver, ComponentFinder componentFinder) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+    this.responseFormatter = responseFormatter;
+    this.newCodePeriodResolver = newCodePeriodResolver;
+    this.componentFinder = componentFinder;
+  }
+
+  @Override
+  public void define(WebService.NewController controller) {
+    WebService.NewAction action = controller
+      .createAction("list")
+      .setHandler(this)
+      .setInternal(true)
+      .setDescription("List Security Hotpots. This endpoint is used in degraded mode, when issue indexation is running." +
+        "<br>Total number of Security Hotspots will be always equal to a page size, as counting all issues is not supported. " +
+        "<br>Requires the 'Browse' permission on the specified project. ")
+      .setSince("10.2");
+
+    action.addPagingParams(100, MAX_PAGE_SIZE);
+    action.createParam(PARAM_PROJECT)
+      .setDescription("Key of the project")
+      .setRequired(true)
+      .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+    action.createParam(PARAM_BRANCH)
+      .setDescription("Branch key. Not available in the community edition.")
+      .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id. Not available in the community edition.")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
+    action.createParam(PARAM_STATUS)
+      .setDescription("If '%s' is provided, only Security Hotspots with the specified status are returned.", PARAM_PROJECT)
+      .setPossibleValues(STATUSES)
+      .setRequired(false);
+    action.createParam(PARAM_RESOLUTION)
+      .setDescription(format(
+        "If '%s' is provided and if status is '%s', only Security Hotspots with the specified resolution are returned.",
+        PARAM_PROJECT, STATUS_REVIEWED))
+      .setPossibleValues(RESOLUTION_FIXED, RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED)
+      .setRequired(false);
+    action.createParam(PARAM_IN_NEW_CODE_PERIOD)
+      .setDescription("If '%s' is provided, only Security Hotspots created in the new code period are returned.", PARAM_IN_NEW_CODE_PERIOD)
+      .setBooleanPossibleValues()
+      .setDefaultValue("false");
+
+    action.setResponseExample(getClass().getResource("search-example.json"));
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    WsRequest wsRequest = toWsRequest(request);
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      ProjectAndBranch projectAndBranch = validate(dbSession, wsRequest);
+      SearchResponseData searchResponseData = searchHotspots(dbSession, wsRequest, projectAndBranch);
+      loadComponents(dbSession, searchResponseData);
+      writeProtobuf(formatResponse(searchResponseData), request, response);
+    }
+  }
+
+  private static WsRequest toWsRequest(Request request) {
+    return new WsRequest(
+      request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE))
+      .project(request.param(PARAM_PROJECT))
+      .branch(request.param(PARAM_BRANCH))
+      .pullRequest(request.param(PARAM_PULL_REQUEST))
+      .status(request.param(PARAM_STATUS))
+      .resolution(request.param(PARAM_RESOLUTION))
+      .inNewCodePeriod(request.paramAsBoolean(PARAM_IN_NEW_CODE_PERIOD));
+  }
+
+  private ProjectAndBranch validate(DbSession dbSession, WsRequest wsRequest) {
+    Preconditions.checkArgument(isNullOrEmpty(wsRequest.branch) || isNullOrEmpty(wsRequest.pullRequest),
+      "Only one of parameters '%s' and '%s' can be provided", PARAM_BRANCH, PARAM_PULL_REQUEST);
+
+    ProjectAndBranch projectAndBranch = componentFinder.getProjectAndBranch(dbSession, wsRequest.project,
+      wsRequest.branch, wsRequest.pullRequest);
+
+    userSession.checkEntityPermission(USER, projectAndBranch.getProject());
+    return projectAndBranch;
+  }
+
+  private SearchResponseData searchHotspots(DbSession dbSession, WsRequest wsRequest, ProjectAndBranch projectAndBranch) {
+    List<IssueDto> hotspots = getHotspotKeys(dbSession, wsRequest, projectAndBranch);
+
+    Paging paging = forPageIndex(wsRequest.page).withPageSize(wsRequest.pageSize).andTotal(hotspots.size());
+    return new SearchResponseData(paging, hotspots);
+  }
+
+  private List<IssueDto> getHotspotKeys(DbSession dbSession, WsRequest wsRequest, ProjectAndBranch projectAndBranch) {
+    BranchDto branch = projectAndBranch.getBranch();
+    IssueListQuery.IssueListQueryBuilder queryBuilder = IssueListQuery.IssueListQueryBuilder.newIssueListQueryBuilder()
+      .project(wsRequest.project)
+      .branch(branch.getBranchKey())
+      .pullRequest(branch.getPullRequestKey())
+      .statuses(wsRequest.status != null ? singletonList(wsRequest.status) : emptyList())
+      .resolutions(wsRequest.resolution != null ? singletonList(wsRequest.resolution) : emptyList())
+      .types(singletonList(RuleType.SECURITY_HOTSPOT.getDbConstant()));
+
+    String branchKey = branch.getBranchKey();
+    if (wsRequest.inNewCodePeriod && wsRequest.pullRequest == null && branchKey != null) {
+      NewCodePeriodResolver.ResolvedNewCodePeriod newCodePeriod = newCodePeriodResolver.resolveForProjectAndBranch(dbSession, wsRequest.project, branchKey);
+      if (NewCodePeriodType.REFERENCE_BRANCH == newCodePeriod.type()) {
+        queryBuilder.newCodeOnReference(true);
+      } else {
+        queryBuilder.createdAfter(newCodePeriod.periodDate());
+      }
+    }
+
+    Pagination pagination = Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize);
+    return dbClient.issueDao().selectByQuery(dbSession, queryBuilder.build(), pagination);
+  }
+
+  private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) {
+    Set<String> componentUuids = searchResponseData.getHotspots().stream()
+      .flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid()))
+      .collect(Collectors.toSet());
+
+    Set<String> locationComponentUuids = searchResponseData.getHotspots()
+      .stream()
+      .flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream())
+      .collect(Collectors.toSet());
+
+    Set<String> aggregatedComponentUuids = Stream.of(componentUuids, locationComponentUuids)
+      .flatMap(Collection::stream)
+      .collect(Collectors.toSet());
+
+    if (!aggregatedComponentUuids.isEmpty()) {
+      List<ComponentDto> componentDtos = dbClient.componentDao().selectByUuids(dbSession, aggregatedComponentUuids);
+      searchResponseData.addComponents(componentDtos);
+    }
+  }
+
+  private static Set<String> getHotspotLocationComponentUuids(IssueDto hotspot) {
+    Set<String> locationComponentUuids = new HashSet<>();
+    DbIssues.Locations locations = hotspot.parseLocations();
+
+    if (locations == null) {
+      return locationComponentUuids;
+    }
+
+    List<DbIssues.Flow> flows = locations.getFlowList();
+
+    for (DbIssues.Flow flow : flows) {
+      List<DbIssues.Location> flowLocations = flow.getLocationList();
+      for (DbIssues.Location location : flowLocations) {
+        if (location.hasComponentId()) {
+          locationComponentUuids.add(location.getComponentId());
+        }
+      }
+    }
+
+    return locationComponentUuids;
+  }
+
+  private Hotspots.ListWsResponse formatResponse(SearchResponseData searchResponseData) {
+    Hotspots.ListWsResponse.Builder responseBuilder = Hotspots.ListWsResponse.newBuilder();
+    formatPaging(searchResponseData, responseBuilder);
+    if (searchResponseData.isPresent()) {
+      responseFormatter.formatHotspots(searchResponseData, responseBuilder);
+    }
+    return responseBuilder.build();
+  }
+
+  private static void formatPaging(SearchResponseData searchResponseData, Hotspots.ListWsResponse.Builder responseBuilder) {
+    Paging paging = searchResponseData.getPaging();
+    Common.Paging.Builder pagingBuilder = Common.Paging.newBuilder()
+      .setPageIndex(paging.pageIndex())
+      .setPageSize(searchResponseData.getHotspots().size());
+
+    responseBuilder.setPaging(pagingBuilder.build());
+  }
+
+  private static final class WsRequest {
+    private final int page;
+    private final int pageSize;
+    private String project;
+    private String branch;
+    private String pullRequest;
+    private String status;
+    private String resolution;
+    private boolean inNewCodePeriod;
+
+    private WsRequest(int page, int pageSize) {
+      this.page = page;
+      this.pageSize = pageSize;
+    }
+
+    public WsRequest project(@Nullable String project) {
+      this.project = project;
+      return this;
+    }
+
+    public WsRequest branch(@Nullable String branch) {
+      this.branch = branch;
+      return this;
+    }
+
+    public WsRequest pullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
+    public WsRequest status(@Nullable String status) {
+      this.status = status;
+      return this;
+    }
+
+    public WsRequest resolution(@Nullable String resolution) {
+      this.resolution = resolution;
+      return this;
+    }
+
+    public WsRequest inNewCodePeriod(@Nullable Boolean inNewCodePeriod) {
+      this.inNewCodePeriod = inNewCodePeriod != null && inNewCodePeriod;
+      return this;
+    }
+  }
+}
index c5c4e893d096a829651c4816d104871b29091cbe..4176e5c4b0734b53e626712ccbaec5c6bef3c69a 100644 (file)
@@ -23,7 +23,6 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -39,7 +38,6 @@ import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.search.SearchHit;
 import org.jetbrains.annotations.NotNull;
 import org.sonar.api.resources.Qualifiers;
-import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
@@ -55,18 +53,16 @@ import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.protobuf.DbIssues;
-import org.sonar.db.rule.RuleDto;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.component.ComponentFinder.ProjectAndBranch;
 import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.issue.TextRangeResponseFormatter;
+import org.sonar.server.hotspot.ws.HotspotWsResponseFormatter.SearchResponseData;
 import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
 import org.sonar.server.issue.index.IssueQuery;
 import org.sonar.server.security.SecurityStandards;
 import org.sonar.server.user.UserSession;
-import org.sonar.server.ws.MessageFormattingUtils;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Hotspots;
 import org.sonarqube.ws.Hotspots.SearchWsResponse;
@@ -84,7 +80,6 @@ import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
 import static org.sonar.api.server.ws.WebService.Param.PAGE;
 import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
-import static org.sonar.api.utils.DateUtils.formatDateTime;
 import static org.sonar.api.utils.DateUtils.longToDate;
 import static org.sonar.api.utils.Paging.forPageIndex;
 import static org.sonar.api.web.UserRole.USER;
@@ -92,12 +87,10 @@ import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
 import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_INSECURE_INTERACTION;
 import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES;
 import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_RISKY_RESOURCE;
-import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
-import static org.sonarqube.ws.WsUtils.nullToEmpty;
 
 public class SearchAction implements HotspotsWsAction {
   private static final Set<String> SUPPORTED_QUALIFIERS = Set.of(Qualifiers.PROJECT, Qualifiers.APP);
@@ -131,18 +124,16 @@ public class SearchAction implements HotspotsWsAction {
   private final IssueIndex issueIndex;
   private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker;
   private final HotspotWsResponseFormatter responseFormatter;
-  private final TextRangeResponseFormatter textRangeFormatter;
   private final System2 system2;
   private final ComponentFinder componentFinder;
 
   public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex, IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
-    HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter, System2 system2, ComponentFinder componentFinder) {
+    HotspotWsResponseFormatter responseFormatter, System2 system2, ComponentFinder componentFinder) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.issueIndex = issueIndex;
     this.issueIndexSyncProgressChecker = issueIndexSyncProgressChecker;
     this.responseFormatter = responseFormatter;
-    this.textRangeFormatter = textRangeFormatter;
     this.system2 = system2;
     this.componentFinder = componentFinder;
   }
@@ -179,7 +170,6 @@ public class SearchAction implements HotspotsWsAction {
       Optional<ProjectAndBranch> project = getAndValidateProjectOrApplication(dbSession, wsRequest);
       SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project.orElse(null));
       loadComponents(dbSession, searchResponseData);
-      loadRules(dbSession, searchResponseData);
       writeProtobuf(formatResponse(searchResponseData), request, response);
     }
   }
@@ -515,11 +505,11 @@ public class SearchAction implements HotspotsWsAction {
   }
 
   private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) {
-    Set<String> componentUuids = searchResponseData.getOrderedHotspots().stream()
+    Set<String> componentUuids = searchResponseData.getHotspots().stream()
       .flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid()))
       .collect(Collectors.toSet());
 
-    Set<String> locationComponentUuids = searchResponseData.getOrderedHotspots()
+    Set<String> locationComponentUuids = searchResponseData.getHotspots()
       .stream()
       .flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream())
       .collect(Collectors.toSet());
@@ -560,21 +550,11 @@ public class SearchAction implements HotspotsWsAction {
     return locationComponentUuids;
   }
 
-  private void loadRules(DbSession dbSession, SearchResponseData searchResponseData) {
-    Set<RuleKey> ruleKeys = searchResponseData.getOrderedHotspots()
-      .stream()
-      .map(IssueDto::getRuleKey)
-      .collect(Collectors.toSet());
-    if (!ruleKeys.isEmpty()) {
-      searchResponseData.addRules(dbClient.ruleDao().selectByKeys(dbSession, ruleKeys));
-    }
-  }
-
   private SearchWsResponse formatResponse(SearchResponseData searchResponseData) {
     SearchWsResponse.Builder responseBuilder = SearchWsResponse.newBuilder();
     formatPaging(searchResponseData, responseBuilder);
-    if (!searchResponseData.isEmpty()) {
-      formatHotspots(searchResponseData, responseBuilder);
+    if (searchResponseData.isPresent()) {
+      responseFormatter.formatHotspots(searchResponseData, responseBuilder);
       formatComponents(searchResponseData, responseBuilder);
     }
     return responseBuilder.build();
@@ -590,52 +570,6 @@ public class SearchAction implements HotspotsWsAction {
     responseBuilder.setPaging(pagingBuilder.build());
   }
 
-  private void formatHotspots(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
-    List<IssueDto> orderedHotspots = searchResponseData.getOrderedHotspots();
-    if (orderedHotspots.isEmpty()) {
-      return;
-    }
-
-    SearchWsResponse.Hotspot.Builder builder = SearchWsResponse.Hotspot.newBuilder();
-    for (IssueDto hotspot : orderedHotspots) {
-      RuleDto rule = searchResponseData.getRule(hotspot.getRuleKey())
-        // due to join with table Rule when retrieving data from Issues, this can't happen
-        .orElseThrow(() -> new IllegalStateException(format(
-          "Rule with key '%s' not found for Hotspot '%s'", hotspot.getRuleKey(), hotspot.getKey())));
-      SecurityStandards.SQCategory sqCategory = fromSecurityStandards(rule.getSecurityStandards()).getSqCategory();
-      builder
-        .clear()
-        .setKey(hotspot.getKey())
-        .setComponent(hotspot.getComponentKey())
-        .setProject(hotspot.getProjectKey())
-        .setSecurityCategory(sqCategory.getKey())
-        .setVulnerabilityProbability(sqCategory.getVulnerability().name())
-        .setRuleKey(hotspot.getRuleKey().toString());
-      ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus);
-      ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution);
-      ofNullable(hotspot.getLine()).ifPresent(builder::setLine);
-      builder.setMessage(nullToEmpty(hotspot.getMessage()));
-      builder.addAllMessageFormattings(MessageFormattingUtils.dbMessageFormattingToWs(hotspot.parseMessageFormattings()));
-      ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee);
-      builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin()));
-      builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate()));
-      builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));
-      completeHotspotLocations(hotspot, builder, searchResponseData);
-      responseBuilder.addHotspots(builder.build());
-    }
-  }
-
-  private void completeHotspotLocations(IssueDto hotspot, SearchWsResponse.Hotspot.Builder hotspotBuilder, SearchResponseData data) {
-    DbIssues.Locations locations = hotspot.parseLocations();
-
-    if (locations == null) {
-      return;
-    }
-
-    textRangeFormatter.formatTextRange(locations, hotspotBuilder::setTextRange);
-    hotspotBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, hotspotBuilder.getComponent(), data.getComponentsByUuid()));
-  }
-
   private void formatComponents(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
     Collection<ComponentDto> components = searchResponseData.getComponents();
     if (components.isEmpty()) {
@@ -783,62 +717,4 @@ public class SearchAction implements HotspotsWsAction {
       return files;
     }
   }
-
-  private static final class SearchResponseData {
-    private final Paging paging;
-    private final List<IssueDto> orderedHotspots;
-    private final Map<String, ComponentDto> componentsByUuid = new HashMap<>();
-    private final Map<RuleKey, RuleDto> rulesByRuleKey = new HashMap<>();
-    private final Map<String, BranchDto> branchesByBranchUuid = new HashMap<>();
-
-    private SearchResponseData(Paging paging, List<IssueDto> orderedHotspots) {
-      this.paging = paging;
-      this.orderedHotspots = orderedHotspots;
-    }
-
-    boolean isEmpty() {
-      return orderedHotspots.isEmpty();
-    }
-
-    public Paging getPaging() {
-      return paging;
-    }
-
-    List<IssueDto> getOrderedHotspots() {
-      return orderedHotspots;
-    }
-
-    void addComponents(Collection<ComponentDto> components) {
-      for (ComponentDto component : components) {
-        componentsByUuid.put(component.uuid(), component);
-      }
-    }
-
-    public void addBranches(List<BranchDto> branchDtos) {
-      for (BranchDto branch : branchDtos) {
-        branchesByBranchUuid.put(branch.getUuid(), branch);
-      }
-    }
-
-    public BranchDto getBranch(String branchUuid) {
-      return branchesByBranchUuid.get(branchUuid);
-    }
-
-    Collection<ComponentDto> getComponents() {
-      return componentsByUuid.values();
-    }
-
-    public Map<String, ComponentDto> getComponentsByUuid() {
-      return componentsByUuid;
-    }
-
-    void addRules(Collection<RuleDto> rules) {
-      rules.forEach(t -> rulesByRuleKey.put(t.getKey(), t));
-    }
-
-    Optional<RuleDto> getRule(RuleKey ruleKey) {
-      return ofNullable(rulesByRuleKey.get(ruleKey));
-    }
-
-  }
 }
index 02150ba6033032facb84ecd023cc8be7465cee2e..9cd233006ca3232e30349337f8891f91f6dd7df9 100644 (file)
@@ -55,7 +55,6 @@ import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.issue.IssueChangeWSSupport;
 import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext;
 import org.sonar.server.issue.IssueChangeWSSupport.Load;
-import org.sonar.server.issue.TextRangeResponseFormatter;
 import org.sonar.server.issue.ws.UserResponseFormatter;
 import org.sonar.server.security.SecurityStandards;
 import org.sonar.server.ws.MessageFormattingUtils;
@@ -87,16 +86,14 @@ public class ShowAction implements HotspotsWsAction {
   private final DbClient dbClient;
   private final HotspotWsSupport hotspotWsSupport;
   private final HotspotWsResponseFormatter responseFormatter;
-  private final TextRangeResponseFormatter textRangeFormatter;
   private final UserResponseFormatter userFormatter;
   private final IssueChangeWSSupport issueChangeSupport;
 
-  public ShowAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter,
+  public ShowAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, HotspotWsResponseFormatter responseFormatter,
     UserResponseFormatter userFormatter, IssueChangeWSSupport issueChangeSupport) {
     this.dbClient = dbClient;
     this.hotspotWsSupport = hotspotWsSupport;
     this.responseFormatter = responseFormatter;
-    this.textRangeFormatter = textRangeFormatter;
     this.userFormatter = userFormatter;
     this.issueChangeSupport = issueChangeSupport;
   }
@@ -219,7 +216,7 @@ public class ShowAction implements HotspotsWsAction {
   }
 
   private void formatTextRange(ShowWsResponse.Builder hotspotBuilder, IssueDto hotspot) {
-    textRangeFormatter.formatTextRange(hotspot, hotspotBuilder::setTextRange);
+    responseFormatter.formatTextRange(hotspot, hotspotBuilder::setTextRange);
   }
 
   private void formatFlows(DbSession dbSession, ShowWsResponse.Builder hotspotBuilder, IssueDto hotspot) {
@@ -232,7 +229,7 @@ public class ShowAction implements HotspotsWsAction {
     Set<String> componentUuids = readComponentUuidsFromLocations(hotspot, locations);
     Map<String, ComponentDto> componentsByUuids = loadComponents(dbSession, componentUuids);
 
-    hotspotBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, hotspotBuilder.getComponent().getKey(), componentsByUuids));
+    hotspotBuilder.addAllFlows(responseFormatter.formatFlows(locations, hotspotBuilder.getComponent().getKey(), componentsByUuids));
   }
 
   private static Set<String> readComponentUuidsFromLocations(IssueDto hotspot, Locations locations) {
@@ -304,8 +301,6 @@ public class ShowAction implements HotspotsWsAction {
     BranchDto branch = projectAndBranch.getBranch();
     ComponentDto component = dbClient.componentDao().selectByUuid(dbSession, componentUuid)
         .orElseThrow(() -> new NotFoundException(format("Component with uuid '%s' does not exist", componentUuid)));
-    boolean hotspotOnBranch = Objects.equals(branch.getUuid(), componentUuid);
-
     return new Components(projectAndBranch.getProject(), component, branch);
   }
 
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/NewCodePeriodResolver.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/NewCodePeriodResolver.java
new file mode 100644 (file)
index 0000000..c7845b2
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * 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.issue;
+
+import java.time.Clock;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.lang.String.format;
+import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+
+public class NewCodePeriodResolver {
+
+  private final DbClient dbClient;
+  private final Clock clock;
+
+  public NewCodePeriodResolver(DbClient dbClient, Clock clock) {
+    this.dbClient = dbClient;
+    this.clock = clock;
+  }
+
+  public ResolvedNewCodePeriod resolveForProjectAndBranch(DbSession dbSession, String projectKey, String branchKey) {
+    ComponentDto componentDto = dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branchKey)
+      .orElseThrow(() -> new IllegalStateException(format("Could not find component for project: %s,  branch: %s", projectKey, branchKey)));
+    Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, componentDto);
+    if (snapshot.isPresent() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) {
+      return new ResolvedNewCodePeriod(REFERENCE_BRANCH, null);
+    } else {
+      // if last analysis has no period date, then no issue should be considered new.
+      long createdAfterFromSnapshot = snapshot.map(SnapshotDto::getPeriodDate).orElse(clock.millis());
+      return new ResolvedNewCodePeriod(snapshot.map(SnapshotDto::getPeriodMode).map(NewCodePeriodType::valueOf).orElse(null), createdAfterFromSnapshot);
+    }
+  }
+
+  private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, ComponentDto component) {
+    return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid());
+  }
+
+  private boolean isLastAnalysisFromReAnalyzedReferenceBranch(DbSession dbSession, SnapshotDto snapshot) {
+    return isLastAnalysisUsingReferenceBranch(snapshot) &&
+      isLastAnalysisFromSonarQube94Onwards(dbSession, snapshot.getRootComponentUuid());
+  }
+
+  private boolean isLastAnalysisFromSonarQube94Onwards(DbSession dbSession, String componentUuid) {
+    return dbClient.liveMeasureDao().selectMeasure(dbSession, componentUuid, ANALYSIS_FROM_SONARQUBE_9_4_KEY).isPresent();
+  }
+
+  private static boolean isLastAnalysisUsingReferenceBranch(SnapshotDto snapshot) {
+    return !isNullOrEmpty(snapshot.getPeriodMode()) && REFERENCE_BRANCH.name().equals(snapshot.getPeriodMode());
+  }
+
+  public record ResolvedNewCodePeriod(@Nullable NewCodePeriodType type, @Nullable Long periodDate) {
+
+  }
+}
index f4a8367029894af90621a25f1252d34b49d23267..b6aeea2980795ce9cd01d21fce763dc695ff9d9a 100644 (file)
@@ -24,6 +24,7 @@ import org.sonar.server.issue.AvatarResolverImpl;
 import org.sonar.server.issue.IssueChangeWSSupport;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.issue.NewCodePeriodResolver;
 import org.sonar.server.issue.TaintChecker;
 import org.sonar.server.issue.TextRangeResponseFormatter;
 import org.sonar.server.issue.TransitionService;
@@ -56,6 +57,7 @@ public class IssueWsModule extends Module {
       UserResponseFormatter.class,
       SearchResponseFormat.class,
       OperationResponseWriter.class,
+      NewCodePeriodResolver.class,
       AddCommentAction.class,
       EditCommentAction.class,
       DeleteCommentAction.class,
index d61d3990344934d3aef8e4265ac1b2546ae826a5..c53e07826d75a0fd505034d09798277861be000b 100644 (file)
  */
 package org.sonar.server.issue.ws;
 
-import java.time.Clock;
 import com.google.common.base.Preconditions;
 import java.util.EnumSet;
 import java.util.List;
-import java.util.Optional;
 import javax.annotation.Nullable;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.Request;
@@ -36,24 +34,23 @@ import org.sonar.db.DbSession;
 import org.sonar.db.Pagination;
 import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.issue.IssueListQuery;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.component.ComponentFinder.ProjectAndBranch;
+import org.sonar.server.issue.NewCodePeriodResolver;
+import org.sonar.server.issue.NewCodePeriodResolver.ResolvedNewCodePeriod;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Issues;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
-import static java.lang.String.format;
 import static java.util.Collections.singletonList;
-import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY;
 import static org.sonar.api.server.ws.WebService.Param.PAGE;
 import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
 import static org.sonar.api.utils.Paging.forPageIndex;
-import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
 import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE;
 import static org.sonar.server.issue.index.IssueQueryFactory.ISSUE_STATUSES;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
@@ -71,15 +68,16 @@ public class ListAction implements IssuesWsAction {
   private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod";
   private final UserSession userSession;
   private final DbClient dbClient;
-  private final Clock clock;
+  private final NewCodePeriodResolver newCodePeriodResolver;
   private final SearchResponseLoader searchResponseLoader;
   private final SearchResponseFormat searchResponseFormat;
   private final ComponentFinder componentFinder;
 
-  public ListAction(UserSession userSession, DbClient dbClient, Clock clock, SearchResponseLoader searchResponseLoader, SearchResponseFormat searchResponseFormat, ComponentFinder componentFinder) {
+  public ListAction(UserSession userSession, DbClient dbClient, NewCodePeriodResolver newCodePeriodResolver, SearchResponseLoader searchResponseLoader,
+    SearchResponseFormat searchResponseFormat, ComponentFinder componentFinder) {
     this.userSession = userSession;
     this.dbClient = dbClient;
-    this.clock = clock;
+    this.newCodePeriodResolver = newCodePeriodResolver;
     this.searchResponseLoader = searchResponseLoader;
     this.searchResponseFormat = searchResponseFormat;
     this.componentFinder = componentFinder;
@@ -204,8 +202,14 @@ public class ListAction implements IssuesWsAction {
         .statuses(ISSUE_STATUSES)
         .types(wsRequest.types);
 
-      if (wsRequest.inNewCodePeriod) {
-        setNewCodePeriod(dbSession, wsRequest, queryBuilder);
+      String branchKey = branch.getBranchKey();
+      if (wsRequest.inNewCodePeriod && wsRequest.pullRequest == null && branchKey != null) {
+        ResolvedNewCodePeriod newCodePeriod = newCodePeriodResolver.resolveForProjectAndBranch(dbSession, wsRequest.project, branchKey);
+        if (NewCodePeriodType.REFERENCE_BRANCH == newCodePeriod.type()) {
+          queryBuilder.newCodeOnReference(true);
+        } else {
+          queryBuilder.createdAfter(newCodePeriod.periodDate());
+        }
       }
 
       Pagination pagination = Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize);
@@ -213,36 +217,6 @@ public class ListAction implements IssuesWsAction {
     }
   }
 
-  private void setNewCodePeriod(DbSession dbSession, WsRequest wsRequest, IssueListQuery.IssueListQueryBuilder queryBuilder) {
-    ComponentDto componentDto = dbClient.componentDao().selectByKeyAndBranch(dbSession, wsRequest.project, wsRequest.branch)
-      .orElseThrow(() -> new IllegalStateException(format("Could not find component for project: %s,  branch: %s", wsRequest.project, wsRequest.branch)));
-    Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, componentDto);
-    if (snapshot.isPresent() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) {
-      queryBuilder.newCodeOnReference(true);
-    } else {
-      // if last analysis has no period date, then no issue should be considered new.
-      long createdAfterFromSnapshot = snapshot.map(SnapshotDto::getPeriodDate).orElse(clock.millis());
-      queryBuilder.createdAfter(createdAfterFromSnapshot);
-    }
-  }
-
-  private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, ComponentDto component) {
-    return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid());
-  }
-
-  private boolean isLastAnalysisFromReAnalyzedReferenceBranch(DbSession dbSession, SnapshotDto snapshot) {
-    return isLastAnalysisUsingReferenceBranch(snapshot) &&
-      isLastAnalysisFromSonarQube94Onwards(dbSession, snapshot.getRootComponentUuid());
-  }
-
-  private boolean isLastAnalysisFromSonarQube94Onwards(DbSession dbSession, String componentUuid) {
-    return dbClient.liveMeasureDao().selectMeasure(dbSession, componentUuid, ANALYSIS_FROM_SONARQUBE_9_4_KEY).isPresent();
-  }
-
-  private static boolean isLastAnalysisUsingReferenceBranch(SnapshotDto snapshot) {
-    return !isNullOrEmpty(snapshot.getPeriodMode()) && REFERENCE_BRANCH.name().equals(snapshot.getPeriodMode());
-  }
-
   private Issues.ListWsResponse formatResponse(WsRequest request, List<IssueDto> issues) {
     Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder();
     response.setPaging(Common.Paging.newBuilder()
index ed1704a316e83b9c24bc860929fc14f06491eedf..3622343ea1bba97c548c89b11b8f119f97f13f3c 100644 (file)
@@ -121,8 +121,11 @@ public class SearchResponseFormat {
   Issues.ListWsResponse formatList(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging) {
     Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder();
 
-    response.setPaging(formatPaging(paging));
+    response.setPaging(Common.Paging.newBuilder()
+      .setPageIndex(paging.pageIndex())
+      .setPageSize(data.getIssues().size()));
     response.addAllIssues(createIssues(fields, data));
+    response.addAllComponents(formatComponents(data));
     return response.build();
   }
 
index 2d5c3593e06f11c0361012aed7392087b3c9e8b0..132276817c654170042d016cdcd50a108048b037 100644 (file)
@@ -29,6 +29,6 @@ public class HotspotsWsModuleTest {
   public void verify_count_of_added_components() {
     ListContainer container = new ListContainer();
     new HotspotsWsModule().configure(container);
-    assertThat(container.getAddedObjects()).hasSize(12);
+    assertThat(container.getAddedObjects()).hasSize(13);
   }
 }
index 58fcfc367f8f0de34828527ee3974535dc169737..ef4721c44ddade0da1e30475333e43c7f8dd6ac6 100644 (file)
@@ -53,6 +53,12 @@ message SearchWsResponse {
   }
 }
 
+// Response of GET api/hotspots/list
+message ListWsResponse {
+  optional sonarqube.ws.commons.Paging paging = 1;
+  repeated SearchWsResponse.Hotspot hotspots = 2;
+}
+
 // Response of GET api/hotspots/show
 message ShowWsResponse {
   optional string key = 1;
index bb1e56e870c5fa91a307ad35dffe2262b24ed5e9..d545fe7e75d71422ac9622c29809a9afc69ba867 100644 (file)
@@ -301,4 +301,5 @@ message Flow {
 message ListWsResponse {
   optional sonarqube.ws.commons.Paging paging = 1;
   repeated Issue issues = 2;
+  repeated Component components = 3;
 }