Browse Source

SONAR-21042 - Remove ES Rule index usage in Recently Added Rules and Scanner Engine

tags/10.4.0.87286
Jacek Poreda 5 months ago
parent
commit
87614500bf
26 changed files with 770 additions and 365 deletions
  1. 67
    1
      server/sonar-db-dao/src/it/java/org/sonar/db/rule/RuleDaoIT.java
  2. 7
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDao.java
  3. 102
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleListQuery.java
  4. 40
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleListResult.java
  5. 4
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleMapper.java
  6. 37
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/rule/RuleMapper.xml
  7. 7
    1
      server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts
  8. 4
    0
      server/sonar-web/src/main/js/api/rules.ts
  9. 2
    2
      server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx
  10. 0
    51
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/ActiveRuleCompleterIT.java
  11. 86
    17
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/ListActionIT.java
  12. 4
    5
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/SearchActionIT.java
  13. 3
    4
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/ShowActionIT.java
  14. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/CreateAction.java
  15. 172
    24
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ListAction.java
  16. 2
    3
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleMapper.java
  17. 152
    56
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesResponseFormatter.java
  18. 32
    115
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java
  19. 7
    13
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java
  20. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/UpdateAction.java
  21. 2
    2
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  22. 2
    2
      sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java
  23. 12
    15
      sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultActiveRulesLoader.java
  24. 2
    2
      sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/RulesLoader.java
  25. 15
    43
      sonar-scanner-engine/src/test/java/org/sonar/scanner/rule/DefaultActiveRulesLoaderTest.java
  26. 7
    7
      sonar-ws/src/main/protobuf/ws-rules.proto

+ 67
- 1
server/sonar-db-dao/src/it/java/org/sonar/db/rule/RuleDaoIT.java View File

@@ -48,8 +48,10 @@ import org.sonar.api.utils.System2;
import org.sonar.core.util.UuidFactoryFast;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.Pagination;
import org.sonar.db.RowNotFoundException;
import org.sonar.db.issue.ImpactDto;
import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.rule.RuleDto.Scope;

import static com.google.common.collect.Sets.newHashSet;
@@ -63,10 +65,11 @@ import static org.assertj.core.api.Assertions.tuple;
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.rule.RuleStatus.*;
import static org.sonar.api.rule.RuleStatus.DEPRECATED;
import static org.sonar.api.rule.RuleStatus.READY;
import static org.sonar.api.rule.RuleStatus.REMOVED;
import static org.sonar.db.Pagination.forPage;
import static org.sonar.db.rule.RuleListQuery.RuleListQueryBuilder.newRuleListQueryBuilder;

public class RuleDaoIT {
private static final String UNKNOWN_RULE_UUID = "unknown-uuid";
@@ -1224,6 +1227,69 @@ public class RuleDaoIT {
.isInstanceOf(PersistenceException.class);
}

@Test
public void selectRules_whenPagination_shouldReturnSpecificPage() {
db.rules().insert(rule -> rule.setUuid("uuid_1"));
db.rules().insert(rule -> rule.setUuid("uuid_2"));
db.rules().insert(rule -> rule.setUuid("uuid_3"));
db.rules().insert(rule -> rule.setUuid("uuid_4"));
Pagination pagination = Pagination.forPage(1).andSize(2);

RuleListResult ruleListResult = underTest.selectRules(db.getSession(), newRuleListQueryBuilder().build(), pagination);

assertThat(ruleListResult.getTotal()).isEqualTo(4);
assertThat(ruleListResult.getUuids()).containsExactly("uuid_1", "uuid_2");
}

@Test
public void selectRules_whenQueriedWithCreatedAt_shouldReturnRulesCreatedAfterCorrectlySorted() {
long baseTime = 1696028921248L;
db.rules().insert(rule -> rule.setUuid("uuid_1").setCreatedAt(baseTime));
db.rules().insert(rule -> rule.setUuid("uuid_2").setCreatedAt(baseTime + 1000));
db.rules().insert(rule -> rule.setUuid("uuid_3").setCreatedAt(baseTime - 2000));
db.rules().insert(rule -> rule.setUuid("uuid_4").setCreatedAt(baseTime - 1));
db.rules().insert(rule -> rule.setUuid("uuid_5").setCreatedAt(baseTime + 1));
RuleListQuery ruleListQuery = newRuleListQueryBuilder().createdAt(baseTime).sortField("createdAt").sortDirection("desc").build();

RuleListResult ruleListResult = underTest.selectRules(db.getSession(), ruleListQuery, Pagination.all());

assertThat(ruleListResult.getUuids()).containsExactly("uuid_2", "uuid_5", "uuid_1");
assertThat(ruleListResult.getTotal()).isEqualTo(3);
}

@Test
public void selectRules_whenQueriedWithLanguage_shouldReturnOnlyRulesWithLanguage() {
String queriedLang = "java";
db.rules().insert(rule -> rule.setUuid("uuid_1").setLanguage(queriedLang));
db.rules().insert(rule -> rule.setUuid("uuid_2").setLanguage("cpp"));
db.rules().insert(rule -> rule.setUuid("uuid_3").setLanguage("js"));
db.rules().insert(rule -> rule.setUuid("uuid_4").setLanguage(queriedLang));
RuleListQuery ruleListQuery = newRuleListQueryBuilder().language(queriedLang).build();

RuleListResult ruleListResult = underTest.selectRules(db.getSession(), ruleListQuery, Pagination.all());

assertThat(ruleListResult.getUuids()).containsExactly("uuid_1", "uuid_4");
assertThat(ruleListResult.getTotal()).isEqualTo(2);
}

@Test
public void selectRules_whenQueriedWithQualityProfile_shouldReturnOnlyRulesActiveForThisQualityProfile() {
QProfileDto profile = db.qualityProfiles().insert();
RuleDto rule1 = db.rules().insert(rule -> rule.setUuid("uuid_1"));
db.rules().insert(rule -> rule.setUuid("uuid_2"));
RuleDto rule3 = db.rules().insert(rule -> rule.setUuid("uuid_3"));
db.rules().insert(rule -> rule.setUuid("uuid_4"));
db.qualityProfiles().activateRule(profile, rule1);
db.qualityProfiles().activateRule(profile, rule3);

RuleListQuery ruleListQuery = newRuleListQueryBuilder().profileUuid(profile.getRulesProfileUuid()).build();

RuleListResult ruleListResult = underTest.selectRules(db.getSession(), ruleListQuery, Pagination.all());

assertThat(ruleListResult.getUuids()).containsExactly("uuid_1", "uuid_3");
assertThat(ruleListResult.getTotal()).isEqualTo(2);
}

private static ImpactDto newRuleDefaultImpact(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity) {
return new ImpactDto()
.setUuid(UuidFactoryFast.getInstance().create())

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

@@ -270,4 +270,11 @@ public class RuleDao implements Dao {
private static String toLowerCaseAndSurroundWithPercentSigns(@Nullable String query) {
return isBlank(query) ? PERCENT_SIGN : (PERCENT_SIGN + query.toLowerCase(Locale.ENGLISH) + PERCENT_SIGN);
}

public RuleListResult selectRules(DbSession dbSession, RuleListQuery ruleListQuery, Pagination pagination) {
return new RuleListResult(
mapper(dbSession).selectRules(ruleListQuery, pagination),
mapper(dbSession).countByQuery(ruleListQuery));
}

}

+ 102
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleListQuery.java View File

@@ -0,0 +1,102 @@
/*
* 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.rule;

import javax.annotation.Nullable;

public class RuleListQuery {
private final Long createdAt;
private final String language;
private final String profileUuid;
private final String sortField;
private final String sortDirection;

private RuleListQuery(Long createdAt, String language, String profileUuid, String sortField, String sortDirection) {
this.createdAt = createdAt;
this.language = language;
this.profileUuid = profileUuid;
this.sortField = sortField;
this.sortDirection = sortDirection;
}

public Long getCreatedAt() {
return createdAt;
}

public String getLanguage() {
return language;
}

public String getProfileUuid() {
return profileUuid;
}

public String getSortField() {
return sortField;
}

public String getSortDirection() {
return sortDirection;
}

public static final class RuleListQueryBuilder {
private Long createdAt;
private String language;
private String profileUuid;
private String sortField;
private String sortDirection = "asc";

private RuleListQueryBuilder() {
}

public static RuleListQueryBuilder newRuleListQueryBuilder() {
return new RuleListQueryBuilder();
}

public RuleListQueryBuilder createdAt(@Nullable Long createdAt) {
this.createdAt = createdAt;
return this;
}

public RuleListQueryBuilder language(String language) {
this.language = language;
return this;
}

public RuleListQueryBuilder profileUuid(@Nullable String profileUuid) {
this.profileUuid = profileUuid;
return this;
}

public RuleListQueryBuilder sortField(String sortField) {
this.sortField = sortField;
return this;
}

public RuleListQueryBuilder sortDirection(String sortDirection) {
this.sortDirection = sortDirection;
return this;
}

public RuleListQuery build() {
return new RuleListQuery(createdAt, language, profileUuid, sortField, sortDirection);
}
}
}

+ 40
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleListResult.java View File

@@ -0,0 +1,40 @@
/*
* 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.rule;

import java.util.List;

public class RuleListResult {
private final List<String> uuids;
private final long total;

public RuleListResult(List<String> uuids, long total) {
this.uuids = uuids;
this.total = total;
}

public List<String> getUuids() {
return uuids;
}

public long getTotal() {
return total;
}
}

+ 4
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleMapper.java View File

@@ -91,4 +91,8 @@ public interface RuleMapper {
void deleteRuleTags(String ruleUuid);

List<String> selectTags(@Param("query") String query, @Param("pagination") Pagination pagination);

List<String> selectRules(@Param("query") RuleListQuery query, @Param("pagination") Pagination pagination);

Long countByQuery(@Param("query") RuleListQuery query);
}

+ 37
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/rule/RuleMapper.xml View File

@@ -619,5 +619,42 @@
<include refid="org.sonar.db.common.Common.pagination"/>
</select>

<select id="selectRules">
select r.uuid from rules r
<include refid="queryList"/>
<choose>
<when test="query.sortField == 'createdAt'">
order by r.created_at ${query.sortDirection}
</when>
<otherwise>
order by r.uuid
</otherwise>
</choose>
<include refid="org.sonar.db.common.Common.pagination"/>
</select>

<select id="countByQuery" resultType="long">
select count(r.uuid) from rules r
<include refid="queryList"/>
</select>

<sql id="queryList">
<if test="query.profileUuid != null">
inner join active_rules ar on ar.rule_uuid = r.uuid
</if>
where
r.status != 'REMOVED'
and r.is_external = ${_false}
<if test="query.createdAt != null">
and r.created_at &gt;= #{query.createdAt,jdbcType=BIGINT}
</if>
<if test="query.language != null">
and r.language = #{query.language,jdbcType=VARCHAR}
</if>
<if test="query.profileUuid != null">
and ar.profile_uuid = #{query.profileUuid,jdbcType=VARCHAR}
</if>
</sql>

</mapper>


+ 7
- 1
server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts View File

@@ -72,7 +72,7 @@ import {
searchUsers,
setDefaultProfile,
} from '../quality-profiles';
import { getRuleDetails, searchRules } from '../rules';
import { getRuleDetails, searchRules, listRules } from '../rules';

jest.mock('../../api/rules');

@@ -110,6 +110,7 @@ export default class QualityProfilesServiceMock {
jest.mocked(copyProfile).mockImplementation(this.handleCopyProfile);
jest.mocked(getImporters).mockImplementation(this.handleGetImporters);
jest.mocked(searchRules).mockImplementation(this.handleSearchRules);
jest.mocked(listRules).mockImplementation(this.handleListRules);
jest.mocked(compareProfiles).mockImplementation(this.handleCompareQualityProfiles);
jest.mocked(activateRule).mockImplementation(this.handleActivateRule);
jest.mocked(deactivateRule).mockImplementation(this.handleDeactivateRule);
@@ -522,6 +523,11 @@ export default class QualityProfilesServiceMock {
return this.reply(this.searchRulesResponse);
};

handleListRules = (data: SearchRulesQuery): Promise<SearchRulesResponse> => {
// Both APIs are mocked the same way, this method is only here to make it explicit.
return this.handleSearchRules(data);
};

handleGetQualityProfile = () => {
return this.reply({
profile: mockQualityProfile(),

+ 4
- 0
server/sonar-web/src/main/js/api/rules.ts View File

@@ -43,6 +43,10 @@ export function searchRules(data: SearchRulesQuery): Promise<SearchRulesResponse
return getJSON('/api/rules/search', data).catch(throwGlobalError);
}

export function listRules(data: SearchRulesQuery): Promise<SearchRulesResponse> {
return getJSON('/api/rules/list', data).catch(throwGlobalError);
}

export function getRuleRepositories(parameters: {
q: string;
}): Promise<Array<{ key: string; language: string; name: string }>> {

+ 2
- 2
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx View File

@@ -21,7 +21,7 @@ import { DiscreetLink, Link, Note } from 'design-system';
import { noop, sortBy } from 'lodash';
import * as React from 'react';
import { useIntl } from 'react-intl';
import { searchRules } from '../../../api/rules';
import { listRules } from '../../../api/rules';
import { toShortISO8601String } from '../../../helpers/dates';
import { translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
@@ -54,7 +54,7 @@ export default function EvolutionRules() {
s: 'createdAt',
};

searchRules(data).then(({ actives, rules, paging: { total } }) => {
listRules(data).then(({ actives, rules, paging: { total } }) => {
setLatestRules(sortBy(parseRules(rules, actives), 'langName'));
setLatestRulesTotal(total);
}, noop);

+ 0
- 51
server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/ActiveRuleCompleterIT.java View File

@@ -1,51 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.rule.ws;

import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.resources.Languages;
import org.sonar.db.DbTester;
import org.sonar.db.qualityprofile.ActiveRuleDto;
import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.rule.RuleDto;
import org.sonarqube.ws.Rules;

import static org.assertj.core.api.Assertions.assertThat;

public class ActiveRuleCompleterIT {

@Rule
public DbTester dbTester = DbTester.create();

@Test
public void test_completeShow() {
ActiveRuleCompleter underTest = new ActiveRuleCompleter(dbTester.getDbClient(), new Languages());
RuleDto rule = dbTester.rules().insert();
QProfileDto qualityProfile = dbTester.qualityProfiles().insert();
ActiveRuleDto activeRule = dbTester.qualityProfiles().activateRule(qualityProfile, rule);

List<Rules.Active> result = underTest.completeShow(dbTester.getSession(), rule);

assertThat(result).extracting(Rules.Active::getQProfile).containsExactlyInAnyOrder(qualityProfile.getKee());
assertThat(result).extracting(Rules.Active::getSeverity).containsExactlyInAnyOrder(activeRule.getSeverityString());
}
}

+ 86
- 17
server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/ListActionIT.java View File

@@ -19,61 +19,130 @@
*/
package org.sonar.server.rule.ws;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.resources.Languages;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleTesting;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.language.LanguageTesting;
import org.sonar.server.rule.RuleDescriptionFormatter;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.text.MacroInterpreter;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.Rules;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;

public class ListActionIT {

private static final String RULE_KEY_1 = "S001";
private static final String RULE_KEY_2 = "S002";
private static final String RULE_KEY_1 = "java:S001";
private static final String RULE_KEY_2 = "java:S002";

@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);
public DbTester db = DbTester.create(System2.INSTANCE);

ListAction underTest = new ListAction(dbTester.getDbClient());
@org.junit.Rule
public UserSessionRule userSession = UserSessionRule.standalone();

WsActionTester tester = new WsActionTester(underTest);
private final Languages languages = LanguageTesting.newLanguages("java", "js");
private final MacroInterpreter macroInterpreter = mock(MacroInterpreter.class);
private final RuleMapper ruleMapper = new RuleMapper(languages, macroInterpreter, new RuleDescriptionFormatter());
private final RuleWsSupport ruleWsSupport = new RuleWsSupport(db.getDbClient(), userSession);
private final RulesResponseFormatter rulesResponseFormatter = new RulesResponseFormatter(db.getDbClient(), ruleWsSupport, ruleMapper, languages);
private final WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(), rulesResponseFormatter));

@Test
public void define() {
WebService.Action def = tester.getDef();
assertThat(def.params()).isEmpty();
public void define_shouldDefineParameters() {
WebService.Action def = ws.getDef();
assertThat(def.params()).extracting(WebService.Param::key)
.containsExactlyInAnyOrder("asc", "p", "s", "ps", "available_since", "qprofile");
}

@Test
public void return_rules_in_protobuf() {
dbTester.rules().insert(RuleTesting.newRule(RuleKey.of("java", RULE_KEY_1)).setConfigKey(null).setName(null));
dbTester.rules().insert(RuleTesting.newRule(RuleKey.of("java", RULE_KEY_2)).setConfigKey("I002").setName("Rule Two"));
dbTester.getSession().commit();
public void execute_shouldReturnRules() {
db.rules().insert(RuleTesting.newRule(RuleKey.parse(RULE_KEY_1)).setConfigKey(null).setName(null));
db.rules().insert(RuleTesting.newRule(RuleKey.parse(RULE_KEY_2)).setConfigKey("I002").setName("Rule Two"));
db.getSession().commit();

Rules.ListResponse listResponse = tester.newRequest()
Rules.ListResponse listResponse = ws.newRequest()
.executeProtobuf(Rules.ListResponse.class);

assertThat(listResponse.getRulesCount()).isEqualTo(2);

Rules.ListResponse.Rule ruleS001 = getRule(listResponse, RULE_KEY_1);
Rules.Rule ruleS001 = getRule(listResponse, RULE_KEY_1);
assertThat(ruleS001.getKey()).isEqualTo(RULE_KEY_1);
assertThat(ruleS001.getInternalKey()).isEmpty();
assertThat(ruleS001.getName()).isEmpty();

Rules.ListResponse.Rule ruleS002 = getRule(listResponse, RULE_KEY_2);
Rules.Rule ruleS002 = getRule(listResponse, RULE_KEY_2);
assertThat(ruleS002.getKey()).isEqualTo(RULE_KEY_2);
assertThat(ruleS002.getInternalKey()).isEqualTo("I002");
assertThat(ruleS002.getName()).isEqualTo("Rule Two");
}

private Rules.ListResponse.Rule getRule(Rules.ListResponse listResponse, String ruleKey) {
Optional<Rules.ListResponse.Rule> rule = listResponse.getRulesList().stream()
@Test
public void execute_shouldReturnFilteredRules_whenQProfileDefined() {
QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage("java"));
RuleDto rule = db.rules().insert(r -> r.setRuleKey(RuleKey.parse(RULE_KEY_1)));

db.qualityProfiles().activateRule(profile, rule);

Rules.ListResponse result = ws.newRequest()
.setParam("qprofile", profile.getKee())
.executeProtobuf(Rules.ListResponse.class);

assertThat(result.getPaging().getTotal()).isOne();
assertThat(result.getPaging().getPageIndex()).isOne();
assertThat(result.getRulesCount()).isOne();
assertThat(result.getActives()).isNotNull();

Map<String, Rules.ActiveList> activeRules = result.getActives().getActivesMap();
assertThat(activeRules.get(rule.getKey().toString())).isNotNull();
assertThat(activeRules.get(rule.getKey().toString()).getActiveListList()).hasSize(1);
}

@Test
public void execute_shouldReturnFilteredRules_whenAvailableSinceDefined() throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
long recentDay = dateFormat.parse("2023-02-20").getTime();
long oldDay = dateFormat.parse("2022-09-17").getTime();
db.rules().insert(r -> r.setRuleKey(RuleKey.parse(RULE_KEY_1)).setCreatedAt(recentDay));
db.rules().insert(r -> r.setRuleKey(RuleKey.parse(RULE_KEY_2)).setCreatedAt(oldDay));

Rules.ListResponse result = ws.newRequest()
.setParam("available_since", "2022-11-23")
.executeProtobuf(Rules.ListResponse.class);

assertThat(result.getRulesCount()).isOne();
assertThat(result.getRulesList().stream().map(Rules.Rule::getKey)).containsOnly(RULE_KEY_1);
}

@Test
public void execute_shouldFailWithNotFoundException_whenQProfileDoesNotExist() {
String unknownProfile = "unknown_profile";
TestRequest request = ws.newRequest()
.setParam("qprofile", unknownProfile);

assertThatThrownBy(() -> request.executeProtobuf(Rules.SearchResponse.class))
.isInstanceOf(NotFoundException.class)
.hasMessage("The specified qualityProfile '" + unknownProfile + "' does not exist");
}

private Rules.Rule getRule(Rules.ListResponse listResponse, String ruleKey) {
Optional<Rules.Rule> rule = listResponse.getRulesList().stream()
.filter(r -> ruleKey.equals(r.getKey()))
.findFirst();
assertThat(rule).isPresent();

+ 4
- 5
server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/SearchActionIT.java View File

@@ -130,13 +130,12 @@ public class SearchActionIT {
private final RuleIndexer ruleIndexer = new RuleIndexer(es.client(), db.getDbClient());
private final ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(db.getDbClient(), es.client());
private final Languages languages = LanguageTesting.newLanguages(JAVA, "js");
private final ActiveRuleCompleter activeRuleCompleter = new ActiveRuleCompleter(db.getDbClient(), languages);
private final RuleQueryFactory ruleQueryFactory = new RuleQueryFactory(db.getDbClient());
private final MacroInterpreter macroInterpreter = mock(MacroInterpreter.class);
private final QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
private final RuleMapper ruleMapper = new RuleMapper(languages, macroInterpreter, new RuleDescriptionFormatter());
private final SearchAction underTest = new SearchAction(ruleIndex, activeRuleCompleter, ruleQueryFactory, db.getDbClient(), ruleMapper,
new RuleWsSupport(db.getDbClient(), userSession));
private final SearchAction underTest = new SearchAction(ruleIndex, ruleQueryFactory, db.getDbClient(),
new RulesResponseFormatter(db.getDbClient(), new RuleWsSupport(db.getDbClient(), userSession), ruleMapper, languages));
private final TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
private final SonarQubeVersion sonarQubeVersion = new SonarQubeVersion(Version.create(10, 3));
private final RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, db.getDbClient(), typeValidations, userSession,
@@ -528,9 +527,9 @@ public class SearchActionIT {
assertThat(result.getFacets().getFacets(0).getValuesList())
.extracting(v -> entry(v.getVal(), v.getCount())).contains(
entry(CleanCodeAttribute.COMPLETE.getAttributeCategory().name(), 1L),
entry(CleanCodeAttribute.CONVENTIONAL.getAttributeCategory().name(), 0L)
);
entry(CleanCodeAttribute.CONVENTIONAL.getAttributeCategory().name(), 0L));
}

@Test
public void should_included_selected_non_matching_tag_in_facet() {
RuleDto rule = db.rules().insert(setSystemTags("tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8", "tag9", "tagA"));

+ 3
- 4
server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/ShowActionIT.java View File

@@ -87,10 +87,10 @@ public class ShowActionIT {
private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();
private final MacroInterpreter macroInterpreter = mock(MacroInterpreter.class);
private final Languages languages = new Languages(newLanguage("xoo", "Xoo"));
private final RuleWsSupport ruleWsSupport = new RuleWsSupport(db.getDbClient(), userSession);
private final RuleMapper ruleMapper = new RuleMapper(languages, macroInterpreter, new RuleDescriptionFormatter());
private final WsActionTester ws = new WsActionTester(
new ShowAction(db.getDbClient(), new RuleMapper(languages, macroInterpreter, new RuleDescriptionFormatter()),
new ActiveRuleCompleter(db.getDbClient(), languages),
new RuleWsSupport(db.getDbClient(), userSession)));
new ShowAction(db.getDbClient(), new RulesResponseFormatter(db.getDbClient(), ruleWsSupport, ruleMapper, languages)));
private UserDto userDto;

@Before
@@ -150,7 +150,6 @@ public class ShowActionIT {
.containsExactly(rule.getTags().toArray(new String[0]));
}

//<test case name>_when<conditionInCamelCase>_should<assertionInCamelCase>
@Test
public void returnRuleCleanCodeFields_whenEndpointIsCalled() {
RuleDto rule = db.rules().insert(setTags("tag1", "tag2"), r -> r.setNoteData(null).setNoteUserUuid(null));

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/CreateAction.java View File

@@ -187,7 +187,7 @@ public class CreateAction implements RulesWsAction {
templateRule.ifPresent(templateRules::add);
}
List<RuleParamDto> ruleParameters = dbClient.ruleDao().selectRuleParamsByRuleUuids(dbSession, singletonList(rule.getUuid()));
SearchAction.SearchResult searchResult = new SearchAction.SearchResult()
RulesResponseFormatter.SearchResult searchResult = new RulesResponseFormatter.SearchResult()
.setRuleParameters(ruleParameters)
.setTemplateRules(templateRules)
.setTotal(1L);

+ 172
- 24
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ListAction.java View File

@@ -19,61 +19,209 @@
*/
package org.sonar.server.rule.ws;

import com.google.common.collect.Maps;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.NewAction;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.Pagination;
import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.rule.RuleDto;
import org.sonarqube.ws.MediaTypes;
import org.sonar.db.rule.RuleListQuery;
import org.sonar.db.rule.RuleListResult;
import org.sonar.db.rule.RuleParamDto;
import org.sonar.server.rule.ws.RulesResponseFormatter.SearchResult;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Rules;
import org.sonarqube.ws.Rules.ListResponse;

import static com.google.common.base.Strings.nullToEmpty;
import static java.util.stream.Collectors.toSet;
import static org.sonar.api.server.ws.WebService.Param.PAGE;
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
import static org.sonar.db.rule.RuleListQuery.RuleListQueryBuilder.newRuleListQueryBuilder;
import static org.sonar.server.exceptions.NotFoundException.checkFound;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_AVAILABLE_SINCE;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_QPROFILE;
import static org.sonar.server.ws.WsUtils.writeProtobuf;

public class ListAction implements RulesWsAction {

private final DbClient dbClient;
private final RulesResponseFormatter rulesResponseFormatter;

public ListAction(DbClient dbClient) {
public ListAction(DbClient dbClient, RulesResponseFormatter rulesResponseFormatter) {
this.dbClient = dbClient;
this.rulesResponseFormatter = rulesResponseFormatter;
}

@Override
public void define(WebService.NewController controller) {
controller
NewAction action = controller
.createAction("list")
.setDescription("List rules, excluding the manual rules and the rules with status REMOVED. JSON format is not supported for response.")
.setDescription("List rules, excluding the external rules and the rules with status REMOVED. " +
"If a quality profile is provided, it returns only the rules activated in this quality profile. " +
"If no quality profile is provided, it returns all the rules available to the user.")
.setSince("5.2")
.setInternal(true)
.setResponseExample(getClass().getResource("list-example.txt"))
.setHandler(this);

action.setChangelog(
new Change("10.4", "'repository' field changed to 'repo'"),
new Change("10.4", "Extend the response with 'actives', 'qProfiles' fields"),
new Change("10.4", "Add pagination"),
new Change("10.4", String.format("Add the '%s' parameter", PARAM_AVAILABLE_SINCE)),
new Change("10.4", String.format("Add the '%s' parameter", PARAM_QPROFILE)),
new Change("10.4", "Add the 'createdAt' sorting parameter"));

action.createParam(PARAM_AVAILABLE_SINCE)
.setDescription("Filter rules available since the given date. If no value is provided, all rules are returned. Format is yyyy-MM-dd.")
.setExampleValue("2014-06-22")
.setSince("10.4");
action.createParam(PARAM_QPROFILE)
.setDescription("Filter rules that are activated in the given quality profile.")
.setSince("10.4");
action.createSortParams(Set.of("createdAt"), "createdAt", false)
.setSince("10.4");
action.addPagingParamsSince(100, 500, "10.4");
}

@Override
public void handle(Request wsRequest, Response wsResponse) throws Exception {
final ListResponse.Builder listResponseBuilder = ListResponse.newBuilder();
final ListResponse.Rule.Builder ruleBuilder = ListResponse.Rule.newBuilder();
public void handle(Request request, Response response) throws Exception {
try (DbSession dbSession = dbClient.openSession(false)) {
Set<ListResponse.Rule> rules = dbClient.ruleDao().selectEnabled(dbSession).stream()
.map(dto -> toRule(ruleBuilder, dto))
.collect(toSet());
listResponseBuilder.addAllRules(rules);

WsRequest wsRequest = toWsRequest(dbSession, request);
SearchResult searchResult = doSearch(dbSession, wsRequest);
ListResponse listResponse = buildResponse(wsRequest, dbSession, searchResult);

writeProtobuf(listResponse, request, response);
}
}

private WsRequest toWsRequest(DbSession dbSession, Request request) {
WsRequest wsRequest = new WsRequest();

String sortParam = request.param(WebService.Param.SORT);
if (sortParam != null) {
wsRequest.setSortField(sortParam)
.setAscendingSort(request.mandatoryParamAsBoolean(WebService.Param.ASCENDING));
}
// JSON response is voluntarily not supported. This WS is for internal use.
wsResponse.stream().setMediaType(MediaTypes.PROTOBUF);
listResponseBuilder.build().writeTo(wsResponse.stream().output());

return wsRequest
.setQProfile(getQProfile(dbSession, request))
.setPage(request.mandatoryParamAsInt(PAGE))
.setPageSize(request.mandatoryParamAsInt(PAGE_SIZE))
.setAvailableSince(request.paramAsDate(PARAM_AVAILABLE_SINCE));
}

@Nullable
private QProfileDto getQProfile(DbSession dbSession, Request request) {
String profileUuid = request.param(PARAM_QPROFILE);
if (profileUuid == null) {
return null;
}
QProfileDto foundProfile = dbClient.qualityProfileDao().selectByUuid(dbSession, profileUuid);
return checkFound(foundProfile, "The specified qualityProfile '%s' does not exist", profileUuid);
}

private SearchResult doSearch(DbSession dbSession, WsRequest wsRequest) {
RuleListResult ruleListResult = dbClient.ruleDao().selectRules(dbSession,
buildRuleListQuery(wsRequest),
Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize));
Map<String, RuleDto> rulesByUuid = Maps.uniqueIndex(dbClient.ruleDao().selectByUuids(dbSession, ruleListResult.getUuids()), RuleDto::getUuid);
Set<String> ruleUuids = rulesByUuid.keySet();
List<RuleDto> rules = new LinkedList<>(rulesByUuid.values());

List<String> templateRuleUuids = rules.stream()
.map(RuleDto::getTemplateUuid)
.filter(Objects::nonNull)
.toList();
List<RuleDto> templateRules = dbClient.ruleDao().selectByUuids(dbSession, templateRuleUuids);
List<RuleParamDto> ruleParamDtos = dbClient.ruleDao().selectRuleParamsByRuleUuids(dbSession, ruleUuids);

return new SearchResult()
.setRules(rules)
.setRuleParameters(ruleParamDtos)
.setTemplateRules(templateRules)
.setTotal(ruleListResult.getTotal());
}

private static ListResponse.Rule toRule(ListResponse.Rule.Builder ruleBuilder, RuleDto dto) {
return ruleBuilder
.clear()
.setRepository(dto.getRepositoryKey())
.setKey(dto.getRuleKey())
.setName(nullToEmpty(dto.getName()))
.setInternalKey(nullToEmpty(dto.getConfigKey()))
private static RuleListQuery buildRuleListQuery(WsRequest wsRequest) {
return newRuleListQueryBuilder()
.profileUuid(wsRequest.qProfile != null ? wsRequest.qProfile.getRulesProfileUuid() : null)
.createdAt(wsRequest.availableSince != null ? wsRequest.availableSince.getTime() : null)
.sortField(wsRequest.sortField)
.sortDirection(wsRequest.ascendingSort ? "asc" : "desc")
.build();
}

private ListResponse buildResponse(WsRequest wsRequest, DbSession dbSession, SearchResult searchResult) {
QProfileDto qProfile = wsRequest.qProfile;
Rules.Actives actives = rulesResponseFormatter.formatActiveRules(dbSession, qProfile, searchResult.getRules());
Set<String> qProfiles = actives.getActivesMap().values()
.stream()
.map(Rules.ActiveList::getActiveListList)
.flatMap(List::stream)
.map(Rules.Active::getQProfile)
.collect(Collectors.toSet());

return ListResponse.newBuilder()
.addAllRules(rulesResponseFormatter.formatRulesList(dbSession, searchResult))
.setActives(actives)
.setQProfiles(rulesResponseFormatter.formatQualityProfiles(dbSession, qProfiles))
.setPaging(Common.Paging.newBuilder()
.setPageIndex(wsRequest.page)
.setPageSize(searchResult.getRules().size())
.setTotal(searchResult.getTotal().intValue())
.build())
.build();
}

private static class WsRequest {
private String sortField = null;
private boolean ascendingSort = true;
private QProfileDto qProfile = null;
private int page = 1;
private int pageSize = 100;
private Date availableSince = null;

public WsRequest setSortField(String sortField) {
this.sortField = sortField;
return this;
}

public WsRequest setAscendingSort(boolean ascendingSort) {
this.ascendingSort = ascendingSort;
return this;
}

public WsRequest setQProfile(@Nullable QProfileDto qProfile) {
this.qProfile = qProfile;
return this;
}

public WsRequest setPage(int page) {
this.page = page;
return this;
}

public WsRequest setPageSize(int pageSize) {
this.pageSize = pageSize;
return this;
}

public WsRequest setAvailableSince(@Nullable Date availableSince) {
this.availableSince = availableSince;
return this;
}
}
}

+ 2
- 3
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleMapper.java View File

@@ -45,7 +45,7 @@ import org.sonar.db.rule.RuleParamDto;
import org.sonar.db.user.UserDto;
import org.sonar.markdown.Markdown;
import org.sonar.server.rule.RuleDescriptionFormatter;
import org.sonar.server.rule.ws.SearchAction.SearchResult;
import org.sonar.server.rule.ws.RulesResponseFormatter.SearchResult;
import org.sonar.server.text.MacroInterpreter;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Common.RuleScope;
@@ -262,8 +262,7 @@ public class RuleMapper {

private static void setGapDescription(Rules.Rule.Builder ruleResponse, RuleDto ruleDto, Set<String> fieldsToReturn) {
String gapDescription = ruleDto.getGapDescription();
if (shouldReturnField(fieldsToReturn, FIELD_GAP_DESCRIPTION)
&& gapDescription != null) {
if (shouldReturnField(fieldsToReturn, FIELD_GAP_DESCRIPTION) && gapDescription != null) {
ruleResponse.setGapDescription(gapDescription);
}
}

server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ActiveRuleCompleter.java → server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesResponseFormatter.java View File

@@ -25,13 +25,15 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.resources.Language;
import org.sonar.api.resources.Languages;
@@ -46,41 +48,95 @@ import org.sonar.db.qualityprofile.ActiveRuleKey;
import org.sonar.db.qualityprofile.ActiveRuleParamDto;
import org.sonar.db.qualityprofile.OrgActiveRuleDto;
import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.rule.DeprecatedRuleKeyDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleParamDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.es.Facets;
import org.sonar.server.qualityprofile.ActiveRuleInheritance;
import org.sonar.server.rule.index.RuleQuery;
import org.sonarqube.ws.Rules;
import org.sonarqube.ws.Rules.SearchResponse;

import static com.google.common.base.Strings.nullToEmpty;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Optional.ofNullable;
import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEPRECATED_KEYS;

/**
* Add details about active rules to api/rules/search and api/rules/show
* web services.
*/
@ServerSide
public class ActiveRuleCompleter {

public class RulesResponseFormatter {
private final DbClient dbClient;
private final RuleWsSupport ruleWsSupport;
private final RuleMapper mapper;
private final Languages languages;

public ActiveRuleCompleter(DbClient dbClient, Languages languages) {
public RulesResponseFormatter(DbClient dbClient, RuleWsSupport ruleWsSupport, RuleMapper mapper, Languages languages) {
this.dbClient = dbClient;
this.ruleWsSupport = ruleWsSupport;
this.mapper = mapper;
this.languages = languages;
}

void completeSearch(DbSession dbSession, RuleQuery query, List<RuleDto> rules, SearchResponse.Builder searchResponse) {
Set<String> profileUuids = writeActiveRules(dbSession, searchResponse, query, rules);
searchResponse.setQProfiles(buildQProfiles(dbSession, profileUuids));
public List<Rules.Rule> formatRulesSearch(DbSession dbSession, SearchResult result, Set<String> fields) {
List<RuleDto> rules = result.getRules();
Map<String, UserDto> usersByUuid = ruleWsSupport.getUsersByUuid(dbSession, rules);
Map<String, List<DeprecatedRuleKeyDto>> deprecatedRuleKeysByRuleUuid = getDeprecatedRuleKeysByRuleUuid(dbSession, rules, fields);

return rules.stream()
.map(rule -> mapper.toWsRule(rule, result, fields, usersByUuid, deprecatedRuleKeysByRuleUuid))
.toList();
}

public List<Rules.Rule> formatRulesList(DbSession dbSession, SearchResult result) {
Set<String> fields = Set.of("repo", "name", "severity", "lang", "internalKey", "templateKey", "params", "actives", "createdAt", "updatedAt", "deprecatedKeys", "langName");
return formatRulesSearch(dbSession, result, fields);
}

private Map<String, List<DeprecatedRuleKeyDto>> getDeprecatedRuleKeysByRuleUuid(DbSession dbSession, List<RuleDto> rules, Set<String> fields) {
if (!RuleMapper.shouldReturnField(fields, FIELD_DEPRECATED_KEYS)) {
return Collections.emptyMap();
}

Set<String> ruleUuidsSet = rules.stream()
.map(RuleDto::getUuid)
.collect(Collectors.toSet());
if (ruleUuidsSet.isEmpty()) {
return Collections.emptyMap();
} else {
return dbClient.ruleDao().selectDeprecatedRuleKeysByRuleUuids(dbSession, ruleUuidsSet).stream()
.collect(Collectors.groupingBy(DeprecatedRuleKeyDto::getRuleUuid));
}
}

private Set<String> writeActiveRules(DbSession dbSession, SearchResponse.Builder response, RuleQuery query, List<RuleDto> rules) {
final Set<String> profileUuids = new HashSet<>();
Rules.Actives.Builder activesBuilder = response.getActivesBuilder();
public Rules.QProfiles formatQualityProfiles(DbSession dbSession, Set<String> profileUuids) {
Rules.QProfiles.Builder result = Rules.QProfiles.newBuilder();
if (profileUuids.isEmpty()) {
return result.build();
}

// load profiles
Map<String, QProfileDto> profilesByUuid = dbClient.qualityProfileDao().selectByUuids(dbSession, new ArrayList<>(profileUuids))
.stream()
.collect(Collectors.toMap(QProfileDto::getKee, Function.identity()));

// load associated parents
List<String> parentUuids = profilesByUuid.values().stream()
.map(QProfileDto::getParentKee)
.filter(StringUtils::isNotEmpty)
.filter(uuid -> !profilesByUuid.containsKey(uuid))
.toList();
if (!parentUuids.isEmpty()) {
dbClient.qualityProfileDao().selectByUuids(dbSession, parentUuids)
.forEach(p -> profilesByUuid.put(p.getKee(), p));
}

profilesByUuid.values().forEach(p -> writeProfile(result, p));

return result.build();
}

public Rules.Actives formatActiveRules(DbSession dbSession, @Nullable QProfileDto profile, List<RuleDto> rules) {
Rules.Actives.Builder activesBuilder = Rules.Actives.newBuilder();

QProfileDto profile = query.getQProfile();
if (profile != null) {
// Load details of active rules on the selected profile
List<OrgActiveRuleDto> activeRules = dbClient.activeRuleDao().selectByProfile(dbSession, profile);
@@ -91,7 +147,7 @@ public class ActiveRuleCompleter {
for (RuleDto rule : rules) {
OrgActiveRuleDto activeRule = activeRuleByRuleKey.get(rule.getKey());
if (activeRule != null) {
profileUuids.addAll(writeActiveRules(rule.getKey(), singletonList(activeRule), activeRuleParamsByActiveRuleKey, activesBuilder));
writeActiveRules(rule.getKey(), singletonList(activeRule), activeRuleParamsByActiveRuleKey, activesBuilder);
}
}
} else {
@@ -101,25 +157,21 @@ public class ActiveRuleCompleter {
Multimap<RuleKey, OrgActiveRuleDto> activeRulesByRuleKey = activeRules.stream()
.collect(MoreCollectors.index(OrgActiveRuleDto::getRuleKey));
ListMultimap<ActiveRuleKey, ActiveRuleParamDto> activeRuleParamsByActiveRuleKey = loadParams(dbSession, activeRules);
rules.forEach(rule -> profileUuids.addAll(writeActiveRules(rule.getKey(), activeRulesByRuleKey.get(rule.getKey()), activeRuleParamsByActiveRuleKey, activesBuilder)));
rules.forEach(rule -> writeActiveRules(rule.getKey(), activeRulesByRuleKey.get(rule.getKey()), activeRuleParamsByActiveRuleKey, activesBuilder));
}

response.setActives(activesBuilder);
return profileUuids;
return activesBuilder.build();
}

private static Set<String> writeActiveRules(RuleKey ruleKey, Collection<OrgActiveRuleDto> activeRules,
private static void writeActiveRules(RuleKey ruleKey, Collection<OrgActiveRuleDto> activeRules,
ListMultimap<ActiveRuleKey, ActiveRuleParamDto> activeRuleParamsByActiveRuleKey, Rules.Actives.Builder activesBuilder) {
final Set<String> profileUuids = new HashSet<>();
Rules.ActiveList.Builder activeRulesListResponse = Rules.ActiveList.newBuilder();
for (OrgActiveRuleDto activeRule : activeRules) {
activeRulesListResponse.addActiveList(buildActiveRuleResponse(activeRule, activeRuleParamsByActiveRuleKey.get(activeRule.getKey())));
profileUuids.add(activeRule.getOrgProfileUuid());
}
activesBuilder
.getMutableActives()
.put(ruleKey.toString(), activeRulesListResponse.build());
return profileUuids;
}

private ListMultimap<ActiveRuleKey, ActiveRuleParamDto> loadParams(DbSession dbSession, List<OrgActiveRuleDto> activeRules) {
@@ -137,7 +189,7 @@ public class ActiveRuleCompleter {
return activeRuleParamsByActiveRuleKey;
}

List<Rules.Active> completeShow(DbSession dbSession, RuleDto rule) {
public List<Rules.Active> formatActiveRule(DbSession dbSession, RuleDto rule) {
List<OrgActiveRuleDto> activeRules = dbClient.activeRuleDao().selectByOrgRuleUuid(dbSession, rule.getUuid());
Map<String, ActiveRuleKey> activeRuleUuidsByKey = new HashMap<>();
for (OrgActiveRuleDto activeRuleDto : activeRules) {
@@ -175,35 +227,7 @@ public class ActiveRuleCompleter {
return builder.build();
}

private Rules.QProfiles.Builder buildQProfiles(DbSession dbSession, Set<String> profileUuids) {
Rules.QProfiles.Builder result = Rules.QProfiles.newBuilder();
if (profileUuids.isEmpty()) {
return result;
}

// load profiles
Map<String, QProfileDto> profilesByUuid = dbClient.qualityProfileDao().selectByUuids(dbSession, new ArrayList<>(profileUuids))
.stream()
.collect(Collectors.toMap(QProfileDto::getKee, Function.identity()));

// load associated parents
List<String> parentUuids = profilesByUuid.values().stream()
.map(QProfileDto::getParentKee)
.filter(StringUtils::isNotEmpty)
.filter(uuid -> !profilesByUuid.containsKey(uuid))
.toList();
if (!parentUuids.isEmpty()) {
dbClient.qualityProfileDao().selectByUuids(dbSession, parentUuids)
.forEach(p -> profilesByUuid.put(p.getKee(), p));
}

Map<String, Rules.QProfile> qProfilesMapResponse = result.getMutableQProfiles();
profilesByUuid.values().forEach(p -> writeProfile(qProfilesMapResponse, p));

return result;
}

private void writeProfile(Map<String, Rules.QProfile> profilesResponse, QProfileDto profile) {
private void writeProfile(Rules.QProfiles.Builder profilesResponse, QProfileDto profile) {
Rules.QProfile.Builder profileResponse = Rules.QProfile.newBuilder();
ofNullable(profile.getName()).ifPresent(profileResponse::setName);

@@ -215,6 +239,78 @@ public class ActiveRuleCompleter {
}
ofNullable(profile.getParentKee()).ifPresent(profileResponse::setParent);

profilesResponse.put(profile.getKee(), profileResponse.build());
profilesResponse.putQProfiles(profile.getKee(), profileResponse.build());
}

public Rules.Rule formatRule(DbSession dbSession, SearchResult searchResult) {
RuleDto rule = searchResult.getRules().get(0);
return mapper.toWsRule(rule, searchResult, Collections.emptySet(),
ruleWsSupport.getUsersByUuid(dbSession, searchResult.getRules()), emptyMap());
}

static class SearchResult {
private List<RuleDto> rules;
private final ListMultimap<String, RuleParamDto> ruleParamsByRuleUuid;
private final Map<String, RuleDto> templateRulesByRuleUuid;
private Long total;
private Facets facets;

public SearchResult() {
this.rules = new ArrayList<>();
this.ruleParamsByRuleUuid = ArrayListMultimap.create();
this.templateRulesByRuleUuid = new HashMap<>();
}

public List<RuleDto> getRules() {
return rules;
}

public SearchResult setRules(List<RuleDto> rules) {
this.rules = rules;
return this;
}

public ListMultimap<String, RuleParamDto> getRuleParamsByRuleUuid() {
return ruleParamsByRuleUuid;
}

public SearchResult setRuleParameters(List<RuleParamDto> ruleParams) {
ruleParamsByRuleUuid.clear();
for (RuleParamDto ruleParam : ruleParams) {
ruleParamsByRuleUuid.put(ruleParam.getRuleUuid(), ruleParam);
}
return this;
}

public Map<String, RuleDto> getTemplateRulesByRuleUuid() {
return templateRulesByRuleUuid;
}

public SearchResult setTemplateRules(List<RuleDto> templateRules) {
templateRulesByRuleUuid.clear();
for (RuleDto templateRule : templateRules) {
templateRulesByRuleUuid.put(templateRule.getUuid(), templateRule);
}
return this;
}

public Long getTotal() {
return total;
}

public SearchResult setTotal(Long total) {
this.total = total;
return this;
}

@CheckForNull
public Facets getFacets() {
return facets;
}

public SearchResult setFacets(Facets facets) {
this.facets = facets;
return this;
}
}
}

+ 32
- 115
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java View File

@@ -19,24 +19,20 @@
*/
package org.sonar.server.rule.ws;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rule.Severity;
@@ -48,16 +44,16 @@ import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.rule.DeprecatedRuleKeyDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleParamDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.es.Facets;
import org.sonar.server.es.SearchIdResult;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.rule.index.RuleIndex;
import org.sonar.server.rule.index.RuleQuery;
import org.sonar.server.rule.ws.RulesResponseFormatter.SearchResult;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Rules;
import org.sonarqube.ws.Rules.SearchResponse;

import static java.lang.String.format;
@@ -84,7 +80,6 @@ import static org.sonar.server.rule.index.RuleIndex.FACET_SONARSOURCE_SECURITY;
import static org.sonar.server.rule.index.RuleIndex.FACET_STATUSES;
import static org.sonar.server.rule.index.RuleIndex.FACET_TAGS;
import static org.sonar.server.rule.index.RuleIndex.FACET_TYPES;
import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEPRECATED_KEYS;
import static org.sonar.server.rule.ws.RulesWsParameters.OPTIONAL_FIELDS;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_ACTIVE_SEVERITIES;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES;
@@ -107,7 +102,7 @@ public class SearchAction implements RulesWsAction {
public static final String ACTION = "search";

private static final Collection<String> DEFAULT_FACETS = Set.of(PARAM_LANGUAGES, PARAM_REPOSITORIES, "tags");
private static final String[] POSSIBLE_FACETS = new String[]{
private static final String[] POSSIBLE_FACETS = new String[] {
FACET_LANGUAGES,
FACET_REPOSITORIES,
FACET_TAGS,
@@ -129,18 +124,13 @@ public class SearchAction implements RulesWsAction {
private final RuleQueryFactory ruleQueryFactory;
private final DbClient dbClient;
private final RuleIndex ruleIndex;
private final ActiveRuleCompleter activeRuleCompleter;
private final RuleMapper mapper;
private final RuleWsSupport ruleWsSupport;
private final RulesResponseFormatter rulesResponseFormatter;

public SearchAction(RuleIndex ruleIndex, ActiveRuleCompleter activeRuleCompleter, RuleQueryFactory ruleQueryFactory, DbClient dbClient, RuleMapper mapper,
RuleWsSupport ruleWsSupport) {
public SearchAction(RuleIndex ruleIndex, RuleQueryFactory ruleQueryFactory, DbClient dbClient, RulesResponseFormatter rulesResponseFormatter) {
this.ruleIndex = ruleIndex;
this.activeRuleCompleter = activeRuleCompleter;
this.ruleQueryFactory = ruleQueryFactory;
this.dbClient = dbClient;
this.mapper = mapper;
this.ruleWsSupport = ruleWsSupport;
this.rulesResponseFormatter = rulesResponseFormatter;
}

@Override
@@ -236,10 +226,10 @@ public class SearchAction implements RulesWsAction {
}

private static void writeStatistics(SearchResponse.Builder response, SearchResult searchResult, SearchOptions context) {
response.setTotal(searchResult.total);
response.setTotal(searchResult.getTotal());
response.setP(context.getPage());
response.setPs(context.getLimit());
response.setPaging(formatPaging(searchResult.total, context.getPage(), context.getLimit()));
response.setPaging(formatPaging(searchResult.getTotal(), context.getPage(), context.getLimit()));
}

private static Common.Paging.Builder formatPaging(Long total, int pageIndex, int limit) {
@@ -249,29 +239,6 @@ public class SearchAction implements RulesWsAction {
.setTotal(total.intValue());
}

private void writeRules(DbSession dbSession, SearchResponse.Builder response, SearchResult result, SearchOptions context) {
Map<String, UserDto> usersByUuid = ruleWsSupport.getUsersByUuid(dbSession, result.rules);
Map<String, List<DeprecatedRuleKeyDto>> deprecatedRuleKeysByRuleUuid = getDeprecatedRuleKeysByRuleUuid(dbSession, result.rules, context);
result.rules.forEach(rule -> response.addRules(mapper.toWsRule(rule, result, context.getFields(), usersByUuid,
deprecatedRuleKeysByRuleUuid)));
}

private Map<String, List<DeprecatedRuleKeyDto>> getDeprecatedRuleKeysByRuleUuid(DbSession dbSession, List<RuleDto> rules, SearchOptions context) {
if (!RuleMapper.shouldReturnField(context.getFields(), FIELD_DEPRECATED_KEYS)) {
return Collections.emptyMap();
}

Set<String> ruleUuidsSet = rules.stream()
.map(RuleDto::getUuid)
.collect(Collectors.toSet());
if (ruleUuidsSet.isEmpty()) {
return Collections.emptyMap();
} else {
return dbClient.ruleDao().selectDeprecatedRuleKeysByRuleUuids(dbSession, ruleUuidsSet).stream()
.collect(Collectors.groupingBy(DeprecatedRuleKeyDto::getRuleUuid));
}
}

private static SearchOptions buildSearchOptions(SearchRequest request) {
SearchOptions context = loadCommonContext(request);
SearchOptions searchOptions = new SearchOptions()
@@ -304,7 +271,7 @@ public class SearchAction implements RulesWsAction {
List<String> ruleUuids = result.getUuids();
// rule order is managed by ES, this order by must be kept when fetching rule details
Map<String, RuleDto> rulesByRuleKey = Maps.uniqueIndex(dbClient.ruleDao().selectByUuids(dbSession, ruleUuids), RuleDto::getUuid);
List<RuleDto> rules = new ArrayList<>();
List<RuleDto> rules = new LinkedList<>();
for (String ruleUuid : ruleUuids) {
RuleDto rule = rulesByRuleKey.get(ruleUuid);
if (rule != null) {
@@ -328,13 +295,27 @@ public class SearchAction implements RulesWsAction {

private void doContextResponse(DbSession dbSession, SearchRequest request, SearchResult result, SearchResponse.Builder response, RuleQuery query) {
SearchOptions contextForResponse = loadCommonContext(request);
writeRules(dbSession, response, result, contextForResponse);
response.addAllRules(rulesResponseFormatter.formatRulesSearch(dbSession, result, contextForResponse.getFields()));
if (contextForResponse.getFields().contains("actives")) {
activeRuleCompleter.completeSearch(dbSession, query, result.rules, response);
Rules.Actives actives = rulesResponseFormatter.formatActiveRules(dbSession, query.getQProfile(), result.getRules());
Set<String> qProfiles = actives.getActivesMap().values()
.stream()
.map(Rules.ActiveList::getActiveListList)
.flatMap(List::stream)
.map(Rules.Active::getQProfile)
.collect(Collectors.toSet());

Rules.QProfiles profiles = rulesResponseFormatter.formatQualityProfiles(dbSession, qProfiles);
response.setActives(actives);
response.setQProfiles(profiles);
}
}

private static void writeFacets(SearchResponse.Builder response, SearchRequest request, SearchOptions context, SearchResult results) {
Facets resultsFacets = results.getFacets();
if (resultsFacets == null) {
return;
}
addMandatoryFacetValues(results, FACET_LANGUAGES, request.getLanguages());
addMandatoryFacetValues(results, FACET_REPOSITORIES, request.getRepositories());
addMandatoryFacetValues(results, FACET_STATUSES, ALL_STATUSES_EXCEPT_REMOVED);
@@ -372,7 +353,7 @@ public class SearchAction implements RulesWsAction {

for (String facetName : context.getFacets()) {
facet.clear().setProperty(facetName);
Map<String, Long> facets = results.facets.get(facetName);
Map<String, Long> facets = resultsFacets.get(facetName);
if (facets != null) {
Set<String> itemsFromFacets = new HashSet<>();
for (Map.Entry<String, Long> facetValue : facets.entrySet()) {
@@ -406,7 +387,11 @@ public class SearchAction implements RulesWsAction {
}

private static void addMandatoryFacetValues(SearchResult results, String facetName, @Nullable Collection<String> mandatoryValues) {
Map<String, Long> facetValues = results.facets.get(facetName);
Facets facets = results.getFacets();
if (facets == null) {
return;
}
Map<String, Long> facetValues = facets.get(facetName);
if (facetValues != null) {
Collection<String> valuesToAdd = mandatoryValues == null ? Lists.newArrayList() : mandatoryValues;
for (String item : valuesToAdd) {
@@ -441,73 +426,6 @@ public class SearchAction implements RulesWsAction {
.setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY));
}

static class SearchResult {
private List<RuleDto> rules;
private final ListMultimap<String, RuleParamDto> ruleParamsByRuleUuid;
private final Map<String, RuleDto> templateRulesByRuleUuid;
private Long total;
private Facets facets;

public SearchResult() {
this.rules = new ArrayList<>();
this.ruleParamsByRuleUuid = ArrayListMultimap.create();
this.templateRulesByRuleUuid = new HashMap<>();
}

public List<RuleDto> getRules() {
return rules;
}

public SearchResult setRules(List<RuleDto> rules) {
this.rules = rules;
return this;
}

public ListMultimap<String, RuleParamDto> getRuleParamsByRuleUuid() {
return ruleParamsByRuleUuid;
}

public SearchResult setRuleParameters(List<RuleParamDto> ruleParams) {
ruleParamsByRuleUuid.clear();
for (RuleParamDto ruleParam : ruleParams) {
ruleParamsByRuleUuid.put(ruleParam.getRuleUuid(), ruleParam);
}
return this;
}

public Map<String, RuleDto> getTemplateRulesByRuleUuid() {
return templateRulesByRuleUuid;
}

public SearchResult setTemplateRules(List<RuleDto> templateRules) {
templateRulesByRuleUuid.clear();
for (RuleDto templateRule : templateRules) {
templateRulesByRuleUuid.put(templateRule.getUuid(), templateRule);
}
return this;
}

@CheckForNull
public Long getTotal() {
return total;
}

public SearchResult setTotal(Long total) {
this.total = total;
return this;
}

@CheckForNull
public Facets getFacets() {
return facets;
}

public SearchResult setFacets(Facets facets) {
this.facets = facets;
return this;
}
}

private static class SearchRequest {

private List<String> activeSeverities;
@@ -530,7 +448,6 @@ public class SearchAction implements RulesWsAction {
private List<String> impactSoftwareQualities;
private List<String> cleanCodeAttributesCategories;


private SearchRequest setActiveSeverities(List<String> activeSeverities) {
this.activeSeverities = activeSeverities;
return this;

+ 7
- 13
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java View File

@@ -35,7 +35,6 @@ import org.sonar.server.exceptions.NotFoundException;
import org.sonarqube.ws.Rules.ShowResponse;

import static java.lang.String.format;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Optional.ofNullable;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
@@ -49,15 +48,11 @@ public class ShowAction implements RulesWsAction {
public static final String PARAM_ACTIVES = "actives";

private final DbClient dbClient;
private final RuleMapper mapper;
private final ActiveRuleCompleter activeRuleCompleter;
private final RuleWsSupport ruleWsSupport;
private final RulesResponseFormatter rulesResponseFormatter;

public ShowAction(DbClient dbClient, RuleMapper mapper, ActiveRuleCompleter activeRuleCompleter, RuleWsSupport ruleWsSupport) {
public ShowAction(DbClient dbClient, RulesResponseFormatter rulesResponseFormatter) {
this.dbClient = dbClient;
this.activeRuleCompleter = activeRuleCompleter;
this.mapper = mapper;
this.ruleWsSupport = ruleWsSupport;
this.rulesResponseFormatter = rulesResponseFormatter;
}

@Override
@@ -117,7 +112,7 @@ public class ShowAction implements RulesWsAction {

List<RuleParamDto> ruleParameters = dbClient.ruleDao().selectRuleParamsByRuleUuids(dbSession, singletonList(rule.getUuid()));
ShowResponse showResponse = buildResponse(dbSession, request,
new SearchAction.SearchResult()
new RulesResponseFormatter.SearchResult()
.setRules(singletonList(rule))
.setTemplateRules(templateRules)
.setRuleParameters(ruleParameters)
@@ -126,13 +121,12 @@ public class ShowAction implements RulesWsAction {
}
}

private ShowResponse buildResponse(DbSession dbSession, Request request, SearchAction.SearchResult searchResult) {
private ShowResponse buildResponse(DbSession dbSession, Request request, RulesResponseFormatter.SearchResult searchResult) {
ShowResponse.Builder responseBuilder = ShowResponse.newBuilder();
RuleDto rule = searchResult.getRules().get(0);
responseBuilder.setRule(mapper.toWsRule(rule, searchResult, Collections.emptySet(),
ruleWsSupport.getUsersByUuid(dbSession, searchResult.getRules()), emptyMap()));
responseBuilder.setRule(rulesResponseFormatter.formatRule(dbSession, searchResult));
if (request.mandatoryParamAsBoolean(PARAM_ACTIVES)) {
activeRuleCompleter.completeShow(dbSession, rule).forEach(responseBuilder::addActives);
responseBuilder.addAllActives(rulesResponseFormatter.formatActiveRule(dbSession, rule));
}
return responseBuilder.build();
}

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/UpdateAction.java View File

@@ -250,7 +250,7 @@ public class UpdateAction implements RulesWsAction {
}
List<RuleParamDto> ruleParameters = dbClient.ruleDao().selectRuleParamsByRuleUuids(dbSession, singletonList(rule.getUuid()));
UpdateResponse.Builder responseBuilder = UpdateResponse.newBuilder();
SearchAction.SearchResult searchResult = new SearchAction.SearchResult()
RulesResponseFormatter.SearchResult searchResult = new RulesResponseFormatter.SearchResult()
.setRules(singletonList(rule))
.setTemplateRules(templateRules)
.setRuleParameters(ruleParameters)

+ 2
- 2
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -227,11 +227,11 @@ import org.sonar.server.rule.RuleUpdater;
import org.sonar.server.rule.WebServerRuleFinderImpl;
import org.sonar.server.rule.index.RuleIndexDefinition;
import org.sonar.server.rule.index.RuleIndexer;
import org.sonar.server.rule.ws.ActiveRuleCompleter;
import org.sonar.server.rule.ws.RepositoriesAction;
import org.sonar.server.rule.ws.RuleMapper;
import org.sonar.server.rule.ws.RuleQueryFactory;
import org.sonar.server.rule.ws.RuleWsSupport;
import org.sonar.server.rule.ws.RulesResponseFormatter;
import org.sonar.server.rule.ws.RulesWs;
import org.sonar.server.rule.ws.TagsAction;
import org.sonar.server.saml.ws.SamlValidationModule;
@@ -354,7 +354,7 @@ public class PlatformLevel4 extends PlatformLevel {
org.sonar.server.rule.ws.ListAction.class,
TagsAction.class,
RuleMapper.class,
ActiveRuleCompleter.class,
RulesResponseFormatter.class,
RepositoriesAction.class,
RuleQueryFactory.class,
org.sonar.server.rule.ws.AppAction.class,

+ 2
- 2
sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java View File

@@ -80,7 +80,7 @@ import org.sonar.scanner.scan.branch.BranchType;
import org.sonar.scanner.scan.branch.ProjectBranches;
import org.sonarqube.ws.NewCodePeriods;
import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile;
import org.sonarqube.ws.Rules.ListResponse.Rule;
import org.sonarqube.ws.Rules.Rule;

import static java.util.Collections.emptySet;

@@ -168,7 +168,7 @@ public class ScannerMediumTester extends ExternalResource {
public ScannerMediumTester addRule(String key, String repoKey, String internalKey, String name) {
Rule.Builder builder = Rule.newBuilder();
builder.setKey(key);
builder.setRepository(repoKey);
builder.setRepo(repoKey);
if (internalKey != null) {
builder.setInternalKey(internalKey);
}

+ 12
- 15
sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultActiveRulesLoader.java View File

@@ -31,19 +31,18 @@ import org.sonar.api.batch.rule.LoadedActiveRule;
import org.sonar.api.impl.utils.ScannerUtils;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.MessageException;
import org.sonar.scanner.bootstrap.ScannerWsClient;
import org.sonarqube.ws.Common.Paging;
import org.sonarqube.ws.Rules;
import org.sonarqube.ws.Rules.Active;
import org.sonarqube.ws.Rules.Active.Param;
import org.sonarqube.ws.Rules.ActiveList;
import org.sonarqube.ws.Rules.ListResponse;
import org.sonarqube.ws.Rules.Rule;
import org.sonarqube.ws.Rules.SearchResponse;
import org.sonarqube.ws.client.GetRequest;

public class DefaultActiveRulesLoader implements ActiveRulesLoader {
private static final String RULES_SEARCH_URL = "/api/rules/search.protobuf?" +
"f=repo,name,severity,lang,internalKey,templateKey,params,actives,createdAt,updatedAt,deprecatedKeys&activation=true";
private static final String RULES_SEARCH_URL = "/api/rules/list.protobuf?";

private final ScannerWsClient wsClient;

@@ -60,12 +59,14 @@ public class DefaultActiveRulesLoader implements ActiveRulesLoader {

while (true) {
GetRequest getRequest = new GetRequest(getUrl(qualityProfileKey, page, pageSize));
SearchResponse response = loadFromStream(wsClient.call(getRequest).contentStream());
ListResponse response = loadFromStream(wsClient.call(getRequest).contentStream());
List<LoadedActiveRule> pageRules = readPage(response);
ruleList.addAll(pageRules);
loaded += response.getPs();

if (response.getTotal() <= loaded) {
Paging paging = response.getPaging();
loaded += paging.getPageSize();

if (paging.getTotal() <= loaded) {
break;
}
page++;
@@ -77,15 +78,15 @@ public class DefaultActiveRulesLoader implements ActiveRulesLoader {
private static String getUrl(String qualityProfileKey, int page, int pageSize) {
StringBuilder builder = new StringBuilder(1024);
builder.append(RULES_SEARCH_URL);
builder.append("&qprofile=").append(ScannerUtils.encodeForUrl(qualityProfileKey));
builder.append("qprofile=").append(ScannerUtils.encodeForUrl(qualityProfileKey));
builder.append("&ps=").append(pageSize);
builder.append("&p=").append(page);
return builder.toString();
}

private static SearchResponse loadFromStream(InputStream is) {
private static ListResponse loadFromStream(InputStream is) {
try {
return SearchResponse.parseFrom(is);
return ListResponse.parseFrom(is);
} catch (IOException e) {
throw new IllegalStateException("Failed to load quality profiles", e);
} finally {
@@ -93,7 +94,7 @@ public class DefaultActiveRulesLoader implements ActiveRulesLoader {
}
}

private static List<LoadedActiveRule> readPage(SearchResponse response) {
private static List<LoadedActiveRule> readPage(ListResponse response) {
List<LoadedActiveRule> loadedRules = new LinkedList<>();

List<Rule> rulesList = response.getRulesList();
@@ -101,10 +102,6 @@ public class DefaultActiveRulesLoader implements ActiveRulesLoader {

for (Rule r : rulesList) {
ActiveList activeList = actives.get(r.getKey());
if (activeList == null) {
throw MessageException.of("Elasticsearch indices have become inconsistent. Consider re-indexing. " +
"Check documentation for more information https://docs.sonarsource.com/sonarqube/latest/setup/troubleshooting");
}
Active active = activeList.getActiveList(0);

LoadedActiveRule loadedRule = new LoadedActiveRule();

+ 2
- 2
sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/RulesLoader.java View File

@@ -20,8 +20,8 @@
package org.sonar.scanner.rule;

import java.util.List;
import org.sonarqube.ws.Rules.ListResponse.Rule;
import org.sonarqube.ws.Rules;

public interface RulesLoader {
List<Rule> load();
List<Rules.Rule> load();
}

+ 15
- 43
sonar-scanner-engine/src/test/java/org/sonar/scanner/rule/DefaultActiveRulesLoaderTest.java View File

@@ -19,30 +19,27 @@
*/
package org.sonar.scanner.rule;

import com.google.common.collect.ImmutableSortedMap;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Collection;
import java.util.Map;
import java.util.stream.IntStream;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.batch.rule.LoadedActiveRule;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity;
import org.sonar.api.utils.MessageException;
import org.sonar.scanner.WsTestUtil;
import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
import org.sonar.scanner.scan.branch.BranchConfiguration;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Rules;
import org.sonarqube.ws.Rules.Active;
import org.sonarqube.ws.Rules.ActiveList;
import org.sonarqube.ws.Rules.Actives;
import org.sonarqube.ws.Rules.Rule;
import org.sonarqube.ws.Rules.SearchResponse;
import org.sonarqube.ws.Rules.SearchResponse.Builder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -68,11 +65,11 @@ public class DefaultActiveRulesLoaderTest {
}

@Test
public void feed_real_response_encode_qp() {
public void load_shouldRequestRulesAndParseResponse() {
int total = PAGE_SIZE_1 + PAGE_SIZE_2;

WsTestUtil.mockStream(wsClient, urlOfPage(1), responseOfSize(PAGE_SIZE_1, total));
WsTestUtil.mockStream(wsClient, urlOfPage(2), responseOfSize(PAGE_SIZE_2, total));
WsTestUtil.mockStream(wsClient, urlOfPage(1), responseOfSize(1, PAGE_SIZE_1, total));
WsTestUtil.mockStream(wsClient, urlOfPage(2), responseOfSize(2, PAGE_SIZE_2, total));

Collection<LoadedActiveRule> activeRules = loader.load("c+-test_c+-values-17445");
assertThat(activeRules).hasSize(total);
@@ -92,29 +89,20 @@ public class DefaultActiveRulesLoaderTest {
verifyNoMoreInteractions(wsClient);
}

@Test
public void exception_thrown_when_elasticsearch_index_inconsistent() {
WsTestUtil.mockStream(wsClient, urlOfPage(1), prepareCorruptedResponse());
assertThatThrownBy(() -> loader.load("c+-test_c+-values-17445"))
.isInstanceOf(MessageException.class)
.hasMessage("Elasticsearch indices have become inconsistent. Consider re-indexing. " +
"Check documentation for more information https://docs.sonarsource.com/sonarqube/latest/setup/troubleshooting");
}

private String urlOfPage(int page) {
return "/api/rules/search.protobuf?f=repo,name,severity,lang,internalKey,templateKey,params,actives,createdAt,updatedAt,deprecatedKeys&activation=true"
+ ("") + "&qprofile=c%2B-test_c%2B-values-17445&ps=500&p=" + page + "";
return "/api/rules/list.protobuf?qprofile=c%2B-test_c%2B-values-17445&ps=500&p=" + page + "";
}

/**
* Generates an imaginary protobuf result.
*
* @param pageIndex page index, that the response should contain
* @param numberOfRules the number of rules, that the response should contain
* @param total the number of results on all pages
* @return the binary stream
*/
private InputStream responseOfSize(int numberOfRules, int total) {
Builder rules = SearchResponse.newBuilder();
private InputStream responseOfSize(int pageIndex, int numberOfRules, int total) {
Rules.ListResponse.Builder rules = Rules.ListResponse.newBuilder();
Actives.Builder actives = Actives.newBuilder();

IntStream.rangeClosed(1, numberOfRules)
@@ -133,31 +121,15 @@ public class DefaultActiveRulesLoaderTest {
activeBuilder.setSeverity(SEVERITY_VALUE);
}
ActiveList activeList = Rules.ActiveList.newBuilder().addActiveList(activeBuilder).build();
actives.putAllActives(ImmutableSortedMap.of(key.toString(), activeList));
});

rules.setActives(actives);
rules.setPs(numberOfRules);
rules.setTotal(total);
return new ByteArrayInputStream(rules.build().toByteArray());
}

private InputStream prepareCorruptedResponse() {
Builder rules = SearchResponse.newBuilder();
Actives.Builder actives = Actives.newBuilder();

IntStream.rangeClosed(1, 3)
.mapToObj(i -> RuleKey.of("java", "S" + i))
.forEach(key -> {

Rule.Builder ruleBuilder = Rule.newBuilder();
ruleBuilder.setKey(key.toString());
rules.addRules(ruleBuilder);
actives.putAllActives(Map.of(key.toString(), activeList));
});

rules.setActives(actives);
rules.setPs(3);
rules.setTotal(3);
rules.setPaging(Common.Paging.newBuilder()
.setTotal(total)
.setPageIndex(pageIndex)
.setPageSize(numberOfRules)
.build());
return new ByteArrayInputStream(rules.build().toByteArray());
}
}

+ 7
- 7
sonar-ws/src/main/protobuf/ws-rules.proto View File

@@ -29,14 +29,14 @@ option optimize_for = SPEED;
// WS api/rules/list for internal use only
message ListResponse {

message Rule {
optional string repository = 1;
optional string key = 2;
optional string internal_key = 3;
optional string name = 4;
}

repeated Rule rules = 1;

optional Actives actives = 2;

optional QProfiles qProfiles = 3;

optional sonarqube.ws.commons.Paging paging = 4;

}

// WS api/rules/search

Loading…
Cancel
Save