diff options
author | Pierre Guillot <pierre.guillot@sonarsource.com> | 2024-12-10 15:29:09 +0100 |
---|---|---|
committer | Steve Marion <steve.marion@sonarsource.com> | 2024-12-18 11:13:21 +0100 |
commit | d639a965bce7acafb004906cd07a8f0b5f7af993 (patch) | |
tree | 647cd646abddb12dfeeef7e637aa33b4658f1049 /server | |
parent | 451c1c2e4856ec3df87f86189fcdb25b31794027 (diff) | |
download | sonarqube-d639a965bce7acafb004906cd07a8f0b5f7af993.tar.gz sonarqube-d639a965bce7acafb004906cd07a8f0b5f7af993.zip |
SONAR-22998 fetch active rules with a dedicated endpoint
Co-authored-by: Julien HENRY <julien.henry@sonarsource.com>
Diffstat (limited to 'server')
19 files changed, 1034 insertions, 27 deletions
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/qualityprofile/ActiveRuleDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/qualityprofile/ActiveRuleDaoIT.java index b229d7895b6..eeebb9d5b9e 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/qualityprofile/ActiveRuleDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/qualityprofile/ActiveRuleDaoIT.java @@ -62,6 +62,7 @@ import static org.sonar.api.rule.Severity.MAJOR; import static org.sonar.db.qualityprofile.ActiveRuleDto.INHERITED; import static org.sonar.db.qualityprofile.ActiveRuleDto.OVERRIDES; import static org.sonar.db.qualityprofile.ActiveRuleDto.createFor; +import static org.sonar.db.rule.RuleDto.deserializeSecurityStandardsString; class ActiveRuleDaoIT { @@ -113,6 +114,19 @@ class ActiveRuleDaoIT { } @Test + void selectAllParamsByProfileUuids() { + ActiveRuleDto activeRule = createFor(profile1, rule1).setSeverity(BLOCKER); + underTest.insert(dbSession, activeRule); + ActiveRuleParamDto activeRuleParam1 = ActiveRuleParamDto.createFor(rule1Param1); + underTest.insertParam(dbSession, activeRule, activeRuleParam1); + ActiveRuleParamDto activeRuleParam2 = ActiveRuleParamDto.createFor(rule1Param2); + underTest.insertParam(dbSession, activeRule, activeRuleParam2); + dbSession.commit(); + + assertThat(underTest.selectAllParamsByProfileUuids(dbSession, List.of(profile1.getKee()))).hasSize(2); + } + + @Test void selectByKey() { ActiveRuleDto activeRule = createFor(profile1, rule1).setSeverity(BLOCKER); underTest.insert(dbSession, activeRule); @@ -184,6 +198,50 @@ class ActiveRuleDaoIT { } @Test + void selectByProfileUuids_ignores_removed_rules() { + ActiveRuleDto activeRule = createFor(profile1, removedRule).setSeverity(BLOCKER); + underTest.insert(dbSession, activeRule); + + assertThat(underTest.selectByProfileUuids(dbSession, List.of(profile1.getKee()))).isEmpty(); + } + + @Test + void selectByProfileUuids_exclude_other_profiles() { + ActiveRuleDto activeRule = createFor(profile1, rule1).setSeverity(BLOCKER); + underTest.insert(dbSession, activeRule); + ActiveRuleDto activeRule2 = createFor(profile2, rule2).setSeverity(BLOCKER); + underTest.insert(dbSession, activeRule2); + + assertThat(underTest.selectByProfileUuids(dbSession, List.of(profile1.getKee()))).hasSize(1); + } + + @Test + void selectByProfileUuids_returns_all_fields() { + ActiveRuleDto activeRule = createFor(profile1, rule1).setSeverity(BLOCKER); + underTest.insert(dbSession, activeRule); + + List<OrgActiveRuleDto> actualActiveRule = underTest.selectByProfileUuids(dbSession, List.of(profile1.getKee())); + // verify data from the "rules" table + assertThat(actualActiveRule) + .extracting(OrgActiveRuleDto::getRuleKey, org -> deserializeSecurityStandardsString(org.getSecurityStandards()), OrgActiveRuleDto::isExternal, OrgActiveRuleDto::getName, + OrgActiveRuleDto::getConfigKey, OrgActiveRuleDto::getTemplateUuid, OrgActiveRuleDto::getLanguage) + .containsExactly( + tuple(rule1.getKey(), rule1.getSecurityStandards(), rule1.isExternal(), rule1.getName(), rule1.getConfigKey(), rule1.getTemplateUuid(), rule1.getLanguage())); + + // verify data from "active_rules" table + assertThat(actualActiveRule) + .extracting(OrgActiveRuleDto::getUuid, OrgActiveRuleDto::getProfileUuid, OrgActiveRuleDto::getRuleUuid, OrgActiveRuleDto::getSeverity, OrgActiveRuleDto::getInheritance, + OrgActiveRuleDto::isPrioritizedRule, OrgActiveRuleDto::getCreatedAt, OrgActiveRuleDto::getUpdatedAt) + .containsExactly(tuple(activeRule.getUuid(), activeRule.getProfileUuid(), activeRule.getRuleUuid(), activeRule.getSeverity(), activeRule.getInheritance(), + activeRule.isPrioritizedRule(), activeRule.getCreatedAt(), activeRule.getUpdatedAt())); + + // verify data from "rules_profiles" and "org_qprofiles" + assertThat(actualActiveRule) + .extracting(o -> o.getKey().getRuleProfileUuid(), OrgActiveRuleDto::getOrgProfileUuid) + .containsExactly(tuple(activeRule.getKey().getRuleProfileUuid(), profile1.getKee())); + } + + @Test void selectByTypeAndProfileUuids() { RuleDto rule1 = db.rules().insert(r -> r.setType(RuleType.VULNERABILITY.getDbConstant())); ActiveRuleDto activeRule1 = createFor(profile1, rule1).setSeverity(BLOCKER); @@ -192,8 +250,8 @@ class ActiveRuleDaoIT { assertThat(underTest.selectByTypeAndProfileUuids(dbSession, singletonList(RuleType.VULNERABILITY.getDbConstant()), singletonList(profile1.getKee()))) - .extracting(OrgActiveRuleDto::getOrgProfileUuid, OrgActiveRuleDto::getRuleUuid) - .contains(tuple(profile1.getKee(), rule1.getUuid())); + .extracting(OrgActiveRuleDto::getOrgProfileUuid, OrgActiveRuleDto::getRuleUuid) + .contains(tuple(profile1.getKee(), rule1.getUuid())); } @Test @@ -205,7 +263,7 @@ class ActiveRuleDaoIT { assertThat(underTest.selectByTypeAndProfileUuids(dbSession, singletonList(RuleType.VULNERABILITY.getDbConstant()), singletonList(profile1.getKee()))) - .isEmpty(); + .isEmpty(); } @Test @@ -219,14 +277,14 @@ class ActiveRuleDaoIT { underTest.selectByTypeAndProfileUuids(dbSession, singletonList(RuleType.VULNERABILITY.getDbConstant()), singletonList(profile1.getKee()))) - .extracting(OrgActiveRuleDto::getOrgProfileUuid, OrgActiveRuleDto::getRuleUuid) - .contains(tuple(profile1.getKee(), rule1.getUuid())); + .extracting(OrgActiveRuleDto::getOrgProfileUuid, OrgActiveRuleDto::getRuleUuid) + .contains(tuple(profile1.getKee(), rule1.getUuid())); assertThat( underTest.selectByTypeAndProfileUuids(dbSession, asList(RuleType.CODE_SMELL.getDbConstant(), RuleType.SECURITY_HOTSPOT.getDbConstant(), RuleType.BUG.getDbConstant()), singletonList(profile1.getKee()))) - .isEmpty(); + .isEmpty(); } @Test @@ -684,10 +742,10 @@ class ActiveRuleDaoIT { ActiveRuleCountQuery.Builder builder = ActiveRuleCountQuery.builder(); assertThat(underTest.countActiveRulesByQuery(dbSession, builder.setProfiles(asList(profile1, profile2)).setInheritance(OVERRIDES).build())) - .containsOnly(entry(profile1.getKee(), 1L), entry(profile2.getKee(), 1L)); + .containsOnly(entry(profile1.getKee(), 1L), entry(profile2.getKee(), 1L)); assertThat(underTest.countActiveRulesByQuery(dbSession, builder.setProfiles(asList(profile1, profile2)).setInheritance(INHERITED).build())) - .containsOnly(entry(profile2.getKee(), 1L)); + .containsOnly(entry(profile2.getKee(), 1L)); } @Test diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDao.java index e085a3cc315..76c9ae977ef 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDao.java @@ -84,8 +84,15 @@ public class ActiveRuleDao implements Dao { /** * Active rule on removed rule are NOT returned */ - public List<OrgActiveRuleDto> selectByProfileUuid(DbSession dbSession, String uuid) { - return mapper(dbSession).selectByProfileUuid(uuid); + public List<OrgActiveRuleDto> selectByProfileUuid(DbSession dbSession, String profileUuid) { + return selectByProfileUuids(dbSession, List.of(profileUuid)); + } + + /** + * Active rule on removed rule are NOT returned + */ + public List<OrgActiveRuleDto> selectByProfileUuids(DbSession dbSession, Collection<String> profileUuids) { + return mapper(dbSession).selectByProfileUuids(profileUuids); } public List<OrgActiveRuleDto> selectByTypeAndProfileUuids(DbSession dbSession, List<Integer> types, List<String> uuids) { @@ -162,6 +169,10 @@ public class ActiveRuleDao implements Dao { return executeLargeInputs(activeRuleUuids, mapper(dbSession)::selectParamsByActiveRuleUuids); } + public List<ActiveRuleParamDto> selectAllParamsByProfileUuids(final DbSession dbSession, Collection<String> profileUuids) { + return mapper(dbSession).selectAllParamsByProfileUuids(profileUuids); + } + public ActiveRuleParamDto insertParam(DbSession dbSession, ActiveRuleDto activeRule, ActiveRuleParamDto activeRuleParam) { checkArgument(activeRule.getUuid() != null, ACTIVE_RULE_IS_NOT_PERSISTED); checkArgument(activeRuleParam.getUuid() == null, ACTIVE_RULE_PARAM_IS_ALREADY_PERSISTED); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDto.java index 906cdb4a2c7..483ed6e34a9 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDto.java @@ -58,10 +58,14 @@ public class ActiveRuleDto { // These fields do not exist in db, it's only retrieve by joins private String repository; private String ruleField; - private String ruleProfileUuid; private String securityStandards; private boolean isExternal; + private String name; + private String configKey; + private String templateUuid; + private String language; + public ActiveRuleDto() { // nothing to do here } @@ -69,12 +73,12 @@ public class ActiveRuleDto { public ActiveRuleDto setKey(ActiveRuleKey key) { this.repository = key.getRuleKey().repository(); this.ruleField = key.getRuleKey().rule(); - this.ruleProfileUuid = key.getRuleProfileUuid(); + this.profileUuid = key.getRuleProfileUuid(); return this; } public ActiveRuleKey getKey() { - return new ActiveRuleKey(ruleProfileUuid, RuleKey.of(repository, ruleField)); + return new ActiveRuleKey(profileUuid, RuleKey.of(repository, ruleField)); } public RuleKey getRuleKey() { @@ -207,6 +211,43 @@ public class ActiveRuleDto { return this; } + public String getName() { + return name; + } + + public ActiveRuleDto setName(String name) { + this.name = name; + return this; + } + + public String getConfigKey() { + return configKey; + } + + public ActiveRuleDto setConfigKey(String configKey) { + this.configKey = configKey; + return this; + } + + @CheckForNull + public String getTemplateUuid() { + return templateUuid; + } + + public ActiveRuleDto setTemplateUuid(@Nullable String templateUuid) { + this.templateUuid = templateUuid; + return this; + } + + public String getLanguage() { + return language; + } + + public ActiveRuleDto setLanguage(String language) { + this.language = language; + return this; + } + public static ActiveRuleDto createFor(QProfileDto profile, RuleDto ruleDto) { requireNonNull(profile.getRulesProfileUuid(), "Profile is not persisted"); requireNonNull(ruleDto.getUuid(), "Rule is not persisted"); @@ -214,6 +255,10 @@ public class ActiveRuleDto { dto.setProfileUuid(profile.getRulesProfileUuid()); dto.setRuleUuid(ruleDto.getUuid()); dto.setKey(ActiveRuleKey.of(profile, ruleDto.getKey())); + dto.setName(ruleDto.getName()); + dto.setConfigKey(ruleDto.getConfigKey()); + dto.setTemplateUuid(ruleDto.getTemplateUuid()); + dto.setLanguage(ruleDto.getLanguage()); dto.setImpacts(ruleDto.getDefaultImpactsMap()); return dto; } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleMapper.java index d2b2dddfdbe..af2d2dcaa98 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleMapper.java @@ -52,7 +52,7 @@ public interface ActiveRuleMapper { List<OrgActiveRuleDto> selectByRuleUuids(@Param("ruleUuids") List<String> partitionOfRuleUuids); - List<OrgActiveRuleDto> selectByProfileUuid(String uuid); + List<OrgActiveRuleDto> selectByProfileUuids(@Param("profileUuids") Collection<String> profileUuids); List<OrgActiveRuleDto> selectByTypeAndProfileUuids(@Param("types") List<Integer> types, @Param("profileUuids") List<String> uuids); @@ -90,4 +90,6 @@ public interface ActiveRuleMapper { void scrollByRuleProfileUuidForIndexing(@Param("ruleProfileUuid") String ruleProfileUuid, ResultHandler<IndexedActiveRuleDto> handler); int countMissingRules(@Param("rulesProfileUuid") String rulesProfileUuid, @Param("compareToRulesProfileUuid") String compareToRulesProfileUuid); + + List<ActiveRuleParamDto> selectAllParamsByProfileUuids(@Param("profileUuids") Collection<String> profileUuids); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileDao.java index f9d0982e4b4..34d9bd3ea71 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileDao.java @@ -168,7 +168,11 @@ public class QualityProfileDao implements Dao { } public List<QProfileDto> selectAssociatedToProjectAndLanguages(DbSession dbSession, ProjectDto project, Collection<String> languages) { - return executeLargeInputs(languages, partition -> mapper(dbSession).selectAssociatedToProjectUuidAndLanguages(project.getUuid(), partition)); + return selectAssociatedToProjectAndLanguages(dbSession, project.getUuid(), languages); + } + + public List<QProfileDto> selectAssociatedToProjectAndLanguages(DbSession dbSession, String projectUuid, Collection<String> languages) { + return executeLargeInputs(languages, partition -> mapper(dbSession).selectAssociatedToProjectUuidAndLanguages(projectUuid, partition)); } public List<QProfileDto> selectQProfilesByProjectUuid(DbSession dbSession, String projectUuid) { @@ -251,6 +255,7 @@ public class QualityProfileDao implements Dao { public List<ProjectQProfileLanguageAssociationDto> selectAllProjectAssociations(DbSession dbSession) { return mapper(dbSession).selectAllProjectAssociations(); } + public Collection<String> selectUuidsOfCustomRulesProfiles(DbSession dbSession, String language, String name) { return mapper(dbSession).selectUuidsOfCustomRuleProfiles(language, name); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java index 6d1f544e076..722f131695b 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java @@ -72,7 +72,7 @@ public class RuleDto { private RuleDto.Format descriptionFormat = null; private RuleStatus status = null; - private Set<ImpactDto> defaultImpacts = new HashSet<>(); + private final Set<ImpactDto> defaultImpacts = new HashSet<>(); private String name = null; private String configKey = null; @@ -375,7 +375,6 @@ public class RuleDto { return this; } - @CheckForNull public String getLanguage() { return language; } @@ -650,13 +649,12 @@ public class RuleDto { @Override public boolean equals(Object obj) { - if (!(obj instanceof RuleDto)) { + if (!(obj instanceof RuleDto other)) { return false; } if (this == obj) { return true; } - RuleDto other = (RuleDto) obj; return Objects.equals(this.uuid, other.uuid); } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/ActiveRuleMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/ActiveRuleMapper.xml index 10442858528..c6facb12ff6 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/ActiveRuleMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/ActiveRuleMapper.xml @@ -26,14 +26,19 @@ a.inheritance as "inheritance", a.impacts as "impactsString", a.prioritized_rule as "prioritizedRule", + a.created_at as "createdAt", + a.updated_at as "updatedAt", + r.plugin_rule_key as "rulefield", r.plugin_name as "repository", r.security_standards as "securityStandards", - rp.uuid as "ruleProfileUuid", - a.created_at as "createdAt", - a.updated_at as "updatedAt", - oqp.uuid as "orgProfileUuid", - r.is_external as "isExternal" + r.is_external as "isExternal", + r.name as "name", + r.plugin_config_key as "configKey", + r.template_uuid as "templateUuid", + r.language as "language", + + oqp.uuid as "orgProfileUuid" </sql> <sql id="activeRuleKeyJoin"> @@ -113,14 +118,18 @@ and r.plugin_name = #{repository, jdbcType=VARCHAR} </select> - <select id="selectByProfileUuid" parameterType="string" resultType="org.sonar.db.qualityprofile.OrgActiveRuleDto"> + <select id="selectByProfileUuids" parameterType="string" resultType="org.sonar.db.qualityprofile.OrgActiveRuleDto"> select <include refid="orgActiveRuleColumns"/> from active_rules a inner join rules_profiles rp on rp.uuid = a.profile_uuid inner join org_qprofiles oqp on oqp.rules_profile_uuid = rp.uuid inner join rules r on r.uuid = a.rule_uuid and r.status != 'REMOVED' - where oqp.uuid = #{id, jdbcType=VARCHAR} + where + oqp.uuid in + <foreach collection="profileUuids" item="profileUuid" separator="," open="(" close=")"> + #{profileUuid, jdbcType=VARCHAR} + </foreach> </select> <select id="selectByTypeAndProfileUuids" parameterType="Map" resultType="org.sonar.db.qualityprofile.OrgActiveRuleDto"> @@ -306,6 +315,22 @@ </where> </select> + <select id="selectAllParamsByProfileUuids" parameterType="map" resultType="ActiveRuleParam"> + select + <include refid="activeRuleParamColumns"/> + from active_rule_parameters p + inner join active_rules ar on ar.uuid = p.active_rule_uuid + inner join rules_profiles rp on rp.uuid = ar.profile_uuid + inner join org_qprofiles oqp on oqp.rules_profile_uuid = rp.uuid + inner join rules r on r.uuid = ar.rule_uuid + <where> + <foreach collection="profileUuids" item="profileUuid" open="(" separator=" or " close=")"> + oqp.uuid = #{profileUuid, jdbcType=VARCHAR} + </foreach> + and r.status != 'REMOVED' + </where> + </select> + <select id="countActiveRulesByQuery" resultType="KeyLongValue" parameterType="map"> select oqp.uuid as "key", count(ar.uuid) as "value" from active_rules ar diff --git a/server/sonar-server-common/src/it/java/org/sonar/server/rule/ActiveRuleServiceIT.java b/server/sonar-server-common/src/it/java/org/sonar/server/rule/ActiveRuleServiceIT.java new file mode 100644 index 00000000000..2d36a185236 --- /dev/null +++ b/server/sonar-server-common/src/it/java/org/sonar/server/rule/ActiveRuleServiceIT.java @@ -0,0 +1,206 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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; + +import java.util.List; +import org.assertj.core.groups.Tuple; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.db.component.ProjectData; +import org.sonar.db.qualityprofile.ActiveRuleDto; +import org.sonar.db.qualityprofile.ActiveRuleKey; +import org.sonar.db.qualityprofile.ActiveRuleParamDto; +import org.sonar.db.qualityprofile.QProfileDto; +import org.sonar.db.rule.RuleDto; +import org.sonar.db.rule.RuleParamDto; +import org.sonar.server.rule.ActiveRuleRestReponse.ActiveRule; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.utils.DateUtils.formatDateTime; + +class ActiveRuleServiceIT { + + public static final Language XOO_LANGUAGE = mock(Language.class); + public static final Language XOO2_LANGUAGE = mock(Language.class); + + public static final String XOO_LANGUAGE_KEY = "xoo"; + public static final String XOO2_LANGUAGE_KEY = "xoo2"; + + static { + when(XOO_LANGUAGE.getKey()).thenReturn(XOO_LANGUAGE_KEY); + when(XOO2_LANGUAGE.getKey()).thenReturn(XOO2_LANGUAGE_KEY); + } + + @RegisterExtension + DbTester db = DbTester.create(System2.INSTANCE); + + private final ActiveRuleService activeRuleService = new ActiveRuleService(db.getDbClient(), new Languages(XOO_LANGUAGE, XOO2_LANGUAGE)); + + @Test + void buildDefaultActiveRules() { + QProfileDto qProfileDto1 = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO_LANGUAGE_KEY)); + db.qualityProfiles().setAsDefault(qProfileDto1); + RuleDto rule1 = db.rules().insert(r -> r.setLanguage(XOO_LANGUAGE_KEY), r -> r.setName("rule1")); + ActiveRuleDto activatedRule1 = db.qualityProfiles().activateRule(qProfileDto1, rule1); + RuleDto rule2 = db.rules().insert(r -> r.setLanguage(XOO_LANGUAGE_KEY), r -> r.setName("rule2")); + ActiveRuleDto activatedRule2 = db.qualityProfiles().activateRule(qProfileDto1, rule2); + + QProfileDto qProfileDto2 = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO2_LANGUAGE_KEY)); + db.qualityProfiles().setAsDefault(qProfileDto2); + RuleDto rule3 = db.rules().insert(r -> r.setLanguage(XOO2_LANGUAGE_KEY), r -> r.setName("rule3")); + ActiveRuleDto activatedRule3 = db.qualityProfiles().activateRule(qProfileDto2, rule3); + + List<ActiveRule> activeRules = activeRuleService.buildDefaultActiveRules(); + assertThat(activeRules).isNotNull().hasSize(3) + .extracting(ActiveRule::ruleKey, ActiveRule::name, ActiveRule::severity, + ActiveRule::createdAt, ActiveRule::updatedAt, ActiveRule::internalKey, + ActiveRule::language, ActiveRule::templateRuleKey, ActiveRule::qProfileKey, + ActiveRule::deprecatedKeys, ActiveRule::params) + .containsExactlyInAnyOrder( + toTuple(activatedRule1, XOO_LANGUAGE_KEY, qProfileDto1, List.of()), + toTuple(activatedRule2, XOO_LANGUAGE_KEY, qProfileDto1, List.of()), + toTuple(activatedRule3, XOO2_LANGUAGE_KEY, qProfileDto2, List.of())); + } + + @Test + void buildActiveRule_with_default_profile() { + QProfileDto qProfileDto1 = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO_LANGUAGE_KEY)); + db.qualityProfiles().setAsDefault(qProfileDto1); + RuleDto rule1 = db.rules().insert(r -> r.setLanguage(XOO_LANGUAGE_KEY), r -> r.setName("rule1")); + ActiveRuleDto activatedRule1 = db.qualityProfiles().activateRule(qProfileDto1, rule1); + RuleDto rule2 = db.rules().insert(r -> r.setLanguage(XOO_LANGUAGE_KEY), r -> r.setName("rule2")); + ActiveRuleDto activatedRule2 = db.qualityProfiles().activateRule(qProfileDto1, rule2); + + QProfileDto qProfileDto2 = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO2_LANGUAGE_KEY)); + db.qualityProfiles().setAsDefault(qProfileDto2); + RuleDto rule3 = db.rules().insert(r -> r.setLanguage(XOO2_LANGUAGE_KEY), r -> r.setName("rule3")); + ActiveRuleDto activatedRule3 = db.qualityProfiles().activateRule(qProfileDto2, rule3); + + var removedLanguageKey = "removed"; + QProfileDto qProfileDtoRemovedLanguage = db.qualityProfiles().insert(qp -> qp.setLanguage(removedLanguageKey)); + db.qualityProfiles().setAsDefault(qProfileDtoRemovedLanguage); + RuleDto ruleRemovedLanguage = db.rules().insert(r -> r.setLanguage(removedLanguageKey), r -> r.setName("ruleRemovedLanguage")); + db.qualityProfiles().activateRule(qProfileDtoRemovedLanguage, ruleRemovedLanguage); + + List<ActiveRule> activeRules = activeRuleService.buildActiveRules("myProjectUuid"); + assertThat(activeRules).isNotNull().hasSize(3) + .extracting(ActiveRule::ruleKey, ActiveRule::name, ActiveRule::severity, + ActiveRule::createdAt, ActiveRule::updatedAt, ActiveRule::internalKey, + ActiveRule::language, ActiveRule::templateRuleKey, ActiveRule::qProfileKey, + ActiveRule::deprecatedKeys, ActiveRule::params) + .containsExactlyInAnyOrder( + toTuple(activatedRule1, XOO_LANGUAGE_KEY, qProfileDto1, List.of()), + toTuple(activatedRule2, XOO_LANGUAGE_KEY, qProfileDto1, List.of()), + toTuple(activatedRule3, XOO2_LANGUAGE_KEY, qProfileDto2, List.of())); + } + + @Test + void projectProfile_overrides_defaultProfile() { + QProfileDto qProfileDto1 = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO_LANGUAGE_KEY)); + db.qualityProfiles().setAsDefault(qProfileDto1); + RuleDto rule1 = db.rules().insert(r -> r.setLanguage(XOO_LANGUAGE_KEY), r -> r.setName("rule1")); + db.qualityProfiles().activateRule(qProfileDto1, rule1); + + QProfileDto qProfileDto2 = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO_LANGUAGE_KEY)); + ProjectData project = db.components().insertPrivateProject(); + db.qualityProfiles().associateWithProject(project.getProjectDto(), qProfileDto2); + ActiveRuleDto activatedRule2 = db.qualityProfiles().activateRule(qProfileDto2, rule1); + + List<ActiveRule> activeRules = activeRuleService.buildActiveRules(project.projectUuid()); + assertThat(activeRules).hasSize(1) + .extracting(ActiveRule::ruleKey, ActiveRule::name, ActiveRule::severity, + ActiveRule::createdAt, ActiveRule::updatedAt, ActiveRule::internalKey, + ActiveRule::language, ActiveRule::templateRuleKey, ActiveRule::qProfileKey, + ActiveRule::deprecatedKeys, ActiveRule::params) + .containsExactlyInAnyOrder( + toTuple(activatedRule2, XOO_LANGUAGE_KEY, qProfileDto2, List.of())); + } + + @Test + void customRuleParam_overrides_defaultRuleParamValue() { + RuleDto rule = db.rules().insert(r -> r.setLanguage(XOO_LANGUAGE_KEY), r -> r.setName("rule")); + RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setDefaultValue("defaultValue")); + QProfileDto defaultQualityProfile = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO_LANGUAGE_KEY)); + db.qualityProfiles().setAsDefault(defaultQualityProfile); + db.qualityProfiles().activateRule(defaultQualityProfile, rule); + + QProfileDto qProfileDto2 = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO_LANGUAGE_KEY)); + ProjectData project = db.components().insertPrivateProject(); + db.qualityProfiles().associateWithProject(project.getProjectDto(), qProfileDto2); + ActiveRuleDto activatedRule2 = db.qualityProfiles().activateRule(qProfileDto2, rule); + db.getDbClient().activeRuleDao().insertParam(db.getSession(), activatedRule2, new ActiveRuleParamDto() + .setRulesParameterUuid(ruleParam.getUuid()) + .setKey(ruleParam.getName()) + .setValue("overriddenValue")); + + db.commit(); + + List<ActiveRule> activeRules = activeRuleService.buildActiveRules(project.projectUuid()); + assertThat(activeRules).hasSize(1) + .extracting(ActiveRule::ruleKey, ActiveRule::name, ActiveRule::severity, + ActiveRule::createdAt, ActiveRule::updatedAt, ActiveRule::internalKey, + ActiveRule::language, ActiveRule::templateRuleKey, ActiveRule::qProfileKey, + ActiveRule::deprecatedKeys, ActiveRule::params) + .containsExactlyInAnyOrder( + toTuple(activatedRule2, XOO_LANGUAGE_KEY, qProfileDto2, List.of(new ActiveRuleRestReponse.Param(ruleParam.getName(), "overriddenValue")))); + } + + @Test + void returnsRuleTemplate() { + RuleDto templateRule = db.rules().insert(r -> r.setLanguage(XOO_LANGUAGE_KEY) + .setIsTemplate(true)); + RuleDto rule = db.rules().insert(r -> r.setLanguage(XOO_LANGUAGE_KEY) + .setTemplateUuid(templateRule.getUuid())); + RuleParamDto ruleParam = db.rules().insertRuleParam(rule); + QProfileDto defaultQualityProfile = db.qualityProfiles().insert(qp -> qp.setLanguage(XOO_LANGUAGE_KEY)); + db.qualityProfiles().setAsDefault(defaultQualityProfile); + ActiveRuleDto activeRule = db.qualityProfiles().activateRule(defaultQualityProfile, rule); + ProjectData project = db.components().insertPrivateProject(); + + List<ActiveRule> activeRules = activeRuleService.buildActiveRules(project.projectUuid()); + assertThat(activeRules).hasSize(1) + .extracting(ActiveRule::ruleKey, ActiveRule::name, ActiveRule::severity, + ActiveRule::createdAt, ActiveRule::updatedAt, ActiveRule::internalKey, + ActiveRule::language, ActiveRule::templateRuleKey, ActiveRule::qProfileKey, + ActiveRule::deprecatedKeys, ActiveRule::params) + .containsExactlyInAnyOrder( + tuple(toRuleKey(activeRule.getKey()), activeRule.getName(), activeRule.getSeverityString(), formatDateTime(activeRule.getCreatedAt()), + formatDateTime(activeRule.getUpdatedAt()), activeRule.getConfigKey(), XOO_LANGUAGE_KEY, templateRule.getKey().toString(), defaultQualityProfile.getKee(), List.of(), + List.of(new ActiveRuleRestReponse.Param(ruleParam.getName(), ruleParam.getDefaultValue())))); + + } + + private Tuple toTuple(ActiveRuleDto activatedRule, String language, QProfileDto qProfileDto, List<ActiveRuleRestReponse.Param> expectedParams) { + return tuple(toRuleKey(activatedRule.getKey()), activatedRule.getName(), activatedRule.getSeverityString(), formatDateTime(activatedRule.getCreatedAt()), + formatDateTime(activatedRule.getUpdatedAt()), activatedRule.getConfigKey(), language, null, qProfileDto.getKee(), List.of(), expectedParams); + } + + private ActiveRuleRestReponse.RuleKey toRuleKey(ActiveRuleKey key) { + return new ActiveRuleRestReponse.RuleKey(key.getRuleKey().repository(), key.getRuleKey().rule()); + } + +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/ActiveRuleRestReponse.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/ActiveRuleRestReponse.java new file mode 100644 index 00000000000..f802e7f004d --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/ActiveRuleRestReponse.java @@ -0,0 +1,123 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; + +public class ActiveRuleRestReponse { + + public record ActiveRule( + RuleKey ruleKey, + String name, + String severity, + String createdAt, + String updatedAt, + @JsonInclude(JsonInclude.Include.NON_NULL) @Nullable String internalKey, + String language, + @JsonInclude(JsonInclude.Include.NON_NULL) @Nullable String templateRuleKey, + String qProfileKey, + @JsonInclude(JsonInclude.Include.NON_EMPTY) List<RuleKey> deprecatedKeys, + @JsonInclude(JsonInclude.Include.NON_EMPTY) List<Param> params, + @JsonInclude(JsonInclude.Include.NON_EMPTY) Map<SoftwareQuality, Severity> impacts) { + } + + public record RuleKey(String repository, String rule) { + } + + public record Param(String key, String value) { + } + + public static class Builder { + private RuleKey ruleKey; + private String name; + private String severity; + private String createdAt; + private String updatedAt; + private String internalKey; + private String language; + private String templateRuleKey; + private String qProfilKey; + private final List<RuleKey> deprecatedKeys = new ArrayList<>(); + private final List<Param> params = new ArrayList<>(); + private final Map<SoftwareQuality, Severity> impacts = new EnumMap<>(SoftwareQuality.class); + + public void setRuleKey(RuleKey ruleKey) { + this.ruleKey = ruleKey; + } + + public void setName(String name) { + this.name = name; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } + + public void setInternalKey(String internalKey) { + this.internalKey = internalKey; + } + + public void setLanguage(String language) { + this.language = language; + } + + public void setTemplateRuleKey(String templateRuleKey) { + this.templateRuleKey = templateRuleKey; + } + + public void setQProfilKey(String qProfilKey) { + this.qProfilKey = qProfilKey; + } + + public void setParams(List<Param> params) { + this.params.clear(); + this.params.addAll(params); + } + + public void addAllDeprecatedKeys(List<RuleKey> deprecatedKeys) { + this.deprecatedKeys.addAll(deprecatedKeys); + } + + public void setImpacts(Map<SoftwareQuality, Severity> impacts) { + this.impacts.clear(); + this.impacts.putAll(impacts); + } + + public ActiveRule build() { + return new ActiveRule(ruleKey, name, severity, createdAt, updatedAt, internalKey, language, templateRuleKey, qProfilKey, deprecatedKeys, params, impacts); + } + } + +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/ActiveRuleService.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/ActiveRuleService.java new file mode 100644 index 00000000000..5a7960a6010 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/ActiveRuleService.java @@ -0,0 +1,177 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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; + +import com.google.common.base.Strings; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.DateUtils; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +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 static java.util.Optional.ofNullable; + +public class ActiveRuleService { + + private final DbClient dbClient; + private final Languages languages; + + public ActiveRuleService(DbClient dbClient, Languages languages) { + this.dbClient = dbClient; + this.languages = languages; + } + + public List<ActiveRuleRestReponse.ActiveRule> buildDefaultActiveRules() { + try (DbSession dbSession = dbClient.openSession(false)) { + List<QProfileDto> qualityProfiles = dbClient.qualityProfileDao().selectDefaultProfiles(dbSession, getLanguageKeys()); + return loadActiveRules(qualityProfiles, dbSession); + } + } + + public List<ActiveRuleRestReponse.ActiveRule> buildActiveRules(String projectUuid) { + try (DbSession dbSession = dbClient.openSession(false)) { + List<QProfileDto> qualityProfiles = getQualityProfiles(dbSession, projectUuid); + return loadActiveRules(qualityProfiles, dbSession); + } + } + + private Set<String> getLanguageKeys() { + return Arrays.stream(languages.all()).map(Language::getKey).collect(Collectors.toSet()); + } + + private List<ActiveRuleRestReponse.ActiveRule> loadActiveRules(List<QProfileDto> qualityProfiles, DbSession dbSession) { + + Map<String, QProfileDto> profilesByUuids = qualityProfiles.stream().collect(Collectors.toMap(QProfileDto::getKee, Function.identity())); + + Map<String, List<RuleParamDto>> ruleParamsByRuleUuid = dbClient.ruleDao().selectAllRuleParams(dbSession).stream() + .collect(Collectors.groupingBy(RuleParamDto::getRuleUuid)); + + Map<String, List<ActiveRuleParamDto>> activeRuleParamsByActiveRuleUuid = dbClient.activeRuleDao().selectAllParamsByProfileUuids(dbSession, profilesByUuids.keySet()).stream() + .collect(Collectors.groupingBy(ActiveRuleParamDto::getActiveRuleUuid)); + + List<OrgActiveRuleDto> activeRuleDtos = dbClient.activeRuleDao().selectByProfileUuids(dbSession, profilesByUuids.keySet()); + Map<String, RuleDto> templateRulesByUuid = getTemplateRulesByUuid(dbSession, activeRuleDtos); + Map<String, List<DeprecatedRuleKeyDto>> deprecatedRuleKeysByRuleUuid = getDeprecatedRuleKeysByRuleUuid(dbSession); + + return activeRuleDtos.stream() + .map( + activeRuleDto -> buildActiveRule(activeRuleDto, profilesByUuids, templateRulesByUuid, deprecatedRuleKeysByRuleUuid, ruleParamsByRuleUuid, activeRuleParamsByActiveRuleUuid)) + .toList(); + } + + private static ActiveRuleRestReponse.ActiveRule buildActiveRule(OrgActiveRuleDto activeRuleDto, + Map<String, QProfileDto> profilesByUuids, + Map<String, RuleDto> templateRulesByUuid, + Map<String, List<DeprecatedRuleKeyDto>> deprecatedRuleKeysByRuleUuid, + Map<String, List<RuleParamDto>> ruleParamsByRuleUuid, + Map<String, List<ActiveRuleParamDto>> activeRuleParamsByActiveRuleUuid) { + ActiveRuleRestReponse.Builder activeRuleBuilder = new ActiveRuleRestReponse.Builder(); + activeRuleBuilder.setRuleKey(toRuleKey(activeRuleDto.getRuleKey())); + activeRuleBuilder.setName(activeRuleDto.getName()); + activeRuleBuilder.setSeverity(activeRuleDto.getSeverityString()); + activeRuleBuilder.setCreatedAt(DateUtils.formatDateTime(activeRuleDto.getCreatedAt())); + activeRuleBuilder.setUpdatedAt(DateUtils.formatDateTime(activeRuleDto.getUpdatedAt())); + // same as RuleForIndexingDto mapping + activeRuleBuilder.setInternalKey(activeRuleDto.getConfigKey()); + activeRuleBuilder.setQProfilKey(profilesByUuids.get(activeRuleDto.getOrgProfileUuid()).getKee()); + activeRuleBuilder.setLanguage(activeRuleDto.getLanguage()); + Optional<RuleDto> templateRule = ofNullable(templateRulesByUuid.get(activeRuleDto.getTemplateUuid())); + templateRule.ifPresent(template -> activeRuleBuilder.setTemplateRuleKey(template.getKey().toString())); + + List<ActiveRuleRestReponse.RuleKey> deprecatedKeys = buildDeprecatedKeys(activeRuleDto, deprecatedRuleKeysByRuleUuid); + activeRuleBuilder.addAllDeprecatedKeys(deprecatedKeys); + + List<RuleParamDto> ruleParams = ruleParamsByRuleUuid.get(activeRuleDto.getRuleUuid()); + List<ActiveRuleParamDto> activeRuleParams = ofNullable(activeRuleParamsByActiveRuleUuid.get(activeRuleDto.getUuid())).orElse(List.of()); + List<ActiveRuleRestReponse.Param> params = buildParams(ruleParams, activeRuleParams); + activeRuleBuilder.setParams(params); + + activeRuleBuilder.setImpacts(activeRuleDto.getImpacts()); + + return activeRuleBuilder.build(); + } + + private static List<ActiveRuleRestReponse.Param> buildParams(@Nullable List<RuleParamDto> ruleParams, List<ActiveRuleParamDto> activeRuleParams) { + if (ruleParams == null) { + return List.of(); + } + return ruleParams.stream() + .map(parameter -> { + Optional<ActiveRuleParamDto> activeRuleParamDto = activeRuleParams.stream() + .filter(arp -> arp.getRulesParameterUuid().equals(parameter.getUuid())) + .findAny(); + String paramValue = activeRuleParamDto.map(ActiveRuleParamDto::getValue).orElse(parameter.getDefaultValue()); + return new ActiveRuleRestReponse.Param(parameter.getName(), Strings.nullToEmpty(paramValue)); + }) + .toList(); + } + + private static List<ActiveRuleRestReponse.RuleKey> buildDeprecatedKeys(OrgActiveRuleDto activeRuleDto, Map<String, List<DeprecatedRuleKeyDto>> deprecatedRuleKeysByRuleUuid) { + List<DeprecatedRuleKeyDto> deprecatedRuleKeyDtos = deprecatedRuleKeysByRuleUuid.getOrDefault(activeRuleDto.getUuid(), List.of()); + return deprecatedRuleKeyDtos.stream() + .map(ActiveRuleService::toRuleKey) + .toList(); + } + + private List<QProfileDto> getQualityProfiles(DbSession dbSession, String projectUuid) { + List<QProfileDto> defaultProfiles = dbClient.qualityProfileDao().selectDefaultProfiles(dbSession, getLanguageKeys()); + Map<String, QProfileDto> projectProfiles = dbClient.qualityProfileDao().selectAssociatedToProjectAndLanguages(dbSession, projectUuid, getLanguageKeys()) + .stream().collect(Collectors.toMap(QProfileDto::getLanguage, Function.identity())); + + return defaultProfiles.stream() + .map(defaultProfile -> projectProfiles.getOrDefault(defaultProfile.getLanguage(), defaultProfile)) + .toList(); + } + + private Map<String, RuleDto> getTemplateRulesByUuid(DbSession dbSession, List<OrgActiveRuleDto> activeRuleDtos) { + Set<String> templateRuleUuids = activeRuleDtos.stream().map(OrgActiveRuleDto::getTemplateUuid).filter(Objects::nonNull).collect(Collectors.toSet()); + return dbClient.ruleDao().selectByUuids(dbSession, templateRuleUuids).stream().collect(Collectors.toMap(RuleDto::getUuid, p -> p)); + } + + private static ActiveRuleRestReponse.RuleKey toRuleKey(RuleKey ruleKey) { + return new ActiveRuleRestReponse.RuleKey(ruleKey.repository(), ruleKey.rule()); + } + + private static ActiveRuleRestReponse.RuleKey toRuleKey(DeprecatedRuleKeyDto r) { + return new ActiveRuleRestReponse.RuleKey(r.getOldRepositoryKey(), r.getOldRuleKey()); + } + + private Map<String, List<DeprecatedRuleKeyDto>> getDeprecatedRuleKeysByRuleUuid(DbSession dbSession) { + return dbClient.ruleDao().selectAllDeprecatedRuleKeys(dbSession).stream() + .collect(Collectors.groupingBy(DeprecatedRuleKeyDto::getRuleUuid)); + } + +} diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/rule/CachingRuleFinder.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/rule/CachingRuleFinder.java index 4809218d3e2..3b6c7007df7 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/rule/CachingRuleFinder.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/rule/CachingRuleFinder.java @@ -20,7 +20,6 @@ package org.sonar.server.rule; import com.google.common.collect.Ordering; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java index d77f2fda48a..1d053c5049f 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java @@ -51,6 +51,7 @@ public class WebApiEndpoints { public static final String ANALYSIS_DOMAIN = "/analysis"; public static final String VERSION_ENDPOINT = ANALYSIS_DOMAIN + "/version"; public static final String JRE_ENDPOINT = ANALYSIS_DOMAIN + "/jres"; + public static final String ACTIVE_RULES_ENDPOINT = ANALYSIS_DOMAIN + "/active_rules"; public static final String SCANNER_ENGINE_ENDPOINT = ANALYSIS_DOMAIN + "/engine"; private WebApiEndpoints() { diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/controller/ActiveRulesController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/controller/ActiveRulesController.java new file mode 100644 index 00000000000..905420589a3 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/controller/ActiveRulesController.java @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.analysis.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import java.util.List; +import org.sonar.server.rule.ActiveRuleRestReponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.sonar.server.v2.WebApiEndpoints.ACTIVE_RULES_ENDPOINT; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@RequestMapping(value = ACTIVE_RULES_ENDPOINT, produces = APPLICATION_JSON_VALUE) +@ResponseStatus(OK) +@RestController +public interface ActiveRulesController { + + String PROJECT_KEY_PARAM_DESCRIPTION = "Project Key"; + + @GetMapping + @Operation(summary = "Get all active rules for a specific project", description = "Used by the scanner-engine to get all active rules for a given project.") + List<ActiveRuleRestReponse.ActiveRule> getActiveRules(@RequestParam(value = "projectKey") @Parameter(description = PROJECT_KEY_PARAM_DESCRIPTION) String projectKey); + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/controller/DefaultActiveRulesController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/controller/DefaultActiveRulesController.java new file mode 100644 index 00000000000..9a51bd5972b --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/controller/DefaultActiveRulesController.java @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.analysis.controller; + +import java.util.List; +import org.sonar.server.rule.ActiveRuleRestReponse; +import org.sonar.server.v2.api.analysis.service.ActiveRulesHandler; + +public class DefaultActiveRulesController implements ActiveRulesController { + + private final ActiveRulesHandler activeRulesHandler; + + public DefaultActiveRulesController(ActiveRulesHandler activeRulesHandler) { + this.activeRulesHandler = activeRulesHandler; + } + + @Override + public List<ActiveRuleRestReponse.ActiveRule> getActiveRules(String projectKey) { + return activeRulesHandler.getActiveRules(projectKey); + } +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandler.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandler.java new file mode 100644 index 00000000000..d14a66a6d7a --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandler.java @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.analysis.service; + +import java.util.List; +import org.sonar.server.rule.ActiveRuleRestReponse; + +public interface ActiveRulesHandler { + List<ActiveRuleRestReponse.ActiveRule> getActiveRules(String projectKey); +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandlerImpl.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandlerImpl.java new file mode 100644 index 00000000000..8b4562bc4f9 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandlerImpl.java @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.analysis.service; + +import java.util.List; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.server.rule.ActiveRuleRestReponse; +import org.sonar.server.rule.ActiveRuleService; + +public class ActiveRulesHandlerImpl implements ActiveRulesHandler { + + private final DbClient dbClient; + private final ActiveRuleService activeRuleService; + + public ActiveRulesHandlerImpl(DbClient dbClient, ActiveRuleService activeRuleService) { + this.dbClient = dbClient; + this.activeRuleService = activeRuleService; + } + + @Override + public List<ActiveRuleRestReponse.ActiveRule> getActiveRules(String projectKey) { + try (DbSession dbSession = dbClient.openSession(false)) { + return dbClient.projectDao().selectProjectByKey(dbSession, projectKey) + .map(projectDto -> activeRuleService.buildActiveRules(projectDto.getUuid())) + .orElse(activeRuleService.buildDefaultActiveRules()); + } + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java index 20880ad7420..2ae1170b7af 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java @@ -48,16 +48,21 @@ import org.sonar.server.platform.ServerFileSystem; import org.sonar.server.platform.db.migration.DatabaseMigrationState; import org.sonar.server.platform.db.migration.version.DatabaseVersion; import org.sonar.server.qualitygate.QualityGateConditionsValidator; +import org.sonar.server.rule.ActiveRuleService; import org.sonar.server.rule.RuleDescriptionFormatter; import org.sonar.server.setting.SettingsChangeNotifier; import org.sonar.server.user.SystemPasscode; import org.sonar.server.user.UserSession; +import org.sonar.server.v2.api.analysis.controller.ActiveRulesController; +import org.sonar.server.v2.api.analysis.controller.DefaultActiveRulesController; import org.sonar.server.v2.api.analysis.controller.DefaultJresController; import org.sonar.server.v2.api.analysis.controller.DefaultScannerEngineController; import org.sonar.server.v2.api.analysis.controller.DefaultVersionController; import org.sonar.server.v2.api.analysis.controller.JresController; import org.sonar.server.v2.api.analysis.controller.ScannerEngineController; import org.sonar.server.v2.api.analysis.controller.VersionController; +import org.sonar.server.v2.api.analysis.service.ActiveRulesHandler; +import org.sonar.server.v2.api.analysis.service.ActiveRulesHandlerImpl; import org.sonar.server.v2.api.analysis.service.JresHandler; import org.sonar.server.v2.api.analysis.service.JresHandlerImpl; import org.sonar.server.v2.api.analysis.service.ScannerEngineHandler; @@ -227,4 +232,19 @@ public class PlatformLevel4WebConfig { return new DefaultModeController(userSession, dbClient, configuration, settingsChangeNotifier, notificationManager, qualityGateConditionsValidator); } + @Bean + public ActiveRuleService activeRuleService(DbClient dbClient, Languages languages) { + return new ActiveRuleService(dbClient, languages); + } + + @Bean + public ActiveRulesHandler activeRulesHandler(DbClient dbClient, ActiveRuleService activeRuleService) { + return new ActiveRulesHandlerImpl(dbClient, activeRuleService); + } + + @Bean + public ActiveRulesController activeRulesController(ActiveRulesHandler activeRulesHandler) { + return new DefaultActiveRulesController(activeRulesHandler); + } + } diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/analysis/controller/DefaultActiveRulesControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/analysis/controller/DefaultActiveRulesControllerTest.java new file mode 100644 index 00000000000..c35ecfda28a --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/analysis/controller/DefaultActiveRulesControllerTest.java @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.analysis.controller; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; +import org.sonar.server.rule.ActiveRuleRestReponse; +import org.sonar.server.v2.api.analysis.service.ActiveRulesHandler; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.server.v2.WebApiEndpoints.ACTIVE_RULES_ENDPOINT; +import static org.sonar.server.v2.api.ControllerTester.getMockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class DefaultActiveRulesControllerTest { + + private final ActiveRulesHandler handler = mock(ActiveRulesHandler.class); + + private final MockMvc mockMvc = getMockMvc(new DefaultActiveRulesController(handler)); + + @Test + void getActiveRules_shouldReturnActiveRulesAsJson() throws Exception { + var minimalAr = new ActiveRuleRestReponse.ActiveRule( + new ActiveRuleRestReponse.RuleKey("xoo", "rule1"), + "Rule 1", + "MAJOR", + "2024-12-09", + "2024-12-09", + null, + "java", + null, + "qProfileKey", + List.of(), + List.of(), + Map.of()); + var maximalAr = new ActiveRuleRestReponse.ActiveRule( + new ActiveRuleRestReponse.RuleKey("xoo", "rule1"), + "Rule 1", + "MAJOR", + "2024-12-09", + "2024-12-09", + "someInternalKey", + "java", + "templateKey", + "qProfileKey", + List.of(new ActiveRuleRestReponse.RuleKey("old", "rule")), + List.of(new ActiveRuleRestReponse.Param("key", "value")), + Map.of(SoftwareQuality.MAINTAINABILITY, Severity.HIGH)); + when(handler.getActiveRules("someKey")).thenReturn(List.of(minimalAr, maximalAr)); + + String expectedJson = """ + [ + { + "ruleKey":{"repository":"xoo","rule":"rule1"}, + "name":"Rule 1", + "severity":"MAJOR", + "createdAt":"2024-12-09", + "updatedAt":"2024-12-09", + "language":"java", + "qProfileKey":"qProfileKey" + }, + { + "ruleKey":{"repository":"xoo","rule":"rule1"}, + "name":"Rule 1","severity":"MAJOR", + "createdAt":"2024-12-09", + "updatedAt":"2024-12-09", + "internalKey":"someInternalKey", + "language":"java", + "templateRuleKey":"templateKey", + "qProfileKey":"qProfileKey", + "deprecatedKeys":[{"repository":"old","rule":"rule"}], + "params":[{"key":"key","value":"value"}], + "impacts":{"MAINTAINABILITY":"HIGH"}} + ] + """; + + mockMvc.perform(get(ACTIVE_RULES_ENDPOINT + "?projectKey=someKey")) + .andExpectAll( + status().isOk(), + content().json(expectedJson, true)); + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandlerImplTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandlerImplTest.java new file mode 100644 index 00000000000..26c8983195a --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/analysis/service/ActiveRulesHandlerImplTest.java @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.analysis.service; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.rule.ActiveRuleRestReponse; +import org.sonar.server.rule.ActiveRuleService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ActiveRulesHandlerImplTest { + + private final DbClient dbClient = mock(DbClient.class, RETURNS_DEEP_STUBS); + private final DbSession dbSession = mock(); + private final ActiveRuleService activeRuleService = mock(ActiveRuleService.class); + + @BeforeEach + void setup() { + when(dbClient.openSession(false)).thenReturn(dbSession); + } + + ActiveRulesHandlerImpl underTest = new ActiveRulesHandlerImpl(dbClient, activeRuleService); + + @Test + void getActiveRules_returns_default_quality_profile_for_unknown_project() { + var defaultActiveRule1 = mock(ActiveRuleRestReponse.ActiveRule.class); + when(activeRuleService.buildDefaultActiveRules()).thenReturn(List.of(defaultActiveRule1)); + + List<ActiveRuleRestReponse.ActiveRule> result = underTest.getActiveRules("unknown-project"); + + assertThat(result).containsExactly(defaultActiveRule1); + } + + @Test + void getActiveRules_returns_associated_quality_profile_for_known_project() { + when(dbClient.projectDao().selectProjectByKey(dbSession, "my-project")).thenReturn(Optional.of(new ProjectDto().setUuid("someProjectUuid"))); + var activeRule1 = mock(ActiveRuleRestReponse.ActiveRule.class); + when(activeRuleService.buildActiveRules("someProjectUuid")).thenReturn(List.of(activeRule1)); + + List<ActiveRuleRestReponse.ActiveRule> result = underTest.getActiveRules("my-project"); + + assertThat(result).containsExactly(activeRule1); + } + +} |