Browse Source

SONAR-20021 Add filter and facets for Software Quality, Severity and Clean Code Attribute Category

- Refactor issue indexer to use MyBatis mapping with cursor
tags/10.2.0.77647
Jacek Poreda 9 months ago
parent
commit
c5f6c08551
17 changed files with 1083 additions and 237 deletions
  1. 36
    0
      server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java
  2. 316
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IndexedIssueDto.java
  3. 8
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
  4. 3
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
  5. 69
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
  6. 89
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/issue/IndexedIssueDtoTest.java
  7. 9
    0
      server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIndexerIT.java
  8. 1
    0
      server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java
  9. 32
    0
      server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
  10. 11
    0
      server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java
  11. 93
    189
      server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
  12. 0
    1
      server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java
  13. 115
    16
      server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
  14. 36
    0
      server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
  15. 168
    10
      server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java
  16. 92
    21
      server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
  17. 5
    0
      sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java

+ 36
- 0
server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java View File

@@ -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;
@@ -198,6 +200,40 @@ public class IssueDaoIT {
tuple(Severity.LOW, SoftwareQuality.SECURITY));
}

@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

+ 316
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IndexedIssueDto.java View File

@@ -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;
}
}

+ 8
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java View File

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

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

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

+ 69
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml View File

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

+ 89
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IndexedIssueDtoTest.java View File

@@ -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);
}

}

+ 9
- 0
server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIndexerIT.java View File

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

+ 1
- 0
server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java View File

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

+ 32
- 0
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java View File

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

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

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

+ 93
- 189
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java View File

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

+ 0
- 1
server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java View File

@@ -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;

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

@@ -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),
@@ -444,6 +464,10 @@ public class IssueIndex {
filters.addFilter(FIELD_ISSUE_LANGUAGE, LANGUAGES.getFilterScope(), createTermsFilter(FIELD_ISSUE_LANGUAGE, query.languages()));
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()));
@@ -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);
}
}

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

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

+ 168
- 10
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java View File

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

+ 92
- 21
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java View File

@@ -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));
}

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

@@ -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";

Loading…
Cancel
Save