Browse Source

SONAR-19728 add `api/hotspots/list` endpoint

tags/10.2.0.77647
Jacek 10 months ago
parent
commit
7c322c39eb
17 changed files with 1116 additions and 216 deletions
  1. 3
    2
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java
  2. 520
    0
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ListActionIT.java
  3. 27
    27
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/SearchActionIT.java
  4. 3
    3
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java
  5. 13
    1
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java
  6. 129
    2
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java
  7. 1
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java
  8. 303
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ListAction.java
  9. 6
    130
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java
  10. 3
    8
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
  11. 79
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/NewCodePeriodResolver.java
  12. 2
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
  13. 15
    41
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java
  14. 4
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
  15. 1
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java
  16. 6
    0
      sonar-ws/src/main/protobuf/ws-hotspots.proto
  17. 1
    0
      sonar-ws/src/main/protobuf/ws-issues.proto

+ 3
- 2
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java View File

@@ -21,6 +21,7 @@ package org.sonar.db.issue;

import java.util.Collection;
import java.util.Collections;
import javax.annotation.Nullable;

import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
@@ -121,12 +122,12 @@ public class IssueListQuery {
return this;
}

public IssueListQueryBuilder branch(String branch) {
public IssueListQueryBuilder branch(@Nullable String branch) {
this.branch = branch;
return this;
}

public IssueListQueryBuilder pullRequest(String pullRequest) {
public IssueListQueryBuilder pullRequest(@Nullable String pullRequest) {
this.pullRequest = pullRequest;
return this;
}

+ 520
- 0
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ListActionIT.java View File

@@ -0,0 +1,520 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.hotspot.ws;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.time.Clock;
import java.util.List;
import java.util.stream.IntStream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rules.RuleType;
import org.sonar.core.util.UuidFactoryFast;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ProjectData;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.metric.MetricDto;
import org.sonar.db.protobuf.DbIssues;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.TestComponentFinder;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.issue.NewCodePeriodResolver;
import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.MessageFormattingUtils;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Hotspots;

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.tuple;
import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY;
import static org.sonar.api.utils.DateUtils.formatDateTime;
import static org.sonar.api.utils.DateUtils.parseDate;
import static org.sonar.api.utils.DateUtils.parseDateTime;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
import static org.sonar.db.protobuf.DbIssues.MessageFormattingType.CODE;
import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
import static org.sonar.db.rule.RuleTesting.XOO_X1;
import static org.sonar.db.rule.RuleTesting.XOO_X2;
import static org.sonar.db.rule.RuleTesting.newRule;
import static org.sonar.server.tester.UserSessionRule.standalone;

@RunWith(DataProviderRunner.class)
public class ListActionIT {

public static final DbIssues.MessageFormatting MESSAGE_FORMATTING = DbIssues.MessageFormatting.newBuilder()
.setStart(0).setEnd(11).setType(CODE).build();
private final UuidFactoryFast uuidFactory = UuidFactoryFast.getInstance();
@Rule
public UserSessionRule userSession = standalone();
@Rule
public DbTester db = DbTester.create();
private final DbClient dbClient = db.getDbClient();
private final TextRangeResponseFormatter textRangeResponseFormatter = new TextRangeResponseFormatter();
private final HotspotWsResponseFormatter hotspotWsResponseFormatter = new HotspotWsResponseFormatter(textRangeResponseFormatter);
private final ComponentFinder componentFinder = TestComponentFinder.from(db);
private final WsActionTester ws = new WsActionTester(
new ListAction(dbClient, userSession, hotspotWsResponseFormatter, new NewCodePeriodResolver(dbClient, Clock.systemUTC()), componentFinder));

@Test
public void whenNoProjectProvided_shouldFailWithMessage() {
TestRequest request = ws.newRequest();
assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("The 'project' parameter is missing");
}

@Test
public void whenBranchAndPullRequestProvided_shouldFailWithMessage() {
TestRequest request = ws.newRequest()
.setParam("project", "some-project")
.setParam("branch", "some-branch")
.setParam("pullRequest", "some-pr");
assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Only one of parameters 'branch' and 'pullRequest' can be provided");
}

@Test
public void whenAnonymousUser_shouldFailIfInsufficientPrivileges() {
UserDto user = db.users().insertUser();

ProjectData projectData = db.components().insertPrivateProject();
ComponentDto project = projectData.getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
UserDto simon = db.users().insertUser();
RuleDto rule = newHotspotRule();
db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_RESOLVED)
.setResolution(RESOLUTION_FIXED)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04"))
.setCodeVariants(List.of("variant1", "variant2")));

userSession
.logIn(user)
.registerProjects(projectData.getProjectDto());

TestRequest request = ws.newRequest()
.setParam("project", projectData.projectKey())
.setParam("branch", projectData.getMainBranchDto().getKey());
assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class))
.isInstanceOf(ForbiddenException.class)
.hasMessage("Insufficient privileges");
}

@Test
public void whenListHotspotsByProject_shouldReturnAllFields() {
UserDto user = db.users().insertUser();

ProjectData projectData = db.components().insertPublicProject();
ComponentDto project = projectData.getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
UserDto simon = db.users().insertUser();
RuleDto rule = newHotspotRule();
IssueDto hotspot = db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_TO_REVIEW)
.setResolution(null)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04")));

ComponentDto anotherBranch = db.components().insertProjectBranch(project, b -> b.setKey("branch1"));

ComponentDto fileFromAnotherBranch = db.components().insertComponent(newFileDto(anotherBranch));
db.issues().insertHotspot(rule, anotherBranch, fileFromAnotherBranch, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_REVIEWED)
.setResolution(RESOLUTION_FIXED)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04")));

userSession
.logIn(user)
.registerProjects(projectData.getProjectDto());

Hotspots.ListWsResponse response = ws.newRequest()
.setParam("project", projectData.projectKey())
.executeProtobuf(Hotspots.ListWsResponse.class);

assertThat(response.getHotspotsList())
.extracting(
Hotspots.SearchWsResponse.Hotspot::getKey, Hotspots.SearchWsResponse.Hotspot::getRuleKey, Hotspots.SearchWsResponse.Hotspot::getSecurityCategory,
Hotspots.SearchWsResponse.Hotspot::getComponent, Hotspots.SearchWsResponse.Hotspot::getResolution, Hotspots.SearchWsResponse.Hotspot::getStatus,
Hotspots.SearchWsResponse.Hotspot::getMessage, Hotspots.SearchWsResponse.Hotspot::getMessageFormattingsList,
Hotspots.SearchWsResponse.Hotspot::getAssignee, Hotspots.SearchWsResponse.Hotspot::getAuthor, Hotspots.SearchWsResponse.Hotspot::getLine,
Hotspots.SearchWsResponse.Hotspot::getCreationDate, Hotspots.SearchWsResponse.Hotspot::getUpdateDate)
.containsExactlyInAnyOrder(
tuple(hotspot.getKey(), rule.getKey().toString(), "others", file.getKey(), "", STATUS_TO_REVIEW, "the message",
MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), simon.getUuid(), "John", 42,
formatDateTime(hotspot.getIssueCreationDate()), formatDateTime(hotspot.getIssueUpdateDate())));
}

@Test
public void whenListHotspotsByResolution_shouldReturnValidHotspots() {
UserDto user = db.users().insertUser();

ProjectData projectData = db.components().insertPublicProject();
ComponentDto project = projectData.getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
UserDto simon = db.users().insertUser();
RuleDto rule = newHotspotRule();
IssueDto hotspot1 = db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_REVIEWED)
.setResolution(RESOLUTION_FIXED)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04")));

IssueDto hotspot2 = db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_REVIEWED)
.setResolution(RESOLUTION_FIXED)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04")));

IssueDto hotspot3 = db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_REVIEWED)
.setResolution(RESOLUTION_SAFE)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04")));

RuleDto vulnerabilityRule = newIssueRule(XOO_X2, RuleType.VULNERABILITY);
IssueDto vulnerabilityIssue = db.issues().insertIssue(vulnerabilityRule, project, file, i -> i
.setType(RuleType.VULNERABILITY)
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_RESOLVED)
.setResolution(RESOLUTION_WONT_FIX)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04"))
.setCodeVariants(List.of("variant1", "variant2")));

userSession
.logIn(user)
.registerProjects(projectData.getProjectDto());

Hotspots.ListWsResponse response = ws.newRequest()
.setParam("project", projectData.getProjectDto().getKey())
.setParam("branch", projectData.getMainBranchDto().getKey())
.setParam("resolution", RESOLUTION_FIXED)
.executeProtobuf(Hotspots.ListWsResponse.class);

assertThat(response.getHotspotsList())
.extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
.containsExactlyInAnyOrder(hotspot1.getKey(), hotspot2.getKey())
.doesNotContain(hotspot3.getKey(), vulnerabilityIssue.getKey());

response = ws.newRequest()
.setParam("project", projectData.getProjectDto().getKey())
.setParam("branch", projectData.getMainBranchDto().getKey())
.setParam("resolution", RESOLUTION_SAFE)
.executeProtobuf(Hotspots.ListWsResponse.class);

assertThat(response.getHotspotsList())
.extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
.containsExactlyInAnyOrder(hotspot3.getKey())
.doesNotContain(hotspot1.getKey(), hotspot2.getKey(), vulnerabilityIssue.getKey());

response = ws.newRequest()
.setParam("project", projectData.getProjectDto().getKey())
.setParam("branch", projectData.getMainBranchDto().getKey())
.setParam("resolution", RESOLUTION_ACKNOWLEDGED)
.executeProtobuf(Hotspots.ListWsResponse.class);

assertThat(response.getHotspotsList()).isEmpty();
}

@Test
public void whenListHotspotsByNewCodePeriodDate_shouldReturnHotspots() {
UserDto user = db.users().insertUser();

ProjectData projectData = db.components().insertPublicProject();
ComponentDto project = projectData.getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
UserDto simon = db.users().insertUser();
RuleDto rule = newHotspotRule();

db.components().insertSnapshot(project, s -> s.setLast(true).setPeriodDate(parseDateTime("2014-09-05T00:00:00+0100").getTime()));

List<String> beforeNewCodePeriod = IntStream.range(0, 10).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_TO_REVIEW)
.setResolution(null)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04"))
.setCodeVariants(List.of("variant1", "variant2"))))
.map(IssueDto::getKey)
.toList();

List<String> afterNewCodePeriod = IntStream.range(0, 5).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_TO_REVIEW)
.setResolution(null)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2015-01-02"))
.setIssueUpdateDate(parseDate("2017-12-04"))
.setCodeVariants(List.of("variant1", "variant2"))))
.map(IssueDto::getKey)
.toList();

userSession
.logIn(user)
.registerProjects(projectData.getProjectDto());

Hotspots.ListWsResponse response = ws.newRequest()
.setParam("project", projectData.projectKey())
.setParam("branch", projectData.getMainBranchDto().getKey())
.setParam("inNewCodePeriod", "true")
.executeProtobuf(Hotspots.ListWsResponse.class);

assertThat(response.getHotspotsList())
.extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
.containsExactlyInAnyOrderElementsOf(afterNewCodePeriod)
.doesNotContainAnyElementsOf(beforeNewCodePeriod);
}

@Test
public void whenListHotspotsByNewCodePeriodReferenceBranch_shouldReturnHotspots() {
UserDto user = db.users().insertUser();

ProjectData projectData = db.components().insertPublicProject();
ComponentDto project = projectData.getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
UserDto simon = db.users().insertUser();
RuleDto rule = newHotspotRule();

db.components().insertSnapshot(project, s -> s.setLast(true).setPeriodMode(REFERENCE_BRANCH.name()));
MetricDto metric = db.measures().insertMetric(metricDto -> metricDto.setKey(ANALYSIS_FROM_SONARQUBE_9_4_KEY));
db.measures().insertLiveMeasure(project, metric);

List<String> beforeNewCodePeriod = IntStream.range(0, 10).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_TO_REVIEW)
.setResolution(null)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04"))
.setCodeVariants(List.of("variant1", "variant2"))))
.map(IssueDto::getKey)
.toList();

List<String> afterNewCodePeriod = IntStream.range(0, 5).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_TO_REVIEW)
.setResolution(null)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2015-01-02"))
.setIssueUpdateDate(parseDate("2017-12-04"))
.setCodeVariants(List.of("variant1", "variant2"))))
.peek(issueDto -> db.issues().insertNewCodeReferenceIssue(issueDto))
.map(IssueDto::getKey)
.toList();

userSession
.logIn(user)
.registerProjects(projectData.getProjectDto());

Hotspots.ListWsResponse response = ws.newRequest()
.setParam("project", projectData.projectKey())
.setParam("branch", projectData.getMainBranchDto().getKey())
.setParam("inNewCodePeriod", "true")
.executeProtobuf(Hotspots.ListWsResponse.class);

assertThat(response.getHotspotsList())
.extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
.containsExactlyInAnyOrderElementsOf(afterNewCodePeriod)
.doesNotContainAnyElementsOf(beforeNewCodePeriod);
}

@Test
@UseDataProvider("pages")
public void whenUsingPagination_shouldReturnPaginatedResults(String page, int expectedNumberOfIssues) {
UserDto user = db.users().insertUser();

ProjectData projectData = db.components().insertPublicProject();
ComponentDto project = projectData.getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
UserDto simon = db.users().insertUser();
RuleDto rule = newHotspotRule();
IntStream.range(0, 10).forEach(number -> db.issues().insertHotspot(rule, project, file, i -> i
.setEffort(10L)
.setLine(42)
.setChecksum("a227e508d6646b55a086ee11d63b21e9")
.setMessage("the message")
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build())
.setStatus(STATUS_TO_REVIEW)
.setResolution(null)
.setSeverity("MAJOR")
.setAuthorLogin("John")
.setAssigneeUuid(simon.getUuid())
.setTags(asList("bug", "owasp"))
.setIssueCreationDate(parseDate("2014-09-03"))
.setIssueUpdateDate(parseDate("2017-12-04"))
.setCodeVariants(List.of("variant1", "variant2"))));

userSession
.logIn(user)
.registerProjects(projectData.getProjectDto());

Hotspots.ListWsResponse response = ws.newRequest()
.setParam("project", projectData.projectKey())
.setParam("branch", projectData.getMainBranchDto().getKey())
.setParam("p", page)
.setParam("ps", "3")
.executeProtobuf(Hotspots.ListWsResponse.class);

assertThat(response.getHotspotsList()).hasSize(expectedNumberOfIssues);
assertThat(response.getPaging())
.extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal)
.containsExactly(Integer.parseInt(page), expectedNumberOfIssues, 0);
}

private RuleDto newHotspotRule() {
return newIssueRule(XOO_X1, RuleType.SECURITY_HOTSPOT);
}

private RuleDto newIssueRule(RuleKey ruleKey, RuleType ruleType) {
RuleDto rule = newRule(ruleKey, createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule desc"))
.setLanguage("xoo")
.setName("Rule name")
.setType(ruleType)
.setStatus(RuleStatus.READY);
db.rules().insert(rule);
return rule;
}

@DataProvider
public static Object[][] pages() {
return new Object[][] {
{"1", 3},
{"2", 3},
{"3", 3},
{"4", 1},
};
}
}

+ 27
- 27
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/SearchActionIT.java View File

@@ -155,11 +155,11 @@ public class SearchActionIT {
private final IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), mock(AsyncIssueIndexing.class));
private final ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client());
private final PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter();
private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(new TextRangeResponseFormatter());
private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
private final ComponentFinder componentFinder = TestComponentFinder.from(dbTester);
private final SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex,
issueIndexSyncProgressChecker, responseFormatter, new TextRangeResponseFormatter(), system2, componentFinder);
issueIndexSyncProgressChecker, responseFormatter, system2, componentFinder);
private final WsActionTester actionTester = new WsActionTester(underTest);

@Test
@@ -252,8 +252,8 @@ public class SearchActionIT {
@DataProvider
public static Object[][] badStatuses() {
return Stream.concat(
Issue.STATUSES.stream(),
Stream.of(randomAlphabetic(3)))
Issue.STATUSES.stream(),
Stream.of(randomAlphabetic(3)))
.filter(t -> !STATUS_REVIEWED.equals(t))
.filter(t -> !STATUS_TO_REVIEW.equals(t))
.map(t -> new Object[] {t})
@@ -288,9 +288,9 @@ public class SearchActionIT {
@DataProvider
public static Object[][] badResolutions() {
return Stream.of(
Issue.RESOLUTIONS.stream(),
Issue.SECURITY_HOTSPOT_RESOLUTIONS.stream(),
Stream.of(randomAlphabetic(4)))
Issue.RESOLUTIONS.stream(),
Issue.SECURITY_HOTSPOT_RESOLUTIONS.stream(),
Stream.of(randomAlphabetic(4)))
.flatMap(t -> t)
.filter(t -> !RESOLUTION_TYPES.contains(t))
.map(t -> new Object[] {t})
@@ -1260,14 +1260,14 @@ public class SearchActionIT {
ComponentDto file3 = dbTester.components().insertComponent(newFileDto(project).setPath("a/a/d"));
RuleDto rule = newRule(SECURITY_HOTSPOT);
List<IssueDto> hotspots = Stream.of(
newHotspot(rule, project, file3).setLine(8),
newHotspot(rule, project, file3).setLine(10),
newHotspot(rule, project, file1).setLine(null),
newHotspot(rule, project, file1).setLine(9),
newHotspot(rule, project, file1).setLine(11).setKee("a"),
newHotspot(rule, project, file1).setLine(11).setKee("b"),
newHotspot(rule, project, file2).setLine(null),
newHotspot(rule, project, file2).setLine(2))
newHotspot(rule, project, file3).setLine(8),
newHotspot(rule, project, file3).setLine(10),
newHotspot(rule, project, file1).setLine(null),
newHotspot(rule, project, file1).setLine(9),
newHotspot(rule, project, file1).setLine(11).setKee("a"),
newHotspot(rule, project, file1).setLine(11).setKee("b"),
newHotspot(rule, project, file2).setLine(null),
newHotspot(rule, project, file2).setLine(2))
.collect(toList());
String[] expectedHotspotKeys = hotspots.stream().map(IssueDto::getKey).toArray(String[]::new);
// insert hotspots in random order
@@ -1294,11 +1294,11 @@ public class SearchActionIT {
ComponentDto anotherFile = dbTester.components().insertComponent(newFileDto(project));

List<DbIssues.Location> hotspotLocations = Stream.of(
newHotspotLocation(file.uuid(), "security hotspot flow message 0", 1, 1, 0, 12),
newHotspotLocation(file.uuid(), "security hotspot flow message 1", 3, 3, 0, 10),
newHotspotLocation(anotherFile.uuid(), "security hotspot flow message 2", 5, 5, 0, 15),
newHotspotLocation(anotherFile.uuid(), "security hotspot flow message 3", 7, 7, 0, 18),
newHotspotLocation(null, "security hotspot flow message 4", 12, 12, 2, 8))
newHotspotLocation(file.uuid(), "security hotspot flow message 0", 1, 1, 0, 12),
newHotspotLocation(file.uuid(), "security hotspot flow message 1", 3, 3, 0, 10),
newHotspotLocation(anotherFile.uuid(), "security hotspot flow message 2", 5, 5, 0, 15),
newHotspotLocation(anotherFile.uuid(), "security hotspot flow message 3", 7, 7, 0, 18),
newHotspotLocation(null, "security hotspot flow message 4", 12, 12, 2, 8))
.collect(toList());

DbIssues.Locations.Builder locations = DbIssues.Locations.newBuilder().addFlow(DbIssues.Flow.newBuilder().addAllLocation(hotspotLocations));
@@ -1781,9 +1781,9 @@ public class SearchActionIT {
assertThat(responseAll.getHotspotsList())
.extracting(SearchWsResponse.Hotspot::getKey)
.containsExactlyInAnyOrder(Stream.of(
hotspotsInLeakPeriod.stream(),
atLeakPeriod.stream(),
hotspotsBefore.stream())
hotspotsInLeakPeriod.stream(),
atLeakPeriod.stream(),
hotspotsBefore.stream())
.flatMap(t -> t)
.map(IssueDto::getKey)
.toArray(String[]::new));
@@ -1794,8 +1794,8 @@ public class SearchActionIT {
assertThat(responseOnLeak.getHotspotsList())
.extracting(SearchWsResponse.Hotspot::getKey)
.containsExactlyInAnyOrder(Stream.concat(
hotspotsInLeakPeriod.stream(),
atLeakPeriod.stream())
hotspotsInLeakPeriod.stream(),
atLeakPeriod.stream())
.map(IssueDto::getKey)
.toArray(String[]::new));
}
@@ -1826,8 +1826,8 @@ public class SearchActionIT {
assertThat(responseAll.getHotspotsList())
.extracting(SearchWsResponse.Hotspot::getKey)
.containsExactlyInAnyOrder(Stream.of(
hotspotsInLeakPeriod.stream(),
hotspotsNotInLeakPeriod.stream())
hotspotsInLeakPeriod.stream(),
hotspotsNotInLeakPeriod.stream())
.flatMap(t -> t)
.map(IssueDto::getKey)
.toArray(String[]::new));

+ 3
- 3
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java View File

@@ -123,12 +123,12 @@ public class ShowActionIT {

private final DbClient dbClient = dbTester.getDbClient();
private final AvatarResolver avatarResolver = new AvatarResolverImpl();
private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter();
private final TextRangeResponseFormatter textRangeFormatter = new TextRangeResponseFormatter();
private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(textRangeFormatter);
private final IssueChangeWSSupport issueChangeSupport = Mockito.mock(IssueChangeWSSupport.class);
private final HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, System2.INSTANCE);
private final UserResponseFormatter userFormatter = new UserResponseFormatter(new AvatarResolverImpl());
private final TextRangeResponseFormatter textRangeFormatter = new TextRangeResponseFormatter();
private final ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, textRangeFormatter, userFormatter, issueChangeSupport);
private final ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, userFormatter, issueChangeSupport);
private final WsActionTester actionTester = new WsActionTester(underTest);
private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();


+ 13
- 1
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java View File

@@ -50,6 +50,7 @@ import org.sonar.server.component.TestComponentFinder;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.issue.AvatarResolverImpl;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.NewCodePeriodResolver;
import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.issue.TransitionService;
import org.sonar.server.issue.workflow.FunctionExecutor;
@@ -58,6 +59,7 @@ import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.MessageFormattingUtils;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Common.Severity;
import org.sonarqube.ws.Issues;
import org.sonarqube.ws.Issues.Issue;
@@ -104,7 +106,7 @@ public class ListActionIT {
private final SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new TextRangeResponseFormatter(), userFormatter);
private final ComponentFinder componentFinder = TestComponentFinder.from(db);
private final WsActionTester ws = new WsActionTester(
new ListAction(userSession, dbClient, Clock.systemUTC(), searchResponseLoader, searchResponseFormat, componentFinder));
new ListAction(userSession, dbClient, new NewCodePeriodResolver(dbClient, Clock.systemUTC()), searchResponseLoader, searchResponseFormat, componentFinder));

@Before
public void setUp() {
@@ -220,6 +222,13 @@ public class ListActionIT {
MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), "10min",
simon.getLogin(), "John", 42, "a227e508d6646b55a086ee11d63b21e9", asList("bug", "owasp"), formatDateTime(issue.getIssueCreationDate()),
formatDateTime(issue.getIssueUpdateDate()), false, List.of("variant1", "variant2")));

assertThat(response.getComponentsList())
.extracting(
Issues.Component::getKey, Issues.Component::getName, Issues.Component::getQualifier, Issues.Component::getLongName, Issues.Component::getPath)
.containsExactlyInAnyOrder(
tuple(project.getKey(), project.name(), project.qualifier(), project.longName(), ""),
tuple(file.getKey(), file.name(), file.qualifier(), file.longName(), file.path()));
}

@Test
@@ -723,6 +732,9 @@ public class ListActionIT {
.executeProtobuf(Issues.ListWsResponse.class);

assertThat(response.getIssuesList()).hasSize(expectedNumberOfIssues);
assertThat(response.getPaging())
.extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal)
.containsExactly(Integer.parseInt(page), expectedNumberOfIssues, 0);
}

private RuleDto newIssueRule() {

+ 129
- 2
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java View File

@@ -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<Hotspots.SearchWsResponse.Hotspot> mapHotspots(SearchResponseData searchResponseData) {
List<IssueDto> hotspots = searchResponseData.getHotspots();
if (hotspots.isEmpty()) {
return emptyList();
}

Hotspots.SearchWsResponse.Hotspot.Builder builder = Hotspots.SearchWsResponse.Hotspot.newBuilder();
List<Hotspots.SearchWsResponse.Hotspot> hotspotsList = new ArrayList<>(hotspots.size());
for (IssueDto hotspot : hotspots) {
SecurityStandards.SQCategory sqCategory = fromSecurityStandards(hotspot.getSecurityStandards()).getSqCategory();
builder
.clear()
.setKey(hotspot.getKey())
.setComponent(hotspot.getComponentKey())
.setProject(hotspot.getProjectKey())
.setSecurityCategory(sqCategory.getKey())
.setVulnerabilityProbability(sqCategory.getVulnerability().name())
.setRuleKey(hotspot.getRuleKey().toString());
ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus);
ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution);
ofNullable(hotspot.getLine()).ifPresent(builder::setLine);
builder.setMessage(nullToEmpty(hotspot.getMessage()));
builder.addAllMessageFormattings(MessageFormattingUtils.dbMessageFormattingToWs(hotspot.parseMessageFormattings()));
ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee);
builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin()));
builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate()));
builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));
completeHotspotLocations(hotspot, builder, searchResponseData);
hotspotsList.add(builder.build());
}
return hotspotsList;
}

void completeHotspotLocations(IssueDto hotspot, Hotspots.SearchWsResponse.Hotspot.Builder hotspotBuilder, SearchResponseData data) {
DbIssues.Locations locations = hotspot.parseLocations();

if (locations == null) {
return;
}

textRangeFormatter.formatTextRange(locations, hotspotBuilder::setTextRange);
hotspotBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, hotspotBuilder.getComponent(), data.getComponentsByUuid()));
}

Hotspots.Component formatComponent(Hotspots.Component.Builder builder, ComponentDto component, @Nullable BranchDto branchDto) {
if (branchDto == null || branchDto.isMain()) {
return formatComponent(builder, component, null, null);
@@ -65,4 +136,60 @@ public class HotspotWsResponseFormatter {
return formatComponent(builder, component, branchDto.getBranchKey(), branchDto.getPullRequestKey());
}

void formatTextRange(IssueDto dto, Consumer<Common.TextRange> rangeConsumer) {
textRangeFormatter.formatTextRange(dto, rangeConsumer);
}

List<Common.Flow> formatFlows(DbIssues.Locations locations, String issueComponent, Map<String, ComponentDto> componentsByUuid) {
return textRangeFormatter.formatFlows(locations, issueComponent, componentsByUuid);
}

static final class SearchResponseData {
private final Paging paging;
private final List<IssueDto> hotspots;
private final Map<String, ComponentDto> componentsByUuid = new HashMap<>();
private final Map<String, BranchDto> branchesByBranchUuid = new HashMap<>();

SearchResponseData(Paging paging, List<IssueDto> hotspots) {
this.paging = paging;
this.hotspots = hotspots;
}

boolean isPresent() {
return !hotspots.isEmpty();
}

public Paging getPaging() {
return paging;
}

List<IssueDto> getHotspots() {
return hotspots;
}

void addComponents(Collection<ComponentDto> components) {
for (ComponentDto component : components) {
componentsByUuid.put(component.uuid(), component);
}
}

public void addBranches(List<BranchDto> branchDtos) {
for (BranchDto branch : branchDtos) {
branchesByBranchUuid.put(branch.getUuid(), branch);
}
}

public BranchDto getBranch(String branchUuid) {
return branchesByBranchUuid.get(branchUuid);
}

Collection<ComponentDto> getComponents() {
return componentsByUuid.values();
}

public Map<String, ComponentDto> getComponentsByUuid() {
return componentsByUuid;
}
}

}

+ 1
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java View File

@@ -29,6 +29,7 @@ public class HotspotsWsModule extends Module {
HotspotWsSupport.class,
AssignAction.class,
SearchAction.class,
ListAction.class,
ShowAction.class,
ChangeStatusAction.class,
AddCommentAction.class,

+ 303
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ListAction.java View File

@@ -0,0 +1,303 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.hotspot.ws;

import com.google.common.base.Preconditions;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.Paging;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.Pagination;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.issue.IssueListQuery;
import org.sonar.db.newcodeperiod.NewCodePeriodType;
import org.sonar.db.protobuf.DbIssues;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentFinder.ProjectAndBranch;
import org.sonar.server.hotspot.ws.HotspotWsResponseFormatter.SearchResponseData;
import org.sonar.server.issue.NewCodePeriodResolver;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Hotspots;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
import static org.sonar.api.server.ws.WebService.Param.PAGE;
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
import static org.sonar.api.utils.Paging.forPageIndex;
import static org.sonar.api.web.UserRole.USER;
import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE;
import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
import static org.sonar.server.ws.WsUtils.writeProtobuf;

public class ListAction implements HotspotsWsAction {
private static final String PARAM_PROJECT = "project";
private static final String PARAM_BRANCH = "branch";
private static final String PARAM_PULL_REQUEST = "pullRequest";
private static final String PARAM_STATUS = "status";
private static final String PARAM_RESOLUTION = "resolution";
private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod";
private static final List<String> STATUSES = List.of(STATUS_TO_REVIEW, STATUS_REVIEWED);
private final DbClient dbClient;
private final UserSession userSession;
private final HotspotWsResponseFormatter responseFormatter;
private final NewCodePeriodResolver newCodePeriodResolver;
private final ComponentFinder componentFinder;

public ListAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter,
NewCodePeriodResolver newCodePeriodResolver, ComponentFinder componentFinder) {
this.dbClient = dbClient;
this.userSession = userSession;
this.responseFormatter = responseFormatter;
this.newCodePeriodResolver = newCodePeriodResolver;
this.componentFinder = componentFinder;
}

@Override
public void define(WebService.NewController controller) {
WebService.NewAction action = controller
.createAction("list")
.setHandler(this)
.setInternal(true)
.setDescription("List Security Hotpots. This endpoint is used in degraded mode, when issue indexation is running." +
"<br>Total number of Security Hotspots will be always equal to a page size, as counting all issues is not supported. " +
"<br>Requires the 'Browse' permission on the specified project. ")
.setSince("10.2");

action.addPagingParams(100, MAX_PAGE_SIZE);
action.createParam(PARAM_PROJECT)
.setDescription("Key of the project")
.setRequired(true)
.setExampleValue(KEY_PROJECT_EXAMPLE_001);
action.createParam(PARAM_BRANCH)
.setDescription("Branch key. Not available in the community edition.")
.setExampleValue(KEY_BRANCH_EXAMPLE_001);
action.createParam(PARAM_PULL_REQUEST)
.setDescription("Pull request id. Not available in the community edition.")
.setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
action.createParam(PARAM_STATUS)
.setDescription("If '%s' is provided, only Security Hotspots with the specified status are returned.", PARAM_PROJECT)
.setPossibleValues(STATUSES)
.setRequired(false);
action.createParam(PARAM_RESOLUTION)
.setDescription(format(
"If '%s' is provided and if status is '%s', only Security Hotspots with the specified resolution are returned.",
PARAM_PROJECT, STATUS_REVIEWED))
.setPossibleValues(RESOLUTION_FIXED, RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED)
.setRequired(false);
action.createParam(PARAM_IN_NEW_CODE_PERIOD)
.setDescription("If '%s' is provided, only Security Hotspots created in the new code period are returned.", PARAM_IN_NEW_CODE_PERIOD)
.setBooleanPossibleValues()
.setDefaultValue("false");

action.setResponseExample(getClass().getResource("search-example.json"));
}

@Override
public void handle(Request request, Response response) throws Exception {
WsRequest wsRequest = toWsRequest(request);
try (DbSession dbSession = dbClient.openSession(false)) {
ProjectAndBranch projectAndBranch = validate(dbSession, wsRequest);
SearchResponseData searchResponseData = searchHotspots(dbSession, wsRequest, projectAndBranch);
loadComponents(dbSession, searchResponseData);
writeProtobuf(formatResponse(searchResponseData), request, response);
}
}

private static WsRequest toWsRequest(Request request) {
return new WsRequest(
request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE))
.project(request.param(PARAM_PROJECT))
.branch(request.param(PARAM_BRANCH))
.pullRequest(request.param(PARAM_PULL_REQUEST))
.status(request.param(PARAM_STATUS))
.resolution(request.param(PARAM_RESOLUTION))
.inNewCodePeriod(request.paramAsBoolean(PARAM_IN_NEW_CODE_PERIOD));
}

private ProjectAndBranch validate(DbSession dbSession, WsRequest wsRequest) {
Preconditions.checkArgument(isNullOrEmpty(wsRequest.branch) || isNullOrEmpty(wsRequest.pullRequest),
"Only one of parameters '%s' and '%s' can be provided", PARAM_BRANCH, PARAM_PULL_REQUEST);

ProjectAndBranch projectAndBranch = componentFinder.getProjectAndBranch(dbSession, wsRequest.project,
wsRequest.branch, wsRequest.pullRequest);

userSession.checkEntityPermission(USER, projectAndBranch.getProject());
return projectAndBranch;
}

private SearchResponseData searchHotspots(DbSession dbSession, WsRequest wsRequest, ProjectAndBranch projectAndBranch) {
List<IssueDto> hotspots = getHotspotKeys(dbSession, wsRequest, projectAndBranch);

Paging paging = forPageIndex(wsRequest.page).withPageSize(wsRequest.pageSize).andTotal(hotspots.size());
return new SearchResponseData(paging, hotspots);
}

private List<IssueDto> getHotspotKeys(DbSession dbSession, WsRequest wsRequest, ProjectAndBranch projectAndBranch) {
BranchDto branch = projectAndBranch.getBranch();
IssueListQuery.IssueListQueryBuilder queryBuilder = IssueListQuery.IssueListQueryBuilder.newIssueListQueryBuilder()
.project(wsRequest.project)
.branch(branch.getBranchKey())
.pullRequest(branch.getPullRequestKey())
.statuses(wsRequest.status != null ? singletonList(wsRequest.status) : emptyList())
.resolutions(wsRequest.resolution != null ? singletonList(wsRequest.resolution) : emptyList())
.types(singletonList(RuleType.SECURITY_HOTSPOT.getDbConstant()));

String branchKey = branch.getBranchKey();
if (wsRequest.inNewCodePeriod && wsRequest.pullRequest == null && branchKey != null) {
NewCodePeriodResolver.ResolvedNewCodePeriod newCodePeriod = newCodePeriodResolver.resolveForProjectAndBranch(dbSession, wsRequest.project, branchKey);
if (NewCodePeriodType.REFERENCE_BRANCH == newCodePeriod.type()) {
queryBuilder.newCodeOnReference(true);
} else {
queryBuilder.createdAfter(newCodePeriod.periodDate());
}
}

Pagination pagination = Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize);
return dbClient.issueDao().selectByQuery(dbSession, queryBuilder.build(), pagination);
}

private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) {
Set<String> componentUuids = searchResponseData.getHotspots().stream()
.flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid()))
.collect(Collectors.toSet());

Set<String> locationComponentUuids = searchResponseData.getHotspots()
.stream()
.flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream())
.collect(Collectors.toSet());

Set<String> aggregatedComponentUuids = Stream.of(componentUuids, locationComponentUuids)
.flatMap(Collection::stream)
.collect(Collectors.toSet());

if (!aggregatedComponentUuids.isEmpty()) {
List<ComponentDto> componentDtos = dbClient.componentDao().selectByUuids(dbSession, aggregatedComponentUuids);
searchResponseData.addComponents(componentDtos);
}
}

private static Set<String> getHotspotLocationComponentUuids(IssueDto hotspot) {
Set<String> locationComponentUuids = new HashSet<>();
DbIssues.Locations locations = hotspot.parseLocations();

if (locations == null) {
return locationComponentUuids;
}

List<DbIssues.Flow> flows = locations.getFlowList();

for (DbIssues.Flow flow : flows) {
List<DbIssues.Location> flowLocations = flow.getLocationList();
for (DbIssues.Location location : flowLocations) {
if (location.hasComponentId()) {
locationComponentUuids.add(location.getComponentId());
}
}
}

return locationComponentUuids;
}

private Hotspots.ListWsResponse formatResponse(SearchResponseData searchResponseData) {
Hotspots.ListWsResponse.Builder responseBuilder = Hotspots.ListWsResponse.newBuilder();
formatPaging(searchResponseData, responseBuilder);
if (searchResponseData.isPresent()) {
responseFormatter.formatHotspots(searchResponseData, responseBuilder);
}
return responseBuilder.build();
}

private static void formatPaging(SearchResponseData searchResponseData, Hotspots.ListWsResponse.Builder responseBuilder) {
Paging paging = searchResponseData.getPaging();
Common.Paging.Builder pagingBuilder = Common.Paging.newBuilder()
.setPageIndex(paging.pageIndex())
.setPageSize(searchResponseData.getHotspots().size());

responseBuilder.setPaging(pagingBuilder.build());
}

private static final class WsRequest {
private final int page;
private final int pageSize;
private String project;
private String branch;
private String pullRequest;
private String status;
private String resolution;
private boolean inNewCodePeriod;

private WsRequest(int page, int pageSize) {
this.page = page;
this.pageSize = pageSize;
}

public WsRequest project(@Nullable String project) {
this.project = project;
return this;
}

public WsRequest branch(@Nullable String branch) {
this.branch = branch;
return this;
}

public WsRequest pullRequest(@Nullable String pullRequest) {
this.pullRequest = pullRequest;
return this;
}

public WsRequest status(@Nullable String status) {
this.status = status;
return this;
}

public WsRequest resolution(@Nullable String resolution) {
this.resolution = resolution;
return this;
}

public WsRequest inNewCodePeriod(@Nullable Boolean inNewCodePeriod) {
this.inNewCodePeriod = inNewCodePeriod != null && inNewCodePeriod;
return this;
}
}
}

+ 6
- 130
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java View File

@@ -23,7 +23,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -39,7 +38,6 @@ import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.SearchHit;
import org.jetbrains.annotations.NotNull;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
@@ -55,18 +53,16 @@ import org.sonar.db.component.SnapshotDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.protobuf.DbIssues;
import org.sonar.db.rule.RuleDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentFinder.ProjectAndBranch;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.hotspot.ws.HotspotWsResponseFormatter.SearchResponseData;
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
import org.sonar.server.issue.index.IssueQuery;
import org.sonar.server.security.SecurityStandards;
import org.sonar.server.user.UserSession;
import org.sonar.server.ws.MessageFormattingUtils;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Hotspots;
import org.sonarqube.ws.Hotspots.SearchWsResponse;
@@ -84,7 +80,6 @@ import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
import static org.sonar.api.server.ws.WebService.Param.PAGE;
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
import static org.sonar.api.utils.DateUtils.formatDateTime;
import static org.sonar.api.utils.DateUtils.longToDate;
import static org.sonar.api.utils.Paging.forPageIndex;
import static org.sonar.api.web.UserRole.USER;
@@ -92,12 +87,10 @@ import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_INSECURE_INTERACTION;
import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES;
import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_RISKY_RESOURCE;
import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.WsUtils.nullToEmpty;

public class SearchAction implements HotspotsWsAction {
private static final Set<String> SUPPORTED_QUALIFIERS = Set.of(Qualifiers.PROJECT, Qualifiers.APP);
@@ -131,18 +124,16 @@ public class SearchAction implements HotspotsWsAction {
private final IssueIndex issueIndex;
private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker;
private final HotspotWsResponseFormatter responseFormatter;
private final TextRangeResponseFormatter textRangeFormatter;
private final System2 system2;
private final ComponentFinder componentFinder;

public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex, IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter, System2 system2, ComponentFinder componentFinder) {
HotspotWsResponseFormatter responseFormatter, System2 system2, ComponentFinder componentFinder) {
this.dbClient = dbClient;
this.userSession = userSession;
this.issueIndex = issueIndex;
this.issueIndexSyncProgressChecker = issueIndexSyncProgressChecker;
this.responseFormatter = responseFormatter;
this.textRangeFormatter = textRangeFormatter;
this.system2 = system2;
this.componentFinder = componentFinder;
}
@@ -179,7 +170,6 @@ public class SearchAction implements HotspotsWsAction {
Optional<ProjectAndBranch> project = getAndValidateProjectOrApplication(dbSession, wsRequest);
SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project.orElse(null));
loadComponents(dbSession, searchResponseData);
loadRules(dbSession, searchResponseData);
writeProtobuf(formatResponse(searchResponseData), request, response);
}
}
@@ -515,11 +505,11 @@ public class SearchAction implements HotspotsWsAction {
}

private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) {
Set<String> componentUuids = searchResponseData.getOrderedHotspots().stream()
Set<String> componentUuids = searchResponseData.getHotspots().stream()
.flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid()))
.collect(Collectors.toSet());

Set<String> locationComponentUuids = searchResponseData.getOrderedHotspots()
Set<String> locationComponentUuids = searchResponseData.getHotspots()
.stream()
.flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream())
.collect(Collectors.toSet());
@@ -560,21 +550,11 @@ public class SearchAction implements HotspotsWsAction {
return locationComponentUuids;
}

private void loadRules(DbSession dbSession, SearchResponseData searchResponseData) {
Set<RuleKey> ruleKeys = searchResponseData.getOrderedHotspots()
.stream()
.map(IssueDto::getRuleKey)
.collect(Collectors.toSet());
if (!ruleKeys.isEmpty()) {
searchResponseData.addRules(dbClient.ruleDao().selectByKeys(dbSession, ruleKeys));
}
}

private SearchWsResponse formatResponse(SearchResponseData searchResponseData) {
SearchWsResponse.Builder responseBuilder = SearchWsResponse.newBuilder();
formatPaging(searchResponseData, responseBuilder);
if (!searchResponseData.isEmpty()) {
formatHotspots(searchResponseData, responseBuilder);
if (searchResponseData.isPresent()) {
responseFormatter.formatHotspots(searchResponseData, responseBuilder);
formatComponents(searchResponseData, responseBuilder);
}
return responseBuilder.build();
@@ -590,52 +570,6 @@ public class SearchAction implements HotspotsWsAction {
responseBuilder.setPaging(pagingBuilder.build());
}

private void formatHotspots(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
List<IssueDto> orderedHotspots = searchResponseData.getOrderedHotspots();
if (orderedHotspots.isEmpty()) {
return;
}

SearchWsResponse.Hotspot.Builder builder = SearchWsResponse.Hotspot.newBuilder();
for (IssueDto hotspot : orderedHotspots) {
RuleDto rule = searchResponseData.getRule(hotspot.getRuleKey())
// due to join with table Rule when retrieving data from Issues, this can't happen
.orElseThrow(() -> new IllegalStateException(format(
"Rule with key '%s' not found for Hotspot '%s'", hotspot.getRuleKey(), hotspot.getKey())));
SecurityStandards.SQCategory sqCategory = fromSecurityStandards(rule.getSecurityStandards()).getSqCategory();
builder
.clear()
.setKey(hotspot.getKey())
.setComponent(hotspot.getComponentKey())
.setProject(hotspot.getProjectKey())
.setSecurityCategory(sqCategory.getKey())
.setVulnerabilityProbability(sqCategory.getVulnerability().name())
.setRuleKey(hotspot.getRuleKey().toString());
ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus);
ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution);
ofNullable(hotspot.getLine()).ifPresent(builder::setLine);
builder.setMessage(nullToEmpty(hotspot.getMessage()));
builder.addAllMessageFormattings(MessageFormattingUtils.dbMessageFormattingToWs(hotspot.parseMessageFormattings()));
ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee);
builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin()));
builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate()));
builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));
completeHotspotLocations(hotspot, builder, searchResponseData);
responseBuilder.addHotspots(builder.build());
}
}

private void completeHotspotLocations(IssueDto hotspot, SearchWsResponse.Hotspot.Builder hotspotBuilder, SearchResponseData data) {
DbIssues.Locations locations = hotspot.parseLocations();

if (locations == null) {
return;
}

textRangeFormatter.formatTextRange(locations, hotspotBuilder::setTextRange);
hotspotBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, hotspotBuilder.getComponent(), data.getComponentsByUuid()));
}

private void formatComponents(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
Collection<ComponentDto> components = searchResponseData.getComponents();
if (components.isEmpty()) {
@@ -783,62 +717,4 @@ public class SearchAction implements HotspotsWsAction {
return files;
}
}

private static final class SearchResponseData {
private final Paging paging;
private final List<IssueDto> orderedHotspots;
private final Map<String, ComponentDto> componentsByUuid = new HashMap<>();
private final Map<RuleKey, RuleDto> rulesByRuleKey = new HashMap<>();
private final Map<String, BranchDto> branchesByBranchUuid = new HashMap<>();

private SearchResponseData(Paging paging, List<IssueDto> orderedHotspots) {
this.paging = paging;
this.orderedHotspots = orderedHotspots;
}

boolean isEmpty() {
return orderedHotspots.isEmpty();
}

public Paging getPaging() {
return paging;
}

List<IssueDto> getOrderedHotspots() {
return orderedHotspots;
}

void addComponents(Collection<ComponentDto> components) {
for (ComponentDto component : components) {
componentsByUuid.put(component.uuid(), component);
}
}

public void addBranches(List<BranchDto> branchDtos) {
for (BranchDto branch : branchDtos) {
branchesByBranchUuid.put(branch.getUuid(), branch);
}
}

public BranchDto getBranch(String branchUuid) {
return branchesByBranchUuid.get(branchUuid);
}

Collection<ComponentDto> getComponents() {
return componentsByUuid.values();
}

public Map<String, ComponentDto> getComponentsByUuid() {
return componentsByUuid;
}

void addRules(Collection<RuleDto> rules) {
rules.forEach(t -> rulesByRuleKey.put(t.getKey(), t));
}

Optional<RuleDto> getRule(RuleKey ruleKey) {
return ofNullable(rulesByRuleKey.get(ruleKey));
}

}
}

+ 3
- 8
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java View File

@@ -55,7 +55,6 @@ import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.IssueChangeWSSupport;
import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext;
import org.sonar.server.issue.IssueChangeWSSupport.Load;
import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.issue.ws.UserResponseFormatter;
import org.sonar.server.security.SecurityStandards;
import org.sonar.server.ws.MessageFormattingUtils;
@@ -87,16 +86,14 @@ public class ShowAction implements HotspotsWsAction {
private final DbClient dbClient;
private final HotspotWsSupport hotspotWsSupport;
private final HotspotWsResponseFormatter responseFormatter;
private final TextRangeResponseFormatter textRangeFormatter;
private final UserResponseFormatter userFormatter;
private final IssueChangeWSSupport issueChangeSupport;

public ShowAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter,
public ShowAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, HotspotWsResponseFormatter responseFormatter,
UserResponseFormatter userFormatter, IssueChangeWSSupport issueChangeSupport) {
this.dbClient = dbClient;
this.hotspotWsSupport = hotspotWsSupport;
this.responseFormatter = responseFormatter;
this.textRangeFormatter = textRangeFormatter;
this.userFormatter = userFormatter;
this.issueChangeSupport = issueChangeSupport;
}
@@ -219,7 +216,7 @@ public class ShowAction implements HotspotsWsAction {
}

private void formatTextRange(ShowWsResponse.Builder hotspotBuilder, IssueDto hotspot) {
textRangeFormatter.formatTextRange(hotspot, hotspotBuilder::setTextRange);
responseFormatter.formatTextRange(hotspot, hotspotBuilder::setTextRange);
}

private void formatFlows(DbSession dbSession, ShowWsResponse.Builder hotspotBuilder, IssueDto hotspot) {
@@ -232,7 +229,7 @@ public class ShowAction implements HotspotsWsAction {
Set<String> componentUuids = readComponentUuidsFromLocations(hotspot, locations);
Map<String, ComponentDto> componentsByUuids = loadComponents(dbSession, componentUuids);

hotspotBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, hotspotBuilder.getComponent().getKey(), componentsByUuids));
hotspotBuilder.addAllFlows(responseFormatter.formatFlows(locations, hotspotBuilder.getComponent().getKey(), componentsByUuids));
}

private static Set<String> readComponentUuidsFromLocations(IssueDto hotspot, Locations locations) {
@@ -304,8 +301,6 @@ public class ShowAction implements HotspotsWsAction {
BranchDto branch = projectAndBranch.getBranch();
ComponentDto component = dbClient.componentDao().selectByUuid(dbSession, componentUuid)
.orElseThrow(() -> new NotFoundException(format("Component with uuid '%s' does not exist", componentUuid)));
boolean hotspotOnBranch = Objects.equals(branch.getUuid(), componentUuid);

return new Components(projectAndBranch.getProject(), component, branch);
}


+ 79
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/NewCodePeriodResolver.java View File

@@ -0,0 +1,79 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.issue;

import java.time.Clock;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.newcodeperiod.NewCodePeriodType;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY;
import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;

public class NewCodePeriodResolver {

private final DbClient dbClient;
private final Clock clock;

public NewCodePeriodResolver(DbClient dbClient, Clock clock) {
this.dbClient = dbClient;
this.clock = clock;
}

public ResolvedNewCodePeriod resolveForProjectAndBranch(DbSession dbSession, String projectKey, String branchKey) {
ComponentDto componentDto = dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branchKey)
.orElseThrow(() -> new IllegalStateException(format("Could not find component for project: %s, branch: %s", projectKey, branchKey)));
Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, componentDto);
if (snapshot.isPresent() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) {
return new ResolvedNewCodePeriod(REFERENCE_BRANCH, null);
} else {
// if last analysis has no period date, then no issue should be considered new.
long createdAfterFromSnapshot = snapshot.map(SnapshotDto::getPeriodDate).orElse(clock.millis());
return new ResolvedNewCodePeriod(snapshot.map(SnapshotDto::getPeriodMode).map(NewCodePeriodType::valueOf).orElse(null), createdAfterFromSnapshot);
}
}

private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, ComponentDto component) {
return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid());
}

private boolean isLastAnalysisFromReAnalyzedReferenceBranch(DbSession dbSession, SnapshotDto snapshot) {
return isLastAnalysisUsingReferenceBranch(snapshot) &&
isLastAnalysisFromSonarQube94Onwards(dbSession, snapshot.getRootComponentUuid());
}

private boolean isLastAnalysisFromSonarQube94Onwards(DbSession dbSession, String componentUuid) {
return dbClient.liveMeasureDao().selectMeasure(dbSession, componentUuid, ANALYSIS_FROM_SONARQUBE_9_4_KEY).isPresent();
}

private static boolean isLastAnalysisUsingReferenceBranch(SnapshotDto snapshot) {
return !isNullOrEmpty(snapshot.getPeriodMode()) && REFERENCE_BRANCH.name().equals(snapshot.getPeriodMode());
}

public record ResolvedNewCodePeriod(@Nullable NewCodePeriodType type, @Nullable Long periodDate) {

}
}

+ 2
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java View File

@@ -24,6 +24,7 @@ import org.sonar.server.issue.AvatarResolverImpl;
import org.sonar.server.issue.IssueChangeWSSupport;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.IssueFinder;
import org.sonar.server.issue.NewCodePeriodResolver;
import org.sonar.server.issue.TaintChecker;
import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.issue.TransitionService;
@@ -56,6 +57,7 @@ public class IssueWsModule extends Module {
UserResponseFormatter.class,
SearchResponseFormat.class,
OperationResponseWriter.class,
NewCodePeriodResolver.class,
AddCommentAction.class,
EditCommentAction.class,
DeleteCommentAction.class,

+ 15
- 41
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java View File

@@ -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<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()

+ 4
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java View File

@@ -121,8 +121,11 @@ public class SearchResponseFormat {
Issues.ListWsResponse formatList(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging) {
Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder();

response.setPaging(formatPaging(paging));
response.setPaging(Common.Paging.newBuilder()
.setPageIndex(paging.pageIndex())
.setPageSize(data.getIssues().size()));
response.addAllIssues(createIssues(fields, data));
response.addAllComponents(formatComponents(data));
return response.build();
}


+ 1
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java View File

@@ -29,6 +29,6 @@ public class HotspotsWsModuleTest {
public void verify_count_of_added_components() {
ListContainer container = new ListContainer();
new HotspotsWsModule().configure(container);
assertThat(container.getAddedObjects()).hasSize(12);
assertThat(container.getAddedObjects()).hasSize(13);
}
}

+ 6
- 0
sonar-ws/src/main/protobuf/ws-hotspots.proto View File

@@ -53,6 +53,12 @@ message SearchWsResponse {
}
}

// Response of GET api/hotspots/list
message ListWsResponse {
optional sonarqube.ws.commons.Paging paging = 1;
repeated SearchWsResponse.Hotspot hotspots = 2;
}

// Response of GET api/hotspots/show
message ShowWsResponse {
optional string key = 1;

+ 1
- 0
sonar-ws/src/main/protobuf/ws-issues.proto View File

@@ -301,4 +301,5 @@ message Flow {
message ListWsResponse {
optional sonarqube.ws.commons.Paging paging = 1;
repeated Issue issues = 2;
repeated Component components = 3;
}

Loading…
Cancel
Save