Browse Source

SONAR-21259 added new param to api/issues/search

tags/10.4.0.87286
lukasz-jarocki-sonarsource 4 months ago
parent
commit
90af6b13ef

+ 0
- 7
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java View File

@@ -77,13 +77,6 @@ public class BranchDao implements Dao {
return mapper(dbSession).selectByBranchKeys(branchKeyByProjectUuid);
}

public List<BranchDto> selectByPullRequestKeys(DbSession dbSession, Map<String, String> prKeyByProjectUuid) {
if (prKeyByProjectUuid.isEmpty()) {
return emptyList();
}
return mapper(dbSession).selectByPullRequestKeys(prKeyByProjectUuid);
}

public Optional<BranchDto> selectByPullRequestKey(DbSession dbSession, String projectUuid, String key) {
return selectByKey(dbSession, projectUuid, key, BranchType.PULL_REQUEST);
}

+ 11
- 0
server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java View File

@@ -75,6 +75,7 @@ public class SearchRequest {
private String timeZone;
private Integer owaspAsvsLevel;
private List<String> codeVariants;
private String fixedInPullRequest;

public SearchRequest() {
// nothing to do here
@@ -553,4 +554,14 @@ public class SearchRequest {
this.cleanCodeAttributesCategories = cleanCodeAttributesCategories;
return this;
}

@CheckForNull
public String getFixedInPullRequest() {
return fixedInPullRequest;
}

public SearchRequest setFixedInPullRequest(@Nullable String fixedInPullRequest) {
this.fixedInPullRequest = fixedInPullRequest;
return this;
}
}

+ 14
- 1
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java View File

@@ -465,7 +465,7 @@ public class IssueIndex {
}

// Field Filters
filters.addFilter(FIELD_ISSUE_KEY, new SimpleFieldFilterScope(FIELD_ISSUE_KEY), createTermsFilter(FIELD_ISSUE_KEY, query.issueKeys()));
filters.addFilter(FIELD_ISSUE_KEY, new SimpleFieldFilterScope(FIELD_ISSUE_KEY), createTermsFilterForNullableCollection(FIELD_ISSUE_KEY, query.issueKeys()));
filters.addFilter(FIELD_ISSUE_ASSIGNEE_UUID, ASSIGNEES.getFilterScope(), createTermsFilter(FIELD_ISSUE_ASSIGNEE_UUID, query.assignees()));
filters.addFilter(FIELD_ISSUE_SCOPE, SCOPES.getFilterScope(), createTermsFilter(FIELD_ISSUE_SCOPE, query.scopes()));
filters.addFilter(FIELD_ISSUE_LANGUAGE, LANGUAGES.getFilterScope(), createTermsFilter(FIELD_ISSUE_LANGUAGE, query.languages()));
@@ -741,11 +741,24 @@ public class IssueIndex {
return FACET_MODE_EFFORT.equals(query.facetMode());
}

/**
* This method is for creating a filter that passes null to the elasticsearch query whenever empty or null collection is passed.
* This means that filter will not filter anything, all the documents (issues) will be returned in this case.
*/
@CheckForNull
private static QueryBuilder createTermsFilter(String field, Collection<?> values) {
return values.isEmpty() ? null : termsQuery(field, values);
}

/**
* This method is for creating a filter that passes null to the elasticsearch query only when null collection is passed.
* This ensures that whenever we pass empty collection to the filter, it will filter out all the documents (issues).
*/
@CheckForNull
private static QueryBuilder createTermsFilterForNullableCollection(String field, @Nullable Collection<?> values) {
return values != null ? termsQuery(field, values) : null;
}

@CheckForNull
private static QueryBuilder createTermFilter(String field, @Nullable String value) {
return value == null ? null : termQuery(field, value);

+ 5
- 1
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java View File

@@ -105,7 +105,7 @@ public class IssueQuery {
private final Collection<String> cleanCodeAttributesCategories;

private IssueQuery(Builder builder) {
this.issueKeys = defaultCollection(builder.issueKeys);
this.issueKeys = nullableDefaultCollection(builder.issueKeys);
this.severities = defaultCollection(builder.severities);
this.impactSeverities = defaultCollection(builder.impactSeverities);
this.impactSoftwareQualities = defaultCollection(builder.impactSoftwareQualities);
@@ -679,6 +679,10 @@ public class IssueQuery {
return c == null ? Collections.emptyList() : Collections.unmodifiableCollection(c);
}

private static <T> Collection<T> nullableDefaultCollection(@Nullable Collection<T> c) {
return c == null ? null : Collections.unmodifiableCollection(c);
}

private static <K, V> Map<K, V> defaultMap(@Nullable Map<K, V> map) {
return map == null ? Collections.emptyMap() : Collections.unmodifiableMap(map);
}

+ 66
- 10
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java View File

@@ -53,7 +53,9 @@ import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.issue.IssueFixedDto;
import org.sonar.db.permission.GlobalPermission;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.server.issue.SearchRequest;
import org.sonar.server.issue.index.IssueQuery.PeriodStart;
@@ -79,6 +81,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENTS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_UUIDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AFTER;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_IN_LAST;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FIXED_IN_PULL_REQUEST;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IN_NEW_CODE_PERIOD;

/**
@@ -99,7 +102,8 @@ public class IssueQueryFactory {
.map(Enum::name)
.collect(Collectors.toSet());
private static final ComponentDto UNKNOWN_COMPONENT = new ComponentDto().setUuid(UNKNOWN).setBranchUuid(UNKNOWN);
private static final Set<String> QUALIFIERS_WITHOUT_LEAK_PERIOD = new HashSet<>(Arrays.asList(Qualifiers.APP, Qualifiers.VIEW, Qualifiers.SUBVIEW));
private static final Set<String> QUALIFIERS_WITHOUT_LEAK_PERIOD = new HashSet<>(Arrays.asList(Qualifiers.APP, Qualifiers.VIEW,
Qualifiers.SUBVIEW));
private final DbClient dbClient;
private final Clock clock;
private final UserSession userSession;
@@ -116,13 +120,14 @@ public class IssueQueryFactory {

Collection<RuleDto> ruleDtos = ruleKeysToRuleId(dbSession, request.getRules());
Collection<String> ruleUuids = ruleDtos.stream().map(RuleDto::getUuid).collect(Collectors.toSet());
Collection<String> issueKeys = collectIssueKeys(dbSession, request);

if (request.getRules() != null && request.getRules().stream().collect(Collectors.toSet()).size() != ruleDtos.size()) {
ruleUuids.add("non-existing-uuid");
}

IssueQuery.Builder builder = IssueQuery.builder()
.issueKeys(request.getIssues())
.issueKeys(issueKeys)
.severities(request.getSeverities())
.cleanCodeAttributesCategories(request.getCleanCodeAttributesCategories())
.impactSoftwareQualities(request.getImpactSoftwareQualities())
@@ -169,6 +174,49 @@ public class IssueQueryFactory {
}
}

private Collection<String> collectIssueKeys(DbSession dbSession, SearchRequest request) {
Collection<String> issueKeys = null;
if (request.getFixedInPullRequest() != null) {
issueKeys = getIssuesFixedByPullRequest(dbSession, request);
}
if (request.getIssues() != null && !request.getIssues().isEmpty()) {
if (issueKeys == null) {
issueKeys = new ArrayList<>();
}
issueKeys.addAll(request.getIssues());
}

return issueKeys;
}

private Collection<String> getIssuesFixedByPullRequest(DbSession dbSession, SearchRequest request) {
String fixedInPullRequest = request.getFixedInPullRequest();
List<String> componentKeys = request.getComponentKeys();
if (componentKeys == null || componentKeys.size() != 1) {
throw new IllegalArgumentException("Exactly one project needs to be provided in the " +
"'" + PARAM_COMPONENTS + "' param when used together with '" + PARAM_FIXED_IN_PULL_REQUEST + "' param");
}
String projectKey = componentKeys.get(0);
ProjectDto projectDto = dbClient.projectDao().selectProjectByKey(dbSession, projectKey)
.orElseThrow(() -> new IllegalArgumentException("Project with key '" + projectKey + "' does not exist"));
BranchDto pullRequest = dbClient.branchDao().selectByPullRequestKey(dbSession, projectDto.getUuid(), fixedInPullRequest)
.orElseThrow(() -> new IllegalArgumentException("Pull request with key '" + fixedInPullRequest + "' does not exist for a project " +
projectKey));

if (request.getBranch() != null) {
BranchDto targetBranch = dbClient.branchDao().selectByBranchKey(dbSession, projectDto.getUuid(), request.getBranch())
.orElseThrow(() -> new IllegalArgumentException("Branch with key '" + request.getBranch() + "' does not exist"));
if (!Objects.equals(targetBranch.getUuid(), pullRequest.getMergeBranchUuid())) {
throw new IllegalArgumentException("Pull request with key '" + fixedInPullRequest + "' does not target branch '" + request.getBranch() + "'");
}
}
return dbClient.issueFixedDao().selectByPullRequest(dbSession, pullRequest.getUuid())
.stream()
.map(IssueFixedDto::issueKey)
.collect(Collectors.toSet());
}


private static Optional<ZoneId> parseTimeZone(@Nullable String timeZone) {
if (timeZone == null) {
return Optional.empty();
@@ -181,7 +229,8 @@ public class IssueQueryFactory {
}
}

private void setCreatedAfterFromDates(IssueQuery.Builder builder, @Nullable Date createdAfter, @Nullable String createdInLast, boolean createdAfterInclusive) {
private void setCreatedAfterFromDates(IssueQuery.Builder builder, @Nullable Date createdAfter, @Nullable String createdInLast,
boolean createdAfterInclusive) {
Date actualCreatedAfter = createdAfter;
if (createdInLast != null) {
actualCreatedAfter = Date.from(
@@ -192,16 +241,19 @@ public class IssueQueryFactory {
builder.createdAfter(actualCreatedAfter, createdAfterInclusive);
}

private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request, List<ComponentDto> componentUuids, ZoneId timeZone) {
private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request,
List<ComponentDto> componentUuids, ZoneId timeZone) {
Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter(), timeZone);
String createdInLast = request.getCreatedInLast();

if (notInNewCodePeriod(request)) {
checkArgument(createdAfter == null || createdInLast == null, format("Parameters %s and %s cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_CREATED_IN_LAST));
checkArgument(createdAfter == null || createdInLast == null, format("Parameters %s and %s cannot be set simultaneously",
PARAM_CREATED_AFTER, PARAM_CREATED_IN_LAST));
setCreatedAfterFromDates(builder, createdAfter, createdInLast, true);
} else {
// If the filter is on leak period
checkArgument(createdAfter == null, "Parameters '%s' and '%s' cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_IN_NEW_CODE_PERIOD);
checkArgument(createdAfter == null, "Parameters '%s' and '%s' cannot be set simultaneously", PARAM_CREATED_AFTER,
PARAM_IN_NEW_CODE_PERIOD);
checkArgument(createdInLast == null,
format("Parameters '%s' and '%s' cannot be set simultaneously", PARAM_CREATED_IN_LAST, PARAM_IN_NEW_CODE_PERIOD));

@@ -306,7 +358,8 @@ public class IssueQueryFactory {
.collect(Collectors.toSet());
}

private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> components, SearchRequest request) {
private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> components,
SearchRequest request) {
if (components.isEmpty()) {
return;
}
@@ -369,7 +422,8 @@ public class IssueQueryFactory {
builder.viewUuids(filteredViewUuids);
}

private void addApplications(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> appBranchComponents, SearchRequest request) {
private void addApplications(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> appBranchComponents,
SearchRequest request) {
Set<String> authorizedAppBranchUuids = appBranchComponents.stream()
.filter(app -> userSession.hasComponentPermission(USER, app) && userSession.hasChildProjectsPermission(USER, app))
.map(ComponentDto::uuid)
@@ -379,7 +433,8 @@ public class IssueQueryFactory {
addCreatedAfterByProjects(builder, dbSession, request, authorizedAppBranchUuids);
}

private void addCreatedAfterByProjects(IssueQuery.Builder builder, DbSession dbSession, SearchRequest request, Set<String> appBranchUuids) {
private void addCreatedAfterByProjects(IssueQuery.Builder builder, DbSession dbSession, SearchRequest request,
Set<String> appBranchUuids) {
if (notInNewCodePeriod(request) || request.getPullRequest() != null) {
return;
}
@@ -415,7 +470,8 @@ public class IssueQueryFactory {
builder.directories(paths);
}

private List<ComponentDto> getComponentsFromKeys(DbSession dbSession, Collection<String> componentKeys, @Nullable String branch, @Nullable String pullRequest) {
private List<ComponentDto> getComponentsFromKeys(DbSession dbSession, Collection<String> componentKeys, @Nullable String branch,
@Nullable String pullRequest) {
List<ComponentDto> componentDtos = dbClient.componentDao().selectByKeys(dbSession, componentKeys, branch, pullRequest);
if (!componentKeys.isEmpty() && componentDtos.isEmpty()) {
return singletonList(UNKNOWN_COMPONENT);

+ 58
- 0
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java View File

@@ -135,6 +135,64 @@ public class IssueQueryFactoryTest {
assertThat(query.codeVariants()).containsOnly("variant1", "variant2");
}

@Test
public void getIssuesFixedByPullRequest_returnIssuesFixedByThePullRequest() {
String ruleAdHocName = "New Name";
UserDto user = db.users().insertUser(u -> u.setLogin("joanna"));
ProjectData projectData = db.components().insertPrivateProject();
ComponentDto project = projectData.getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));

RuleDto rule1 = ruleDbTester.insert(r -> r.setAdHocName(ruleAdHocName));
RuleDto rule2 = ruleDbTester.insert(r -> r.setAdHocName(ruleAdHocName));
newRule(RuleKey.of("findbugs", "NullReference"));
SearchRequest request = new SearchRequest()
.setIssues(asList("anIssueKey"))
.setSeverities(asList("MAJOR", "MINOR"))
.setStatuses(asList("CLOSED"))
.setResolutions(asList("FALSE-POSITIVE"))
.setResolved(true)
.setProjectKeys(asList(project.getKey()))
.setDirectories(asList("aDirPath"))
.setFiles(asList(file.uuid()))
.setAssigneesUuid(asList(user.getUuid()))
.setScopes(asList("MAIN", "TEST"))
.setLanguages(asList("xoo"))
.setTags(asList("tag1", "tag2"))
.setAssigned(true)
.setCreatedAfter("2013-04-16T09:08:24+0200")
.setCreatedBefore("2013-04-17T09:08:24+0200")
.setRules(asList(rule1.getKey().toString(), rule2.getKey().toString()))
.setSort("CREATION_DATE")
.setAsc(true)
.setCodeVariants(asList("variant1", "variant2"));

IssueQuery query = underTest.create(request);

assertThat(query.issueKeys()).containsOnly("anIssueKey");
assertThat(query.severities()).containsOnly("MAJOR", "MINOR");
assertThat(query.statuses()).containsOnly("CLOSED");
assertThat(query.resolutions()).containsOnly("FALSE-POSITIVE");
assertThat(query.resolved()).isTrue();
assertThat(query.projectUuids()).containsOnly(projectData.projectUuid());
assertThat(query.files()).containsOnly(file.uuid());
assertThat(query.assignees()).containsOnly(user.getUuid());
assertThat(query.scopes()).containsOnly("TEST", "MAIN");
assertThat(query.languages()).containsOnly("xoo");
assertThat(query.tags()).containsOnly("tag1", "tag2");
assertThat(query.onComponentOnly()).isFalse();
assertThat(query.assigned()).isTrue();
assertThat(query.rules()).hasSize(2);
assertThat(query.ruleUuids()).hasSize(2);
assertThat(query.directories()).containsOnly("aDirPath");
assertThat(query.createdAfter().date()).isEqualTo(parseDateTime("2013-04-16T09:08:24+0200"));
assertThat(query.createdAfter().inclusive()).isTrue();
assertThat(query.createdBefore()).isEqualTo(parseDateTime("2013-04-17T09:08:24+0200"));
assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE);
assertThat(query.asc()).isTrue();
assertThat(query.codeVariants()).containsOnly("variant1", "variant2");
}

@Test
public void create_with_rule_key_that_does_not_exist_in_the_db() {
db.users().insertUser(u -> u.setLogin("joanna"));

+ 3
- 3
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java View File

@@ -153,7 +153,7 @@ public class IssueQueryTest {
}

@Test
public void collection_params_should_not_be_null_but_empty() {
public void collection_params_should_not_be_null_but_empty_except_issue_keys() {
IssueQuery query = IssueQuery.builder()
.issueKeys(null)
.projectUuids(null)
@@ -171,7 +171,7 @@ public class IssueQueryTest {
.cwe(null)
.createdAfterByProjectUuids(null)
.build();
assertThat(query.issueKeys()).isEmpty();
assertThat(query.issueKeys()).isNull();
assertThat(query.projectUuids()).isEmpty();
assertThat(query.componentUuids()).isEmpty();
assertThat(query.statuses()).isEmpty();
@@ -191,7 +191,6 @@ public class IssueQueryTest {
@Test
public void test_default_query() {
IssueQuery query = IssueQuery.builder().build();
assertThat(query.issueKeys()).isEmpty();
assertThat(query.projectUuids()).isEmpty();
assertThat(query.componentUuids()).isEmpty();
assertThat(query.statuses()).isEmpty();
@@ -202,6 +201,7 @@ public class IssueQueryTest {
assertThat(query.languages()).isEmpty();
assertThat(query.tags()).isEmpty();
assertThat(query.types()).isEmpty();
assertThat(query.issueKeys()).isNull();
assertThat(query.branchUuid()).isNull();
assertThat(query.assigned()).isNull();
assertThat(query.createdAfter()).isNull();

+ 180
- 7
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java View File

@@ -54,6 +54,7 @@ import org.sonar.core.util.Uuids;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ProjectData;
@@ -61,6 +62,7 @@ import org.sonar.db.component.SnapshotDto;
import org.sonar.db.issue.ImpactDto;
import org.sonar.db.issue.IssueChangeDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.issue.IssueFixedDto;
import org.sonar.db.permission.GroupPermissionDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.protobuf.DbCommons;
@@ -1921,10 +1923,10 @@ public class SearchActionIT {

@Test
public void fail_if_trying_to_filter_issues_by_hotspots() {
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
ComponentDto mainBranch = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(mainBranch));
RuleDto hotspotRule = newHotspotRule();
db.issues().insertHotspot(hotspotRule, project, file);
db.issues().insertHotspot(hotspotRule, mainBranch, file);
insertIssues(i -> i.setType(RuleType.BUG), i -> i.setType(RuleType.VULNERABILITY),
i -> i.setType(RuleType.CODE_SMELL));
indexPermissionsAndIssues();
@@ -2082,7 +2084,7 @@ public class SearchActionIT {
"createdBefore", "createdInLast", "directories", "facets", "files", "issues", "scopes", "languages", "onComponentOnly",
"p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "statuses", "tags", "types", "pciDss-3.2", "pciDss-4.0", "owaspAsvs-4.0",
"owaspAsvsLevel", "owaspTop10", "owaspTop10-2021", "sansTop25", "cwe", "sonarsourceSecurity", "timeZone", "inNewCodePeriod", "codeVariants",
"cleanCodeAttributeCategories", "impactSeverities", "impactSoftwareQualities", "issueStatuses");
"cleanCodeAttributeCategories", "impactSeverities", "impactSoftwareQualities", "issueStatuses", "fixedInPullRequest");

WebService.Param branch = def.param(PARAM_BRANCH);
assertThat(branch.isInternal()).isFalse();
@@ -2145,6 +2147,167 @@ public class SearchActionIT {
.extracting(Issue::getRuleDescriptionContextKey).containsExactly("spring");
}

@Test
public void search_whenFixedInPullRequestSetAndNoComponentsSet_throwException() {
TestRequest request = ws.newRequest()
.setParam("fixedInPullRequest", "1000");

assertThatThrownBy(request::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Exactly one project needs to be provided in the 'components' param when used together with 'fixedInPullRequest' param");
}

@Test
public void search_whenFixedInPullRequestSetAndWrongBranchIsSet_throwException() {
String pullRequestId = "1000";
String pullRequestUuid = "pullRequestUuid";
userSession.logIn(db.users().insertUser());
ProjectData project = db.components().insertPublicProject();
db.getDbClient().branchDao().insert(session, new BranchDto()
.setUuid(pullRequestUuid)
.setProjectUuid(project.projectUuid())
.setKey(pullRequestId)
.setIsMain(false)
.setBranchType(BranchType.PULL_REQUEST)
.setMergeBranchUuid(project.mainBranchUuid()));
db.getDbClient().branchDao().insert(session, new BranchDto()
.setUuid("wrongBranchUuid")
.setProjectUuid(project.projectUuid())
.setKey("wrongBranch")
.setIsMain(false)
.setBranchType(BranchType.BRANCH)
.setMergeBranchUuid("wrongTargetBranchUuid"));

session.commit();
TestRequest request = ws.newRequest().setParam("fixedInPullRequest", pullRequestId).setParam("components", project.projectKey())
.setParam("branch", "wrongBranch");

assertThatThrownBy(request::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Pull request with key '1000' does not target branch 'wrongBranch'");
}

@Test
public void search_whenFixedInPullRequestSetAndProjectDoesNotExist_throwException() {
String pullRequestId = "1000";
String pullRequestUuid = "pullRequestUuid";
userSession.logIn(db.users().insertUser());
ProjectData project = db.components().insertPublicProject();
db.getDbClient().branchDao().insert(session, new BranchDto()
.setUuid(pullRequestUuid)
.setProjectUuid(project.projectUuid())
.setKey(pullRequestId)
.setIsMain(false)
.setBranchType(BranchType.PULL_REQUEST)
.setMergeBranchUuid(project.mainBranchUuid()));

session.commit();
TestRequest request = ws.newRequest().setParam("fixedInPullRequest", pullRequestId).setParam("components", "nonExistingProjectKey");

assertThatThrownBy(request::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Project with key 'nonExistingProjectKey' does not exist");
}

@Test
public void search_whenWrongFixedInPullRequestSet_throwException() {
String pullRequestId = "wrongPullRequest";
String pullRequestUuid = "pullRequestUuid";
userSession.logIn(db.users().insertUser());
ProjectData project = db.components().insertPublicProject();
db.getDbClient().branchDao().insert(session, new BranchDto()
.setUuid(pullRequestUuid)
.setProjectUuid(project.projectUuid())
.setKey("pullRequestId")
.setIsMain(false)
.setBranchType(BranchType.PULL_REQUEST)
.setMergeBranchUuid(project.mainBranchUuid()));

session.commit();
TestRequest request = ws.newRequest().setParam("fixedInPullRequest", pullRequestId).setParam("components", project.projectKey());

assertThatThrownBy(request::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Pull request with key 'wrongPullRequest' does not exist for a project " + project.projectKey());
}

@Test
public void search_whenFixedInPullRequestSetAndNonExistingBranchIsSet_throwException() {
String pullRequestId = "1000";
String pullRequestUuid = "pullRequestUuid";
userSession.logIn(db.users().insertUser());
ProjectData project = db.components().insertPublicProject();
db.getDbClient().branchDao().insert(session, new BranchDto()
.setUuid(pullRequestUuid)
.setProjectUuid(project.projectUuid())
.setKey(pullRequestId)
.setIsMain(false)
.setBranchType(BranchType.PULL_REQUEST)
.setMergeBranchUuid(project.mainBranchUuid()));

session.commit();
TestRequest request = ws.newRequest().setParam("fixedInPullRequest", pullRequestId).setParam("components", project.projectKey())
.setParam("branch", "nonExistingBranch");

assertThatThrownBy(request::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Branch with key 'nonExistingBranch' does not exist");
}

@Test
public void search_whenFixedInPullRequestSetAndComponentsIsSetButNoIssueFixedInPR_returnZeroIssues() {
String pullRequestId = "1000";
String pullRequestUuid = "pullRequestUuid";
String issueKey = "issueKey";
userSession.logIn(db.users().insertUser());
ProjectData project = db.components().insertPublicProject();
db.getDbClient().branchDao().insert(session, new BranchDto()
.setUuid(pullRequestUuid)
.setProjectUuid(project.projectUuid())
.setKey(pullRequestId)
.setIsMain(false)
.setBranchType(BranchType.PULL_REQUEST)
.setMergeBranchUuid(project.mainBranchUuid()));

TestRequest request = ws.newRequest().setParam("components", project.projectKey()).setParam("fixedInPullRequest", pullRequestId);
insertIssues(project.getMainBranchComponent(), i -> i.setKee(issueKey));
session.commit();
indexPermissionsAndIssues();

SearchWsResponse response = request.executeProtobuf(SearchWsResponse.class);

List<Issue> issuesList = response.getIssuesList();
assertThat(issuesList).isEmpty();
}

@Test
public void search_whenFixedInPullRequestSetAndComponentsIsSet_returnOneIssueFixedInPR() {
String pullRequestId = "1000";
String pullRequestUuid = "pullRequestUuid";
String issueKey = "issueKey";
userSession.logIn(db.users().insertUser());
ProjectData project = db.components().insertPublicProject();
db.getDbClient().branchDao().insert(session, new BranchDto()
.setUuid(pullRequestUuid)
.setProjectUuid(project.projectUuid())
.setKey(pullRequestId)
.setIsMain(false)
.setBranchType(BranchType.PULL_REQUEST)
.setMergeBranchUuid(project.mainBranchUuid()));

TestRequest request = ws.newRequest().setParam("components", project.projectKey()).setParam("fixedInPullRequest", pullRequestId);
insertIssues(project.getMainBranchComponent(), i -> i.setKee(issueKey));
db.getDbClient().issueFixedDao().insert(session, new IssueFixedDto(pullRequestUuid, issueKey));
session.commit();
indexPermissionsAndIssues();

SearchWsResponse response = request.executeProtobuf(SearchWsResponse.class);

List<Issue> issuesList = response.getIssuesList();
assertThat(issuesList).hasSize(1);
assertThat(issuesList.get(0).getKey()).isEqualTo(issueKey);
}

private RuleDto newIssueRule() {
RuleDto rule = newRule(XOO_X1, createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule desc"))
.setLanguage("xoo")
@@ -2200,10 +2363,20 @@ public class SearchActionIT {
UserDto john = db.users().insertUser();
userSession.logIn(john);
RuleDto rule = db.rules().insertIssueRule();
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
ComponentDto branch = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(branch));
for (Consumer<IssueDto> populator : populators) {
db.issues().insertIssue(rule, branch, file, populator);
}
}

private void insertIssues(ComponentDto branch, Consumer<IssueDto>... populators) {
UserDto john = db.users().insertUser();
userSession.logIn(john);
RuleDto rule = db.rules().insertIssueRule();
ComponentDto file = db.components().insertComponent(newFileDto(branch));
for (Consumer<IssueDto> populator : populators) {
db.issues().insertIssue(rule, project, file, populator);
db.issues().insertIssue(rule, branch, file, populator);
}
}


+ 42
- 20
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java View File

@@ -110,10 +110,12 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_IN_
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CWE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FIXED_IN_PULL_REQUEST;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IMPACT_SEVERITIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IMPACT_SOFTWARE_QUALITIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IN_NEW_CODE_PERIOD;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE_STATUSES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ON_COMPONENT_ONLY;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_40;
@@ -130,7 +132,6 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SANS_TOP_25;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SCOPES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SEVERITIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE_STATUSES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SONARSOURCE_SECURITY;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_STATUSES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TAGS;
@@ -140,7 +141,8 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TYPES;
public class SearchAction implements IssuesWsAction {
private static final String LOGIN_MYSELF = "__me__";
private static final Set<String> ISSUE_SCOPES = Arrays.stream(IssueScope.values()).map(Enum::name).collect(Collectors.toSet());
private static final EnumSet<RuleType> ALL_RULE_TYPES_EXCEPT_SECURITY_HOTSPOTS = EnumSet.complementOf(EnumSet.of(RuleType.SECURITY_HOTSPOT));
private static final EnumSet<RuleType> ALL_RULE_TYPES_EXCEPT_SECURITY_HOTSPOTS =
EnumSet.complementOf(EnumSet.of(RuleType.SECURITY_HOTSPOT));

static final List<String> SUPPORTED_FACETS = List.of(
FACET_PROJECTS,
@@ -172,7 +174,8 @@ public class SearchAction implements IssuesWsAction {
PARAM_IMPACT_SEVERITIES,
PARAM_ISSUE_STATUSES);

private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. ";
private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of " +
"the componentKeys parameter. ";
private static final String NEW_FACET_ADDED_MESSAGE = "Facet '%s' has been added";
private static final String NEW_PARAM_ADDED_MESSAGE = "Param '%s' has been added";
private static final Set<String> FACETS_REQUIRING_PROJECT = newHashSet(PARAM_FILES, PARAM_DIRECTORIES);
@@ -186,7 +189,8 @@ public class SearchAction implements IssuesWsAction {
private final System2 system2;
private final DbClient dbClient;

public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory, IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory,
IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
SearchResponseLoader searchResponseLoader, SearchResponseFormat searchResponseFormat, System2 system2, DbClient dbClient) {
this.userSession = userSession;
this.issueIndex = issueIndex;
@@ -208,7 +212,10 @@ public class SearchAction implements IssuesWsAction {
+ "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
.setSince("3.6")
.setChangelog(
new Change("10.4", "Value '%s' for 'transition' response field is deprecated, use '%s' instead".formatted(DefaultTransitions.WONT_FIX, DefaultTransitions.ACCEPT)),
new Change("10.4", "Added new param '%s'".formatted(PARAM_FIXED_IN_PULL_REQUEST)),
new Change("10.4",
"Value '%s' for 'transition' response field is deprecated, use '%s' instead".formatted(DefaultTransitions.WONT_FIX,
DefaultTransitions.ACCEPT)),
new Change("10.4", "Possible value '%s' for 'transition' response field has been added".formatted(DefaultTransitions.ACCEPT)),
new Change("10.4", format(NEW_PARAM_ADDED_MESSAGE, PARAM_ISSUE_STATUSES)),
new Change("10.4", format("Parameters '%s' and '%s' are deprecated in favor of '%s'.", PARAM_RESOLUTIONS, PARAM_STATUSES, PARAM_ISSUE_STATUSES)),
@@ -251,10 +258,12 @@ public class SearchAction implements IssuesWsAction {
new Change("8.5", "Internal parameter 'fileUuids' has been dropped"),
new Change("8.4", "parameters 'componentUuids', 'projectKeys' has been dropped."),
new Change("8.2", "'REVIEWED', 'TO_REVIEW' status param values are no longer supported"),
new Change("8.2", "Security hotspots are no longer returned as type 'SECURITY_HOTSPOT' is not supported anymore, use dedicated api/hotspots"),
new Change("8.2", "Security hotspots are no longer returned as type 'SECURITY_HOTSPOT' is not supported anymore, use dedicated " +
"api/hotspots"),
new Change("8.2", "response field 'fromHotspot' has been deprecated and is no more populated"),
new Change("8.2", "Status 'IN_REVIEW' for Security Hotspots has been deprecated"),
new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED)),
new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW,
STATUS_REVIEWED)),
new Change("7.8", "Security hotspots are returned by default"),
new Change("7.7", format("Value 'authors' in parameter '%s' is deprecated, please use '%s' instead", FACETS, PARAM_AUTHOR)),
new Change("7.6", format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT_KEYS)),
@@ -268,7 +277,8 @@ public class SearchAction implements IssuesWsAction {
new Change("6.5", "parameters 'projects', 'projectUuids', 'moduleUuids', 'directories', 'fileUuids' are marked as internal"),
new Change("6.3", "response field 'email' is renamed 'avatar'"),
new Change("5.5", "response fields 'reporter' and 'actionPlan' are removed (drop of action plan and manual issue features)"),
new Change("5.5", "parameters 'reporters', 'actionPlans' and 'planned' are dropped and therefore ignored (drop of action plan and manual issue features)"),
new Change("5.5", "parameters 'reporters', 'actionPlans' and 'planned' are dropped and therefore ignored (drop of action plan and" +
" manual issue features)"),
new Change("5.5", "response field 'debt' is renamed 'effort'"))
.setResponseExample(getClass().getResource("search-example.json"));

@@ -279,7 +289,8 @@ public class SearchAction implements IssuesWsAction {
action.addSortParams(IssueQuery.SORTS, null, true);
action.createParam(PARAM_ADDITIONAL_FIELDS)
.setSince("5.2")
.setDescription("Comma-separated list of the optional fields to be returned in response. Action plans are dropped in 5.5, it is not returned in the response.")
.setDescription("Comma-separated list of the optional fields to be returned in response. Action plans are dropped in 5.5, it is not" +
" returned in the response.")
.setPossibleValues(SearchAdditionalField.possibleValues());
addComponentRelatedParams(action);
action.createParam(PARAM_ISSUES)
@@ -363,7 +374,8 @@ public class SearchAction implements IssuesWsAction {
.setDescription("Comma-separated list of CWE identifiers. Use '" + UNKNOWN_STANDARD + "' to select issues not associated to any CWE.")
.setExampleValue("12,125," + UNKNOWN_STANDARD);
action.createParam(PARAM_SONARSOURCE_SECURITY)
.setDescription("Comma-separated list of SonarSource security categories. Use '" + SQCategory.OTHERS.getKey() + "' to select issues not associated" +
.setDescription("Comma-separated list of SonarSource security categories. Use '" + SQCategory.OTHERS.getKey() + "' to select issues" +
" not associated" +
" with any category")
.setSince("7.8")
.setPossibleValues(Arrays.stream(SQCategory.values()).map(SQCategory::getKey).toList());
@@ -371,7 +383,8 @@ public class SearchAction implements IssuesWsAction {
.setDescription("SCM accounts. To set several values, the parameter must be called once for each value.")
.setExampleValue("author=torvalds@linux-foundation.org&author=linux@fondation.org");
action.createParam(PARAM_ASSIGNEES)
.setDescription("Comma-separated list of assignee logins. The value '__me__' can be used as a placeholder for user who performs the request")
.setDescription("Comma-separated list of assignee logins. The value '__me__' can be used as a placeholder for user who performs the" +
" request")
.setExampleValue("admin,usera,__me__");
action.createParam(PARAM_ASSIGNED)
.setDescription("To retrieve assigned or unassigned issues")
@@ -407,7 +420,8 @@ public class SearchAction implements IssuesWsAction {
.setSince("9.4");
action.createParam(PARAM_TIMEZONE)
.setDescription(
"To resolve dates passed to '" + PARAM_CREATED_AFTER + "' or '" + PARAM_CREATED_BEFORE + "' (does not apply to datetime) and to compute creation date histogram")
"To resolve dates passed to '" + PARAM_CREATED_AFTER + "' or '" + PARAM_CREATED_BEFORE + "' (does not apply to datetime) and to " +
"compute creation date histogram")
.setRequired(false)
.setExampleValue("'Europe/Paris', 'Z' or '+02:00'")
.setSince("8.6");
@@ -431,7 +445,8 @@ public class SearchAction implements IssuesWsAction {

action.createParam(PARAM_COMPONENTS)
.setDeprecatedKey(PARAM_COMPONENT_KEYS, "10.2")
.setDescription("Comma-separated list of component keys. Retrieve issues associated to a specific list of components (and all its descendants). " +
.setDescription("Comma-separated list of component keys. Retrieve issues associated to a specific list of components (and all its " +
"descendants). " +
"A component can be a portfolio, project, module, directory or file.")
.setExampleValue(KEY_PROJECT_EXAMPLE_001);

@@ -465,6 +480,13 @@ public class SearchAction implements IssuesWsAction {
.setDescription("Pull request id. Not available in the community edition.")
.setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
.setSince("7.1");

action.createParam(PARAM_FIXED_IN_PULL_REQUEST)
.setDescription("Pull request id to filter issues that would be fixed in the specified project or branch by the pull request. " +
"Should not be used together with + '" + PARAM_PULL_REQUEST + "'. At least the '" + PARAM_COMPONENTS + "' must be be specified " +
"when this param is used. Not available in the community edition.")
.setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
.setSince("10.4");
}

@Override
@@ -487,7 +509,7 @@ public class SearchAction implements IssuesWsAction {
.filter(FACETS_REQUIRING_PROJECT::contains)
.collect(Collectors.toSet());
checkArgument(facetsRequiringProjectParameter.isEmpty() ||
(!query.projectUuids().isEmpty()), "Facet(s) '%s' require to also filter by project",
(!query.projectUuids().isEmpty()), "Facet(s) '%s' require to also filter by project",
String.join(",", facetsRequiringProjectParameter));

// execute request
@@ -519,7 +541,8 @@ public class SearchAction implements IssuesWsAction {
}

private static TotalHits getTotalHits(SearchResponse response) {
return ofNullable(response.getHits().getTotalHits()).orElseThrow(() -> new IllegalStateException("Could not get total hits of search results"));
return ofNullable(response.getHits().getTotalHits()).orElseThrow(() -> new IllegalStateException("Could not get total hits of search " +
"results"));
}

private static SearchOptions createSearchOptionsFromRequest(SearchRequest request) {
@@ -527,12 +550,10 @@ public class SearchAction implements IssuesWsAction {
options.setPage(request.getPage(), request.getPageSize());

List<String> facets = request.getFacets();

if (facets == null || facets.isEmpty()) {
return options;
if (facets != null && !facets.isEmpty()) {
options.addFacets(facets);
}

options.addFacets(facets);
return options;
}

@@ -657,7 +678,8 @@ public class SearchAction implements IssuesWsAction {
.setCwe(request.paramAsStrings(PARAM_CWE))
.setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY))
.setTimeZone(request.param(PARAM_TIMEZONE))
.setCodeVariants(request.paramAsStrings(PARAM_CODE_VARIANTS));
.setCodeVariants(request.paramAsStrings(PARAM_CODE_VARIANTS))
.setFixedInPullRequest(request.param(PARAM_FIXED_IN_PULL_REQUEST));
}

private void checkIfNeedIssueSync(DbSession dbSession, SearchRequest searchRequest) {

+ 0
- 4
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java View File

@@ -23,14 +23,10 @@ public class KeyExamples {
public static final String KEY_APPLICATION_EXAMPLE_001 = "my_app";

public static final String KEY_FILE_EXAMPLE_001 = "my_project:/src/foo/Bar.php";
public static final String KEY_FILE_EXAMPLE_002 = "another_project:/src/foo/Foo.php";
public static final String KEY_PROJECT_EXAMPLE_001 = "my_project";
public static final String KEY_PROJECT_EXAMPLE_002 = "another_project";
public static final String KEY_PROJECT_EXAMPLE_003 = "third_project";

public static final String KEY_ORG_EXAMPLE_001 = "my-org";
public static final String KEY_ORG_EXAMPLE_002 = "foo-company";

public static final String KEY_BRANCH_EXAMPLE_001 = "feature/my_branch";
public static final String KEY_PULL_REQUEST_EXAMPLE_001 = "5461";


+ 1
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java View File

@@ -109,6 +109,7 @@ public class IssuesWsParameters {
public static final String PARAM_ADDITIONAL_FIELDS = "additionalFields";
public static final String PARAM_TIMEZONE = "timeZone";
public static final String PARAM_CODE_VARIANTS = "codeVariants";
public static final String PARAM_FIXED_IN_PULL_REQUEST = "fixedInPullRequest";

public static final String FACET_MODE_EFFORT = "effort";


Loading…
Cancel
Save