import java.util.Collection;
import java.util.Collections;
+import javax.annotation.Nullable;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
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;
}
--- /dev/null
+/*
+ * 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},
+ };
+ }
+}
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
@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})
@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})
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
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));
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));
assertThat(responseOnLeak.getHotspotsList())
.extracting(SearchWsResponse.Hotspot::getKey)
.containsExactlyInAnyOrder(Stream.concat(
- hotspotsInLeakPeriod.stream(),
- atLeakPeriod.stream())
+ hotspotsInLeakPeriod.stream(),
+ atLeakPeriod.stream())
.map(IssueDto::getKey)
.toArray(String[]::new));
}
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));
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();
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;
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;
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() {
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
.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() {
*/
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) {
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);
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;
+ }
+ }
+
}
HotspotWsSupport.class,
AssignAction.class,
SearchAction.class,
+ ListAction.class,
ShowAction.class,
ChangeStatusAction.class,
AddCommentAction.class,
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
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;
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;
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;
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;
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);
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;
}
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);
}
}
}
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());
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();
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()) {
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));
- }
-
- }
}
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;
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;
}
}
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) {
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) {
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);
}
--- /dev/null
+/*
+ * 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) {
+
+ }
+}
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;
UserResponseFormatter.class,
SearchResponseFormat.class,
OperationResponseWriter.class,
+ NewCodePeriodResolver.class,
AddCommentAction.class,
EditCommentAction.class,
DeleteCommentAction.class,
*/
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;
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;
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;
.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);
}
}
- 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()
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();
}
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);
}
}
}
}
+// 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;
message ListWsResponse {
optional sonarqube.ws.commons.Paging paging = 1;
repeated Issue issues = 2;
+ repeated Component components = 3;
}