From 7c322c39eb267da94b1875aebd948e7409c6a9a2 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Jul 2023 14:52:20 +0200 Subject: [PATCH] SONAR-19728 add `api/hotspots/list` endpoint --- .../org/sonar/db/issue/IssueListQuery.java | 5 +- .../sonar/server/hotspot/ws/ListActionIT.java | 520 ++++++++++++++++++ .../server/hotspot/ws/SearchActionIT.java | 54 +- .../sonar/server/hotspot/ws/ShowActionIT.java | 6 +- .../sonar/server/issue/ws/ListActionIT.java | 14 +- .../ws/HotspotWsResponseFormatter.java | 131 ++++- .../server/hotspot/ws/HotspotsWsModule.java | 1 + .../sonar/server/hotspot/ws/ListAction.java | 303 ++++++++++ .../sonar/server/hotspot/ws/SearchAction.java | 136 +---- .../sonar/server/hotspot/ws/ShowAction.java | 11 +- .../server/issue/NewCodePeriodResolver.java | 79 +++ .../sonar/server/issue/ws/IssueWsModule.java | 2 + .../org/sonar/server/issue/ws/ListAction.java | 56 +- .../server/issue/ws/SearchResponseFormat.java | 5 +- .../hotspot/ws/HotspotsWsModuleTest.java | 2 +- sonar-ws/src/main/protobuf/ws-hotspots.proto | 6 + sonar-ws/src/main/protobuf/ws-issues.proto | 1 + 17 files changed, 1116 insertions(+), 216 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ListActionIT.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ListAction.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/NewCodePeriodResolver.java diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java index f4b4f8e0337..217f047ab6c 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java @@ -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 index 00000000000..ccc613468d9 --- /dev/null +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ListActionIT.java @@ -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 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 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 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 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}, + }; + } +} diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/SearchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/SearchActionIT.java index 66551da50c4..85dc8b69cb7 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/SearchActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/SearchActionIT.java @@ -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 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 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)); diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java index 7dda20b840f..bf7e5ee10c9 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java @@ -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(); diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java index 12a603d00d8..31e86637251 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java @@ -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() { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java index f1b2b4957a3..fecd5efd5fd 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java @@ -19,18 +19,37 @@ */ 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 mapHotspots(SearchResponseData searchResponseData) { + List hotspots = searchResponseData.getHotspots(); + if (hotspots.isEmpty()) { + return emptyList(); + } + + Hotspots.SearchWsResponse.Hotspot.Builder builder = Hotspots.SearchWsResponse.Hotspot.newBuilder(); + List 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 rangeConsumer) { + textRangeFormatter.formatTextRange(dto, rangeConsumer); + } + + List formatFlows(DbIssues.Locations locations, String issueComponent, Map componentsByUuid) { + return textRangeFormatter.formatFlows(locations, issueComponent, componentsByUuid); + } + + static final class SearchResponseData { + private final Paging paging; + private final List hotspots; + private final Map componentsByUuid = new HashMap<>(); + private final Map branchesByBranchUuid = new HashMap<>(); + + SearchResponseData(Paging paging, List hotspots) { + this.paging = paging; + this.hotspots = hotspots; + } + + boolean isPresent() { + return !hotspots.isEmpty(); + } + + public Paging getPaging() { + return paging; + } + + List getHotspots() { + return hotspots; + } + + void addComponents(Collection components) { + for (ComponentDto component : components) { + componentsByUuid.put(component.uuid(), component); + } + } + + public void addBranches(List branchDtos) { + for (BranchDto branch : branchDtos) { + branchesByBranchUuid.put(branch.getUuid(), branch); + } + } + + public BranchDto getBranch(String branchUuid) { + return branchesByBranchUuid.get(branchUuid); + } + + Collection getComponents() { + return componentsByUuid.values(); + } + + public Map getComponentsByUuid() { + return componentsByUuid; + } + } + } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java index 888410bb4cf..3edc0e5aaf8 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java @@ -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 index 00000000000..1f44254a255 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ListAction.java @@ -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 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." + + "
Total number of Security Hotspots will be always equal to a page size, as counting all issues is not supported. " + + "
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 hotspots = getHotspotKeys(dbSession, wsRequest, projectAndBranch); + + Paging paging = forPageIndex(wsRequest.page).withPageSize(wsRequest.pageSize).andTotal(hotspots.size()); + return new SearchResponseData(paging, hotspots); + } + + private List 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 componentUuids = searchResponseData.getHotspots().stream() + .flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid())) + .collect(Collectors.toSet()); + + Set locationComponentUuids = searchResponseData.getHotspots() + .stream() + .flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream()) + .collect(Collectors.toSet()); + + Set aggregatedComponentUuids = Stream.of(componentUuids, locationComponentUuids) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + if (!aggregatedComponentUuids.isEmpty()) { + List componentDtos = dbClient.componentDao().selectByUuids(dbSession, aggregatedComponentUuids); + searchResponseData.addComponents(componentDtos); + } + } + + private static Set getHotspotLocationComponentUuids(IssueDto hotspot) { + Set locationComponentUuids = new HashSet<>(); + DbIssues.Locations locations = hotspot.parseLocations(); + + if (locations == null) { + return locationComponentUuids; + } + + List flows = locations.getFlowList(); + + for (DbIssues.Flow flow : flows) { + List 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; + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java index c5c4e893d09..4176e5c4b07 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java @@ -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 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 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 componentUuids = searchResponseData.getOrderedHotspots().stream() + Set componentUuids = searchResponseData.getHotspots().stream() .flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid())) .collect(Collectors.toSet()); - Set locationComponentUuids = searchResponseData.getOrderedHotspots() + Set 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 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 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 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 orderedHotspots; - private final Map componentsByUuid = new HashMap<>(); - private final Map rulesByRuleKey = new HashMap<>(); - private final Map branchesByBranchUuid = new HashMap<>(); - - private SearchResponseData(Paging paging, List orderedHotspots) { - this.paging = paging; - this.orderedHotspots = orderedHotspots; - } - - boolean isEmpty() { - return orderedHotspots.isEmpty(); - } - - public Paging getPaging() { - return paging; - } - - List getOrderedHotspots() { - return orderedHotspots; - } - - void addComponents(Collection components) { - for (ComponentDto component : components) { - componentsByUuid.put(component.uuid(), component); - } - } - - public void addBranches(List branchDtos) { - for (BranchDto branch : branchDtos) { - branchesByBranchUuid.put(branch.getUuid(), branch); - } - } - - public BranchDto getBranch(String branchUuid) { - return branchesByBranchUuid.get(branchUuid); - } - - Collection getComponents() { - return componentsByUuid.values(); - } - - public Map getComponentsByUuid() { - return componentsByUuid; - } - - void addRules(Collection rules) { - rules.forEach(t -> rulesByRuleKey.put(t.getKey(), t)); - } - - Optional getRule(RuleKey ruleKey) { - return ofNullable(rulesByRuleKey.get(ruleKey)); - } - - } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java index 02150ba6033..9cd233006ca 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java @@ -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 componentUuids = readComponentUuidsFromLocations(hotspot, locations); Map 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 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 index 00000000000..c7845b2f0e9 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/NewCodePeriodResolver.java @@ -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 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 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) { + + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java index f4a83670298..b6aeea29807 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java @@ -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, diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java index d61d3990344..c53e07826d7 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java @@ -19,11 +19,9 @@ */ 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 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 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 issues) { Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder(); response.setPaging(Common.Paging.newBuilder() diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java index ed1704a316e..3622343ea1b 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java @@ -121,8 +121,11 @@ public class SearchResponseFormat { Issues.ListWsResponse formatList(Set 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(); } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java index 2d5c3593e06..132276817c6 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java @@ -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); } } diff --git a/sonar-ws/src/main/protobuf/ws-hotspots.proto b/sonar-ws/src/main/protobuf/ws-hotspots.proto index 58fcfc367f8..ef4721c44dd 100644 --- a/sonar-ws/src/main/protobuf/ws-hotspots.proto +++ b/sonar-ws/src/main/protobuf/ws-hotspots.proto @@ -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; diff --git a/sonar-ws/src/main/protobuf/ws-issues.proto b/sonar-ws/src/main/protobuf/ws-issues.proto index bb1e56e870c..d545fe7e75d 100644 --- a/sonar-ws/src/main/protobuf/ws-issues.proto +++ b/sonar-ws/src/main/protobuf/ws-issues.proto @@ -301,4 +301,5 @@ message Flow { message ListWsResponse { optional sonarqube.ws.commons.Paging paging = 1; repeated Issue issues = 2; + repeated Component components = 3; } -- 2.39.5