@@ -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; | |||
} |
@@ -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}, | |||
}; | |||
} | |||
} |
@@ -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)); |
@@ -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(); | |||
@@ -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() { |
@@ -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; | |||
} | |||
} | |||
} |
@@ -29,6 +29,7 @@ public class HotspotsWsModule extends Module { | |||
HotspotWsSupport.class, | |||
AssignAction.class, | |||
SearchAction.class, | |||
ListAction.class, | |||
ShowAction.class, | |||
ChangeStatusAction.class, | |||
AddCommentAction.class, |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
@@ -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) { | |||
} | |||
} |
@@ -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, |
@@ -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() |
@@ -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(); | |||
} | |||
@@ -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); | |||
} | |||
} |
@@ -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; |
@@ -301,4 +301,5 @@ message Flow { | |||
message ListWsResponse { | |||
optional sonarqube.ws.commons.Paging paging = 1; | |||
repeated Issue issues = 2; | |||
repeated Component components = 3; | |||
} |