import java.util.Collection; | import java.util.Collection; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import javax.annotation.Nullable; | |||||
import static java.util.Collections.emptyList; | import static java.util.Collections.emptyList; | ||||
import static java.util.Optional.ofNullable; | import static java.util.Optional.ofNullable; | ||||
return this; | return this; | ||||
} | } | ||||
public IssueListQueryBuilder branch(String branch) { | |||||
public IssueListQueryBuilder branch(@Nullable String branch) { | |||||
this.branch = branch; | this.branch = branch; | ||||
return this; | return this; | ||||
} | } | ||||
public IssueListQueryBuilder pullRequest(String pullRequest) { | |||||
public IssueListQueryBuilder pullRequest(@Nullable String pullRequest) { | |||||
this.pullRequest = pullRequest; | this.pullRequest = pullRequest; | ||||
return this; | return this; | ||||
} | } |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2023 SonarSource SA | |||||
* mailto:info AT sonarsource DOT com | |||||
* | |||||
* This program is free software; you can redistribute it and/or | |||||
* modify it under the terms of the GNU Lesser General Public | |||||
* License as published by the Free Software Foundation; either | |||||
* version 3 of the License, or (at your option) any later version. | |||||
* | |||||
* This program is distributed in the hope that it will be useful, | |||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||||
* Lesser General Public License for more details. | |||||
* | |||||
* You should have received a copy of the GNU Lesser General Public License | |||||
* along with this program; if not, write to the Free Software Foundation, | |||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||||
*/ | |||||
package org.sonar.server.hotspot.ws; | |||||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||||
import java.time.Clock; | |||||
import java.util.List; | |||||
import java.util.stream.IntStream; | |||||
import org.junit.Rule; | |||||
import org.junit.Test; | |||||
import org.junit.runner.RunWith; | |||||
import org.sonar.api.rule.RuleKey; | |||||
import org.sonar.api.rule.RuleStatus; | |||||
import org.sonar.api.rules.RuleType; | |||||
import org.sonar.core.util.UuidFactoryFast; | |||||
import org.sonar.db.DbClient; | |||||
import org.sonar.db.DbTester; | |||||
import org.sonar.db.component.ComponentDto; | |||||
import org.sonar.db.component.ProjectData; | |||||
import org.sonar.db.issue.IssueDto; | |||||
import org.sonar.db.metric.MetricDto; | |||||
import org.sonar.db.protobuf.DbIssues; | |||||
import org.sonar.db.rule.RuleDto; | |||||
import org.sonar.db.user.UserDto; | |||||
import org.sonar.server.component.ComponentFinder; | |||||
import org.sonar.server.component.TestComponentFinder; | |||||
import org.sonar.server.exceptions.ForbiddenException; | |||||
import org.sonar.server.issue.NewCodePeriodResolver; | |||||
import org.sonar.server.issue.TextRangeResponseFormatter; | |||||
import org.sonar.server.tester.UserSessionRule; | |||||
import org.sonar.server.ws.MessageFormattingUtils; | |||||
import org.sonar.server.ws.TestRequest; | |||||
import org.sonar.server.ws.WsActionTester; | |||||
import org.sonarqube.ws.Common; | |||||
import org.sonarqube.ws.Hotspots; | |||||
import static java.util.Arrays.asList; | |||||
import static org.assertj.core.api.Assertions.assertThat; | |||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||||
import static org.assertj.core.api.AssertionsForClassTypes.tuple; | |||||
import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED; | |||||
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; | |||||
import static org.sonar.api.issue.Issue.RESOLUTION_SAFE; | |||||
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; | |||||
import static org.sonar.api.issue.Issue.STATUS_RESOLVED; | |||||
import static org.sonar.api.issue.Issue.STATUS_REVIEWED; | |||||
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW; | |||||
import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY; | |||||
import static org.sonar.api.utils.DateUtils.formatDateTime; | |||||
import static org.sonar.api.utils.DateUtils.parseDate; | |||||
import static org.sonar.api.utils.DateUtils.parseDateTime; | |||||
import static org.sonar.db.component.ComponentTesting.newFileDto; | |||||
import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; | |||||
import static org.sonar.db.protobuf.DbIssues.MessageFormattingType.CODE; | |||||
import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection; | |||||
import static org.sonar.db.rule.RuleTesting.XOO_X1; | |||||
import static org.sonar.db.rule.RuleTesting.XOO_X2; | |||||
import static org.sonar.db.rule.RuleTesting.newRule; | |||||
import static org.sonar.server.tester.UserSessionRule.standalone; | |||||
@RunWith(DataProviderRunner.class) | |||||
public class ListActionIT { | |||||
public static final DbIssues.MessageFormatting MESSAGE_FORMATTING = DbIssues.MessageFormatting.newBuilder() | |||||
.setStart(0).setEnd(11).setType(CODE).build(); | |||||
private final UuidFactoryFast uuidFactory = UuidFactoryFast.getInstance(); | |||||
@Rule | |||||
public UserSessionRule userSession = standalone(); | |||||
@Rule | |||||
public DbTester db = DbTester.create(); | |||||
private final DbClient dbClient = db.getDbClient(); | |||||
private final TextRangeResponseFormatter textRangeResponseFormatter = new TextRangeResponseFormatter(); | |||||
private final HotspotWsResponseFormatter hotspotWsResponseFormatter = new HotspotWsResponseFormatter(textRangeResponseFormatter); | |||||
private final ComponentFinder componentFinder = TestComponentFinder.from(db); | |||||
private final WsActionTester ws = new WsActionTester( | |||||
new ListAction(dbClient, userSession, hotspotWsResponseFormatter, new NewCodePeriodResolver(dbClient, Clock.systemUTC()), componentFinder)); | |||||
@Test | |||||
public void whenNoProjectProvided_shouldFailWithMessage() { | |||||
TestRequest request = ws.newRequest(); | |||||
assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class)) | |||||
.isInstanceOf(IllegalArgumentException.class) | |||||
.hasMessage("The 'project' parameter is missing"); | |||||
} | |||||
@Test | |||||
public void whenBranchAndPullRequestProvided_shouldFailWithMessage() { | |||||
TestRequest request = ws.newRequest() | |||||
.setParam("project", "some-project") | |||||
.setParam("branch", "some-branch") | |||||
.setParam("pullRequest", "some-pr"); | |||||
assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class)) | |||||
.isInstanceOf(IllegalArgumentException.class) | |||||
.hasMessage("Only one of parameters 'branch' and 'pullRequest' can be provided"); | |||||
} | |||||
@Test | |||||
public void whenAnonymousUser_shouldFailIfInsufficientPrivileges() { | |||||
UserDto user = db.users().insertUser(); | |||||
ProjectData projectData = db.components().insertPrivateProject(); | |||||
ComponentDto project = projectData.getMainBranchComponent(); | |||||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||||
UserDto simon = db.users().insertUser(); | |||||
RuleDto rule = newHotspotRule(); | |||||
db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_RESOLVED) | |||||
.setResolution(RESOLUTION_FIXED) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04")) | |||||
.setCodeVariants(List.of("variant1", "variant2"))); | |||||
userSession | |||||
.logIn(user) | |||||
.registerProjects(projectData.getProjectDto()); | |||||
TestRequest request = ws.newRequest() | |||||
.setParam("project", projectData.projectKey()) | |||||
.setParam("branch", projectData.getMainBranchDto().getKey()); | |||||
assertThatThrownBy(() -> request.executeProtobuf(Hotspots.ListWsResponse.class)) | |||||
.isInstanceOf(ForbiddenException.class) | |||||
.hasMessage("Insufficient privileges"); | |||||
} | |||||
@Test | |||||
public void whenListHotspotsByProject_shouldReturnAllFields() { | |||||
UserDto user = db.users().insertUser(); | |||||
ProjectData projectData = db.components().insertPublicProject(); | |||||
ComponentDto project = projectData.getMainBranchComponent(); | |||||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||||
UserDto simon = db.users().insertUser(); | |||||
RuleDto rule = newHotspotRule(); | |||||
IssueDto hotspot = db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_TO_REVIEW) | |||||
.setResolution(null) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04"))); | |||||
ComponentDto anotherBranch = db.components().insertProjectBranch(project, b -> b.setKey("branch1")); | |||||
ComponentDto fileFromAnotherBranch = db.components().insertComponent(newFileDto(anotherBranch)); | |||||
db.issues().insertHotspot(rule, anotherBranch, fileFromAnotherBranch, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_REVIEWED) | |||||
.setResolution(RESOLUTION_FIXED) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04"))); | |||||
userSession | |||||
.logIn(user) | |||||
.registerProjects(projectData.getProjectDto()); | |||||
Hotspots.ListWsResponse response = ws.newRequest() | |||||
.setParam("project", projectData.projectKey()) | |||||
.executeProtobuf(Hotspots.ListWsResponse.class); | |||||
assertThat(response.getHotspotsList()) | |||||
.extracting( | |||||
Hotspots.SearchWsResponse.Hotspot::getKey, Hotspots.SearchWsResponse.Hotspot::getRuleKey, Hotspots.SearchWsResponse.Hotspot::getSecurityCategory, | |||||
Hotspots.SearchWsResponse.Hotspot::getComponent, Hotspots.SearchWsResponse.Hotspot::getResolution, Hotspots.SearchWsResponse.Hotspot::getStatus, | |||||
Hotspots.SearchWsResponse.Hotspot::getMessage, Hotspots.SearchWsResponse.Hotspot::getMessageFormattingsList, | |||||
Hotspots.SearchWsResponse.Hotspot::getAssignee, Hotspots.SearchWsResponse.Hotspot::getAuthor, Hotspots.SearchWsResponse.Hotspot::getLine, | |||||
Hotspots.SearchWsResponse.Hotspot::getCreationDate, Hotspots.SearchWsResponse.Hotspot::getUpdateDate) | |||||
.containsExactlyInAnyOrder( | |||||
tuple(hotspot.getKey(), rule.getKey().toString(), "others", file.getKey(), "", STATUS_TO_REVIEW, "the message", | |||||
MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), simon.getUuid(), "John", 42, | |||||
formatDateTime(hotspot.getIssueCreationDate()), formatDateTime(hotspot.getIssueUpdateDate()))); | |||||
} | |||||
@Test | |||||
public void whenListHotspotsByResolution_shouldReturnValidHotspots() { | |||||
UserDto user = db.users().insertUser(); | |||||
ProjectData projectData = db.components().insertPublicProject(); | |||||
ComponentDto project = projectData.getMainBranchComponent(); | |||||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||||
UserDto simon = db.users().insertUser(); | |||||
RuleDto rule = newHotspotRule(); | |||||
IssueDto hotspot1 = db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_REVIEWED) | |||||
.setResolution(RESOLUTION_FIXED) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04"))); | |||||
IssueDto hotspot2 = db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_REVIEWED) | |||||
.setResolution(RESOLUTION_FIXED) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04"))); | |||||
IssueDto hotspot3 = db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_REVIEWED) | |||||
.setResolution(RESOLUTION_SAFE) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04"))); | |||||
RuleDto vulnerabilityRule = newIssueRule(XOO_X2, RuleType.VULNERABILITY); | |||||
IssueDto vulnerabilityIssue = db.issues().insertIssue(vulnerabilityRule, project, file, i -> i | |||||
.setType(RuleType.VULNERABILITY) | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_RESOLVED) | |||||
.setResolution(RESOLUTION_WONT_FIX) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04")) | |||||
.setCodeVariants(List.of("variant1", "variant2"))); | |||||
userSession | |||||
.logIn(user) | |||||
.registerProjects(projectData.getProjectDto()); | |||||
Hotspots.ListWsResponse response = ws.newRequest() | |||||
.setParam("project", projectData.getProjectDto().getKey()) | |||||
.setParam("branch", projectData.getMainBranchDto().getKey()) | |||||
.setParam("resolution", RESOLUTION_FIXED) | |||||
.executeProtobuf(Hotspots.ListWsResponse.class); | |||||
assertThat(response.getHotspotsList()) | |||||
.extracting(Hotspots.SearchWsResponse.Hotspot::getKey) | |||||
.containsExactlyInAnyOrder(hotspot1.getKey(), hotspot2.getKey()) | |||||
.doesNotContain(hotspot3.getKey(), vulnerabilityIssue.getKey()); | |||||
response = ws.newRequest() | |||||
.setParam("project", projectData.getProjectDto().getKey()) | |||||
.setParam("branch", projectData.getMainBranchDto().getKey()) | |||||
.setParam("resolution", RESOLUTION_SAFE) | |||||
.executeProtobuf(Hotspots.ListWsResponse.class); | |||||
assertThat(response.getHotspotsList()) | |||||
.extracting(Hotspots.SearchWsResponse.Hotspot::getKey) | |||||
.containsExactlyInAnyOrder(hotspot3.getKey()) | |||||
.doesNotContain(hotspot1.getKey(), hotspot2.getKey(), vulnerabilityIssue.getKey()); | |||||
response = ws.newRequest() | |||||
.setParam("project", projectData.getProjectDto().getKey()) | |||||
.setParam("branch", projectData.getMainBranchDto().getKey()) | |||||
.setParam("resolution", RESOLUTION_ACKNOWLEDGED) | |||||
.executeProtobuf(Hotspots.ListWsResponse.class); | |||||
assertThat(response.getHotspotsList()).isEmpty(); | |||||
} | |||||
@Test | |||||
public void whenListHotspotsByNewCodePeriodDate_shouldReturnHotspots() { | |||||
UserDto user = db.users().insertUser(); | |||||
ProjectData projectData = db.components().insertPublicProject(); | |||||
ComponentDto project = projectData.getMainBranchComponent(); | |||||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||||
UserDto simon = db.users().insertUser(); | |||||
RuleDto rule = newHotspotRule(); | |||||
db.components().insertSnapshot(project, s -> s.setLast(true).setPeriodDate(parseDateTime("2014-09-05T00:00:00+0100").getTime())); | |||||
List<String> beforeNewCodePeriod = IntStream.range(0, 10).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_TO_REVIEW) | |||||
.setResolution(null) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04")) | |||||
.setCodeVariants(List.of("variant1", "variant2")))) | |||||
.map(IssueDto::getKey) | |||||
.toList(); | |||||
List<String> afterNewCodePeriod = IntStream.range(0, 5).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_TO_REVIEW) | |||||
.setResolution(null) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2015-01-02")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04")) | |||||
.setCodeVariants(List.of("variant1", "variant2")))) | |||||
.map(IssueDto::getKey) | |||||
.toList(); | |||||
userSession | |||||
.logIn(user) | |||||
.registerProjects(projectData.getProjectDto()); | |||||
Hotspots.ListWsResponse response = ws.newRequest() | |||||
.setParam("project", projectData.projectKey()) | |||||
.setParam("branch", projectData.getMainBranchDto().getKey()) | |||||
.setParam("inNewCodePeriod", "true") | |||||
.executeProtobuf(Hotspots.ListWsResponse.class); | |||||
assertThat(response.getHotspotsList()) | |||||
.extracting(Hotspots.SearchWsResponse.Hotspot::getKey) | |||||
.containsExactlyInAnyOrderElementsOf(afterNewCodePeriod) | |||||
.doesNotContainAnyElementsOf(beforeNewCodePeriod); | |||||
} | |||||
@Test | |||||
public void whenListHotspotsByNewCodePeriodReferenceBranch_shouldReturnHotspots() { | |||||
UserDto user = db.users().insertUser(); | |||||
ProjectData projectData = db.components().insertPublicProject(); | |||||
ComponentDto project = projectData.getMainBranchComponent(); | |||||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||||
UserDto simon = db.users().insertUser(); | |||||
RuleDto rule = newHotspotRule(); | |||||
db.components().insertSnapshot(project, s -> s.setLast(true).setPeriodMode(REFERENCE_BRANCH.name())); | |||||
MetricDto metric = db.measures().insertMetric(metricDto -> metricDto.setKey(ANALYSIS_FROM_SONARQUBE_9_4_KEY)); | |||||
db.measures().insertLiveMeasure(project, metric); | |||||
List<String> beforeNewCodePeriod = IntStream.range(0, 10).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_TO_REVIEW) | |||||
.setResolution(null) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04")) | |||||
.setCodeVariants(List.of("variant1", "variant2")))) | |||||
.map(IssueDto::getKey) | |||||
.toList(); | |||||
List<String> afterNewCodePeriod = IntStream.range(0, 5).mapToObj(number -> db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_TO_REVIEW) | |||||
.setResolution(null) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2015-01-02")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04")) | |||||
.setCodeVariants(List.of("variant1", "variant2")))) | |||||
.peek(issueDto -> db.issues().insertNewCodeReferenceIssue(issueDto)) | |||||
.map(IssueDto::getKey) | |||||
.toList(); | |||||
userSession | |||||
.logIn(user) | |||||
.registerProjects(projectData.getProjectDto()); | |||||
Hotspots.ListWsResponse response = ws.newRequest() | |||||
.setParam("project", projectData.projectKey()) | |||||
.setParam("branch", projectData.getMainBranchDto().getKey()) | |||||
.setParam("inNewCodePeriod", "true") | |||||
.executeProtobuf(Hotspots.ListWsResponse.class); | |||||
assertThat(response.getHotspotsList()) | |||||
.extracting(Hotspots.SearchWsResponse.Hotspot::getKey) | |||||
.containsExactlyInAnyOrderElementsOf(afterNewCodePeriod) | |||||
.doesNotContainAnyElementsOf(beforeNewCodePeriod); | |||||
} | |||||
@Test | |||||
@UseDataProvider("pages") | |||||
public void whenUsingPagination_shouldReturnPaginatedResults(String page, int expectedNumberOfIssues) { | |||||
UserDto user = db.users().insertUser(); | |||||
ProjectData projectData = db.components().insertPublicProject(); | |||||
ComponentDto project = projectData.getMainBranchComponent(); | |||||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||||
UserDto simon = db.users().insertUser(); | |||||
RuleDto rule = newHotspotRule(); | |||||
IntStream.range(0, 10).forEach(number -> db.issues().insertHotspot(rule, project, file, i -> i | |||||
.setEffort(10L) | |||||
.setLine(42) | |||||
.setChecksum("a227e508d6646b55a086ee11d63b21e9") | |||||
.setMessage("the message") | |||||
.setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) | |||||
.setStatus(STATUS_TO_REVIEW) | |||||
.setResolution(null) | |||||
.setSeverity("MAJOR") | |||||
.setAuthorLogin("John") | |||||
.setAssigneeUuid(simon.getUuid()) | |||||
.setTags(asList("bug", "owasp")) | |||||
.setIssueCreationDate(parseDate("2014-09-03")) | |||||
.setIssueUpdateDate(parseDate("2017-12-04")) | |||||
.setCodeVariants(List.of("variant1", "variant2")))); | |||||
userSession | |||||
.logIn(user) | |||||
.registerProjects(projectData.getProjectDto()); | |||||
Hotspots.ListWsResponse response = ws.newRequest() | |||||
.setParam("project", projectData.projectKey()) | |||||
.setParam("branch", projectData.getMainBranchDto().getKey()) | |||||
.setParam("p", page) | |||||
.setParam("ps", "3") | |||||
.executeProtobuf(Hotspots.ListWsResponse.class); | |||||
assertThat(response.getHotspotsList()).hasSize(expectedNumberOfIssues); | |||||
assertThat(response.getPaging()) | |||||
.extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal) | |||||
.containsExactly(Integer.parseInt(page), expectedNumberOfIssues, 0); | |||||
} | |||||
private RuleDto newHotspotRule() { | |||||
return newIssueRule(XOO_X1, RuleType.SECURITY_HOTSPOT); | |||||
} | |||||
private RuleDto newIssueRule(RuleKey ruleKey, RuleType ruleType) { | |||||
RuleDto rule = newRule(ruleKey, createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule desc")) | |||||
.setLanguage("xoo") | |||||
.setName("Rule name") | |||||
.setType(ruleType) | |||||
.setStatus(RuleStatus.READY); | |||||
db.rules().insert(rule); | |||||
return rule; | |||||
} | |||||
@DataProvider | |||||
public static Object[][] pages() { | |||||
return new Object[][] { | |||||
{"1", 3}, | |||||
{"2", 3}, | |||||
{"3", 3}, | |||||
{"4", 1}, | |||||
}; | |||||
} | |||||
} |
private final IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), mock(AsyncIssueIndexing.class)); | private final IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), mock(AsyncIssueIndexing.class)); | ||||
private final ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client()); | private final ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client()); | ||||
private final PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer); | 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 IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class); | ||||
private final ComponentFinder componentFinder = TestComponentFinder.from(dbTester); | private final ComponentFinder componentFinder = TestComponentFinder.from(dbTester); | ||||
private final SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex, | 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); | private final WsActionTester actionTester = new WsActionTester(underTest); | ||||
@Test | @Test | ||||
@DataProvider | @DataProvider | ||||
public static Object[][] badStatuses() { | public static Object[][] badStatuses() { | ||||
return Stream.concat( | 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_REVIEWED.equals(t)) | ||||
.filter(t -> !STATUS_TO_REVIEW.equals(t)) | .filter(t -> !STATUS_TO_REVIEW.equals(t)) | ||||
.map(t -> new Object[] {t}) | .map(t -> new Object[] {t}) | ||||
@DataProvider | @DataProvider | ||||
public static Object[][] badResolutions() { | public static Object[][] badResolutions() { | ||||
return Stream.of( | 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) | .flatMap(t -> t) | ||||
.filter(t -> !RESOLUTION_TYPES.contains(t)) | .filter(t -> !RESOLUTION_TYPES.contains(t)) | ||||
.map(t -> new Object[] {t}) | .map(t -> new Object[] {t}) | ||||
ComponentDto file3 = dbTester.components().insertComponent(newFileDto(project).setPath("a/a/d")); | ComponentDto file3 = dbTester.components().insertComponent(newFileDto(project).setPath("a/a/d")); | ||||
RuleDto rule = newRule(SECURITY_HOTSPOT); | RuleDto rule = newRule(SECURITY_HOTSPOT); | ||||
List<IssueDto> hotspots = Stream.of( | 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()); | .collect(toList()); | ||||
String[] expectedHotspotKeys = hotspots.stream().map(IssueDto::getKey).toArray(String[]::new); | String[] expectedHotspotKeys = hotspots.stream().map(IssueDto::getKey).toArray(String[]::new); | ||||
// insert hotspots in random order | // insert hotspots in random order | ||||
ComponentDto anotherFile = dbTester.components().insertComponent(newFileDto(project)); | ComponentDto anotherFile = dbTester.components().insertComponent(newFileDto(project)); | ||||
List<DbIssues.Location> hotspotLocations = Stream.of( | 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()); | .collect(toList()); | ||||
DbIssues.Locations.Builder locations = DbIssues.Locations.newBuilder().addFlow(DbIssues.Flow.newBuilder().addAllLocation(hotspotLocations)); | DbIssues.Locations.Builder locations = DbIssues.Locations.newBuilder().addFlow(DbIssues.Flow.newBuilder().addAllLocation(hotspotLocations)); | ||||
assertThat(responseAll.getHotspotsList()) | assertThat(responseAll.getHotspotsList()) | ||||
.extracting(SearchWsResponse.Hotspot::getKey) | .extracting(SearchWsResponse.Hotspot::getKey) | ||||
.containsExactlyInAnyOrder(Stream.of( | .containsExactlyInAnyOrder(Stream.of( | ||||
hotspotsInLeakPeriod.stream(), | |||||
atLeakPeriod.stream(), | |||||
hotspotsBefore.stream()) | |||||
hotspotsInLeakPeriod.stream(), | |||||
atLeakPeriod.stream(), | |||||
hotspotsBefore.stream()) | |||||
.flatMap(t -> t) | .flatMap(t -> t) | ||||
.map(IssueDto::getKey) | .map(IssueDto::getKey) | ||||
.toArray(String[]::new)); | .toArray(String[]::new)); | ||||
assertThat(responseOnLeak.getHotspotsList()) | assertThat(responseOnLeak.getHotspotsList()) | ||||
.extracting(SearchWsResponse.Hotspot::getKey) | .extracting(SearchWsResponse.Hotspot::getKey) | ||||
.containsExactlyInAnyOrder(Stream.concat( | .containsExactlyInAnyOrder(Stream.concat( | ||||
hotspotsInLeakPeriod.stream(), | |||||
atLeakPeriod.stream()) | |||||
hotspotsInLeakPeriod.stream(), | |||||
atLeakPeriod.stream()) | |||||
.map(IssueDto::getKey) | .map(IssueDto::getKey) | ||||
.toArray(String[]::new)); | .toArray(String[]::new)); | ||||
} | } | ||||
assertThat(responseAll.getHotspotsList()) | assertThat(responseAll.getHotspotsList()) | ||||
.extracting(SearchWsResponse.Hotspot::getKey) | .extracting(SearchWsResponse.Hotspot::getKey) | ||||
.containsExactlyInAnyOrder(Stream.of( | .containsExactlyInAnyOrder(Stream.of( | ||||
hotspotsInLeakPeriod.stream(), | |||||
hotspotsNotInLeakPeriod.stream()) | |||||
hotspotsInLeakPeriod.stream(), | |||||
hotspotsNotInLeakPeriod.stream()) | |||||
.flatMap(t -> t) | .flatMap(t -> t) | ||||
.map(IssueDto::getKey) | .map(IssueDto::getKey) | ||||
.toArray(String[]::new)); | .toArray(String[]::new)); |
private final DbClient dbClient = dbTester.getDbClient(); | private final DbClient dbClient = dbTester.getDbClient(); | ||||
private final AvatarResolver avatarResolver = new AvatarResolverImpl(); | 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 IssueChangeWSSupport issueChangeSupport = Mockito.mock(IssueChangeWSSupport.class); | ||||
private final HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, System2.INSTANCE); | private final HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, System2.INSTANCE); | ||||
private final UserResponseFormatter userFormatter = new UserResponseFormatter(new AvatarResolverImpl()); | 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 WsActionTester actionTester = new WsActionTester(underTest); | ||||
private final UuidFactory uuidFactory = UuidFactoryFast.getInstance(); | private final UuidFactory uuidFactory = UuidFactoryFast.getInstance(); | ||||
import org.sonar.server.exceptions.ForbiddenException; | import org.sonar.server.exceptions.ForbiddenException; | ||||
import org.sonar.server.issue.AvatarResolverImpl; | import org.sonar.server.issue.AvatarResolverImpl; | ||||
import org.sonar.server.issue.IssueFieldsSetter; | import org.sonar.server.issue.IssueFieldsSetter; | ||||
import org.sonar.server.issue.NewCodePeriodResolver; | |||||
import org.sonar.server.issue.TextRangeResponseFormatter; | import org.sonar.server.issue.TextRangeResponseFormatter; | ||||
import org.sonar.server.issue.TransitionService; | import org.sonar.server.issue.TransitionService; | ||||
import org.sonar.server.issue.workflow.FunctionExecutor; | import org.sonar.server.issue.workflow.FunctionExecutor; | ||||
import org.sonar.server.ws.MessageFormattingUtils; | import org.sonar.server.ws.MessageFormattingUtils; | ||||
import org.sonar.server.ws.TestRequest; | import org.sonar.server.ws.TestRequest; | ||||
import org.sonar.server.ws.WsActionTester; | import org.sonar.server.ws.WsActionTester; | ||||
import org.sonarqube.ws.Common; | |||||
import org.sonarqube.ws.Common.Severity; | import org.sonarqube.ws.Common.Severity; | ||||
import org.sonarqube.ws.Issues; | import org.sonarqube.ws.Issues; | ||||
import org.sonarqube.ws.Issues.Issue; | import org.sonarqube.ws.Issues.Issue; | ||||
private final SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new TextRangeResponseFormatter(), userFormatter); | private final SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new TextRangeResponseFormatter(), userFormatter); | ||||
private final ComponentFinder componentFinder = TestComponentFinder.from(db); | private final ComponentFinder componentFinder = TestComponentFinder.from(db); | ||||
private final WsActionTester ws = new WsActionTester( | 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 | @Before | ||||
public void setUp() { | public void setUp() { | ||||
MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), "10min", | MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), "10min", | ||||
simon.getLogin(), "John", 42, "a227e508d6646b55a086ee11d63b21e9", asList("bug", "owasp"), formatDateTime(issue.getIssueCreationDate()), | simon.getLogin(), "John", 42, "a227e508d6646b55a086ee11d63b21e9", asList("bug", "owasp"), formatDateTime(issue.getIssueCreationDate()), | ||||
formatDateTime(issue.getIssueUpdateDate()), false, List.of("variant1", "variant2"))); | 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 | @Test | ||||
.executeProtobuf(Issues.ListWsResponse.class); | .executeProtobuf(Issues.ListWsResponse.class); | ||||
assertThat(response.getIssuesList()).hasSize(expectedNumberOfIssues); | 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() { | private RuleDto newIssueRule() { |
*/ | */ | ||||
package org.sonar.server.hotspot.ws; | 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 javax.annotation.Nullable; | ||||
import org.sonar.api.utils.Paging; | |||||
import org.sonar.db.component.BranchDto; | import org.sonar.db.component.BranchDto; | ||||
import org.sonar.db.component.ComponentDto; | import org.sonar.db.component.ComponentDto; | ||||
import org.sonar.db.issue.IssueDto; | |||||
import org.sonar.db.project.ProjectDto; | 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 org.sonarqube.ws.Hotspots; | ||||
import static java.util.Collections.emptyList; | |||||
import static java.util.Optional.ofNullable; | 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 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) { | Hotspots.Component formatProject(Hotspots.Component.Builder builder, ProjectDto project, @Nullable String branch, @Nullable String pullRequest) { | ||||
return builder.build(); | 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) { | Hotspots.Component formatComponent(Hotspots.Component.Builder builder, ComponentDto component, @Nullable BranchDto branchDto) { | ||||
if (branchDto == null || branchDto.isMain()) { | if (branchDto == null || branchDto.isMain()) { | ||||
return formatComponent(builder, component, null, null); | return formatComponent(builder, component, null, null); | ||||
return formatComponent(builder, component, branchDto.getBranchKey(), branchDto.getPullRequestKey()); | return formatComponent(builder, component, branchDto.getBranchKey(), branchDto.getPullRequestKey()); | ||||
} | } | ||||
void formatTextRange(IssueDto dto, Consumer<Common.TextRange> rangeConsumer) { | |||||
textRangeFormatter.formatTextRange(dto, rangeConsumer); | |||||
} | |||||
List<Common.Flow> formatFlows(DbIssues.Locations locations, String issueComponent, Map<String, ComponentDto> componentsByUuid) { | |||||
return textRangeFormatter.formatFlows(locations, issueComponent, componentsByUuid); | |||||
} | |||||
static final class SearchResponseData { | |||||
private final Paging paging; | |||||
private final List<IssueDto> hotspots; | |||||
private final Map<String, ComponentDto> componentsByUuid = new HashMap<>(); | |||||
private final Map<String, BranchDto> branchesByBranchUuid = new HashMap<>(); | |||||
SearchResponseData(Paging paging, List<IssueDto> hotspots) { | |||||
this.paging = paging; | |||||
this.hotspots = hotspots; | |||||
} | |||||
boolean isPresent() { | |||||
return !hotspots.isEmpty(); | |||||
} | |||||
public Paging getPaging() { | |||||
return paging; | |||||
} | |||||
List<IssueDto> getHotspots() { | |||||
return hotspots; | |||||
} | |||||
void addComponents(Collection<ComponentDto> components) { | |||||
for (ComponentDto component : components) { | |||||
componentsByUuid.put(component.uuid(), component); | |||||
} | |||||
} | |||||
public void addBranches(List<BranchDto> branchDtos) { | |||||
for (BranchDto branch : branchDtos) { | |||||
branchesByBranchUuid.put(branch.getUuid(), branch); | |||||
} | |||||
} | |||||
public BranchDto getBranch(String branchUuid) { | |||||
return branchesByBranchUuid.get(branchUuid); | |||||
} | |||||
Collection<ComponentDto> getComponents() { | |||||
return componentsByUuid.values(); | |||||
} | |||||
public Map<String, ComponentDto> getComponentsByUuid() { | |||||
return componentsByUuid; | |||||
} | |||||
} | |||||
} | } |
HotspotWsSupport.class, | HotspotWsSupport.class, | ||||
AssignAction.class, | AssignAction.class, | ||||
SearchAction.class, | SearchAction.class, | ||||
ListAction.class, | |||||
ShowAction.class, | ShowAction.class, | ||||
ChangeStatusAction.class, | ChangeStatusAction.class, | ||||
AddCommentAction.class, | AddCommentAction.class, |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2023 SonarSource SA | |||||
* mailto:info AT sonarsource DOT com | |||||
* | |||||
* This program is free software; you can redistribute it and/or | |||||
* modify it under the terms of the GNU Lesser General Public | |||||
* License as published by the Free Software Foundation; either | |||||
* version 3 of the License, or (at your option) any later version. | |||||
* | |||||
* This program is distributed in the hope that it will be useful, | |||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||||
* Lesser General Public License for more details. | |||||
* | |||||
* You should have received a copy of the GNU Lesser General Public License | |||||
* along with this program; if not, write to the Free Software Foundation, | |||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||||
*/ | |||||
package org.sonar.server.hotspot.ws; | |||||
import com.google.common.base.Preconditions; | |||||
import java.util.Collection; | |||||
import java.util.HashSet; | |||||
import java.util.List; | |||||
import java.util.Set; | |||||
import java.util.stream.Collectors; | |||||
import java.util.stream.Stream; | |||||
import javax.annotation.Nullable; | |||||
import org.sonar.api.rules.RuleType; | |||||
import org.sonar.api.server.ws.Request; | |||||
import org.sonar.api.server.ws.Response; | |||||
import org.sonar.api.server.ws.WebService; | |||||
import org.sonar.api.utils.Paging; | |||||
import org.sonar.db.DbClient; | |||||
import org.sonar.db.DbSession; | |||||
import org.sonar.db.Pagination; | |||||
import org.sonar.db.component.BranchDto; | |||||
import org.sonar.db.component.ComponentDto; | |||||
import org.sonar.db.issue.IssueDto; | |||||
import org.sonar.db.issue.IssueListQuery; | |||||
import org.sonar.db.newcodeperiod.NewCodePeriodType; | |||||
import org.sonar.db.protobuf.DbIssues; | |||||
import org.sonar.server.component.ComponentFinder; | |||||
import org.sonar.server.component.ComponentFinder.ProjectAndBranch; | |||||
import org.sonar.server.hotspot.ws.HotspotWsResponseFormatter.SearchResponseData; | |||||
import org.sonar.server.issue.NewCodePeriodResolver; | |||||
import org.sonar.server.user.UserSession; | |||||
import org.sonarqube.ws.Common; | |||||
import org.sonarqube.ws.Hotspots; | |||||
import static com.google.common.base.Strings.isNullOrEmpty; | |||||
import static java.lang.String.format; | |||||
import static java.util.Collections.emptyList; | |||||
import static java.util.Collections.singletonList; | |||||
import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED; | |||||
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; | |||||
import static org.sonar.api.issue.Issue.RESOLUTION_SAFE; | |||||
import static org.sonar.api.issue.Issue.STATUS_REVIEWED; | |||||
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW; | |||||
import static org.sonar.api.server.ws.WebService.Param.PAGE; | |||||
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | |||||
import static org.sonar.api.utils.Paging.forPageIndex; | |||||
import static org.sonar.api.web.UserRole.USER; | |||||
import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE; | |||||
import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001; | |||||
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; | |||||
import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001; | |||||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||||
public class ListAction implements HotspotsWsAction { | |||||
private static final String PARAM_PROJECT = "project"; | |||||
private static final String PARAM_BRANCH = "branch"; | |||||
private static final String PARAM_PULL_REQUEST = "pullRequest"; | |||||
private static final String PARAM_STATUS = "status"; | |||||
private static final String PARAM_RESOLUTION = "resolution"; | |||||
private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod"; | |||||
private static final List<String> STATUSES = List.of(STATUS_TO_REVIEW, STATUS_REVIEWED); | |||||
private final DbClient dbClient; | |||||
private final UserSession userSession; | |||||
private final HotspotWsResponseFormatter responseFormatter; | |||||
private final NewCodePeriodResolver newCodePeriodResolver; | |||||
private final ComponentFinder componentFinder; | |||||
public ListAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter, | |||||
NewCodePeriodResolver newCodePeriodResolver, ComponentFinder componentFinder) { | |||||
this.dbClient = dbClient; | |||||
this.userSession = userSession; | |||||
this.responseFormatter = responseFormatter; | |||||
this.newCodePeriodResolver = newCodePeriodResolver; | |||||
this.componentFinder = componentFinder; | |||||
} | |||||
@Override | |||||
public void define(WebService.NewController controller) { | |||||
WebService.NewAction action = controller | |||||
.createAction("list") | |||||
.setHandler(this) | |||||
.setInternal(true) | |||||
.setDescription("List Security Hotpots. This endpoint is used in degraded mode, when issue indexation is running." + | |||||
"<br>Total number of Security Hotspots will be always equal to a page size, as counting all issues is not supported. " + | |||||
"<br>Requires the 'Browse' permission on the specified project. ") | |||||
.setSince("10.2"); | |||||
action.addPagingParams(100, MAX_PAGE_SIZE); | |||||
action.createParam(PARAM_PROJECT) | |||||
.setDescription("Key of the project") | |||||
.setRequired(true) | |||||
.setExampleValue(KEY_PROJECT_EXAMPLE_001); | |||||
action.createParam(PARAM_BRANCH) | |||||
.setDescription("Branch key. Not available in the community edition.") | |||||
.setExampleValue(KEY_BRANCH_EXAMPLE_001); | |||||
action.createParam(PARAM_PULL_REQUEST) | |||||
.setDescription("Pull request id. Not available in the community edition.") | |||||
.setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001); | |||||
action.createParam(PARAM_STATUS) | |||||
.setDescription("If '%s' is provided, only Security Hotspots with the specified status are returned.", PARAM_PROJECT) | |||||
.setPossibleValues(STATUSES) | |||||
.setRequired(false); | |||||
action.createParam(PARAM_RESOLUTION) | |||||
.setDescription(format( | |||||
"If '%s' is provided and if status is '%s', only Security Hotspots with the specified resolution are returned.", | |||||
PARAM_PROJECT, STATUS_REVIEWED)) | |||||
.setPossibleValues(RESOLUTION_FIXED, RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED) | |||||
.setRequired(false); | |||||
action.createParam(PARAM_IN_NEW_CODE_PERIOD) | |||||
.setDescription("If '%s' is provided, only Security Hotspots created in the new code period are returned.", PARAM_IN_NEW_CODE_PERIOD) | |||||
.setBooleanPossibleValues() | |||||
.setDefaultValue("false"); | |||||
action.setResponseExample(getClass().getResource("search-example.json")); | |||||
} | |||||
@Override | |||||
public void handle(Request request, Response response) throws Exception { | |||||
WsRequest wsRequest = toWsRequest(request); | |||||
try (DbSession dbSession = dbClient.openSession(false)) { | |||||
ProjectAndBranch projectAndBranch = validate(dbSession, wsRequest); | |||||
SearchResponseData searchResponseData = searchHotspots(dbSession, wsRequest, projectAndBranch); | |||||
loadComponents(dbSession, searchResponseData); | |||||
writeProtobuf(formatResponse(searchResponseData), request, response); | |||||
} | |||||
} | |||||
private static WsRequest toWsRequest(Request request) { | |||||
return new WsRequest( | |||||
request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE)) | |||||
.project(request.param(PARAM_PROJECT)) | |||||
.branch(request.param(PARAM_BRANCH)) | |||||
.pullRequest(request.param(PARAM_PULL_REQUEST)) | |||||
.status(request.param(PARAM_STATUS)) | |||||
.resolution(request.param(PARAM_RESOLUTION)) | |||||
.inNewCodePeriod(request.paramAsBoolean(PARAM_IN_NEW_CODE_PERIOD)); | |||||
} | |||||
private ProjectAndBranch validate(DbSession dbSession, WsRequest wsRequest) { | |||||
Preconditions.checkArgument(isNullOrEmpty(wsRequest.branch) || isNullOrEmpty(wsRequest.pullRequest), | |||||
"Only one of parameters '%s' and '%s' can be provided", PARAM_BRANCH, PARAM_PULL_REQUEST); | |||||
ProjectAndBranch projectAndBranch = componentFinder.getProjectAndBranch(dbSession, wsRequest.project, | |||||
wsRequest.branch, wsRequest.pullRequest); | |||||
userSession.checkEntityPermission(USER, projectAndBranch.getProject()); | |||||
return projectAndBranch; | |||||
} | |||||
private SearchResponseData searchHotspots(DbSession dbSession, WsRequest wsRequest, ProjectAndBranch projectAndBranch) { | |||||
List<IssueDto> hotspots = getHotspotKeys(dbSession, wsRequest, projectAndBranch); | |||||
Paging paging = forPageIndex(wsRequest.page).withPageSize(wsRequest.pageSize).andTotal(hotspots.size()); | |||||
return new SearchResponseData(paging, hotspots); | |||||
} | |||||
private List<IssueDto> getHotspotKeys(DbSession dbSession, WsRequest wsRequest, ProjectAndBranch projectAndBranch) { | |||||
BranchDto branch = projectAndBranch.getBranch(); | |||||
IssueListQuery.IssueListQueryBuilder queryBuilder = IssueListQuery.IssueListQueryBuilder.newIssueListQueryBuilder() | |||||
.project(wsRequest.project) | |||||
.branch(branch.getBranchKey()) | |||||
.pullRequest(branch.getPullRequestKey()) | |||||
.statuses(wsRequest.status != null ? singletonList(wsRequest.status) : emptyList()) | |||||
.resolutions(wsRequest.resolution != null ? singletonList(wsRequest.resolution) : emptyList()) | |||||
.types(singletonList(RuleType.SECURITY_HOTSPOT.getDbConstant())); | |||||
String branchKey = branch.getBranchKey(); | |||||
if (wsRequest.inNewCodePeriod && wsRequest.pullRequest == null && branchKey != null) { | |||||
NewCodePeriodResolver.ResolvedNewCodePeriod newCodePeriod = newCodePeriodResolver.resolveForProjectAndBranch(dbSession, wsRequest.project, branchKey); | |||||
if (NewCodePeriodType.REFERENCE_BRANCH == newCodePeriod.type()) { | |||||
queryBuilder.newCodeOnReference(true); | |||||
} else { | |||||
queryBuilder.createdAfter(newCodePeriod.periodDate()); | |||||
} | |||||
} | |||||
Pagination pagination = Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize); | |||||
return dbClient.issueDao().selectByQuery(dbSession, queryBuilder.build(), pagination); | |||||
} | |||||
private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) { | |||||
Set<String> componentUuids = searchResponseData.getHotspots().stream() | |||||
.flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid())) | |||||
.collect(Collectors.toSet()); | |||||
Set<String> locationComponentUuids = searchResponseData.getHotspots() | |||||
.stream() | |||||
.flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream()) | |||||
.collect(Collectors.toSet()); | |||||
Set<String> aggregatedComponentUuids = Stream.of(componentUuids, locationComponentUuids) | |||||
.flatMap(Collection::stream) | |||||
.collect(Collectors.toSet()); | |||||
if (!aggregatedComponentUuids.isEmpty()) { | |||||
List<ComponentDto> componentDtos = dbClient.componentDao().selectByUuids(dbSession, aggregatedComponentUuids); | |||||
searchResponseData.addComponents(componentDtos); | |||||
} | |||||
} | |||||
private static Set<String> getHotspotLocationComponentUuids(IssueDto hotspot) { | |||||
Set<String> locationComponentUuids = new HashSet<>(); | |||||
DbIssues.Locations locations = hotspot.parseLocations(); | |||||
if (locations == null) { | |||||
return locationComponentUuids; | |||||
} | |||||
List<DbIssues.Flow> flows = locations.getFlowList(); | |||||
for (DbIssues.Flow flow : flows) { | |||||
List<DbIssues.Location> flowLocations = flow.getLocationList(); | |||||
for (DbIssues.Location location : flowLocations) { | |||||
if (location.hasComponentId()) { | |||||
locationComponentUuids.add(location.getComponentId()); | |||||
} | |||||
} | |||||
} | |||||
return locationComponentUuids; | |||||
} | |||||
private Hotspots.ListWsResponse formatResponse(SearchResponseData searchResponseData) { | |||||
Hotspots.ListWsResponse.Builder responseBuilder = Hotspots.ListWsResponse.newBuilder(); | |||||
formatPaging(searchResponseData, responseBuilder); | |||||
if (searchResponseData.isPresent()) { | |||||
responseFormatter.formatHotspots(searchResponseData, responseBuilder); | |||||
} | |||||
return responseBuilder.build(); | |||||
} | |||||
private static void formatPaging(SearchResponseData searchResponseData, Hotspots.ListWsResponse.Builder responseBuilder) { | |||||
Paging paging = searchResponseData.getPaging(); | |||||
Common.Paging.Builder pagingBuilder = Common.Paging.newBuilder() | |||||
.setPageIndex(paging.pageIndex()) | |||||
.setPageSize(searchResponseData.getHotspots().size()); | |||||
responseBuilder.setPaging(pagingBuilder.build()); | |||||
} | |||||
private static final class WsRequest { | |||||
private final int page; | |||||
private final int pageSize; | |||||
private String project; | |||||
private String branch; | |||||
private String pullRequest; | |||||
private String status; | |||||
private String resolution; | |||||
private boolean inNewCodePeriod; | |||||
private WsRequest(int page, int pageSize) { | |||||
this.page = page; | |||||
this.pageSize = pageSize; | |||||
} | |||||
public WsRequest project(@Nullable String project) { | |||||
this.project = project; | |||||
return this; | |||||
} | |||||
public WsRequest branch(@Nullable String branch) { | |||||
this.branch = branch; | |||||
return this; | |||||
} | |||||
public WsRequest pullRequest(@Nullable String pullRequest) { | |||||
this.pullRequest = pullRequest; | |||||
return this; | |||||
} | |||||
public WsRequest status(@Nullable String status) { | |||||
this.status = status; | |||||
return this; | |||||
} | |||||
public WsRequest resolution(@Nullable String resolution) { | |||||
this.resolution = resolution; | |||||
return this; | |||||
} | |||||
public WsRequest inNewCodePeriod(@Nullable Boolean inNewCodePeriod) { | |||||
this.inNewCodePeriod = inNewCodePeriod != null && inNewCodePeriod; | |||||
return this; | |||||
} | |||||
} | |||||
} |
import java.util.Collection; | import java.util.Collection; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.Date; | import java.util.Date; | ||||
import java.util.HashMap; | |||||
import java.util.HashSet; | import java.util.HashSet; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import org.elasticsearch.search.SearchHit; | import org.elasticsearch.search.SearchHit; | ||||
import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||
import org.sonar.api.resources.Qualifiers; | import org.sonar.api.resources.Qualifiers; | ||||
import org.sonar.api.rule.RuleKey; | |||||
import org.sonar.api.rules.RuleType; | import org.sonar.api.rules.RuleType; | ||||
import org.sonar.api.server.ws.Change; | import org.sonar.api.server.ws.Change; | ||||
import org.sonar.api.server.ws.Request; | import org.sonar.api.server.ws.Request; | ||||
import org.sonar.db.issue.IssueDto; | import org.sonar.db.issue.IssueDto; | ||||
import org.sonar.db.project.ProjectDto; | import org.sonar.db.project.ProjectDto; | ||||
import org.sonar.db.protobuf.DbIssues; | import org.sonar.db.protobuf.DbIssues; | ||||
import org.sonar.db.rule.RuleDto; | |||||
import org.sonar.server.component.ComponentFinder; | import org.sonar.server.component.ComponentFinder; | ||||
import org.sonar.server.component.ComponentFinder.ProjectAndBranch; | import org.sonar.server.component.ComponentFinder.ProjectAndBranch; | ||||
import org.sonar.server.es.SearchOptions; | import org.sonar.server.es.SearchOptions; | ||||
import org.sonar.server.exceptions.NotFoundException; | 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.IssueIndex; | ||||
import org.sonar.server.issue.index.IssueIndexSyncProgressChecker; | import org.sonar.server.issue.index.IssueIndexSyncProgressChecker; | ||||
import org.sonar.server.issue.index.IssueQuery; | import org.sonar.server.issue.index.IssueQuery; | ||||
import org.sonar.server.security.SecurityStandards; | import org.sonar.server.security.SecurityStandards; | ||||
import org.sonar.server.user.UserSession; | import org.sonar.server.user.UserSession; | ||||
import org.sonar.server.ws.MessageFormattingUtils; | |||||
import org.sonarqube.ws.Common; | import org.sonarqube.ws.Common; | ||||
import org.sonarqube.ws.Hotspots; | import org.sonarqube.ws.Hotspots; | ||||
import org.sonarqube.ws.Hotspots.SearchWsResponse; | import org.sonarqube.ws.Hotspots.SearchWsResponse; | ||||
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW; | 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; | ||||
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | 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.DateUtils.longToDate; | ||||
import static org.sonar.api.utils.Paging.forPageIndex; | import static org.sonar.api.utils.Paging.forPageIndex; | ||||
import static org.sonar.api.web.UserRole.USER; | import static org.sonar.api.web.UserRole.USER; | ||||
import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_INSECURE_INTERACTION; | import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_INSECURE_INTERACTION; | ||||
import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES; | 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.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_BRANCH_EXAMPLE_001; | ||||
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_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.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001; | ||||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | import static org.sonar.server.ws.WsUtils.writeProtobuf; | ||||
import static org.sonarqube.ws.WsUtils.nullToEmpty; | |||||
public class SearchAction implements HotspotsWsAction { | public class SearchAction implements HotspotsWsAction { | ||||
private static final Set<String> SUPPORTED_QUALIFIERS = Set.of(Qualifiers.PROJECT, Qualifiers.APP); | private static final Set<String> SUPPORTED_QUALIFIERS = Set.of(Qualifiers.PROJECT, Qualifiers.APP); | ||||
private final IssueIndex issueIndex; | private final IssueIndex issueIndex; | ||||
private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker; | private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker; | ||||
private final HotspotWsResponseFormatter responseFormatter; | private final HotspotWsResponseFormatter responseFormatter; | ||||
private final TextRangeResponseFormatter textRangeFormatter; | |||||
private final System2 system2; | private final System2 system2; | ||||
private final ComponentFinder componentFinder; | private final ComponentFinder componentFinder; | ||||
public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex, IssueIndexSyncProgressChecker issueIndexSyncProgressChecker, | 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.dbClient = dbClient; | ||||
this.userSession = userSession; | this.userSession = userSession; | ||||
this.issueIndex = issueIndex; | this.issueIndex = issueIndex; | ||||
this.issueIndexSyncProgressChecker = issueIndexSyncProgressChecker; | this.issueIndexSyncProgressChecker = issueIndexSyncProgressChecker; | ||||
this.responseFormatter = responseFormatter; | this.responseFormatter = responseFormatter; | ||||
this.textRangeFormatter = textRangeFormatter; | |||||
this.system2 = system2; | this.system2 = system2; | ||||
this.componentFinder = componentFinder; | this.componentFinder = componentFinder; | ||||
} | } | ||||
Optional<ProjectAndBranch> project = getAndValidateProjectOrApplication(dbSession, wsRequest); | Optional<ProjectAndBranch> project = getAndValidateProjectOrApplication(dbSession, wsRequest); | ||||
SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project.orElse(null)); | SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project.orElse(null)); | ||||
loadComponents(dbSession, searchResponseData); | loadComponents(dbSession, searchResponseData); | ||||
loadRules(dbSession, searchResponseData); | |||||
writeProtobuf(formatResponse(searchResponseData), request, response); | writeProtobuf(formatResponse(searchResponseData), request, response); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) { | 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())) | .flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid())) | ||||
.collect(Collectors.toSet()); | .collect(Collectors.toSet()); | ||||
Set<String> locationComponentUuids = searchResponseData.getOrderedHotspots() | |||||
Set<String> locationComponentUuids = searchResponseData.getHotspots() | |||||
.stream() | .stream() | ||||
.flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream()) | .flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream()) | ||||
.collect(Collectors.toSet()); | .collect(Collectors.toSet()); | ||||
return locationComponentUuids; | 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) { | private SearchWsResponse formatResponse(SearchResponseData searchResponseData) { | ||||
SearchWsResponse.Builder responseBuilder = SearchWsResponse.newBuilder(); | SearchWsResponse.Builder responseBuilder = SearchWsResponse.newBuilder(); | ||||
formatPaging(searchResponseData, responseBuilder); | formatPaging(searchResponseData, responseBuilder); | ||||
if (!searchResponseData.isEmpty()) { | |||||
formatHotspots(searchResponseData, responseBuilder); | |||||
if (searchResponseData.isPresent()) { | |||||
responseFormatter.formatHotspots(searchResponseData, responseBuilder); | |||||
formatComponents(searchResponseData, responseBuilder); | formatComponents(searchResponseData, responseBuilder); | ||||
} | } | ||||
return responseBuilder.build(); | return responseBuilder.build(); | ||||
responseBuilder.setPaging(pagingBuilder.build()); | responseBuilder.setPaging(pagingBuilder.build()); | ||||
} | } | ||||
private void formatHotspots(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) { | |||||
List<IssueDto> orderedHotspots = searchResponseData.getOrderedHotspots(); | |||||
if (orderedHotspots.isEmpty()) { | |||||
return; | |||||
} | |||||
SearchWsResponse.Hotspot.Builder builder = SearchWsResponse.Hotspot.newBuilder(); | |||||
for (IssueDto hotspot : orderedHotspots) { | |||||
RuleDto rule = searchResponseData.getRule(hotspot.getRuleKey()) | |||||
// due to join with table Rule when retrieving data from Issues, this can't happen | |||||
.orElseThrow(() -> new IllegalStateException(format( | |||||
"Rule with key '%s' not found for Hotspot '%s'", hotspot.getRuleKey(), hotspot.getKey()))); | |||||
SecurityStandards.SQCategory sqCategory = fromSecurityStandards(rule.getSecurityStandards()).getSqCategory(); | |||||
builder | |||||
.clear() | |||||
.setKey(hotspot.getKey()) | |||||
.setComponent(hotspot.getComponentKey()) | |||||
.setProject(hotspot.getProjectKey()) | |||||
.setSecurityCategory(sqCategory.getKey()) | |||||
.setVulnerabilityProbability(sqCategory.getVulnerability().name()) | |||||
.setRuleKey(hotspot.getRuleKey().toString()); | |||||
ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus); | |||||
ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution); | |||||
ofNullable(hotspot.getLine()).ifPresent(builder::setLine); | |||||
builder.setMessage(nullToEmpty(hotspot.getMessage())); | |||||
builder.addAllMessageFormattings(MessageFormattingUtils.dbMessageFormattingToWs(hotspot.parseMessageFormattings())); | |||||
ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee); | |||||
builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin())); | |||||
builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate())); | |||||
builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate())); | |||||
completeHotspotLocations(hotspot, builder, searchResponseData); | |||||
responseBuilder.addHotspots(builder.build()); | |||||
} | |||||
} | |||||
private void completeHotspotLocations(IssueDto hotspot, SearchWsResponse.Hotspot.Builder hotspotBuilder, SearchResponseData data) { | |||||
DbIssues.Locations locations = hotspot.parseLocations(); | |||||
if (locations == null) { | |||||
return; | |||||
} | |||||
textRangeFormatter.formatTextRange(locations, hotspotBuilder::setTextRange); | |||||
hotspotBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, hotspotBuilder.getComponent(), data.getComponentsByUuid())); | |||||
} | |||||
private void formatComponents(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) { | private void formatComponents(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) { | ||||
Collection<ComponentDto> components = searchResponseData.getComponents(); | Collection<ComponentDto> components = searchResponseData.getComponents(); | ||||
if (components.isEmpty()) { | if (components.isEmpty()) { | ||||
return files; | return files; | ||||
} | } | ||||
} | } | ||||
private static final class SearchResponseData { | |||||
private final Paging paging; | |||||
private final List<IssueDto> orderedHotspots; | |||||
private final Map<String, ComponentDto> componentsByUuid = new HashMap<>(); | |||||
private final Map<RuleKey, RuleDto> rulesByRuleKey = new HashMap<>(); | |||||
private final Map<String, BranchDto> branchesByBranchUuid = new HashMap<>(); | |||||
private SearchResponseData(Paging paging, List<IssueDto> orderedHotspots) { | |||||
this.paging = paging; | |||||
this.orderedHotspots = orderedHotspots; | |||||
} | |||||
boolean isEmpty() { | |||||
return orderedHotspots.isEmpty(); | |||||
} | |||||
public Paging getPaging() { | |||||
return paging; | |||||
} | |||||
List<IssueDto> getOrderedHotspots() { | |||||
return orderedHotspots; | |||||
} | |||||
void addComponents(Collection<ComponentDto> components) { | |||||
for (ComponentDto component : components) { | |||||
componentsByUuid.put(component.uuid(), component); | |||||
} | |||||
} | |||||
public void addBranches(List<BranchDto> branchDtos) { | |||||
for (BranchDto branch : branchDtos) { | |||||
branchesByBranchUuid.put(branch.getUuid(), branch); | |||||
} | |||||
} | |||||
public BranchDto getBranch(String branchUuid) { | |||||
return branchesByBranchUuid.get(branchUuid); | |||||
} | |||||
Collection<ComponentDto> getComponents() { | |||||
return componentsByUuid.values(); | |||||
} | |||||
public Map<String, ComponentDto> getComponentsByUuid() { | |||||
return componentsByUuid; | |||||
} | |||||
void addRules(Collection<RuleDto> rules) { | |||||
rules.forEach(t -> rulesByRuleKey.put(t.getKey(), t)); | |||||
} | |||||
Optional<RuleDto> getRule(RuleKey ruleKey) { | |||||
return ofNullable(rulesByRuleKey.get(ruleKey)); | |||||
} | |||||
} | |||||
} | } |
import org.sonar.server.issue.IssueChangeWSSupport; | import org.sonar.server.issue.IssueChangeWSSupport; | ||||
import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; | import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; | ||||
import org.sonar.server.issue.IssueChangeWSSupport.Load; | import org.sonar.server.issue.IssueChangeWSSupport.Load; | ||||
import org.sonar.server.issue.TextRangeResponseFormatter; | |||||
import org.sonar.server.issue.ws.UserResponseFormatter; | import org.sonar.server.issue.ws.UserResponseFormatter; | ||||
import org.sonar.server.security.SecurityStandards; | import org.sonar.server.security.SecurityStandards; | ||||
import org.sonar.server.ws.MessageFormattingUtils; | import org.sonar.server.ws.MessageFormattingUtils; | ||||
private final DbClient dbClient; | private final DbClient dbClient; | ||||
private final HotspotWsSupport hotspotWsSupport; | private final HotspotWsSupport hotspotWsSupport; | ||||
private final HotspotWsResponseFormatter responseFormatter; | private final HotspotWsResponseFormatter responseFormatter; | ||||
private final TextRangeResponseFormatter textRangeFormatter; | |||||
private final UserResponseFormatter userFormatter; | private final UserResponseFormatter userFormatter; | ||||
private final IssueChangeWSSupport issueChangeSupport; | 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) { | UserResponseFormatter userFormatter, IssueChangeWSSupport issueChangeSupport) { | ||||
this.dbClient = dbClient; | this.dbClient = dbClient; | ||||
this.hotspotWsSupport = hotspotWsSupport; | this.hotspotWsSupport = hotspotWsSupport; | ||||
this.responseFormatter = responseFormatter; | this.responseFormatter = responseFormatter; | ||||
this.textRangeFormatter = textRangeFormatter; | |||||
this.userFormatter = userFormatter; | this.userFormatter = userFormatter; | ||||
this.issueChangeSupport = issueChangeSupport; | this.issueChangeSupport = issueChangeSupport; | ||||
} | } | ||||
} | } | ||||
private void formatTextRange(ShowWsResponse.Builder hotspotBuilder, IssueDto hotspot) { | 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) { | private void formatFlows(DbSession dbSession, ShowWsResponse.Builder hotspotBuilder, IssueDto hotspot) { | ||||
Set<String> componentUuids = readComponentUuidsFromLocations(hotspot, locations); | Set<String> componentUuids = readComponentUuidsFromLocations(hotspot, locations); | ||||
Map<String, ComponentDto> componentsByUuids = loadComponents(dbSession, componentUuids); | 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) { | private static Set<String> readComponentUuidsFromLocations(IssueDto hotspot, Locations locations) { | ||||
BranchDto branch = projectAndBranch.getBranch(); | BranchDto branch = projectAndBranch.getBranch(); | ||||
ComponentDto component = dbClient.componentDao().selectByUuid(dbSession, componentUuid) | ComponentDto component = dbClient.componentDao().selectByUuid(dbSession, componentUuid) | ||||
.orElseThrow(() -> new NotFoundException(format("Component with uuid '%s' does not exist", 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); | return new Components(projectAndBranch.getProject(), component, branch); | ||||
} | } | ||||
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2023 SonarSource SA | |||||
* mailto:info AT sonarsource DOT com | |||||
* | |||||
* This program is free software; you can redistribute it and/or | |||||
* modify it under the terms of the GNU Lesser General Public | |||||
* License as published by the Free Software Foundation; either | |||||
* version 3 of the License, or (at your option) any later version. | |||||
* | |||||
* This program is distributed in the hope that it will be useful, | |||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||||
* Lesser General Public License for more details. | |||||
* | |||||
* You should have received a copy of the GNU Lesser General Public License | |||||
* along with this program; if not, write to the Free Software Foundation, | |||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||||
*/ | |||||
package org.sonar.server.issue; | |||||
import java.time.Clock; | |||||
import java.util.Optional; | |||||
import javax.annotation.Nullable; | |||||
import org.sonar.db.DbClient; | |||||
import org.sonar.db.DbSession; | |||||
import org.sonar.db.component.ComponentDto; | |||||
import org.sonar.db.component.SnapshotDto; | |||||
import org.sonar.db.newcodeperiod.NewCodePeriodType; | |||||
import static com.google.common.base.Strings.isNullOrEmpty; | |||||
import static java.lang.String.format; | |||||
import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY; | |||||
import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; | |||||
public class NewCodePeriodResolver { | |||||
private final DbClient dbClient; | |||||
private final Clock clock; | |||||
public NewCodePeriodResolver(DbClient dbClient, Clock clock) { | |||||
this.dbClient = dbClient; | |||||
this.clock = clock; | |||||
} | |||||
public ResolvedNewCodePeriod resolveForProjectAndBranch(DbSession dbSession, String projectKey, String branchKey) { | |||||
ComponentDto componentDto = dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branchKey) | |||||
.orElseThrow(() -> new IllegalStateException(format("Could not find component for project: %s, branch: %s", projectKey, branchKey))); | |||||
Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, componentDto); | |||||
if (snapshot.isPresent() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) { | |||||
return new ResolvedNewCodePeriod(REFERENCE_BRANCH, null); | |||||
} else { | |||||
// if last analysis has no period date, then no issue should be considered new. | |||||
long createdAfterFromSnapshot = snapshot.map(SnapshotDto::getPeriodDate).orElse(clock.millis()); | |||||
return new ResolvedNewCodePeriod(snapshot.map(SnapshotDto::getPeriodMode).map(NewCodePeriodType::valueOf).orElse(null), createdAfterFromSnapshot); | |||||
} | |||||
} | |||||
private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, ComponentDto component) { | |||||
return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid()); | |||||
} | |||||
private boolean isLastAnalysisFromReAnalyzedReferenceBranch(DbSession dbSession, SnapshotDto snapshot) { | |||||
return isLastAnalysisUsingReferenceBranch(snapshot) && | |||||
isLastAnalysisFromSonarQube94Onwards(dbSession, snapshot.getRootComponentUuid()); | |||||
} | |||||
private boolean isLastAnalysisFromSonarQube94Onwards(DbSession dbSession, String componentUuid) { | |||||
return dbClient.liveMeasureDao().selectMeasure(dbSession, componentUuid, ANALYSIS_FROM_SONARQUBE_9_4_KEY).isPresent(); | |||||
} | |||||
private static boolean isLastAnalysisUsingReferenceBranch(SnapshotDto snapshot) { | |||||
return !isNullOrEmpty(snapshot.getPeriodMode()) && REFERENCE_BRANCH.name().equals(snapshot.getPeriodMode()); | |||||
} | |||||
public record ResolvedNewCodePeriod(@Nullable NewCodePeriodType type, @Nullable Long periodDate) { | |||||
} | |||||
} |
import org.sonar.server.issue.IssueChangeWSSupport; | import org.sonar.server.issue.IssueChangeWSSupport; | ||||
import org.sonar.server.issue.IssueFieldsSetter; | import org.sonar.server.issue.IssueFieldsSetter; | ||||
import org.sonar.server.issue.IssueFinder; | import org.sonar.server.issue.IssueFinder; | ||||
import org.sonar.server.issue.NewCodePeriodResolver; | |||||
import org.sonar.server.issue.TaintChecker; | import org.sonar.server.issue.TaintChecker; | ||||
import org.sonar.server.issue.TextRangeResponseFormatter; | import org.sonar.server.issue.TextRangeResponseFormatter; | ||||
import org.sonar.server.issue.TransitionService; | import org.sonar.server.issue.TransitionService; | ||||
UserResponseFormatter.class, | UserResponseFormatter.class, | ||||
SearchResponseFormat.class, | SearchResponseFormat.class, | ||||
OperationResponseWriter.class, | OperationResponseWriter.class, | ||||
NewCodePeriodResolver.class, | |||||
AddCommentAction.class, | AddCommentAction.class, | ||||
EditCommentAction.class, | EditCommentAction.class, | ||||
DeleteCommentAction.class, | DeleteCommentAction.class, |
*/ | */ | ||||
package org.sonar.server.issue.ws; | package org.sonar.server.issue.ws; | ||||
import java.time.Clock; | |||||
import com.google.common.base.Preconditions; | import com.google.common.base.Preconditions; | ||||
import java.util.EnumSet; | import java.util.EnumSet; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Optional; | |||||
import javax.annotation.Nullable; | import javax.annotation.Nullable; | ||||
import org.sonar.api.rules.RuleType; | import org.sonar.api.rules.RuleType; | ||||
import org.sonar.api.server.ws.Request; | import org.sonar.api.server.ws.Request; | ||||
import org.sonar.db.Pagination; | import org.sonar.db.Pagination; | ||||
import org.sonar.db.component.BranchDto; | import org.sonar.db.component.BranchDto; | ||||
import org.sonar.db.component.ComponentDto; | import org.sonar.db.component.ComponentDto; | ||||
import org.sonar.db.component.SnapshotDto; | |||||
import org.sonar.db.issue.IssueDto; | import org.sonar.db.issue.IssueDto; | ||||
import org.sonar.db.issue.IssueListQuery; | import org.sonar.db.issue.IssueListQuery; | ||||
import org.sonar.db.newcodeperiod.NewCodePeriodType; | |||||
import org.sonar.db.project.ProjectDto; | import org.sonar.db.project.ProjectDto; | ||||
import org.sonar.server.component.ComponentFinder; | import org.sonar.server.component.ComponentFinder; | ||||
import org.sonar.server.component.ComponentFinder.ProjectAndBranch; | 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.sonar.server.user.UserSession; | ||||
import org.sonarqube.ws.Common; | import org.sonarqube.ws.Common; | ||||
import org.sonarqube.ws.Issues; | import org.sonarqube.ws.Issues; | ||||
import static com.google.common.base.Strings.isNullOrEmpty; | import static com.google.common.base.Strings.isNullOrEmpty; | ||||
import static java.lang.String.format; | |||||
import static java.util.Collections.singletonList; | 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; | ||||
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | ||||
import static org.sonar.api.utils.Paging.forPageIndex; | 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.es.SearchOptions.MAX_PAGE_SIZE; | ||||
import static org.sonar.server.issue.index.IssueQueryFactory.ISSUE_STATUSES; | import static org.sonar.server.issue.index.IssueQueryFactory.ISSUE_STATUSES; | ||||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | import static org.sonar.server.ws.WsUtils.writeProtobuf; | ||||
private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod"; | private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod"; | ||||
private final UserSession userSession; | private final UserSession userSession; | ||||
private final DbClient dbClient; | private final DbClient dbClient; | ||||
private final Clock clock; | |||||
private final NewCodePeriodResolver newCodePeriodResolver; | |||||
private final SearchResponseLoader searchResponseLoader; | private final SearchResponseLoader searchResponseLoader; | ||||
private final SearchResponseFormat searchResponseFormat; | private final SearchResponseFormat searchResponseFormat; | ||||
private final ComponentFinder componentFinder; | 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.userSession = userSession; | ||||
this.dbClient = dbClient; | this.dbClient = dbClient; | ||||
this.clock = clock; | |||||
this.newCodePeriodResolver = newCodePeriodResolver; | |||||
this.searchResponseLoader = searchResponseLoader; | this.searchResponseLoader = searchResponseLoader; | ||||
this.searchResponseFormat = searchResponseFormat; | this.searchResponseFormat = searchResponseFormat; | ||||
this.componentFinder = componentFinder; | this.componentFinder = componentFinder; | ||||
.statuses(ISSUE_STATUSES) | .statuses(ISSUE_STATUSES) | ||||
.types(wsRequest.types); | .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); | Pagination pagination = Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize); | ||||
} | } | ||||
} | } | ||||
private void setNewCodePeriod(DbSession dbSession, WsRequest wsRequest, IssueListQuery.IssueListQueryBuilder queryBuilder) { | |||||
ComponentDto componentDto = dbClient.componentDao().selectByKeyAndBranch(dbSession, wsRequest.project, wsRequest.branch) | |||||
.orElseThrow(() -> new IllegalStateException(format("Could not find component for project: %s, branch: %s", wsRequest.project, wsRequest.branch))); | |||||
Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, componentDto); | |||||
if (snapshot.isPresent() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) { | |||||
queryBuilder.newCodeOnReference(true); | |||||
} else { | |||||
// if last analysis has no period date, then no issue should be considered new. | |||||
long createdAfterFromSnapshot = snapshot.map(SnapshotDto::getPeriodDate).orElse(clock.millis()); | |||||
queryBuilder.createdAfter(createdAfterFromSnapshot); | |||||
} | |||||
} | |||||
private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, ComponentDto component) { | |||||
return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid()); | |||||
} | |||||
private boolean isLastAnalysisFromReAnalyzedReferenceBranch(DbSession dbSession, SnapshotDto snapshot) { | |||||
return isLastAnalysisUsingReferenceBranch(snapshot) && | |||||
isLastAnalysisFromSonarQube94Onwards(dbSession, snapshot.getRootComponentUuid()); | |||||
} | |||||
private boolean isLastAnalysisFromSonarQube94Onwards(DbSession dbSession, String componentUuid) { | |||||
return dbClient.liveMeasureDao().selectMeasure(dbSession, componentUuid, ANALYSIS_FROM_SONARQUBE_9_4_KEY).isPresent(); | |||||
} | |||||
private static boolean isLastAnalysisUsingReferenceBranch(SnapshotDto snapshot) { | |||||
return !isNullOrEmpty(snapshot.getPeriodMode()) && REFERENCE_BRANCH.name().equals(snapshot.getPeriodMode()); | |||||
} | |||||
private Issues.ListWsResponse formatResponse(WsRequest request, List<IssueDto> issues) { | private Issues.ListWsResponse formatResponse(WsRequest request, List<IssueDto> issues) { | ||||
Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder(); | Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder(); | ||||
response.setPaging(Common.Paging.newBuilder() | response.setPaging(Common.Paging.newBuilder() |
Issues.ListWsResponse formatList(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging) { | Issues.ListWsResponse formatList(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging) { | ||||
Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder(); | 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.addAllIssues(createIssues(fields, data)); | ||||
response.addAllComponents(formatComponents(data)); | |||||
return response.build(); | return response.build(); | ||||
} | } | ||||
public void verify_count_of_added_components() { | public void verify_count_of_added_components() { | ||||
ListContainer container = new ListContainer(); | ListContainer container = new ListContainer(); | ||||
new HotspotsWsModule().configure(container); | new HotspotsWsModule().configure(container); | ||||
assertThat(container.getAddedObjects()).hasSize(12); | |||||
assertThat(container.getAddedObjects()).hasSize(13); | |||||
} | } | ||||
} | } |
} | } | ||||
} | } | ||||
// Response of GET api/hotspots/list | |||||
message ListWsResponse { | |||||
optional sonarqube.ws.commons.Paging paging = 1; | |||||
repeated SearchWsResponse.Hotspot hotspots = 2; | |||||
} | |||||
// Response of GET api/hotspots/show | // Response of GET api/hotspots/show | ||||
message ShowWsResponse { | message ShowWsResponse { | ||||
optional string key = 1; | optional string key = 1; |
message ListWsResponse { | message ListWsResponse { | ||||
optional sonarqube.ws.commons.Paging paging = 1; | optional sonarqube.ws.commons.Paging paging = 1; | ||||
repeated Issue issues = 2; | repeated Issue issues = 2; | ||||
repeated Component components = 3; | |||||
} | } |