diff options
author | Jacek Poreda <jacek.poreda@sonarsource.com> | 2023-08-07 14:53:59 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-08-18 20:02:49 +0000 |
commit | c5f6c08551a945e6cc7c2450b0fb7ae4e795ba90 (patch) | |
tree | d7ae90734d56c8776563ad408f8573b71c7db32f | |
parent | 09a45741162362db5fcf1cdf55fbd43b89785523 (diff) | |
download | sonarqube-c5f6c08551a945e6cc7c2450b0fb7ae4e795ba90.tar.gz sonarqube-c5f6c08551a945e6cc7c2450b0fb7ae4e795ba90.zip |
SONAR-20021 Add filter and facets for Software Quality, Severity and Clean Code Attribute Category
- Refactor issue indexer to use MyBatis mapping with cursor
17 files changed, 1083 insertions, 237 deletions
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java index d9f8ac85ee9..0a4a79a5e86 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java @@ -22,12 +22,14 @@ package org.sonar.db.issue; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.IntStream; import java.util.stream.Stream; import javax.annotation.Nullable; +import org.apache.ibatis.cursor.Cursor; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -199,6 +201,40 @@ public class IssueDaoIT { } @Test + public void scrollIndexationIssues_shouldReturnDto() { + ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent(); + RuleDto rule = db.rules().insert(r -> r.setRepositoryKey("java").setLanguage("java") + .addDefaultImpact(new ImpactDto() + .setUuid(UuidFactoryFast.getInstance().create()) + .setSoftwareQuality(SoftwareQuality.RELIABILITY) + .setSeverity(Severity.MEDIUM))); + + ComponentDto branchA = db.components().insertProjectBranch(project, b -> b.setKey("branchA")); + ComponentDto fileA = db.components().insertComponent(newFileDto(branchA)); + + IntStream.range(0, 100).forEach(i -> insertBranchIssue(branchA, fileA, rule, "A" + i, STATUS_OPEN, 1_340_000_000_000L)); + + Cursor<IndexedIssueDto> issues = underTest.scrollIssuesForIndexation(db.getSession(), null, null); + + Iterator<IndexedIssueDto> iterator = issues.iterator(); + int issueCount = 0; + while (iterator.hasNext()) { + IndexedIssueDto next = iterator.next(); + assertThat(next.getRuleDefaultImpacts()).hasSize(2) + .extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity) + .containsExactlyInAnyOrder( + tuple(SoftwareQuality.RELIABILITY, Severity.MEDIUM), + tuple(SoftwareQuality.MAINTAINABILITY, Severity.HIGH)); + assertThat(next.getImpacts()) + .extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity) + .containsExactlyInAnyOrder( + tuple(SoftwareQuality.MAINTAINABILITY, Severity.HIGH)); + issueCount++; + } + assertThat(issueCount).isEqualTo(100); + } + + @Test public void selectIssueKeysByComponentUuid() { // contains I1 and I2 prepareTables(); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IndexedIssueDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IndexedIssueDto.java new file mode 100644 index 00000000000..dc502006d56 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IndexedIssueDto.java @@ -0,0 +1,316 @@ +/* + * 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.db.issue; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; + +public final class IndexedIssueDto { + private String issueKey = null; + private String assignee = null; + private Integer line = null; + private String resolution = null; + private String cleanCodeAttribute = null; + private String severity = null; + private String status = null; + private Long effort = null; + private String authorLogin = null; + private Long issueCloseDate = null; + private Long issueCreationDate = null; + private Long issueUpdateDate = null; + private String ruleUuid = null; + private String language = null; + private String componentUuid = null; + private String path = null; + private String scope = null; + private String branchUuid = null; + private boolean isMain = false; + private String projectUuid = null; + private String tags = null; + private Integer issueType = null; + private String securityStandards = null; + private String qualifier = null; + private boolean isNewCodeReferenceIssue = false; + private String codeVariants = null; + + private Set<ImpactDto> impacts = new HashSet<>(); + private Set<ImpactDto> ruleDefaultImpacts = new HashSet<>(); + + public IndexedIssueDto() { + // empty constructor + } + + public String getIssueKey() { + return issueKey; + } + + public IndexedIssueDto setIssueKey(String issueKey) { + this.issueKey = issueKey; + return this; + } + + public String getAssignee() { + return assignee; + } + + public IndexedIssueDto setAssignee(String assignee) { + this.assignee = assignee; + return this; + } + + public Integer getLine() { + return line; + } + + public IndexedIssueDto setLine(Integer line) { + this.line = line; + return this; + } + + public String getResolution() { + return resolution; + } + + public IndexedIssueDto setResolution(String resolution) { + this.resolution = resolution; + return this; + } + + public String getSeverity() { + return severity; + } + + public IndexedIssueDto setSeverity(String severity) { + this.severity = severity; + return this; + } + + public String getStatus() { + return status; + } + + public IndexedIssueDto setStatus(String status) { + this.status = status; + return this; + } + + public Long getEffort() { + return effort; + } + + public IndexedIssueDto setEffort(Long effort) { + this.effort = effort; + return this; + } + + public String getAuthorLogin() { + return authorLogin; + } + + public IndexedIssueDto setAuthorLogin(String authorLogin) { + this.authorLogin = authorLogin; + return this; + } + + public Long getIssueCloseDate() { + return issueCloseDate; + } + + public IndexedIssueDto setIssueCloseDate(Long issueCloseDate) { + this.issueCloseDate = issueCloseDate; + return this; + } + + public Long getIssueCreationDate() { + return issueCreationDate; + } + + public IndexedIssueDto setIssueCreationDate(Long issueCreationDate) { + this.issueCreationDate = issueCreationDate; + return this; + } + + public Long getIssueUpdateDate() { + return issueUpdateDate; + } + + public IndexedIssueDto setIssueUpdateDate(Long issueUpdateDate) { + this.issueUpdateDate = issueUpdateDate; + return this; + } + + public String getRuleUuid() { + return ruleUuid; + } + + public IndexedIssueDto setRuleUuid(String ruleUuid) { + this.ruleUuid = ruleUuid; + return this; + } + + public String getLanguage() { + return language; + } + + public IndexedIssueDto setLanguage(String language) { + this.language = language; + return this; + } + + public String getComponentUuid() { + return componentUuid; + } + + public IndexedIssueDto setComponentUuid(String componentUuid) { + this.componentUuid = componentUuid; + return this; + } + + public String getPath() { + return path; + } + + public IndexedIssueDto setPath(String path) { + this.path = path; + return this; + } + + public String getScope() { + return scope; + } + + public IndexedIssueDto setScope(String scope) { + this.scope = scope; + return this; + } + + public String getBranchUuid() { + return branchUuid; + } + + public IndexedIssueDto setBranchUuid(String branchUuid) { + this.branchUuid = branchUuid; + return this; + } + + public boolean isMain() { + return isMain; + } + + public IndexedIssueDto setIsMain(boolean isMain) { + this.isMain = isMain; + return this; + } + + public String getProjectUuid() { + return projectUuid; + } + + public IndexedIssueDto setProjectUuid(String projectUuid) { + this.projectUuid = projectUuid; + return this; + } + + public String getTags() { + return tags; + } + + public IndexedIssueDto setTags(String tags) { + this.tags = tags; + return this; + } + + @Deprecated + public Integer getIssueType() { + return issueType; + } + + @Deprecated + public IndexedIssueDto setIssueType(Integer issueType) { + this.issueType = issueType; + return this; + } + + public String getSecurityStandards() { + return securityStandards; + } + + public IndexedIssueDto setSecurityStandards(String securityStandards) { + this.securityStandards = securityStandards; + return this; + } + + public String getQualifier() { + return qualifier; + } + + public IndexedIssueDto setQualifier(String qualifier) { + this.qualifier = qualifier; + return this; + } + + public boolean isNewCodeReferenceIssue() { + return isNewCodeReferenceIssue; + } + + public IndexedIssueDto setNewCodeReferenceIssue(boolean newCodeReferenceIssue) { + isNewCodeReferenceIssue = newCodeReferenceIssue; + return this; + } + + public String getCodeVariants() { + return codeVariants; + } + + public IndexedIssueDto setCodeVariants(String codeVariants) { + this.codeVariants = codeVariants; + return this; + } + + public Set<ImpactDto> getImpacts() { + return impacts; + } + + public Set<ImpactDto> getRuleDefaultImpacts() { + return ruleDefaultImpacts; + } + + + public Map<SoftwareQuality, Severity> getEffectiveImpacts() { + EnumMap<SoftwareQuality, Severity> effectiveImpacts = new EnumMap<>(SoftwareQuality.class); + ruleDefaultImpacts.forEach(impact -> effectiveImpacts.put(impact.getSoftwareQuality(), impact.getSeverity())); + impacts.forEach(impact -> effectiveImpacts.put(impact.getSoftwareQuality(), impact.getSeverity())); + return Collections.unmodifiableMap(effectiveImpacts); + } + + public String getCleanCodeAttribute() { + return cleanCodeAttribute; + } + + public IndexedIssueDto setCleanCodeAttribute(String cleanCodeAttribute) { + this.cleanCodeAttribute = cleanCodeAttribute; + return this; + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java index 6efd896f392..af95135fb9f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java @@ -23,6 +23,9 @@ import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; +import javax.annotation.Nullable; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.cursor.Cursor; import org.sonar.db.Dao; import org.sonar.db.DbSession; import org.sonar.db.Pagination; @@ -91,6 +94,11 @@ public class IssueDao implements Dao { return mapper(dbSession).selectIssueGroupsByComponent(component, leakPeriodBeginningDate); } + public Cursor<IndexedIssueDto> scrollIssuesForIndexation(DbSession dbSession, @Nullable @Param("branchUuid") String branchUuid, + @Nullable @Param("issueKeys") Collection<String> issueKeys) { + return mapper(dbSession).scrollIssuesForIndexation(branchUuid, issueKeys); + } + public void insert(DbSession session, IssueDto dto) { mapper(session).insert(dto); updateIssueImpacts(dto, mapper(session)); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index 579c69c13f6..93951e438e6 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Set; import javax.annotation.Nullable; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.session.ResultHandler; import org.sonar.db.Pagination; import org.sonar.db.component.ComponentDto; @@ -72,6 +73,8 @@ public interface IssueMapper { void scrollClosedByComponentUuid(@Param("componentUuid") String componentUuid, @Param("closeDateAfter") long closeDateAfter, ResultHandler<IssueDto> handler); + Cursor<IndexedIssueDto> scrollIssuesForIndexation(@Nullable @Param("branchUuid") String branchUuid, @Nullable @Param("issueKeys") Collection<String> issueKeys); + Collection<IssueGroupDto> selectIssueGroupsByComponent(@Param("component") ComponentDto component, @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate); List<IssueDto> selectByBranch(@Param("keys") Set<String> keys, @Nullable @Param("changedSince") Long changedSince); diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index e1966bf9aa6..17889d58ee3 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -317,6 +317,75 @@ i.kee, ic.issue_change_creation_date desc </select> + <resultMap id="indexedIssueResultMap" type="org.sonar.db.issue.IndexedIssueDto" autoMapping="true"> + <id property="issueKey" column="issueKey"/> + + <collection property="impacts" column="ii_uuid" notNullColumn="ii_uuid" + javaType="java.util.Set" ofType="Impact"> + <id property="uuid" column="ii_uuid"/> + <result property="softwareQuality" column="ii_softwareQuality"/> + <result property="severity" column="ii_severity"/> + </collection> + <collection property="ruleDefaultImpacts" column="rdi_uuid" notNullColumn="rdi_uuid" + javaType="java.util.Set" ofType="Impact"> + <id property="uuid" column="rdi_uuid"/> + <result property="softwareQuality" column="rdi_softwareQuality"/> + <result property="severity" column="rdi_severity"/> + </collection> + </resultMap> + + <select id="scrollIssuesForIndexation" parameterType="map" resultMap="indexedIssueResultMap" fetchSize="${_scrollFetchSize}" + resultSetType="FORWARD_ONLY" resultOrdered="true"> + select + i.kee as issueKey, + i.assignee, + i.line, + i.resolution, + i.severity, + i.status, + i.effort, + i.author_login as authorLogin, + i.issue_close_date as issueCloseDate, + i.issue_creation_date as issueCreationDate, + i.issue_update_date as issueUpdateDate, + r.uuid as ruleUuid, + r.language as language, + r.clean_code_attribute as cleanCodeAttribute, + c.uuid as componentUuid, + c.path, + c.scope, + c.branch_uuid as branchUuid, + pb.is_main as isMain, + pb.project_uuid as projectUuid, + i.tags, + i.issue_type as issueType, + r.security_standards as securityStandards, + c.qualifier, + i.code_variants as codeVariants, + <include refid="issueImpactsColumns"/> + <include refid="ruleDefaultImpactsColumns"/> + <include refid="isNewCodeReferenceIssue"/> + from issues i + inner join rules r on r.uuid = i.rule_uuid + inner join components c on c.uuid = i.component_uuid + inner join project_branches pb on c.branch_uuid = pb.uuid + left join new_code_reference_issues n on n.issue_key = i.kee + left outer join issues_impacts ii on i.kee = ii.issue_key + left outer join rules_default_impacts rdi on r.uuid = rdi.rule_uuid + <where> + <if test="branchUuid != null"> + and c.branch_uuid = #{branchUuid,jdbcType=VARCHAR} and i.project_uuid = #{branchUuid,jdbcType=VARCHAR} + </if> + <if test="issueKeys != null"> + and i.kee in + <foreach collection="issueKeys" open="(" close=")" item="key" separator=","> + #{key,jdbcType=VARCHAR} + </foreach> + </if> + </where> + order by i.kee + </select> + <select id="selectComponentUuidsOfOpenIssuesForProjectUuid" parameterType="string" resultType="string"> select distinct(i.component_uuid) from issues i diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IndexedIssueDtoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IndexedIssueDtoTest.java new file mode 100644 index 00000000000..24ba25aed7f --- /dev/null +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IndexedIssueDtoTest.java @@ -0,0 +1,89 @@ +/* + * 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.db.issue; + +import org.junit.Test; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; + +public class IndexedIssueDtoTest { + + @Test + public void settersGetters_shouldSetAndGetValues() { + IndexedIssueDto indexedIssueDto = new IndexedIssueDto() + .setIssueKey("issueKey") + .setAssignee("assignee") + .setAuthorLogin("authorLogin") + .setStatus("status") + .setNewCodeReferenceIssue(true) + .setCleanCodeAttribute("cleanCodeAttribute") + .setCodeVariants("codeVariants") + .setSecurityStandards("securityStandards") + .setComponentUuid("componentUuid") + .setIssueCloseDate(1L) + .setIssueCreationDate(2L) + .setIssueUpdateDate(3L) + .setEffort(4L) + .setIsMain(true) + .setLanguage("language") + .setLine(5) + .setPath("path") + .setProjectUuid("projectUuid") + .setQualifier("qualifier") + .setResolution("resolution") + .setRuleUuid("ruleUuid") + .setScope("scope") + .setSeverity("severity") + .setTags("tags") + .setIssueType(6) + .setBranchUuid("branchUuid"); + + indexedIssueDto.getImpacts().add(new ImpactDto().setSoftwareQuality(SoftwareQuality.SECURITY).setSeverity(Severity.HIGH)); + indexedIssueDto.getRuleDefaultImpacts().add(new ImpactDto().setSoftwareQuality(SoftwareQuality.MAINTAINABILITY).setSeverity(Severity.MEDIUM)); + + assertThat(indexedIssueDto) + .extracting(IndexedIssueDto::getIssueKey, IndexedIssueDto::getAssignee, IndexedIssueDto::getAuthorLogin, IndexedIssueDto::getStatus, + IndexedIssueDto::isNewCodeReferenceIssue, IndexedIssueDto::getCleanCodeAttribute, IndexedIssueDto::getCodeVariants, + IndexedIssueDto::getSecurityStandards, IndexedIssueDto::getComponentUuid, IndexedIssueDto::getIssueCloseDate, IndexedIssueDto::getIssueCreationDate, + IndexedIssueDto::getIssueUpdateDate, IndexedIssueDto::getEffort, IndexedIssueDto::isMain, IndexedIssueDto::getLanguage, IndexedIssueDto::getLine, + IndexedIssueDto::getPath, IndexedIssueDto::getProjectUuid, IndexedIssueDto::getQualifier, IndexedIssueDto::getResolution, + IndexedIssueDto::getRuleUuid, IndexedIssueDto::getScope, IndexedIssueDto::getSeverity, IndexedIssueDto::getTags, IndexedIssueDto::getIssueType, + IndexedIssueDto::getBranchUuid) + .containsExactly("issueKey", "assignee", "authorLogin", "status", true, "cleanCodeAttribute", "codeVariants", "securityStandards", + "componentUuid", 1L, 2L, 3L, 4L, true, "language", 5, "path", "projectUuid", "qualifier", "resolution", "ruleUuid", + "scope", "severity", "tags", 6, "branchUuid"); + + assertThat(indexedIssueDto.getImpacts()) + .extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity) + .containsExactly(tuple(SoftwareQuality.SECURITY, Severity.HIGH)); + + assertThat(indexedIssueDto.getRuleDefaultImpacts()) + .extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity) + .containsExactly(tuple(SoftwareQuality.MAINTAINABILITY, Severity.MEDIUM)); + + assertThat(indexedIssueDto.getEffectiveImpacts()) + .containsEntry(SoftwareQuality.MAINTAINABILITY, Severity.MEDIUM) + .containsEntry(SoftwareQuality.SECURITY, Severity.HIGH); + } + +} diff --git a/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIndexerIT.java b/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIndexerIT.java index 54e6ab950fa..5e2db299839 100644 --- a/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIndexerIT.java +++ b/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIndexerIT.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Predicate; import org.assertj.core.api.Assertions; @@ -31,6 +32,8 @@ import org.elasticsearch.search.SearchHit; import org.junit.Rule; import org.junit.Test; import org.slf4j.event.Level; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.resources.Qualifiers; import org.sonar.api.testfixtures.log.LogTester; import org.sonar.db.DbSession; @@ -68,6 +71,8 @@ import static org.sonar.server.es.Indexers.BranchEvent.DELETION; import static org.sonar.server.es.Indexers.EntityEvent.PROJECT_KEY_UPDATE; import static org.sonar.server.es.Indexers.EntityEvent.PROJECT_TAGS_UPDATE; import static org.sonar.server.issue.IssueDocTesting.newDoc; +import static org.sonar.server.issue.index.IssueIndexDefinition.SUB_FIELD_SOFTWARE_QUALITY; +import static org.sonar.server.issue.index.IssueIndexDefinition.SUB_FIELD_SEVERITY; import static org.sonar.server.issue.index.IssueIndexDefinition.TYPE_ISSUE; import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES; @@ -144,6 +149,10 @@ public class IssueIndexerIT { assertThat(doc.getSansTop25()).isEmpty(); assertThat(doc.getSonarSourceSecurityCategory()).isEqualTo(SQCategory.OTHERS); assertThat(doc.getVulnerabilityProbability()).isEqualTo(VulnerabilityProbability.LOW); + assertThat(doc.impacts()) + .containsExactlyInAnyOrder(Map.of( + SUB_FIELD_SOFTWARE_QUALITY, SoftwareQuality.MAINTAINABILITY.name(), + SUB_FIELD_SEVERITY, Severity.HIGH.name())); } @Test diff --git a/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java b/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java index 2a4e1052b8e..15291533dc1 100644 --- a/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java +++ b/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java @@ -94,6 +94,7 @@ public class IssueIteratorFactoryIT { assertThat(issue.effort().toMinutes()).isPositive(); assertThat(issue.type().getDbConstant()).isEqualTo(2); assertThat(issue.getCodeVariants()).containsOnly("variant1", "variant2"); + assertThat(issue.cleanCodeAttributeCategory()).isEqualTo("INTENTIONAL"); } @Test diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java index c02ff1f7782..f2e8f5986f4 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java @@ -22,9 +22,11 @@ package org.sonar.server.issue.index; import com.google.common.collect.Maps; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.Map; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.Duration; @@ -33,6 +35,8 @@ import org.sonar.server.permission.index.AuthorizationDoc; import org.sonar.server.security.SecurityStandards; import org.sonar.server.security.SecurityStandards.VulnerabilityProbability; +import static org.sonar.server.issue.index.IssueIndexDefinition.SUB_FIELD_SOFTWARE_QUALITY; +import static org.sonar.server.issue.index.IssueIndexDefinition.SUB_FIELD_SEVERITY; import static org.sonar.server.issue.index.IssueIndexDefinition.TYPE_ISSUE; public class IssueDoc extends BaseDoc { @@ -86,6 +90,14 @@ public class IssueDoc extends BaseDoc { return getField(IssueIndexDefinition.FIELD_ISSUE_SEVERITY); } + public String cleanCodeAttributeCategory() { + return getField(IssueIndexDefinition.FIELD_ISSUE_CLEAN_CODE_ATTRIBUTE_CATEGORY); + } + + public Collection<Map<String, String>> impacts() { + return getField(IssueIndexDefinition.FIELD_ISSUE_IMPACTS); + } + @CheckForNull public Integer line() { return getNullableField(IssueIndexDefinition.FIELD_ISSUE_LINE); @@ -129,6 +141,7 @@ public class IssueDoc extends BaseDoc { return getNullableField(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN); } + @Deprecated public RuleType type() { return RuleType.valueOf(getField(IssueIndexDefinition.FIELD_ISSUE_TYPE)); } @@ -190,12 +203,18 @@ public class IssueDoc extends BaseDoc { return this; } + @Deprecated public IssueDoc setSeverity(@Nullable String s) { setField(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, s); setField(IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE, Severity.ALL.indexOf(s)); return this; } + public IssueDoc setCleanCodeAttributeCategory(@Nullable String s) { + setField(IssueIndexDefinition.FIELD_ISSUE_CLEAN_CODE_ATTRIBUTE_CATEGORY, s); + return this; + } + public IssueDoc setLine(@Nullable Integer i) { setField(IssueIndexDefinition.FIELD_ISSUE_LINE, i); return this; @@ -261,11 +280,24 @@ public class IssueDoc extends BaseDoc { return this; } + @Deprecated public IssueDoc setType(RuleType type) { setField(IssueIndexDefinition.FIELD_ISSUE_TYPE, type.toString()); return this; } + public IssueDoc setImpacts(Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> softwareQualities) { + List<Map<String, String>> convertedMap = softwareQualities + .entrySet() + .stream() + .map(entry -> Map.of( + SUB_FIELD_SOFTWARE_QUALITY, entry.getKey().name(), + SUB_FIELD_SEVERITY, entry.getValue().name())) + .toList(); + setField(IssueIndexDefinition.FIELD_ISSUE_IMPACTS, convertedMap); + return this; + } + @CheckForNull public Collection<String> getPciDss32() { return getNullableField(IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_32); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java index fdfaba17578..5c760d638bf 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java @@ -104,6 +104,12 @@ public class IssueIndexDefinition implements IndexDefinition { * Whether issue is new code for a branch using the reference branch new code definition. */ public static final String FIELD_ISSUE_NEW_CODE_REFERENCE = "isNewCodeReference"; + public static final String FIELD_ISSUE_CLEAN_CODE_ATTRIBUTE_CATEGORY = "cleanCodeAttributeCategory"; + public static final String FIELD_ISSUE_IMPACTS = "impacts"; + public static final String SUB_FIELD_SOFTWARE_QUALITY = "softwareQuality"; + public static final String SUB_FIELD_SEVERITY = "severity"; + public static final String FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY = FIELD_ISSUE_IMPACTS + "." + SUB_FIELD_SOFTWARE_QUALITY; + public static final String FIELD_ISSUE_IMPACT_SEVERITY = FIELD_ISSUE_IMPACTS + "." + SUB_FIELD_SEVERITY; private final Configuration config; private final boolean enableSource; @@ -157,6 +163,11 @@ public class IssueIndexDefinition implements IndexDefinition { mapping.keywordFieldBuilder(FIELD_ISSUE_RULE_UUID).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_ISSUE_SEVERITY).disableNorms().build(); mapping.createByteField(FIELD_ISSUE_SEVERITY_VALUE); + mapping.keywordFieldBuilder(FIELD_ISSUE_CLEAN_CODE_ATTRIBUTE_CATEGORY).disableNorms().build(); + mapping.nestedFieldBuilder(FIELD_ISSUE_IMPACTS) + .addKeywordField(SUB_FIELD_SOFTWARE_QUALITY) + .addKeywordField(SUB_FIELD_SEVERITY) + .build(); mapping.keywordFieldBuilder(FIELD_ISSUE_STATUS).disableNorms().addSubFields(SORTABLE_ANALYZER).build(); mapping.keywordFieldBuilder(FIELD_ISSUE_TAGS).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_ISSUE_TYPE).disableNorms().build(); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java index 86d2196a20e..deec19dbba9 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java @@ -21,29 +21,25 @@ package org.sonar.server.issue.index; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.Collection; import java.util.HashMap; -import java.util.stream.Collectors; -import java.util.stream.IntStream; +import java.util.Iterator; +import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; -import org.apache.commons.lang.StringUtils; +import org.apache.ibatis.cursor.Cursor; import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Scopes; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.db.DatabaseUtils; import org.sonar.db.DbClient; import org.sonar.db.DbSession; -import org.sonar.db.ResultSetIterator; +import org.sonar.db.issue.IndexedIssueDto; import org.sonar.server.security.SecurityStandards; import static com.google.common.base.Preconditions.checkArgument; -import static org.elasticsearch.common.Strings.isNullOrEmpty; import static org.sonar.api.utils.DateUtils.longToDate; -import static org.sonar.db.DatabaseUtils.getLong; import static org.sonar.db.rule.RuleDto.deserializeSecurityStandardsString; import static org.sonar.server.security.SecurityStandards.fromSecurityStandards; @@ -53,85 +49,25 @@ import static org.sonar.server.security.SecurityStandards.fromSecurityStandards; */ class IssueIteratorForSingleChunk implements IssueIterator { - private static final String[] FIELDS = { - "i.kee", - "i.assignee", - "i.line", - "i.resolution", - "i.severity", - "i.status", - "i.effort", - "i.author_login", - "i.issue_close_date", - "i.issue_creation_date", - "i.issue_update_date", - "r.uuid", - "r.language", - "c.uuid", - "c.path", - "c.scope", - "c.branch_uuid", - "pb.is_main", - "pb.project_uuid", - "i.tags", - "i.issue_type", - "r.security_standards", - "c.qualifier", - "n.uuid", - "i.code_variants" - }; - - private static final String SQL_ALL = "select " + StringUtils.join(FIELDS, ",") + " from issues i " + - "inner join rules r on r.uuid = i.rule_uuid " + - "inner join components c on c.uuid = i.component_uuid " + - "inner join project_branches pb on c.branch_uuid = pb.uuid "; - - private static final String SQL_NEW_CODE_JOIN = "left join new_code_reference_issues n on n.issue_key = i.kee "; - - private static final String BRANCH_FILTER = " and c.branch_uuid = ? and i.project_uuid = ? "; - private static final String ISSUE_KEY_FILTER_PREFIX = " and i.kee in ("; - private static final String ISSUE_KEY_FILTER_SUFFIX = ") "; - static final Splitter STRING_LIST_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings(); private final DbSession session; - @CheckForNull - private final String branchUuid; - - @CheckForNull - private final Collection<String> issueKeys; - - private final PreparedStatement stmt; - private final ResultSetIterator<IssueDoc> iterator; + private final Iterator<IndexedIssueDto> iterator; IssueIteratorForSingleChunk(DbClient dbClient, @Nullable String branchUuid, @Nullable Collection<String> issueKeys) { checkArgument(issueKeys == null || issueKeys.size() <= DatabaseUtils.PARTITION_SIZE_FOR_ORACLE, "Cannot search for more than " + DatabaseUtils.PARTITION_SIZE_FOR_ORACLE + " issue keys at once. Please provide the keys in smaller chunks."); - this.branchUuid = branchUuid; - this.issueKeys = issueKeys; this.session = dbClient.openSession(false); - try { - String sql = createSql(); - stmt = dbClient.getMyBatis().newScrollingSelectStatement(session, sql); - iterator = createIterator(); + Cursor<IndexedIssueDto> indexCursor = dbClient.issueDao().scrollIssuesForIndexation(session, branchUuid, issueKeys); + iterator = indexCursor.iterator(); } catch (Exception e) { session.close(); throw new IllegalStateException("Fail to prepare SQL request to select all issues", e); } } - private IssueIteratorInternal createIterator() { - try { - setParameters(stmt); - return new IssueIteratorInternal(stmt); - } catch (SQLException e) { - DatabaseUtils.closeQuietly(stmt); - throw new IllegalStateException("Fail to prepare SQL request to select all issues", e); - } - } - @Override public boolean hasNext() { return iterator.hasNext(); @@ -139,133 +75,101 @@ class IssueIteratorForSingleChunk implements IssueIterator { @Override public IssueDoc next() { - return iterator.next(); + return toIssueDoc(iterator.next()); } - private String createSql() { - String sql = SQL_ALL; - sql += branchUuid == null ? "" : BRANCH_FILTER; - if (issueKeys != null && !issueKeys.isEmpty()) { - sql += ISSUE_KEY_FILTER_PREFIX; - sql += IntStream.range(0, issueKeys.size()).mapToObj(i -> "?").collect(Collectors.joining(",")); - sql += ISSUE_KEY_FILTER_SUFFIX; - } - sql += SQL_NEW_CODE_JOIN; - return sql; + private static IssueDoc toIssueDoc(IndexedIssueDto indexedIssueDto) { + IssueDoc doc = new IssueDoc(new HashMap<>(30)); + + String key = indexedIssueDto.getIssueKey(); + + // all the fields must be present, even if value is null + doc.setKey(key); + doc.setAssigneeUuid(indexedIssueDto.getAssignee()); + doc.setLine(indexedIssueDto.getLine()); + doc.setResolution(indexedIssueDto.getResolution()); + doc.setSeverity(indexedIssueDto.getSeverity()); + String cleanCodeAttributeCategory = Optional.ofNullable(indexedIssueDto.getCleanCodeAttribute()) + .map(CleanCodeAttribute::valueOf) + .map(cleanCodeAttribute -> cleanCodeAttribute.getAttributeCategory().name()) + .orElse(null); + //TODO:: uncomment once clean code attribute is set to not-null + //.orElseThrow(() -> new IllegalStateException("Clean Code Attribute is missing for issue " + key)); + doc.setCleanCodeAttributeCategory(cleanCodeAttributeCategory); + doc.setStatus(indexedIssueDto.getStatus()); + doc.setEffort(indexedIssueDto.getEffort()); + doc.setAuthorLogin(indexedIssueDto.getAuthorLogin()); + + doc.setFuncCloseDate(longToDate(indexedIssueDto.getIssueCloseDate())); + doc.setFuncCreationDate(longToDate(indexedIssueDto.getIssueCreationDate())); + doc.setFuncUpdateDate(longToDate(indexedIssueDto.getIssueUpdateDate())); + + doc.setRuleUuid(indexedIssueDto.getRuleUuid()); + doc.setLanguage(indexedIssueDto.getLanguage()); + doc.setComponentUuid(indexedIssueDto.getComponentUuid()); + String scope = indexedIssueDto.getScope(); + String filePath = extractFilePath(indexedIssueDto.getPath(), scope); + doc.setFilePath(filePath); + doc.setDirectoryPath(extractDirPath(doc.filePath(), scope)); + String branchUuid = indexedIssueDto.getBranchUuid(); + boolean isMainBranch = indexedIssueDto.isMain(); + String projectUuid = indexedIssueDto.getProjectUuid(); + doc.setBranchUuid(branchUuid); + doc.setIsMainBranch(isMainBranch); + doc.setProjectUuid(projectUuid); + String tags = indexedIssueDto.getTags(); + doc.setTags(STRING_LIST_SPLITTER.splitToList(tags == null ? "" : tags)); + doc.setType(RuleType.valueOf(indexedIssueDto.getIssueType())); + doc.setImpacts(indexedIssueDto.getEffectiveImpacts()); + SecurityStandards securityStandards = fromSecurityStandards(deserializeSecurityStandardsString(indexedIssueDto.getSecurityStandards())); + SecurityStandards.SQCategory sqCategory = securityStandards.getSqCategory(); + doc.setOwaspTop10(securityStandards.getOwaspTop10()); + doc.setOwaspTop10For2021(securityStandards.getOwaspTop10For2021()); + doc.setPciDss32(securityStandards.getPciDss32()); + doc.setPciDss40(securityStandards.getPciDss40()); + doc.setOwaspAsvs40(securityStandards.getOwaspAsvs40()); + doc.setCwe(securityStandards.getCwe()); + doc.setSansTop25(securityStandards.getSansTop25()); + doc.setSonarSourceSecurityCategory(sqCategory); + doc.setVulnerabilityProbability(sqCategory.getVulnerability()); + + doc.setScope(Qualifiers.UNIT_TEST_FILE.equals(indexedIssueDto.getQualifier()) ? IssueScope.TEST : IssueScope.MAIN); + doc.setIsNewCodeReference(indexedIssueDto.isNewCodeReferenceIssue()); + String codeVariants = indexedIssueDto.getCodeVariants(); + doc.setCodeVariants(STRING_LIST_SPLITTER.splitToList(codeVariants == null ? "" : codeVariants)); + return doc; + } - private void setParameters(PreparedStatement stmt) throws SQLException { - int index = 1; - if (branchUuid != null) { - stmt.setString(index, branchUuid); - index++; - stmt.setString(index, branchUuid); - index++; - } - if (issueKeys != null) { - for (String key : issueKeys) { - stmt.setString(index, key); - index++; + @CheckForNull + private static String extractDirPath(@Nullable String filePath, String scope) { + if (filePath != null) { + if (Scopes.DIRECTORY.equals(scope)) { + return filePath; + } + int lastSlashIndex = CharMatcher.anyOf("/").lastIndexIn(filePath); + if (lastSlashIndex > 0) { + return filePath.substring(0, lastSlashIndex); } + return "/"; } + return null; } - @Override - public void close() { - try { - iterator.close(); - } finally { - DatabaseUtils.closeQuietly(stmt); - session.close(); + @CheckForNull + private static String extractFilePath(@Nullable String filePath, String scope) { + // On modules, the path contains the relative path of the module starting from its parent, and in E/S we're only interested in the + // path + // of files and directories. + // That's why the file path should be null on modules and projects. + if (filePath != null && !Scopes.PROJECT.equals(scope)) { + return filePath; } + return null; } - private static final class IssueIteratorInternal extends ResultSetIterator<IssueDoc> { - - public IssueIteratorInternal(PreparedStatement stmt) throws SQLException { - super(stmt); - } - - @Override - protected IssueDoc read(ResultSet rs) throws SQLException { - IssueDoc doc = new IssueDoc(new HashMap<>(30)); - - String key = rs.getString(1); - - // all the fields must be present, even if value is null - doc.setKey(key); - doc.setAssigneeUuid(rs.getString(2)); - doc.setLine(DatabaseUtils.getInt(rs, 3)); - doc.setResolution(rs.getString(4)); - doc.setSeverity(rs.getString(5)); - doc.setStatus(rs.getString(6)); - doc.setEffort(getLong(rs, 7)); - doc.setAuthorLogin(rs.getString(8)); - doc.setFuncCloseDate(longToDate(getLong(rs, 9))); - doc.setFuncCreationDate(longToDate(getLong(rs, 10))); - doc.setFuncUpdateDate(longToDate(getLong(rs, 11))); - doc.setRuleUuid(rs.getString(12)); - doc.setLanguage(rs.getString(13)); - doc.setComponentUuid(rs.getString(14)); - String scope = rs.getString(16); - String filePath = extractFilePath(rs.getString(15), scope); - doc.setFilePath(filePath); - doc.setDirectoryPath(extractDirPath(doc.filePath(), scope)); - String branchUuid = rs.getString(17); - boolean isMainBranch = rs.getBoolean( 18); - String projectUuid = rs.getString(19); - doc.setBranchUuid(branchUuid); - doc.setIsMainBranch(isMainBranch); - doc.setProjectUuid(projectUuid); - String tags = rs.getString(20); - doc.setTags(STRING_LIST_SPLITTER.splitToList(tags == null ? "" : tags)); - doc.setType(RuleType.valueOf(rs.getInt(21))); - - SecurityStandards securityStandards = fromSecurityStandards(deserializeSecurityStandardsString(rs.getString(22))); - SecurityStandards.SQCategory sqCategory = securityStandards.getSqCategory(); - doc.setOwaspTop10(securityStandards.getOwaspTop10()); - doc.setOwaspTop10For2021(securityStandards.getOwaspTop10For2021()); - doc.setPciDss32(securityStandards.getPciDss32()); - doc.setPciDss40(securityStandards.getPciDss40()); - doc.setOwaspAsvs40(securityStandards.getOwaspAsvs40()); - doc.setCwe(securityStandards.getCwe()); - doc.setSansTop25(securityStandards.getSansTop25()); - doc.setSonarSourceSecurityCategory(sqCategory); - doc.setVulnerabilityProbability(sqCategory.getVulnerability()); - - doc.setScope(Qualifiers.UNIT_TEST_FILE.equals(rs.getString(23)) ? IssueScope.TEST : IssueScope.MAIN); - doc.setIsNewCodeReference(!isNullOrEmpty(rs.getString(24))); - String codeVariants = rs.getString(25); - doc.setCodeVariants(STRING_LIST_SPLITTER.splitToList(codeVariants == null ? "" : codeVariants)); - return doc; - } - - @CheckForNull - private static String extractDirPath(@Nullable String filePath, String scope) { - if (filePath != null) { - if (Scopes.DIRECTORY.equals(scope)) { - return filePath; - } - int lastSlashIndex = CharMatcher.anyOf("/").lastIndexIn(filePath); - if (lastSlashIndex > 0) { - return filePath.substring(0, lastSlashIndex); - } - return "/"; - } - return null; - } - - @CheckForNull - private static String extractFilePath(@Nullable String filePath, String scope) { - // On modules, the path contains the relative path of the module starting from its parent, and in E/S we're only interested in the - // path - // of files and directories. - // That's why the file path should be null on modules and projects. - if (filePath != null && !Scopes.PROJECT.equals(scope)) { - return filePath; - } - return null; - } - + @Override + public void close() { + session.close(); } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java index 0b90f2486c5..92874fc6ced 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java @@ -19,7 +19,6 @@ */ package org.sonar.server.measure.index; -import com.google.common.collect.ImmutableMap; import java.util.Collection; import java.util.Date; import java.util.HashMap; diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java index 9d30e3e5ec0..a8991a87577 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java @@ -40,6 +40,7 @@ import java.util.stream.Stream; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; +import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.query.BoolQueryBuilder; @@ -51,6 +52,7 @@ import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.BucketOrder; import org.elasticsearch.search.aggregations.HasAggregations; import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator; import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; import org.elasticsearch.search.aggregations.bucket.histogram.LongBounds; @@ -66,7 +68,9 @@ import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.sort.FieldSortBuilder; import org.joda.time.Duration; import org.sonar.api.issue.Issue; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.Severity; +import org.sonar.api.rules.CleanCodeAttributeCategory; import org.sonar.api.rules.RuleType; import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version; @@ -101,10 +105,12 @@ import static java.util.stream.Collectors.toCollection; import static org.elasticsearch.index.query.QueryBuilders.boolQuery; import static org.elasticsearch.index.query.QueryBuilders.existsQuery; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.index.query.QueryBuilders.nestedQuery; import static org.elasticsearch.index.query.QueryBuilders.prefixQuery; import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.index.query.QueryBuilders.termsQuery; +import static org.elasticsearch.search.aggregations.AggregationBuilders.filters; import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; import static org.sonar.api.rules.RuleType.VULNERABILITY; import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars; @@ -116,6 +122,7 @@ import static org.sonar.server.es.searchrequest.TopAggregationHelper.NO_OTHER_SU import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNED_TO_ME; import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNEES; import static org.sonar.server.issue.index.IssueIndex.Facet.AUTHOR; +import static org.sonar.server.issue.index.IssueIndex.Facet.CLEAN_CODE_ATTRIBUTE_CATEGORY; import static org.sonar.server.issue.index.IssueIndex.Facet.CODE_VARIANTS; import static org.sonar.server.issue.index.IssueIndex.Facet.CREATED_AT; import static org.sonar.server.issue.index.IssueIndex.Facet.CWE; @@ -133,6 +140,8 @@ import static org.sonar.server.issue.index.IssueIndex.Facet.RULES; import static org.sonar.server.issue.index.IssueIndex.Facet.SANS_TOP_25; import static org.sonar.server.issue.index.IssueIndex.Facet.SCOPES; import static org.sonar.server.issue.index.IssueIndex.Facet.SEVERITIES; +import static org.sonar.server.issue.index.IssueIndex.Facet.IMPACT_SOFTWARE_QUALITY; +import static org.sonar.server.issue.index.IssueIndex.Facet.IMPACT_SEVERITY; import static org.sonar.server.issue.index.IssueIndex.Facet.SONARSOURCE_SECURITY; import static org.sonar.server.issue.index.IssueIndex.Facet.STATUSES; import static org.sonar.server.issue.index.IssueIndex.Facet.TAGS; @@ -140,6 +149,7 @@ import static org.sonar.server.issue.index.IssueIndex.Facet.TYPES; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID; +import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_CLEAN_CODE_ATTRIBUTE_CATEGORY; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_CODE_VARIANTS; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_CWE; @@ -166,6 +176,9 @@ import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SANS import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SCOPE; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SEVERITY; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE; +import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_IMPACTS; +import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY; +import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_IMPACT_SEVERITY; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SQ_SECURITY_CATEGORY; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_STATUS; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_TAGS; @@ -180,6 +193,7 @@ import static org.sonar.server.view.index.ViewIndexDefinition.TYPE_VIEW; import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHOR; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CODE_VARIANTS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AT; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CWE; @@ -196,6 +210,8 @@ 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_SOFTWARE_QUALITIES; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SOFTWARE_QUALITIES_SEVERTIIES; 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; @@ -239,6 +255,10 @@ public class IssueIndex { public enum Facet { SEVERITIES(PARAM_SEVERITIES, FIELD_ISSUE_SEVERITY, STICKY, Severity.ALL.size()), + IMPACT_SOFTWARE_QUALITY(PARAM_SOFTWARE_QUALITIES, FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY, STICKY, SoftwareQuality.values().length), + IMPACT_SEVERITY(PARAM_SOFTWARE_QUALITIES_SEVERTIIES, FIELD_ISSUE_IMPACT_SEVERITY, STICKY, + org.sonar.api.issue.impact.Severity.values().length), + CLEAN_CODE_ATTRIBUTE_CATEGORY(PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES, FIELD_ISSUE_CLEAN_CODE_ATTRIBUTE_CATEGORY, STICKY, CleanCodeAttributeCategory.values().length), STATUSES(PARAM_STATUSES, FIELD_ISSUE_STATUS, STICKY, Issue.STATUSES.size()), // Resolutions facet returns one more element than the number of resolutions to take into account unresolved issues RESOLUTIONS(PARAM_RESOLUTIONS, FIELD_ISSUE_RESOLUTION, STICKY, Issue.RESOLUTIONS.size() + 1), @@ -445,6 +465,10 @@ public class IssueIndex { filters.addFilter(FIELD_ISSUE_TAGS, TAGS.getFilterScope(), createTermsFilter(FIELD_ISSUE_TAGS, query.tags())); filters.addFilter(FIELD_ISSUE_TYPE, TYPES.getFilterScope(), createTermsFilter(FIELD_ISSUE_TYPE, query.types())); filters.addFilter( + FIELD_ISSUE_CLEAN_CODE_ATTRIBUTE_CATEGORY, + CLEAN_CODE_ATTRIBUTE_CATEGORY.getFilterScope(), + createTermsFilter(FIELD_ISSUE_CLEAN_CODE_ATTRIBUTE_CATEGORY, query.cleanCodeAttributesCategories())); + filters.addFilter( FIELD_ISSUE_RESOLUTION, RESOLUTIONS.getFilterScope(), createTermsFilter(FIELD_ISSUE_RESOLUTION, query.resolutions())); filters.addFilter( @@ -468,7 +492,7 @@ public class IssueIndex { addSecurityCategoryFilter(FIELD_ISSUE_SQ_SECURITY_CATEGORY, SONARSOURCE_SECURITY, query.sonarsourceSecurity(), filters); addSeverityFilter(query, filters); - + addImpactFilters(query, filters); addComponentRelatedFilters(query, filters); addDatesFilter(filters, query); addCreatedAfterByProjectsFilter(filters, query); @@ -490,7 +514,6 @@ public class IssueIndex { } } - private static Set<String> calculateRequirementsForOwaspAsvs40Params(IssueQuery query) { int level = query.getOwaspAsvsLevel().orElse(3); List<String> levelRequirements = OWASP_ASVS_40_REQUIREMENTS_BY_LEVEL.get(level); @@ -573,6 +596,31 @@ public class IssueIndex { } } + private static void addImpactFilters(IssueQuery query, AllFilters allFilters) { + if (query.impactSoftwareQualities().isEmpty() && query.impactSeverities().isEmpty()) { + return; + } + if (!query.impactSoftwareQualities().isEmpty()) { + allFilters.addFilter( + FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY, + IMPACT_SOFTWARE_QUALITY.getFilterScope(), + nestedQuery( + FIELD_ISSUE_IMPACTS, + termsQuery(FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY, query.impactSoftwareQualities()), + ScoreMode.Avg)); + } + + if (!query.impactSeverities().isEmpty()) { + allFilters.addFilter( + FIELD_ISSUE_IMPACT_SEVERITY, + IMPACT_SEVERITY.getFilterScope(), + nestedQuery( + FIELD_ISSUE_IMPACTS, + termsQuery(FIELD_ISSUE_IMPACT_SEVERITY, query.impactSeverities()), + ScoreMode.Avg)); + } + } + private static void addComponentRelatedFilters(IssueQuery query, AllFilters filters) { addCommonComponentRelatedFilters(query, filters); if (query.viewUuids().isEmpty()) { @@ -647,11 +695,11 @@ public class IssueIndex { private static RequestFiltersComputer newFilterComputer(SearchOptions options, AllFilters allFilters) { Collection<String> facetNames = options.getFacets(); Set<TopAggregationDefinition<?>> facets = Stream.concat( - Stream.of(EFFORT_TOP_AGGREGATION), - facetNames.stream() - .map(FACETS_BY_NAME::get) - .filter(Objects::nonNull) - .map(Facet::getTopAggregationDef)) + Stream.of(EFFORT_TOP_AGGREGATION), + facetNames.stream() + .map(FACETS_BY_NAME::get) + .filter(Objects::nonNull) + .map(Facet::getTopAggregationDef)) .collect(Collectors.toSet()); return new RequestFiltersComputer(allFilters, facets); @@ -789,6 +837,7 @@ public class IssueIndex { addFacetIfNeeded(options, aggregationHelper, esRequest, TAGS, query.tags().toArray()); addFacetIfNeeded(options, aggregationHelper, esRequest, TYPES, query.types().toArray()); addFacetIfNeeded(options, aggregationHelper, esRequest, CODE_VARIANTS, query.codeVariants().toArray()); + addFacetIfNeeded(options, aggregationHelper, esRequest, CLEAN_CODE_ATTRIBUTE_CATEGORY, query.cleanCodeAttributesCategories().toArray()); addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_32, PCI_DSS_32, options, aggregationHelper, esRequest, query.pciDss32().toArray()); addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_40, PCI_DSS_40, options, aggregationHelper, esRequest, query.pciDss40().toArray()); @@ -800,6 +849,8 @@ public class IssueIndex { addSecurityCategoryFacetIfNeeded(PARAM_SONARSOURCE_SECURITY, SONARSOURCE_SECURITY, options, aggregationHelper, esRequest, query.sonarsourceSecurity().toArray()); addSeverityFacetIfNeeded(options, aggregationHelper, esRequest); + addImpactSoftwareQualityFacetIfNeeded(options, query, aggregationHelper, esRequest); + addImpactSeverityFacetIfNeeded(options, query, aggregationHelper, esRequest); addResolutionFacetIfNeeded(options, query, aggregationHelper, esRequest); addAssigneesFacetIfNeeded(options, query, aggregationHelper, esRequest); addCreatedAtFacetIfNeeded(options, query, aggregationHelper, queryFilters, esRequest); @@ -848,6 +899,54 @@ public class IssueIndex { esRequest.aggregation(aggregation); } + private static void addImpactSoftwareQualityFacetIfNeeded(SearchOptions options, IssueQuery query, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) { + if (!options.getFacets().contains(PARAM_SOFTWARE_QUALITIES)) { + return; + } + + Function<SoftwareQuality, BoolQueryBuilder> mainQuery = softwareQuality -> boolQuery() + .filter(termQuery(FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY, softwareQuality.name())); + + FiltersAggregator.KeyedFilter[] keyedFilters = Arrays.stream(SoftwareQuality.values()) + .map(softwareQuality -> new FiltersAggregator.KeyedFilter(softwareQuality.name(), + query.impactSeverities().isEmpty() ? mainQuery.apply(softwareQuality) + : mainQuery.apply(softwareQuality) + .filter(termsQuery(FIELD_ISSUE_IMPACT_SEVERITY, query.impactSeverities())))) + .toArray(FiltersAggregator.KeyedFilter[]::new); + + AggregationBuilder aggregation = aggregationHelper.buildTopAggregation( + IMPACT_SOFTWARE_QUALITY.getName(), IMPACT_SOFTWARE_QUALITY.getTopAggregationDef(), + NO_EXTRA_FILTER, + t -> t.subAggregation(AggregationBuilders.nested("nested_" + IMPACT_SOFTWARE_QUALITY.getName(), FIELD_ISSUE_IMPACTS) + .subAggregation(filters(IMPACT_SOFTWARE_QUALITY.getName(), keyedFilters)))); + + esRequest.aggregation(aggregation); + } + + private static void addImpactSeverityFacetIfNeeded(SearchOptions options, IssueQuery query, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) { + if (!options.getFacets().contains(PARAM_SOFTWARE_QUALITIES_SEVERTIIES)) { + return; + } + + Function<org.sonar.api.issue.impact.Severity, BoolQueryBuilder> mainQuery = softwareQuality -> boolQuery() + .filter(termQuery(FIELD_ISSUE_IMPACT_SEVERITY, softwareQuality.name())); + + FiltersAggregator.KeyedFilter[] keyedFilters = Arrays.stream(org.sonar.api.issue.impact.Severity.values()) + .map(severity -> new FiltersAggregator.KeyedFilter(severity.name(), + query.impactSoftwareQualities().isEmpty() ? mainQuery.apply(severity) + : mainQuery.apply(severity) + .filter(termsQuery(FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY, query.impactSoftwareQualities())))) + .toArray(FiltersAggregator.KeyedFilter[]::new); + + AggregationBuilder aggregation = aggregationHelper.buildTopAggregation( + IMPACT_SEVERITY.getName(), IMPACT_SEVERITY.getTopAggregationDef(), + NO_EXTRA_FILTER, + t -> t.subAggregation(AggregationBuilders.nested("nested_" + IMPACT_SEVERITY.getName(), FIELD_ISSUE_IMPACTS) + .subAggregation(filters(IMPACT_SEVERITY.getName(), + keyedFilters)))); + esRequest.aggregation(aggregation); + } + private static void addResolutionFacetIfNeeded(SearchOptions options, IssueQuery query, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) { if (!options.getFacets().contains(PARAM_RESOLUTIONS)) { return; @@ -857,11 +956,11 @@ public class IssueIndex { RESOLUTIONS.getName(), RESOLUTIONS.getTopAggregationDef(), RESOLUTIONS.getNumberOfTerms(), NO_EXTRA_FILTER, t -> - // add aggregation of type "missing" to return count of unresolved issues in the facet - t.subAggregation( - addEffortAggregationIfNeeded(query, AggregationBuilders - .missing(RESOLUTIONS.getName() + FACET_SUFFIX_MISSING) - .field(RESOLUTIONS.getFieldName())))); + // add aggregation of type "missing" to return count of unresolved issues in the facet + t.subAggregation( + addEffortAggregationIfNeeded(query, AggregationBuilders + .missing(RESOLUTIONS.getName() + FACET_SUFFIX_MISSING) + .field(RESOLUTIONS.getFieldName())))); esRequest.aggregation(aggregation); } @@ -980,10 +1079,10 @@ public class IssueIndex { ASSIGNED_TO_ME.getTopAggregationDef(), NO_EXTRA_FILTER, t -> - // add sub-aggregation to return issue count for current user - aggregationHelper.getSubAggregationHelper() - .buildSelectedItemsAggregation(ASSIGNED_TO_ME.getName(), ASSIGNED_TO_ME.getTopAggregationDef(), new String[]{uuid}) - .ifPresent(t::subAggregation)); + // add sub-aggregation to return issue count for current user + aggregationHelper.getSubAggregationHelper() + .buildSelectedItemsAggregation(ASSIGNED_TO_ME.getName(), ASSIGNED_TO_ME.getTopAggregationDef(), new String[] {uuid}) + .ifPresent(t::subAggregation)); esRequest.aggregation(aggregation); } } diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java index a6c367cc8c4..11cd3614c1f 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java @@ -59,6 +59,8 @@ public class IssueQuery { private final Collection<String> issueKeys; private final Collection<String> severities; + private final Collection<String> impactSeverities; + private final Collection<String> impactSoftwareQualities; private final Collection<String> statuses; private final Collection<String> resolutions; private final Collection<String> components; @@ -99,10 +101,13 @@ public class IssueQuery { private final Boolean newCodeOnReference; private final Collection<String> newCodeOnReferenceByProjectUuids; private final Collection<String> codeVariants; + private Collection<String> cleanCodeAttributesCategories; private IssueQuery(Builder builder) { this.issueKeys = defaultCollection(builder.issueKeys); this.severities = defaultCollection(builder.severities); + this.impactSeverities = defaultCollection(builder.impactSeverities); + this.impactSoftwareQualities = defaultCollection(builder.impactSoftwareQualities); this.statuses = defaultCollection(builder.statuses); this.resolutions = defaultCollection(builder.resolutions); this.components = defaultCollection(builder.components); @@ -143,6 +148,7 @@ public class IssueQuery { this.newCodeOnReference = builder.newCodeOnReference; this.newCodeOnReferenceByProjectUuids = defaultCollection(builder.newCodeOnReferenceByProjectUuids); this.codeVariants = defaultCollection(builder.codeVariants); + this.cleanCodeAttributesCategories = defaultCollection(builder.cleanCodeAttributesCategories); } public Collection<String> issueKeys() { @@ -153,6 +159,14 @@ public class IssueQuery { return severities; } + public Collection<String> impactSeverities() { + return impactSeverities; + } + + public Collection<String> impactSoftwareQualities() { + return impactSoftwareQualities; + } + public Collection<String> statuses() { return statuses; } @@ -338,9 +352,15 @@ public class IssueQuery { return codeVariants; } + public Collection<String> cleanCodeAttributesCategories() { + return cleanCodeAttributesCategories; + } + public static class Builder { private Collection<String> issueKeys; private Collection<String> severities; + private Collection<String> impactSeverities; + private Collection<String> impactSoftwareQualities; private Collection<String> statuses; private Collection<String> resolutions; private Collection<String> components; @@ -381,6 +401,7 @@ public class IssueQuery { private Boolean newCodeOnReference = null; private Collection<String> newCodeOnReferenceByProjectUuids; private Collection<String> codeVariants; + private Collection<String> cleanCodeAttributesCategories; private Builder() { @@ -421,6 +442,16 @@ public class IssueQuery { return this; } + public Builder impactSeverities(@Nullable Collection<String> l) { + this.impactSeverities = l; + return this; + } + + public Builder impactSoftwareQualities(@Nullable Collection<String> l) { + this.impactSoftwareQualities = l; + return this; + } + public Builder files(@Nullable Collection<String> l) { this.files = l; return this; @@ -626,6 +657,11 @@ public class IssueQuery { this.codeVariants = codeVariants; return this; } + + public Builder cleanCodeAttributesCategories(@Nullable Collection<String> cleanCodeAttributesCategories) { + this.cleanCodeAttributesCategories = cleanCodeAttributesCategories; + return this; + } } private static <T> Collection<T> defaultCollection(@Nullable Collection<T> c) { diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java index 00046aa6012..cc9ede60838 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java @@ -23,8 +23,10 @@ import java.time.ZoneId; import java.util.Collections; import java.util.Date; import java.util.Map; +import java.util.Set; import org.elasticsearch.action.search.SearchResponse; import org.junit.Test; +import org.sonar.api.issue.impact.Severity; import org.sonar.api.rules.RuleType; import org.sonar.api.server.rule.RulesDefinition.OwaspAsvsVersion; import org.sonar.db.component.ComponentDto; @@ -47,11 +49,17 @@ import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; import static org.sonar.api.issue.Issue.STATUS_OPEN; import static org.sonar.api.issue.Issue.STATUS_REOPENED; import static org.sonar.api.issue.Issue.STATUS_RESOLVED; +import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY; +import static org.sonar.api.issue.impact.SoftwareQuality.RELIABILITY; import static org.sonar.api.rule.Severity.BLOCKER; import static org.sonar.api.rule.Severity.CRITICAL; import static org.sonar.api.rule.Severity.INFO; import static org.sonar.api.rule.Severity.MAJOR; import static org.sonar.api.rule.Severity.MINOR; +import static org.sonar.api.rules.CleanCodeAttributeCategory.ADAPTABLE; +import static org.sonar.api.rules.CleanCodeAttributeCategory.CONSISTENT; +import static org.sonar.api.rules.CleanCodeAttributeCategory.INTENTIONAL; +import static org.sonar.api.rules.CleanCodeAttributeCategory.RESPONSIBLE; import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2017; import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021; import static org.sonar.api.server.rule.RulesDefinition.PciDssVersion.V3_2; @@ -137,7 +145,8 @@ public class IssueIndexFacetsTest extends IssueIndexTestCommon { @Test public void facet_on_directories_return_100_entries_plus_selected_values() { ComponentDto project = newPrivateProjectDto(); - indexIssues(rangeClosed(1, 110).mapToObj(i -> newDoc(newFileDto(project, newDirectory(project, "dir" + i)), project.uuid()).setDirectoryPath("a" + i)).toArray(IssueDoc[]::new)); + indexIssues( + rangeClosed(1, 110).mapToObj(i -> newDoc(newFileDto(project, newDirectory(project, "dir" + i)), project.uuid()).setDirectoryPath("a" + i)).toArray(IssueDoc[]::new)); IssueDoc issue1 = newDoc(newFileDto(project, newDirectory(project, "path1")), project.uuid()).setDirectoryPath("directory1"); IssueDoc issue2 = newDoc(newFileDto(project, newDirectory(project, "path2")), project.uuid()).setDirectoryPath("directory2"); indexIssues(issue1, issue2); @@ -539,8 +548,8 @@ public class IssueIndexFacetsTest extends IssueIndexTestCommon { SearchOptions options = fixtureForCreatedAtFacet(); SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) - .createdBefore(parseDateTime("2014-09-21T00:00:00+0100")).build(), + .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) + .createdBefore(parseDateTime("2014-09-21T00:00:00+0100")).build(), options); Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt"); assertThat(createdAt).containsOnly( @@ -555,8 +564,8 @@ public class IssueIndexFacetsTest extends IssueIndexTestCommon { SearchOptions options = fixtureForCreatedAtFacet(); SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) - .createdBefore(parseDateTime("2015-01-19T00:00:00+0100")).build(), + .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) + .createdBefore(parseDateTime("2015-01-19T00:00:00+0100")).build(), options); Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt"); assertThat(createdAt).containsOnly( @@ -573,8 +582,8 @@ public class IssueIndexFacetsTest extends IssueIndexTestCommon { SearchOptions options = fixtureForCreatedAtFacet(); SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2011-01-01T00:00:00+0100")) - .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(), + .createdAfter(parseDateTime("2011-01-01T00:00:00+0100")) + .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(), options); Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt"); assertThat(createdAt).containsOnly( @@ -591,8 +600,8 @@ public class IssueIndexFacetsTest extends IssueIndexTestCommon { SearchOptions options = fixtureForCreatedAtFacet(); SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2014-09-01T00:00:00-0100")) - .createdBefore(parseDateTime("2014-09-02T00:00:00-0100")).build(), + .createdAfter(parseDateTime("2014-09-01T00:00:00-0100")) + .createdBefore(parseDateTime("2014-09-02T00:00:00-0100")).build(), options); Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt"); assertThat(createdAt).containsOnly( @@ -624,7 +633,7 @@ public class IssueIndexFacetsTest extends IssueIndexTestCommon { SearchOptions searchOptions = fixtureForCreatedAtFacet(); SearchResponse result = underTest.search(IssueQuery.builder() - .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(), + .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(), searchOptions); Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt"); assertThat(createdAt).containsOnly( @@ -661,6 +670,155 @@ public class IssueIndexFacetsTest extends IssueIndexTestCommon { entry("variant3", 1L)); } + @Test + public void search_shouldReturnSoftwareQualityFacet() { + ComponentDto project = newPrivateProjectDto(); + ComponentDto file = newFileDto(project); + + indexIssues( + newDoc("I1", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.HIGH, + RELIABILITY, org.sonar.api.issue.impact.Severity.MEDIUM)), + newDoc("I2", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW)), + newDoc("I3", project.uuid(), file).setImpacts(Map.of( + RELIABILITY, org.sonar.api.issue.impact.Severity.HIGH)), + newDoc("I4", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW))); + + assertThatFacetHasOnly(IssueQuery.builder(), "softwareQualities", + entry("MAINTAINABILITY", 3L), + entry("RELIABILITY", 2L), + entry("SECURITY", 0L)); + } + + @Test + public void search_whenFilteredOnSeverity_shouldReturnSoftwareQualityFacet() { + ComponentDto project = newPrivateProjectDto(); + ComponentDto file = newFileDto(project); + + indexIssues( + newDoc("I1", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.HIGH, + RELIABILITY, org.sonar.api.issue.impact.Severity.MEDIUM)) + .setTags(singletonList("my-tag")), + newDoc("I2", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW)), + newDoc("I3", project.uuid(), file).setImpacts(Map.of( + RELIABILITY, org.sonar.api.issue.impact.Severity.HIGH)), + newDoc("I4", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW))); + + assertThatFacetHasOnly(IssueQuery.builder().impactSoftwareQualities(Set.of(MAINTAINABILITY.name())).impactSeverities(Set.of(org.sonar.api.issue.impact.Severity.LOW.name())), + "softwareQualities", + entry("MAINTAINABILITY", 2L), + entry("RELIABILITY", 0L), + entry("SECURITY", 0L)); + + assertThatFacetHasOnly(IssueQuery.builder().impactSeverities(Set.of(Severity.MEDIUM.name())), "softwareQualities", + entry("MAINTAINABILITY", 0L), + entry("RELIABILITY", 1L), + entry("SECURITY", 0L)); + + assertThatFacetHasOnly(IssueQuery.builder().impactSeverities(Set.of(org.sonar.api.issue.impact.Severity.HIGH.name())), "softwareQualities", + entry("MAINTAINABILITY", 1L), + entry("RELIABILITY", 1L), + entry("SECURITY", 0L)); + + assertThatFacetHasOnly(IssueQuery.builder() + .tags(singletonList("my-tag")) + .impactSeverities(Set.of(org.sonar.api.issue.impact.Severity.HIGH.name())), "softwareQualities", + entry("MAINTAINABILITY", 1L), + entry("RELIABILITY", 0L), + entry("SECURITY", 0L)); + } + + @Test + public void search_shouldReturnSoftwareQualitySeverityFacet() { + ComponentDto project = newPrivateProjectDto(); + ComponentDto file = newFileDto(project); + + indexIssues( + newDoc("I1", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.HIGH, + RELIABILITY, org.sonar.api.issue.impact.Severity.MEDIUM)), + newDoc("I2", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW)), + newDoc("I3", project.uuid(), file).setImpacts(Map.of( + RELIABILITY, org.sonar.api.issue.impact.Severity.HIGH)), + newDoc("I4", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW))); + + assertThatFacetHasOnly(IssueQuery.builder(), "softwareQualitiesSeverities", + entry("HIGH", 2L), + entry("MEDIUM", 1L), + entry("LOW", 2L)); + } + + @Test + public void search_whenFilteredOnSoftwareQuality_shouldReturnSoftwareQualitySeverityFacet() { + ComponentDto project = newPrivateProjectDto(); + ComponentDto file = newFileDto(project); + + indexIssues( + newDoc("I1", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.HIGH, + RELIABILITY, org.sonar.api.issue.impact.Severity.MEDIUM)), + newDoc("I2", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW)), + newDoc("I3", project.uuid(), file).setImpacts(Map.of( + RELIABILITY, org.sonar.api.issue.impact.Severity.HIGH)), + newDoc("I4", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW))); + + assertThatFacetHasOnly(IssueQuery.builder().impactSoftwareQualities(Set.of(MAINTAINABILITY.name())), "softwareQualitiesSeverities", + entry("HIGH", 1L), + entry("MEDIUM", 0L), + entry("LOW", 2L)); + } + + @Test + public void search_shouldReturnCleanCodeAttributeCategoryFacet() { + ComponentDto project = newPrivateProjectDto(); + ComponentDto file = newFileDto(project); + + indexIssues( + newDoc("I1", project.uuid(), file).setCleanCodeAttributeCategory(ADAPTABLE.name()), + newDoc("I2", project.uuid(), file).setCleanCodeAttributeCategory(ADAPTABLE.name()), + newDoc("I3", project.uuid(), file).setCleanCodeAttributeCategory(CONSISTENT.name()), + newDoc("I4", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I5", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I6", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I7", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I8", project.uuid(), file).setCleanCodeAttributeCategory(RESPONSIBLE.name())); + + assertThatFacetHasOnly(IssueQuery.builder(), "cleanCodeAttributeCategories", + entry("INTENTIONAL", 4L), + entry("ADAPTABLE", 2L), + entry("CONSISTENT", 1L), + entry("RESPONSIBLE", 1L)); + } + + @Test + public void search_whenFilteredByTags_shouldReturnCleanCodeAttributeCategoryFacet() { + ComponentDto project = newPrivateProjectDto(); + ComponentDto file = newFileDto(project); + + indexIssues( + newDoc("I1", project.uuid(), file).setCleanCodeAttributeCategory(ADAPTABLE.name()).setTags(singletonList("tag-1")), + newDoc("I2", project.uuid(), file).setCleanCodeAttributeCategory(ADAPTABLE.name()).setTags(singletonList("tag-1")), + newDoc("I3", project.uuid(), file).setCleanCodeAttributeCategory(CONSISTENT.name()), + newDoc("I4", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()).setTags(singletonList("tag-1")), + newDoc("I5", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I6", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I7", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I8", project.uuid(), file).setCleanCodeAttributeCategory(RESPONSIBLE.name()).setTags(singletonList("tag-3"))); + + assertThatFacetHasOnly(IssueQuery.builder().tags(singletonList("tag-1")), "cleanCodeAttributeCategories", + entry("INTENTIONAL", 1L), + entry("ADAPTABLE", 2L)); + } + private SearchOptions fixtureForCreatedAtFacet() { ComponentDto project = newPrivateProjectDto(); ComponentDto file = newFileDto(project); diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java index 6ad32771478..d6f0c5a7519 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java @@ -19,9 +19,9 @@ */ package org.sonar.server.issue.index; -import com.google.common.collect.ImmutableMap; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Set; import org.assertj.core.api.Fail; import org.junit.Test; @@ -39,7 +39,14 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY; +import static org.sonar.api.issue.impact.SoftwareQuality.RELIABILITY; +import static org.sonar.api.issue.impact.SoftwareQuality.SECURITY; import static org.sonar.api.resources.Qualifiers.APP; +import static org.sonar.api.rules.CleanCodeAttributeCategory.ADAPTABLE; +import static org.sonar.api.rules.CleanCodeAttributeCategory.CONSISTENT; +import static org.sonar.api.rules.CleanCodeAttributeCategory.INTENTIONAL; +import static org.sonar.api.rules.CleanCodeAttributeCategory.RESPONSIBLE; import static org.sonar.api.utils.DateUtils.addDays; import static org.sonar.api.utils.DateUtils.parseDate; import static org.sonar.api.utils.DateUtils.parseDateTime; @@ -347,25 +354,25 @@ public class IssueIndexFiltersTest extends IssueIndexTestCommon { // Search for issues of project 1 having less than 15 days assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfterByProjectUuids(ImmutableMap.of(project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -15), true))), + .createdAfterByProjectUuids(Map.of(project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -15), true))), project1Issue1.key()); // Search for issues of project 1 having less than 14 days and project 2 having less then 25 days assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfterByProjectUuids(ImmutableMap.of( - project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -14), true), - project2.uuid(), new IssueQuery.PeriodStart(addDays(now, -25), true))), + .createdAfterByProjectUuids(Map.of( + project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -14), true), + project2.uuid(), new IssueQuery.PeriodStart(addDays(now, -25), true))), project1Issue1.key(), project2Issue1.key()); // Search for issues of project 1 having less than 30 days assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfterByProjectUuids(ImmutableMap.of( - project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -30), true))), + .createdAfterByProjectUuids(Map.of( + project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -30), true))), project1Issue1.key(), project1Issue2.key()); // Search for issues of project 1 and project 2 having less than 5 days assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfterByProjectUuids(ImmutableMap.of( + .createdAfterByProjectUuids(Map.of( project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -5), true), project2.uuid(), new IssueQuery.PeriodStart(addDays(now, -5), true)))); } @@ -396,29 +403,29 @@ public class IssueIndexFiltersTest extends IssueIndexTestCommon { // Search for issues of project 1 branch 1 having less than 15 days assertThatSearchReturnsOnly(IssueQuery.builder() - .mainBranch(false) - .createdAfterByProjectUuids(ImmutableMap.of(project1Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -15), true))), + .mainBranch(false) + .createdAfterByProjectUuids(Map.of(project1Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -15), true))), project1Branch1Issue1.key()); // Search for issues of project 1 branch 1 having less than 14 days and project 2 branch 1 having less then 25 days assertThatSearchReturnsOnly(IssueQuery.builder() - .mainBranch(false) - .createdAfterByProjectUuids(ImmutableMap.of( - project1Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -14), true), - project2Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -25), true))), + .mainBranch(false) + .createdAfterByProjectUuids(Map.of( + project1Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -14), true), + project2Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -25), true))), project1Branch1Issue1.key(), project2Branch1Issue1.key()); // Search for issues of project 1 branch 1 having less than 30 days assertThatSearchReturnsOnly(IssueQuery.builder() - .mainBranch(false) - .createdAfterByProjectUuids(ImmutableMap.of( - project1Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -30), true))), + .mainBranch(false) + .createdAfterByProjectUuids(Map.of( + project1Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -30), true))), project1Branch1Issue1.key(), project1Branch1Issue2.key()); // Search for issues of project 1 branch 1 and project 2 branch 2 having less than 5 days assertThatSearchReturnsOnly(IssueQuery.builder() .mainBranch(false) - .createdAfterByProjectUuids(ImmutableMap.of( + .createdAfterByProjectUuids(Map.of( project1Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -5), true), project2Branch1.uuid(), new IssueQuery.PeriodStart(addDays(now, -5), true)))); } @@ -435,7 +442,7 @@ public class IssueIndexFiltersTest extends IssueIndexTestCommon { // Search for issues of project 1 and project 2 that are new code on a branch using reference for new code assertThatSearchReturnsOnly(IssueQuery.builder() - .newCodeOnReferenceByProjectUuids(Set.of(project1.uuid(), project2.uuid())), + .newCodeOnReferenceByProjectUuids(Set.of(project1.uuid(), project2.uuid())), project1Issue1.key(), project2Issue2.key()); } @@ -463,8 +470,8 @@ public class IssueIndexFiltersTest extends IssueIndexTestCommon { // Search for issues of project 1 branch 1 and project 2 branch 1 that are new code on a branch using reference for new code assertThatSearchReturnsOnly(IssueQuery.builder() - .mainBranch(false) - .newCodeOnReferenceByProjectUuids(Set.of(project1Branch1.uuid(), project2Branch1.uuid())), + .mainBranch(false) + .newCodeOnReferenceByProjectUuids(Set.of(project1Branch1.uuid(), project2Branch1.uuid())), project1Branch1Issue2.key(), project2Branch1Issue2.key()); } @@ -852,6 +859,70 @@ public class IssueIndexFiltersTest extends IssueIndexTestCommon { assertThatSearchReturnsOnly(IssueQuery.builder().codeVariants(singletonList("variant2")), "I1", "I2"); } + @Test + public void search_whenFilteringBySoftwareQualities_shouldReturnRelevantIssues() { + ComponentDto project = newPrivateProjectDto(); + ComponentDto file = newFileDto(project); + + indexIssues( + newDoc("I1", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.HIGH, + SECURITY, org.sonar.api.issue.impact.Severity.LOW, + RELIABILITY, org.sonar.api.issue.impact.Severity.MEDIUM)), + + newDoc("I2", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW, + SECURITY, org.sonar.api.issue.impact.Severity.LOW)), + newDoc("I3", project.uuid(), file).setImpacts(Map.of( + RELIABILITY, org.sonar.api.issue.impact.Severity.HIGH)), + newDoc("I4", project.uuid(), file).setImpacts(Map.of( + MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW))); + + assertThatSearchReturnsOnly(IssueQuery.builder().impactSoftwareQualities(Set.of(MAINTAINABILITY.name())), + "I1", "I2", "I4"); + + assertThatSearchReturnsOnly(IssueQuery.builder().impactSoftwareQualities(Set.of(MAINTAINABILITY.name(), RELIABILITY.name())), + "I1", "I2", "I3", "I4"); + + assertThatSearchReturnsOnly(IssueQuery.builder().impactSeverities(Set.of(org.sonar.api.issue.impact.Severity.HIGH.name())), + "I1", "I3"); + + assertThatSearchReturnsOnly(IssueQuery.builder().impactSeverities(Set.of(org.sonar.api.issue.impact.Severity.LOW.name(), org.sonar.api.issue.impact.Severity.MEDIUM.name())), + "I1", "I2", "I4"); + + assertThatSearchReturnsOnly(IssueQuery.builder() + .impactSoftwareQualities(Set.of(MAINTAINABILITY.name())) + .impactSeverities(Set.of(org.sonar.api.issue.impact.Severity.HIGH.name())), + "I1"); + + } + + @Test + public void search_whenFilteringByCleanCodeAttributeCategory_shouldReturnRelevantIssues() { + ComponentDto project = newPrivateProjectDto(); + ComponentDto file = newFileDto(project); + + indexIssues( + newDoc("I1", project.uuid(), file).setCleanCodeAttributeCategory(ADAPTABLE.name()), + newDoc("I2", project.uuid(), file).setCleanCodeAttributeCategory(ADAPTABLE.name()), + newDoc("I3", project.uuid(), file).setCleanCodeAttributeCategory(CONSISTENT.name()), + newDoc("I4", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I5", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I6", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I7", project.uuid(), file).setCleanCodeAttributeCategory(INTENTIONAL.name()), + newDoc("I8", project.uuid(), file).setCleanCodeAttributeCategory(RESPONSIBLE.name())); + + assertThatSearchReturnsOnly(IssueQuery.builder().cleanCodeAttributesCategories(Set.of(ADAPTABLE.name())), + "I1", "I2"); + + assertThatSearchReturnsOnly(IssueQuery.builder().cleanCodeAttributesCategories(Set.of(CONSISTENT.name(), INTENTIONAL.name())), + "I3", "I4", "I5", "I6", "I7"); + + assertThatSearchReturnsOnly(IssueQuery.builder().cleanCodeAttributesCategories( + Set.of(CONSISTENT.name(), INTENTIONAL.name(), RESPONSIBLE.name(), ADAPTABLE.name())), + "I1", "I2", "I3", "I4", "I5", "I6", "I7", "I8"); + } + private void indexView(String viewUuid, List<String> projectBranchUuids) { viewIndexer.index(new ViewDoc().setUuid(viewUuid).setProjectBranchUuids(projectBranchUuids)); } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java index 1284b1461b8..176b205b349 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java @@ -54,6 +54,11 @@ public class IssuesWsParameters { public static final String PARAM_TYPE = "type"; public static final String PARAM_ISSUES = "issues"; public static final String PARAM_SEVERITIES = "severities"; + public static final String PARAM_SOFTWARE_QUALITIES = "softwareQualities"; + + //TODO: To be discussed for the naming + public static final String PARAM_SOFTWARE_QUALITIES_SEVERTIIES = "softwareQualitiesSeverities"; + public static final String PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES = "cleanCodeAttributeCategories"; public static final String PARAM_STATUSES = "statuses"; public static final String PARAM_RESOLUTIONS = "resolutions"; public static final String PARAM_RESOLVED = "resolved"; |