--- /dev/null
+/*
+ * 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.util;
+
+import java.util.Arrays;
+
+public class TypeValidationsTesting {
+ private TypeValidationsTesting() {
+ // utility class
+ }
+
+ public static TypeValidations newFullTypeValidations() {
+ return new TypeValidations(Arrays.asList(
+ new BooleanTypeValidation(),
+ new IntegerTypeValidation(),
+ new FloatTypeValidation(),
+ new StringTypeValidation(),
+ new StringListTypeValidation()
+ ));
+ }
+}
testImplementation project(':sonar-testing-harness')
testImplementation testFixtures(project(':server:sonar-db-dao'))
-
+ testImplementation testFixtures(project(':server:sonar-server-common'))
+ testImplementation testFixtures(project(':server:sonar-webserver-api'))
}
--- /dev/null
+/*
+ * 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.common.rule;
+
+import com.google.common.collect.Sets;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import org.assertj.core.api.Fail;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.CleanCodeAttribute;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.issue.ImpactDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleDto.Format;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.server.common.rule.service.NewCustomRule;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.rule.index.RuleIndex;
+import org.sonar.server.rule.index.RuleIndexer;
+import org.sonar.server.rule.index.RuleQuery;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.junit.Assert.fail;
+import static org.sonar.api.issue.impact.Severity.HIGH;
+import static org.sonar.api.issue.impact.Severity.LOW;
+import static org.sonar.api.issue.impact.Severity.MEDIUM;
+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.db.rule.RuleTesting.newCustomRule;
+import static org.sonar.db.rule.RuleTesting.newRule;
+import static org.sonar.server.util.TypeValidationsTesting.newFullTypeValidations;
+
+public class RuleCreatorIT {
+
+ private static final RuleKey CUSTOM_RULE_KEY = RuleKey.parse("java:CUSTOM_RULE");
+ private final System2 system2 = new TestSystem2().setNow(Instant.now().toEpochMilli());
+
+ @Rule
+ public DbTester dbTester = DbTester.create(system2);
+
+ @Rule
+ public EsTester es = EsTester.create();
+
+ private final RuleIndex ruleIndex = new RuleIndex(es.client(), system2);
+ private final RuleIndexer ruleIndexer = new RuleIndexer(es.client(), dbTester.getDbClient());
+ private final DbSession dbSession = dbTester.getSession();
+ private final UuidFactory uuidFactory = new SequenceUuidFactory();
+
+ private final RuleCreator underTest = new RuleCreator(system2, new RuleIndexer(es.client(), dbTester.getDbClient()), dbTester.getDbClient(), newFullTypeValidations(), uuidFactory);
+
+ @Test
+ public void create_custom_rule() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*"));
+ RuleKey customRuleKey = underTest.create(dbSession, newRule).getKey();
+
+ RuleDto rule = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
+ assertThat(rule).isNotNull();
+ assertThat(rule.getKey()).isEqualTo(CUSTOM_RULE_KEY);
+ assertThat(rule.getPluginKey()).isEqualTo("sonarjava");
+ assertThat(rule.getTemplateUuid()).isEqualTo(templateRule.getUuid());
+ assertThat(rule.getName()).isEqualTo("My custom");
+ assertThat(rule.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Some description");
+ assertThat(rule.getEnumType()).isEqualTo(RuleType.CODE_SMELL);
+ assertCleanCodeInformation(rule);
+ assertThat(rule.getSeverityString()).isEqualTo("MAJOR");
+ assertThat(rule.getStatus()).isEqualTo(RuleStatus.READY);
+ assertThat(rule.getLanguage()).isEqualTo("java");
+ assertThat(rule.getConfigKey()).isEqualTo("S001");
+ assertDefRemediation(rule);
+ assertThat(rule.getGapDescription()).isEqualTo("desc");
+ assertThat(rule.getTags()).containsOnly("usertag1", "usertag2");
+ assertThat(rule.getSystemTags()).containsOnly("tag1", "tag4");
+ assertThat(rule.getSecurityStandards()).containsOnly("owaspTop10:a1", "cwe:123");
+ assertThat(rule.isExternal()).isFalse();
+ assertThat(rule.isAdHoc()).isFalse();
+
+ List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
+ assertThat(params).hasSize(1);
+
+ RuleParamDto param = params.get(0);
+ // From template rule
+ assertThat(param.getName()).isEqualTo("regex");
+ assertThat(param.getDescription()).isEqualTo("Reg ex");
+ assertThat(param.getType()).isEqualTo("STRING");
+ // From user
+ assertThat(param.getDefaultValue()).isEqualTo("a.*");
+
+ assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule.getUuid(), templateRule.getUuid());
+ }
+
+ private static void assertCleanCodeInformation(RuleDto rule) {
+ assertThat(rule.getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.CONVENTIONAL);
+ assertThat(rule.getDefaultImpacts()).extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity)
+ .containsExactly(tuple(MAINTAINABILITY, MEDIUM));
+ }
+
+ private static void assertDefRemediation(RuleDto rule) {
+ assertThat(rule.getDefRemediationFunction()).isEqualTo("LINEAR_OFFSET");
+ assertThat(rule.getDefRemediationGapMultiplier()).isEqualTo("1h");
+ assertThat(rule.getDefRemediationBaseEffort()).isEqualTo("5min");
+ }
+
+ @Test
+ public void create_shouldSetCleanCodeAttributeAndImpacts() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setStatus(RuleStatus.READY)
+ .setCleanCodeAttribute(CleanCodeAttribute.MODULAR)
+ .setImpacts(List.of(
+ new NewCustomRule.Impact(RELIABILITY, HIGH),
+ new NewCustomRule.Impact(SECURITY, LOW)));
+ RuleKey customRuleKey = underTest.create(dbSession, newRule).getKey();
+
+ RuleDto rule = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
+ assertThat(rule).isNotNull();
+ assertThat(rule.getKey()).isEqualTo(CUSTOM_RULE_KEY);
+ assertThat(rule.getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.MODULAR);
+ assertThat(rule.getDefaultImpacts()).extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity)
+ .containsExactlyInAnyOrder(tuple(RELIABILITY, HIGH), tuple(SECURITY, LOW));
+ // Back-mapped from the impact
+ assertThat(rule.getType()).isEqualTo(RuleType.VULNERABILITY.getDbConstant());
+ assertThat(rule.getSeverityString()).isEqualTo(Severity.MINOR);
+ }
+
+ @Test
+ public void create_whenImpactsAndTypeAreSet_shouldFail() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setStatus(RuleStatus.READY)
+ .setType(RuleType.BUG)
+ .setImpacts(List.of(
+ new NewCustomRule.Impact(RELIABILITY, HIGH),
+ new NewCustomRule.Impact(SECURITY, LOW)));
+
+ assertThatThrownBy(() -> underTest.create(dbSession, newRule))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("The rule cannot have both impacts and type/severity specified");
+ }
+
+ @Test
+ public void create_whenImpactsAndSeverityAreSet_shouldFail() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setStatus(RuleStatus.READY)
+ .setSeverity(Severity.CRITICAL)
+ .setImpacts(List.of(
+ new NewCustomRule.Impact(RELIABILITY, HIGH),
+ new NewCustomRule.Impact(SECURITY, LOW)));
+
+ assertThatThrownBy(() -> underTest.create(dbSession, newRule))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("The rule cannot have both impacts and type/severity specified");
+ }
+
+ @Test
+ public void create_whenRuleAndTemplateHaveDifferentRepo_shouldFail() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(RuleKey.parse("web:CUSTOM_RULE"), templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setStatus(RuleStatus.READY);
+
+ assertThatThrownBy(() -> underTest.create(dbSession, newRule))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Custom and template keys must be in the same repository");
+ }
+
+ @Test
+ public void create_custom_rule_with_empty_parameter_value() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", ""));
+
+ RuleKey customRuleKey = underTest.create(dbSession, newRule).getKey();
+
+ List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
+ assertThat(params).hasSize(1);
+ RuleParamDto param = params.get(0);
+ assertThat(param.getName()).isEqualTo("regex");
+ assertThat(param.getDescription()).isEqualTo("Reg ex");
+ assertThat(param.getType()).isEqualTo("STRING");
+ assertThat(param.getDefaultValue()).isNull();
+ }
+
+ @Test
+ public void create_whenTypeIsHotspot_shouldNotComputeDefaultImpact() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setType(RuleType.SECURITY_HOTSPOT)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", ""));
+
+ RuleKey customRuleKey = underTest.create(dbSession, newRule).getKey();
+
+ RuleDto rule = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
+ assertThat(rule.getDefaultImpacts()).isEmpty();
+ }
+
+ @Test
+ public void create_custom_rule_with_no_parameter_value() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRuleWithIntArrayParam();
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY);
+
+ RuleKey customRuleKey = underTest.create(dbSession, newRule).getKey();
+
+ List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
+ assertThat(params).hasSize(1);
+ RuleParamDto param = params.get(0);
+ assertThat(param.getName()).isEqualTo("myIntegers");
+ assertThat(param.getDescription()).isEqualTo("My Integers");
+ assertThat(param.getType()).isEqualTo("INTEGER,multiple=true,values=1;2;3");
+ assertThat(param.getDefaultValue()).isNull();
+ }
+
+ @Test
+ public void create_custom_rule_with_multiple_parameter_values() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRuleWithIntArrayParam();
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("myIntegers", "1,3"));
+
+ RuleKey customRuleKey = underTest.create(dbSession, newRule).getKey();
+
+ List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
+ assertThat(params).hasSize(1);
+ RuleParamDto param = params.get(0);
+ assertThat(param.getName()).isEqualTo("myIntegers");
+ assertThat(param.getDescription()).isEqualTo("My Integers");
+ assertThat(param.getType()).isEqualTo("INTEGER,multiple=true,values=1;2;3");
+ assertThat(param.getDefaultValue()).isEqualTo("1,3");
+ }
+
+ @Test
+ public void batch_create_custom_rules() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRuleWithIntArrayParam();
+
+ NewCustomRule firstRule = NewCustomRule.createForCustomRule(RuleKey.parse("java:CUSTOM_RULE_1"), templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY);
+
+ NewCustomRule secondRule = NewCustomRule.createForCustomRule(RuleKey.parse("java:CUSTOM_RULE_2"), templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY);
+
+ List<RuleKey> customRuleKeys = underTest.create(dbSession, Arrays.asList(firstRule, secondRule))
+ .stream()
+ .map(RuleDto::getKey)
+ .toList();
+
+ List<RuleDto> rules = dbTester.getDbClient().ruleDao().selectByKeys(dbSession, customRuleKeys);
+
+ assertThat(rules).hasSize(2);
+ assertThat(rules).asList()
+ .extracting("ruleKey")
+ .containsOnly("CUSTOM_RULE_1", "CUSTOM_RULE_2");
+ }
+
+ @Test
+ public void fail_to_create_custom_rules_when_wrong_rule_template() {
+ // insert rule
+ RuleDto rule = newRule(RuleKey.of("java", "S001")).setIsTemplate(false);
+ dbTester.rules().insert(rule);
+ dbSession.commit();
+
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, rule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*"));
+
+ assertThatThrownBy(() -> underTest.create(dbSession, singletonList(newRule)))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("This rule is not a template rule: java:S001");
+ }
+
+ @Test
+ public void fail_to_create_custom_rules_when_removed_rule_template() {
+ // insert rule
+ RuleDto rule = createTemplateRule();
+ newRule(RuleKey.of("java", "S001")).setIsTemplate(false);
+ rule.setStatus(RuleStatus.REMOVED);
+ dbTester.rules().update(rule);
+ dbSession.commit();
+
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, rule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY);
+
+ List<NewCustomRule> newRules = singletonList(newRule);
+ assertThatThrownBy(() -> underTest.create(dbSession, newRules))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("The template key doesn't exist: java:S001");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_with_invalid_parameter() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRuleWithIntArrayParam();
+
+ assertThatThrownBy(() -> {
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("myIntegers", "1,polop,2"));
+ underTest.create(dbSession, newRule);
+ })
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Value 'polop' must be an integer.");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_with_invalid_parameters() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRuleWithTwoIntParams();
+
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("first", "polop", "second", "palap"));
+ try {
+ underTest.create(dbSession, newRule);
+ Fail.failBecauseExceptionWasNotThrown(BadRequestException.class);
+ } catch (BadRequestException badRequest) {
+ assertThat(badRequest.errors().toString()).contains("palap").contains("polop");
+ }
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_with_empty_description() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRuleWithTwoIntParams();
+
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setMarkdownDescription("");
+ assertThatExceptionOfType(BadRequestException.class)
+ .isThrownBy(() -> underTest.create(dbSession, newRule))
+ .withMessage("The description is missing");
+ }
+
+ @Test
+ public void reactivate_custom_rule_if_already_exists_in_removed_status() {
+ RuleDto templateRule = createTemplateRule();
+
+ RuleDto rule = newCustomRule(templateRule, "Old description")
+ .setRuleKey(CUSTOM_RULE_KEY)
+ .setStatus(RuleStatus.REMOVED)
+ .setName("Old name")
+ .setDescriptionFormat(Format.MARKDOWN)
+ .setSeverity(Severity.INFO);
+ dbTester.rules().insert(rule);
+ dbTester.rules().insertRuleParam(rule, param -> param.setDefaultValue("a.*"));
+ dbSession.commit();
+
+ // Create custom rule with same key, but with different values
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("New name")
+ .setMarkdownDescription("New description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "c.*"));
+ RuleKey customRuleKey = underTest.create(dbSession, newRule).getKey();
+
+ RuleDto result = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
+ assertThat(result.getKey()).isEqualTo(CUSTOM_RULE_KEY);
+ assertThat(result.getStatus()).isEqualTo(RuleStatus.READY);
+
+ // These values should be the same than before
+ assertThat(result.getName()).isEqualTo("Old name");
+ assertThat(result.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Old description");
+ assertThat(result.getSeverityString()).isEqualTo(Severity.INFO);
+
+ List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
+ assertThat(params).hasSize(1);
+ assertThat(params.get(0).getDefaultValue()).isEqualTo("a.*");
+ }
+
+ @Test
+ public void generate_reactivation_exception_when_rule_exists_in_removed_status_and_prevent_reactivation_parameter_is_true() {
+ RuleDto templateRule = createTemplateRule();
+
+ RuleDto rule = newCustomRule(templateRule, "Old description")
+ .setRuleKey(CUSTOM_RULE_KEY)
+ .setStatus(RuleStatus.REMOVED)
+ .setName("Old name")
+ .setSeverity(Severity.INFO);
+ dbTester.rules().insert(rule);
+ dbTester.rules().insertRuleParam(rule, param -> param.setDefaultValue("a.*"));
+ dbSession.commit();
+
+ // Create custom rule with same key, but with different values
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("New name")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "c.*"))
+ .setPreventReactivation(true);
+
+ try {
+ underTest.create(dbSession, newRule);
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(ReactivationException.class);
+ ReactivationException reactivationException = (ReactivationException) e;
+ assertThat(reactivationException.ruleKey()).isEqualTo(rule.getKey());
+ }
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_invalid_key() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(RuleKey.of("java", "*INVALID*"), templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*"));
+
+ assertThatThrownBy(() -> underTest.create(dbSession, newRule))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("The rule key \"*INVALID*\" is invalid, it should only contain: a-z, 0-9, \"_\"");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_rule_key_already_exists() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+ // Create a custom rule
+ AtomicReference<NewCustomRule> newRule = new AtomicReference<>(NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*")));
+ underTest.create(dbSession, newRule.get());
+
+ // Create another custom rule having same key
+ newRule.set(NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My another custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*")));
+
+ assertThatThrownBy(() -> underTest.create(dbSession, newRule.get()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("A rule with the key 'CUSTOM_RULE' already exists");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_missing_name() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*"));
+
+ assertThatThrownBy(() -> underTest.create(dbSession, newRule))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("The name is missing");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_missing_description() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+
+ assertThatThrownBy(() -> {
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*"));
+ underTest.create(dbSession, newRule);
+ })
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("The description is missing");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_invalid_severity() {
+ // insert template rule
+ RuleDto templateRule = createTemplateRule();
+
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity("INVALID")
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*"));
+
+ assertThatThrownBy(() -> underTest.create(dbSession, newRule))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Severity \"INVALID\" is invalid");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_wrong_rule_template() {
+ // insert rule
+ RuleDto rule = newRule(RuleKey.of("java", "S001")).setIsTemplate(false);
+ dbTester.rules().insert(rule);
+ dbSession.commit();
+
+ // Create custom rule with unknown template rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, rule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setParameters(Map.of("regex", "a.*"));
+
+ assertThatThrownBy(() -> underTest.create(dbSession, newRule))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("This rule is not a template rule: java:S001");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_null_custom_key() {
+ assertThatThrownBy(() -> NewCustomRule.createForCustomRule(null, CUSTOM_RULE_KEY))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Custom key should be set");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_null_template() {
+ assertThatThrownBy(() -> NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Template key should be set");
+ }
+
+ @Test
+ public void fail_to_create_custom_rule_when_unknown_template() {
+ assertThatThrownBy(() -> {
+ // Create custom rule
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(CUSTOM_RULE_KEY, RuleKey.of("java", "S001"))
+ .setName("My custom")
+ .setMarkdownDescription("Some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY);
+ underTest.create(dbSession, newRule);
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("The template key doesn't exist: java:S001");
+ }
+
+ @Test
+ public void create_givenSecurityHotspotRule_doNotSetCleanCodeAttribute() {
+ RuleDto templateRule = createTemplateRule();
+
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(RuleKey.parse("java:security_hotspots_rule"), templateRule.getKey())
+ .setName("My custom")
+ .setMarkdownDescription("some description")
+ .setSeverity(Severity.MAJOR)
+ .setStatus(RuleStatus.READY)
+ .setType(RuleType.SECURITY_HOTSPOT);
+
+ RuleKey customRuleKey = underTest.create(dbSession, newRule).getKey();
+
+ RuleDto result = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
+
+ assertThat(result.getCleanCodeAttribute()).isNull();
+ }
+
+ private RuleDto createTemplateRule() {
+ RuleDto templateRule = RuleTesting.newRule(RuleKey.of("java", "S001"))
+ .setIsTemplate(true)
+ .setLanguage("java")
+ .setPluginKey("sonarjava")
+ .setConfigKey("S001")
+ .setDefRemediationFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
+ .setDefRemediationGapMultiplier("1h")
+ .setDefRemediationBaseEffort("5min")
+ .setGapDescription("desc")
+ .setTags(Sets.newHashSet("usertag1", "usertag2"))
+ .setSystemTags(Sets.newHashSet("tag1", "tag4"))
+ .setSecurityStandards(Sets.newHashSet("owaspTop10:a1", "cwe:123"))
+ .setCreatedAt(new Date().getTime())
+ .setUpdatedAt(new Date().getTime());
+ dbTester.rules().insert(templateRule);
+ dbTester.rules().insertRuleParam(templateRule, param -> param.setName("regex").setType("STRING").setDescription("Reg ex").setDefaultValue(".*"));
+ ruleIndexer.commitAndIndex(dbTester.getSession(), templateRule.getUuid());
+ return templateRule;
+ }
+
+ private RuleDto createTemplateRuleWithIntArrayParam() {
+ RuleDto templateRule = newRule(RuleKey.of("java", "S002"))
+ .setIsTemplate(true)
+ .setLanguage("java")
+ .setConfigKey("S002")
+ .setDefRemediationFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
+ .setDefRemediationGapMultiplier("1h")
+ .setDefRemediationBaseEffort("5min")
+ .setGapDescription("desc")
+ .setCreatedAt(new Date().getTime())
+ .setUpdatedAt(new Date().getTime());
+ dbTester.rules().insert(templateRule);
+ dbTester.rules().insertRuleParam(templateRule,
+ param -> param.setName("myIntegers").setType("INTEGER,multiple=true,values=1;2;3").setDescription("My Integers").setDefaultValue("1"));
+ ruleIndexer.commitAndIndex(dbTester.getSession(), templateRule.getUuid());
+ return templateRule;
+ }
+
+ private RuleDto createTemplateRuleWithTwoIntParams() {
+ RuleDto templateRule = newRule(RuleKey.of("java", "S003"))
+ .setIsTemplate(true)
+ .setLanguage("java")
+ .setConfigKey("S003")
+ .setDefRemediationFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
+ .setDefRemediationGapMultiplier("1h")
+ .setDefRemediationBaseEffort("5min")
+ .setGapDescription("desc")
+ .setCreatedAt(new Date().getTime())
+ .setUpdatedAt(new Date().getTime());
+ dbTester.rules().insert(templateRule);
+ dbTester.rules().insertRuleParam(templateRule, param -> param.setName("first").setType("INTEGER").setDescription("First integer").setDefaultValue("0"));
+ dbTester.rules().insertRuleParam(templateRule, param -> param.setName("second").setType("INTEGER").setDescription("Second integer").setDefaultValue("0"));
+ return templateRule;
+ }
+
+}
--- /dev/null
+/*
+ * 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.common.rule;
+
+import org.sonar.api.rule.RuleKey;
+
+public class ReactivationException extends RuntimeException {
+
+ private RuleKey ruleKey;
+
+ public ReactivationException(String s, RuleKey ruleKey) {
+ super(s);
+ this.ruleKey = ruleKey;
+ }
+
+ public RuleKey ruleKey() {
+ return ruleKey;
+ }
+}
--- /dev/null
+/*
+ * 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.common.rule;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.issue.impact.SoftwareQuality;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.CleanCodeAttribute;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.server.rule.RuleParamType;
+import org.sonar.api.server.rule.internal.ImpactMapper;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.issue.ImpactDto;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleDto.Format;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.common.rule.service.NewCustomRule;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.rule.index.RuleIndexer;
+import org.sonar.server.util.TypeValidations;
+import org.springframework.util.CollectionUtils;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+@ServerSide
+public class RuleCreator {
+ private static final String TEMPLATE_KEY_NOT_EXIST_FORMAT = "The template key doesn't exist: %s";
+ private static final Pattern RULE_KEY_REGEX = Pattern.compile("^[\\w]+$");
+
+ private final System2 system2;
+ private final RuleIndexer ruleIndexer;
+ private final DbClient dbClient;
+ private final TypeValidations typeValidations;
+ private final UuidFactory uuidFactory;
+
+ public RuleCreator(System2 system2, RuleIndexer ruleIndexer, DbClient dbClient, TypeValidations typeValidations, UuidFactory uuidFactory) {
+ this.system2 = system2;
+ this.ruleIndexer = ruleIndexer;
+ this.dbClient = dbClient;
+ this.typeValidations = typeValidations;
+ this.uuidFactory = uuidFactory;
+ }
+
+ public RuleDto create(DbSession dbSession, NewCustomRule newRule) {
+ RuleKey templateKey = newRule.templateKey();
+ RuleDto templateRule = dbClient.ruleDao().selectByKey(dbSession, templateKey)
+ .orElseThrow(() -> new IllegalArgumentException(format(TEMPLATE_KEY_NOT_EXIST_FORMAT, templateKey)));
+ checkArgument(templateRule.isTemplate(), "This rule is not a template rule: %s", templateKey.toString());
+ checkArgument(templateRule.getStatus() != RuleStatus.REMOVED, TEMPLATE_KEY_NOT_EXIST_FORMAT, templateKey.toString());
+ validateCustomRule(newRule, dbSession, templateKey);
+
+ Optional<RuleDto> definition = loadRule(dbSession, newRule.ruleKey());
+ RuleDto ruleDto = definition.map(d -> updateExistingRule(d, newRule, dbSession))
+ .orElseGet(() -> createCustomRule(newRule, templateRule, dbSession));
+
+ ruleIndexer.commitAndIndex(dbSession, ruleDto.getUuid());
+ return ruleDto;
+ }
+
+ public List<RuleDto> create(DbSession dbSession, List<NewCustomRule> newRules) {
+ Set<RuleKey> templateKeys = newRules.stream().map(NewCustomRule::templateKey).collect(Collectors.toSet());
+ Map<RuleKey, RuleDto> templateRules = dbClient.ruleDao().selectByKeys(dbSession, templateKeys)
+ .stream()
+ .collect(Collectors.toMap(
+ RuleDto::getKey,
+ Function.identity()));
+
+ checkArgument(!templateRules.isEmpty() && templateKeys.size() == templateRules.size(), "Rule template keys should exists for each custom rule!");
+ templateRules.values().forEach(ruleDto -> {
+ checkArgument(ruleDto.isTemplate(), "This rule is not a template rule: %s", ruleDto.getKey().toString());
+ checkArgument(ruleDto.getStatus() != RuleStatus.REMOVED, TEMPLATE_KEY_NOT_EXIST_FORMAT, ruleDto.getKey().toString());
+ });
+
+ List<RuleDto> customRules = newRules.stream()
+ .map(newCustomRule -> {
+ RuleDto templateRule = templateRules.get(newCustomRule.templateKey());
+ validateCustomRule(newCustomRule, dbSession, templateRule.getKey());
+ return createCustomRule(newCustomRule, templateRule, dbSession);
+ })
+ .toList();
+
+ ruleIndexer.commitAndIndex(dbSession, customRules.stream().map(RuleDto::getUuid).toList());
+ return customRules;
+ }
+
+ private void validateCustomRule(NewCustomRule newRule, DbSession dbSession, RuleKey templateKey) {
+ List<String> errors = new ArrayList<>();
+
+ validateRuleKey(errors, newRule.ruleKey(), templateKey);
+ validateName(errors, newRule);
+ validateDescription(errors, newRule);
+
+ String severity = newRule.severity();
+ if (severity != null && !Severity.ALL.contains(severity)) {
+ errors.add(format("Severity \"%s\" is invalid", severity));
+ }
+ if (!CollectionUtils.isEmpty(newRule.getImpacts()) && (StringUtils.isNotBlank(newRule.severity()) || newRule.type() != null)) {
+ errors.add("The rule cannot have both impacts and type/severity specified");
+ }
+
+ for (RuleParamDto ruleParam : dbClient.ruleDao().selectRuleParamsByRuleKey(dbSession, templateKey)) {
+ try {
+ validateParam(ruleParam, newRule.parameter(ruleParam.getName()));
+ } catch (BadRequestException validationError) {
+ errors.addAll(validationError.errors());
+ }
+ }
+ checkRequest(errors.isEmpty(), errors);
+ }
+
+ private void validateParam(RuleParamDto ruleParam, @Nullable String value) {
+ if (value != null) {
+ RuleParamType ruleParamType = RuleParamType.parse(ruleParam.getType());
+ if (ruleParamType.multiple()) {
+ List<String> values = newArrayList(Splitter.on(",").split(value));
+ typeValidations.validate(values, ruleParamType.type(), ruleParamType.values());
+ } else {
+ typeValidations.validate(value, ruleParamType.type(), ruleParamType.values());
+ }
+ }
+ }
+
+ private static void validateName(List<String> errors, NewCustomRule newRule) {
+ if (Strings.isNullOrEmpty(newRule.name())) {
+ errors.add("The name is missing");
+ }
+ }
+
+ private static void validateDescription(List<String> errors, NewCustomRule newRule) {
+ if (Strings.isNullOrEmpty(newRule.markdownDescription())) {
+ errors.add("The description is missing");
+ }
+ }
+
+ private static void validateRuleKey(List<String> errors, RuleKey ruleKey, RuleKey templateKey) {
+ if (!ruleKey.repository().equals(templateKey.repository())) {
+ errors.add("Custom and template keys must be in the same repository");
+ }
+ if (!RULE_KEY_REGEX.matcher(ruleKey.rule()).matches()) {
+ errors.add(format("The rule key \"%s\" is invalid, it should only contain: a-z, 0-9, \"_\"", ruleKey.rule()));
+ }
+ }
+
+ private Optional<RuleDto> loadRule(DbSession dbSession, RuleKey ruleKey) {
+ return dbClient.ruleDao().selectByKey(dbSession, ruleKey);
+ }
+
+ private RuleDto createCustomRule(NewCustomRule newRule, RuleDto templateRuleDto, DbSession dbSession) {
+ RuleDescriptionSectionDto ruleDescriptionSectionDto = createDefaultRuleDescriptionSection(uuidFactory.create(), requireNonNull(newRule.markdownDescription()));
+
+ RuleDto ruleDto = new RuleDto()
+ .setUuid(uuidFactory.create())
+ .setRuleKey(newRule.ruleKey())
+ .setPluginKey(templateRuleDto.getPluginKey())
+ .setTemplateUuid(templateRuleDto.getUuid())
+ .setConfigKey(templateRuleDto.getConfigKey())
+ .setName(newRule.name())
+ .setStatus(ofNullable(newRule.status()).orElse(RuleStatus.READY))
+ .setLanguage(templateRuleDto.getLanguage())
+ .setDefRemediationFunction(templateRuleDto.getDefRemediationFunction())
+ .setDefRemediationGapMultiplier(templateRuleDto.getDefRemediationGapMultiplier())
+ .setDefRemediationBaseEffort(templateRuleDto.getDefRemediationBaseEffort())
+ .setGapDescription(templateRuleDto.getGapDescription())
+ .setScope(templateRuleDto.getScope())
+ .setSystemTags(templateRuleDto.getSystemTags())
+ .setSecurityStandards(templateRuleDto.getSecurityStandards())
+ .setIsExternal(false)
+ .setIsAdHoc(false)
+ .setCreatedAt(system2.now())
+ .setUpdatedAt(system2.now())
+ .setDescriptionFormat(Format.MARKDOWN)
+ .addRuleDescriptionSectionDto(ruleDescriptionSectionDto);
+
+ setCleanCodeAttributeAndImpacts(newRule, ruleDto, templateRuleDto);
+
+ Set<String> tags = templateRuleDto.getTags();
+ if (!tags.isEmpty()) {
+ ruleDto.setTags(tags);
+ }
+ dbClient.ruleDao().insert(dbSession, ruleDto);
+
+ for (RuleParamDto templateRuleParamDto : dbClient.ruleDao().selectRuleParamsByRuleKey(dbSession, templateRuleDto.getKey())) {
+ String customRuleParamValue = Strings.emptyToNull(newRule.parameter(templateRuleParamDto.getName()));
+ createCustomRuleParams(customRuleParamValue, ruleDto, templateRuleParamDto, dbSession);
+ }
+ return ruleDto;
+ }
+
+ private void setCleanCodeAttributeAndImpacts(NewCustomRule newRule, RuleDto ruleDto, RuleDto templateRuleDto) {
+ int type = newRule.type() == null ? templateRuleDto.getType() : newRule.type().getDbConstant();
+ String severity = ofNullable(newRule.severity()).orElse(Severity.MAJOR);
+
+ if (type == RuleType.SECURITY_HOTSPOT.getDbConstant()) {
+ ruleDto.setType(type).setSeverity(severity);
+ } else {
+ ruleDto.setCleanCodeAttribute(ofNullable(newRule.getCleanCodeAttribute()).orElse(CleanCodeAttribute.CONVENTIONAL));
+
+ if (!CollectionUtils.isEmpty(newRule.getImpacts())) {
+ newRule.getImpacts().stream()
+ .map(impact -> new ImpactDto(uuidFactory.create(), impact.softwareQuality(), impact.severity()))
+ .forEach(ruleDto::addDefaultImpact);
+ // Back-map old type and severity from the impact
+ Map.Entry<SoftwareQuality, org.sonar.api.issue.impact.Severity> impact = ImpactMapper.getBestImpactForBackmapping(
+ newRule.getImpacts().stream().collect(Collectors.toMap(NewCustomRule.Impact::softwareQuality, NewCustomRule.Impact::severity)));
+ ruleDto.setType(ImpactMapper.convertToRuleType(impact.getKey()).getDbConstant());
+ ruleDto.setSeverity(ImpactMapper.convertToDeprecatedSeverity(impact.getValue()));
+ } else {
+ // Map old type and severity to impact
+ SoftwareQuality softwareQuality = ImpactMapper.convertToSoftwareQuality(RuleType.valueOf(type));
+ org.sonar.api.issue.impact.Severity impactSeverity = ImpactMapper.convertToImpactSeverity(severity);
+ ruleDto.addDefaultImpact(new ImpactDto().setUuid(uuidFactory.create())
+ .setSoftwareQuality(softwareQuality)
+ .setSeverity(impactSeverity))
+ .setType(type)
+ .setSeverity(severity);
+ }
+ }
+ }
+
+ private void createCustomRuleParams(@Nullable String paramValue, RuleDto ruleDto, RuleParamDto templateRuleParam, DbSession dbSession) {
+ RuleParamDto ruleParamDto = RuleParamDto.createFor(ruleDto)
+ .setName(templateRuleParam.getName())
+ .setType(templateRuleParam.getType())
+ .setDescription(templateRuleParam.getDescription())
+ .setDefaultValue(paramValue);
+ dbClient.ruleDao().insertRuleParam(dbSession, ruleDto, ruleParamDto);
+ }
+
+ private RuleDto updateExistingRule(RuleDto ruleDto, NewCustomRule newRule, DbSession dbSession) {
+ if (ruleDto.getStatus().equals(RuleStatus.REMOVED)) {
+ if (newRule.isPreventReactivation()) {
+ throw new ReactivationException(format("A removed rule with the key '%s' already exists", ruleDto.getKey().rule()), ruleDto.getKey());
+ } else {
+ ruleDto.setStatus(RuleStatus.READY)
+ .setUpdatedAt(system2.now());
+ dbClient.ruleDao().update(dbSession, ruleDto);
+ }
+ } else {
+ throw new IllegalArgumentException(format("A rule with the key '%s' already exists", ruleDto.getKey().rule()));
+ }
+ return ruleDto;
+ }
+
+}
--- /dev/null
+/*
+ * 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.common.rule;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+++ /dev/null
-/*
- * 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.common.rule.service;
-
-public record CreateRuleRequest() {
-}
--- /dev/null
+/*
+ * 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.common.rule.service;
+
+import com.google.common.base.Preconditions;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.issue.impact.Severity;
+import org.sonar.api.issue.impact.SoftwareQuality;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.CleanCodeAttribute;
+import org.sonar.api.rules.RuleType;
+import org.sonar.server.common.rule.ReactivationException;
+
+public class NewCustomRule {
+
+ private RuleKey ruleKey;
+ private RuleKey templateKey;
+ private String name;
+ private String markdownDescription;
+ private String severity;
+ private RuleStatus status;
+ private RuleType type;
+ private Map<String, String> parameters = new HashMap<>();
+ private CleanCodeAttribute cleanCodeAttribute;
+ private List<Impact> impacts;
+ private boolean preventReactivation = false;
+
+ private NewCustomRule() {
+ // No direct call to constructor
+ }
+
+ public RuleKey ruleKey() {
+ return ruleKey;
+ }
+
+ public RuleKey templateKey() {
+ return templateKey;
+ }
+
+ @CheckForNull
+ public String name() {
+ return name;
+ }
+
+ public NewCustomRule setName(@Nullable String name) {
+ this.name = name;
+ return this;
+ }
+
+ @CheckForNull
+ public String markdownDescription() {
+ return markdownDescription;
+ }
+
+ public NewCustomRule setMarkdownDescription(@Nullable String markdownDescription) {
+ this.markdownDescription = markdownDescription;
+ return this;
+ }
+
+ @CheckForNull
+ public String severity() {
+ return severity;
+ }
+
+ @Deprecated(since = "10.4")
+ public NewCustomRule setSeverity(@Nullable String severity) {
+ this.severity = severity;
+ return this;
+ }
+
+ @CheckForNull
+ public RuleStatus status() {
+ return status;
+ }
+
+ public NewCustomRule setStatus(@Nullable RuleStatus status) {
+ this.status = status;
+ return this;
+ }
+
+ @CheckForNull
+ public RuleType type() {
+ return type;
+ }
+
+ @Deprecated(since = "10.4")
+ public NewCustomRule setType(@Nullable RuleType type) {
+ this.type = type;
+ return this;
+ }
+
+ @CheckForNull
+ public String parameter(final String paramKey) {
+ return parameters.get(paramKey);
+ }
+
+ public NewCustomRule setParameters(Map<String, String> params) {
+ this.parameters = params;
+ return this;
+ }
+
+ public CleanCodeAttribute getCleanCodeAttribute() {
+ return cleanCodeAttribute;
+ }
+
+ public NewCustomRule setCleanCodeAttribute(@Nullable CleanCodeAttribute cleanCodeAttribute) {
+ this.cleanCodeAttribute = cleanCodeAttribute;
+ return this;
+ }
+
+ public List<Impact> getImpacts() {
+ return impacts;
+ }
+
+ public NewCustomRule setImpacts(List<Impact> impacts) {
+ this.impacts = impacts;
+ return this;
+ }
+
+ public boolean isPreventReactivation() {
+ return preventReactivation;
+ }
+
+ /**
+ * When true, if the rule already exists in status REMOVED, an {@link ReactivationException} will be thrown
+ */
+ public NewCustomRule setPreventReactivation(boolean preventReactivation) {
+ this.preventReactivation = preventReactivation;
+ return this;
+ }
+
+ public static NewCustomRule createForCustomRule(RuleKey customKey, RuleKey templateKey) {
+ Preconditions.checkArgument(customKey != null, "Custom key should be set");
+ Preconditions.checkArgument(templateKey != null, "Template key should be set");
+ NewCustomRule newRule = new NewCustomRule();
+ newRule.ruleKey = customKey;
+ newRule.templateKey = templateKey;
+ return newRule;
+ }
+
+ public record Impact(SoftwareQuality softwareQuality, Severity severity) {
+ }
+}
*/
package org.sonar.server.common.rule.service;
+import java.util.ArrayList;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.common.rule.RuleCreator;
+
public class RuleService {
- public RuleInformation create(CreateRuleRequest request) {
- return null;
+ private final DbClient dbClient;
+ private final RuleCreator ruleCreator;
+
+ public RuleService(DbClient dbClient, RuleCreator ruleCreator) {
+ this.dbClient = dbClient;
+ this.ruleCreator = ruleCreator;
}
+ public RuleInformation createCustomRule(NewCustomRule newCustomRule) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ return createCustomRule(newCustomRule, dbSession);
+ }
+ }
+
+ public RuleInformation createCustomRule(NewCustomRule newCustomRule, DbSession dbSession) {
+ return new RuleInformation(ruleCreator.create(dbSession, newCustomRule), new ArrayList<>());
+ }
}
*/
package org.sonar.server.v2.api.rule.controller;
+import java.util.HashMap;
+import java.util.Map;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.server.common.rule.ReactivationException;
+import org.sonar.server.common.rule.service.NewCustomRule;
import org.sonar.server.common.rule.service.RuleInformation;
import org.sonar.server.common.rule.service.RuleService;
import org.sonar.server.user.UserSession;
import org.sonar.server.v2.api.rule.converter.RuleRestResponseGenerator;
+import org.sonar.server.v2.api.rule.request.Impact;
import org.sonar.server.v2.api.rule.request.RuleCreateRestRequest;
import org.sonar.server.v2.api.rule.response.RuleRestResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ResponseStatusException;
+
+import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
public class DefaultRuleController implements RuleController {
private final UserSession userSession;
public DefaultRuleController(UserSession userSession, RuleService ruleService, RuleRestResponseGenerator ruleRestResponseGenerator) {
this.userSession = userSession;
-
this.ruleService = ruleService;
this.ruleRestResponseGenerator = ruleRestResponseGenerator;
}
@Override
public RuleRestResponse create(RuleCreateRestRequest request) {
+ userSession
+ .checkLoggedIn()
+ .checkPermission(ADMINISTER_QUALITY_PROFILES);
+ try {
+ RuleInformation ruleInformation = ruleService.createCustomRule(toNewCustomRule(request));
+ return ruleRestResponseGenerator.toRuleRestResponse(ruleInformation);
+ } catch (ReactivationException e) {
+ throw new ResponseStatusException(HttpStatus.CONFLICT, e.getMessage(), e);
+ }
+ }
+ private static NewCustomRule toNewCustomRule(RuleCreateRestRequest request) {
+ NewCustomRule newCustomRule = NewCustomRule.createForCustomRule(RuleKey.parse(request.key()), RuleKey.parse(request.templateKey()))
+ .setName(request.name())
+ .setMarkdownDescription(request.markdownDescription())
+ .setStatus(request.status())
+ .setCleanCodeAttribute(request.cleanCodeAttribute())
+ .setImpacts(request.impacts().stream().map(DefaultRuleController::toNewCustomRuleImpact).toList())
+ .setPreventReactivation(true);
+ if (request.parameters() != null) {
+ Map<String, String> params = new HashMap<>();
+ request.parameters().forEach(p -> params.put(p.key(), p.defaultValue()));
+ newCustomRule.setParameters(params);
+ }
+ return newCustomRule;
+ }
- RuleInformation ruleInformation = ruleService.create(null);
- return ruleRestResponseGenerator.toRuleRestResponse(ruleInformation);
+ private static NewCustomRule.Impact toNewCustomRuleImpact(Impact impact) {
+ return new NewCustomRule.Impact(impact.softwareQuality(), impact.severity());
}
}
package org.sonar.server.v2.api.rule.controller;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import javax.validation.Valid;
import org.sonar.server.v2.api.rule.request.RuleCreateRestRequest;
import org.sonar.server.v2.api.rule.response.RuleRestResponse;
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
- @Operation(summary = "Create a rule", description = """
- Create a rule
- """)
- //TODO Document
+ @Operation(summary = "Custom rule creation", description = """
+ Create a custom rule.
+ Requires the 'Administer Quality Profiles' permission.
+ """,
+ extensions = @Extension(properties = {@ExtensionProperty(name = "internal", value = "true")}))
RuleRestResponse create(@Valid @RequestBody RuleCreateRestRequest request);
-
}
import org.sonar.server.v2.api.rule.response.RuleDescriptionSectionContextRestResponse;
import org.sonar.server.v2.api.rule.response.RuleDescriptionSectionRestResponse;
import org.sonar.server.v2.api.rule.response.RuleImpactRestResponse;
-import org.sonar.server.v2.api.rule.response.RuleParameterRestResponse;
+import org.sonar.server.v2.api.rule.ressource.Parameter;
import org.sonar.server.v2.api.rule.response.RuleRestResponse;
import static java.util.Optional.ofNullable;
});
}
- private static List<RuleParameterRestResponse> toRuleParameterResponse(List<RuleParamDto> ruleParamDtos) {
+ private static List<Parameter> toRuleParameterResponse(List<RuleParamDto> ruleParamDtos) {
return ruleParamDtos.stream()
- .map(p -> new RuleParameterRestResponse(p.getName(), Markdown.convertToHtml(p.getDescription()), p.getDefaultValue(), p.getType()))
+ .map(p -> new Parameter(p.getName(), Markdown.convertToHtml(p.getDescription()), p.getDefaultValue(), p.getType()))
.toList();
}
--- /dev/null
+/*
+ * 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.v2.api.rule.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.NotNull;
+import org.sonar.api.issue.impact.Severity;
+import org.sonar.api.issue.impact.SoftwareQuality;
+
+public record Impact(
+
+ @NotNull
+ @Schema(description = "Software quality")
+ SoftwareQuality softwareQuality,
+
+ @NotNull
+ @Schema(description = "Severity")
+ Severity severity
+) {
+}
*/
package org.sonar.server.v2.api.rule.request;
-public record RuleCreateRestRequest() {
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.CleanCodeAttribute;
+import org.sonar.server.v2.api.rule.ressource.Parameter;
+
+public record RuleCreateRestRequest(
+
+ @NotNull
+ @Size(max = 200)
+ @Schema(description = "Key of the custom rule to create, must include the repository")
+ String key,
+
+ @NotNull
+ @Size(max = 200)
+ @Schema(description = "Key of the rule template to be used to create the custom rule")
+ String templateKey,
+
+ @NotNull
+ @Size(max = 200)
+ @Schema(description = "Rule name")
+ String name,
+
+ @NotNull
+ @Schema(description = "Rule description in markdown format")
+ String markdownDescription,
+
+ @Nullable
+ @Schema(description = "Rule status", defaultValue = "READY")
+ RuleStatus status,
+
+ @Nullable
+ @Schema(description = "Custom rule parameters")
+ List<Parameter> parameters,
+
+ @NotNull
+ @Schema(description = "Clean code attribute")
+ CleanCodeAttribute cleanCodeAttribute,
+
+ @Valid
+ @NotEmpty
+ @Schema(description = "Impacts")
+ List<Impact> impacts
+) {
}
+++ /dev/null
-/*
- * 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.v2.api.rule.response;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import javax.annotation.Nullable;
-
-public record RuleParameterRestResponse(
-
- String key,
- @Schema(accessMode = Schema.AccessMode.READ_ONLY)
- String htmlDescription,
- @Nullable
- String defaultValue,
- @Schema(allowableValues = {
- "STRING",
- "TEXT",
- "BOOLEAN",
- "INTEGER",
- "FLOAT"
- }, accessMode = Schema.AccessMode.READ_ONLY)
- String type
-) {
-}
import org.sonar.server.v2.api.rule.enums.CleanCodeAttributeRestEnum;
import org.sonar.server.v2.api.rule.enums.RuleStatusRestEnum;
import org.sonar.server.v2.api.rule.enums.RuleTypeRestEnum;
+import org.sonar.server.v2.api.rule.ressource.Parameter;
@Schema(accessMode = Schema.AccessMode.READ_ONLY)
public record RuleRestResponse(
String languageKey,
@Nullable
String languageName,
- List<RuleParameterRestResponse> parameters,
+ List<Parameter> parameters,
String remediationFunctionType,
String remediationFunctionGapMultiplier,
String remediationFunctionBaseEffort
private List<String> systemTags;
private String languageKey;
private String languageName;
- private List<RuleParameterRestResponse> parameters;
+ private List<Parameter> parameters;
private String remediationFunctionType;
private String remediationFunctionGapMultiplier;
private String remediationFunctionBaseEffort;
return this;
}
- public Builder setParameters(List<RuleParameterRestResponse> parameters) {
+ public Builder setParameters(List<Parameter> parameters) {
this.parameters = parameters;
return this;
}
--- /dev/null
+/*
+ * 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.v2.api.rule.ressource;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.annotation.Nullable;
+
+import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.*;
+
+public record Parameter(
+
+ @Schema(accessMode = READ_WRITE)
+ String key,
+ @Schema(accessMode = READ_ONLY)
+ String htmlDescription,
+ @Nullable
+ @Schema(accessMode = READ_WRITE)
+ String defaultValue,
+ @Schema(allowableValues = {
+ "STRING",
+ "TEXT",
+ "BOOLEAN",
+ "INTEGER",
+ "FLOAT"
+ }, accessMode = READ_ONLY)
+ String type
+) {
+}
--- /dev/null
+/*
+ * 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.rule.ressource;
+
+import javax.annotation.ParametersAreNonnullByDefault;
*/
package org.sonar.server.v2.api.rule.controller;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@Test
+ @Ignore
public void create() throws Exception {
mockMvc.perform(post(RULES_ENDPOINT).contentType(MediaType.APPLICATION_JSON_VALUE).content("{}"))
.andExpectAll(
}
@Test
+ @Ignore
public void create_shouldReturnExpectedBody() throws Exception {
when(ruleRestResponseGenerator.toRuleRestResponse(any())).thenReturn(RuleRestResponse.Builder.builder().setId("id").build());
testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
testImplementation 'org.springframework:spring-test'
testImplementation testFixtures(project(':server:sonar-server-common'))
+ testImplementation testFixtures(project(':server:sonar-webserver-api'))
testImplementation testFixtures(project(':server:sonar-webserver-auth'))
testImplementation testFixtures(project(':server:sonar-webserver-es'))
testImplementation testFixtures(project(':server:sonar-webserver-ws'))
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleParamDto;
import org.sonar.server.qualityprofile.builtin.QProfileName;
-import org.sonar.server.rule.RuleCreator;
+import org.sonar.server.common.rule.RuleCreator;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
@Test
public void restore_custom_rule() {
- when(ruleCreator.create(any(), anyList())).then(invocation -> Collections.singletonList(db.rules().insert(RuleKey.of("sonarjs", "s001")).getKey()));
+ when(ruleCreator.create(any(), anyList())).then(invocation -> Collections.singletonList(db.rules().insert(RuleKey.of("sonarjs", "s001"))));
Reader backup = new StringReader("<?xml version='1.0' encoding='UTF-8'?>" +
"<profile>" +
+++ /dev/null
-/*
- * 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;
-
-import com.google.common.collect.Sets;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
-import org.assertj.core.api.Fail;
-import org.assertj.core.groups.Tuple;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.impl.utils.TestSystem2;
-import org.sonar.api.issue.impact.SoftwareQuality;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rule.RuleStatus;
-import org.sonar.api.rule.Severity;
-import org.sonar.api.rules.CleanCodeAttribute;
-import org.sonar.api.rules.RuleType;
-import org.sonar.api.server.debt.DebtRemediationFunction;
-import org.sonar.api.utils.System2;
-import org.sonar.core.util.SequenceUuidFactory;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.db.DbSession;
-import org.sonar.db.DbTester;
-import org.sonar.db.rule.RuleDto;
-import org.sonar.db.rule.RuleDto.Format;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.db.rule.RuleTesting;
-import org.sonar.server.es.EsTester;
-import org.sonar.server.es.SearchOptions;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.rule.index.RuleIndex;
-import org.sonar.server.rule.index.RuleIndexer;
-import org.sonar.server.rule.index.RuleQuery;
-
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.junit.Assert.fail;
-import static org.sonar.db.rule.RuleTesting.newCustomRule;
-import static org.sonar.db.rule.RuleTesting.newRule;
-import static org.sonar.server.util.TypeValidationsTesting.newFullTypeValidations;
-
-public class RuleCreatorIT {
-
- private final System2 system2 = new TestSystem2().setNow(Instant.now().toEpochMilli());
-
- @Rule
- public DbTester dbTester = DbTester.create(system2);
-
- @Rule
- public EsTester es = EsTester.create();
-
- private final RuleIndex ruleIndex = new RuleIndex(es.client(), system2);
- private final RuleIndexer ruleIndexer = new RuleIndexer(es.client(), dbTester.getDbClient());
- private final DbSession dbSession = dbTester.getSession();
- private final UuidFactory uuidFactory = new SequenceUuidFactory();
-
- private final RuleCreator underTest = new RuleCreator(system2, new RuleIndexer(es.client(), dbTester.getDbClient()), dbTester.getDbClient(), newFullTypeValidations(), uuidFactory);
-
- @Test
- public void create_custom_rule() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
- // Create custom rule
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("Some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*"));
- RuleKey customRuleKey = underTest.create(dbSession, newRule);
-
- RuleDto rule = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
- assertThat(rule).isNotNull();
- assertThat(rule.getKey()).isEqualTo(RuleKey.of("java", "CUSTOM_RULE"));
- assertThat(rule.getPluginKey()).isEqualTo("sonarjava");
- assertThat(rule.getTemplateUuid()).isEqualTo(templateRule.getUuid());
- assertThat(rule.getName()).isEqualTo("My custom");
- assertThat(rule.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Some description");
- assertThat(rule.getEnumType()).isEqualTo(RuleType.CODE_SMELL);
- assertCleanCodeInformation(rule);
- assertThat(rule.getSeverityString()).isEqualTo("MAJOR");
- assertThat(rule.getStatus()).isEqualTo(RuleStatus.READY);
- assertThat(rule.getLanguage()).isEqualTo("java");
- assertThat(rule.getConfigKey()).isEqualTo("S001");
- assertDefRemediation(rule);
- assertThat(rule.getGapDescription()).isEqualTo("desc");
- assertThat(rule.getTags()).containsOnly("usertag1", "usertag2");
- assertThat(rule.getSystemTags()).containsOnly("tag1", "tag4");
- assertThat(rule.getSecurityStandards()).containsOnly("owaspTop10:a1", "cwe:123");
- assertThat(rule.isExternal()).isFalse();
- assertThat(rule.isAdHoc()).isFalse();
-
- List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
- assertThat(params).hasSize(1);
-
- RuleParamDto param = params.get(0);
- // From template rule
- assertThat(param.getName()).isEqualTo("regex");
- assertThat(param.getDescription()).isEqualTo("Reg ex");
- assertThat(param.getType()).isEqualTo("STRING");
- // From user
- assertThat(param.getDefaultValue()).isEqualTo("a.*");
-
- assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule.getUuid(), templateRule.getUuid());
- }
-
- private static void assertCleanCodeInformation(RuleDto rule) {
- assertThat(rule.getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.CONVENTIONAL);
- assertThat(rule.getDefaultImpacts()).extracting(i -> i.getSoftwareQuality(), i -> i.getSeverity())
- .containsExactly(Tuple.tuple(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.MEDIUM));
- }
-
- private static void assertDefRemediation(RuleDto rule) {
- assertThat(rule.getDefRemediationFunction()).isEqualTo("LINEAR_OFFSET");
- assertThat(rule.getDefRemediationGapMultiplier()).isEqualTo("1h");
- assertThat(rule.getDefRemediationBaseEffort()).isEqualTo("5min");
- }
-
- @Test
- public void create_custom_rule_with_empty_parameter_value() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", ""));
-
- RuleKey customRuleKey = underTest.create(dbSession, newRule);
-
- List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
- assertThat(params).hasSize(1);
- RuleParamDto param = params.get(0);
- assertThat(param.getName()).isEqualTo("regex");
- assertThat(param.getDescription()).isEqualTo("Reg ex");
- assertThat(param.getType()).isEqualTo("STRING");
- assertThat(param.getDefaultValue()).isNull();
- }
-
- @Test
- public void create_whenTypeIsHotspot_shouldNotComputeDefaultImpact() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setType(RuleType.SECURITY_HOTSPOT)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", ""));
-
- RuleKey customRuleKey = underTest.create(dbSession, newRule);
-
- RuleDto rule = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
- assertThat(rule.getDefaultImpacts()).isEmpty();
- }
-
- @Test
- public void create_custom_rule_with_no_parameter_value() {
- // insert template rule
- RuleDto templateRule = createTemplateRuleWithIntArrayParam();
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY);
-
- RuleKey customRuleKey = underTest.create(dbSession, newRule);
-
- List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
- assertThat(params).hasSize(1);
- RuleParamDto param = params.get(0);
- assertThat(param.getName()).isEqualTo("myIntegers");
- assertThat(param.getDescription()).isEqualTo("My Integers");
- assertThat(param.getType()).isEqualTo("INTEGER,multiple=true,values=1;2;3");
- assertThat(param.getDefaultValue()).isNull();
- }
-
- @Test
- public void create_custom_rule_with_multiple_parameter_values() {
- // insert template rule
- RuleDto templateRule = createTemplateRuleWithIntArrayParam();
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("myIntegers", "1,3"));
-
- RuleKey customRuleKey = underTest.create(dbSession, newRule);
-
- List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
- assertThat(params).hasSize(1);
- RuleParamDto param = params.get(0);
- assertThat(param.getName()).isEqualTo("myIntegers");
- assertThat(param.getDescription()).isEqualTo("My Integers");
- assertThat(param.getType()).isEqualTo("INTEGER,multiple=true,values=1;2;3");
- assertThat(param.getDefaultValue()).isEqualTo("1,3");
- }
-
- @Test
- public void batch_create_custom_rules() {
- // insert template rule
- RuleDto templateRule = createTemplateRuleWithIntArrayParam();
-
- NewCustomRule firstRule = NewCustomRule.createForCustomRule("CUSTOM_RULE_1", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY);
-
- NewCustomRule secondRule = NewCustomRule.createForCustomRule("CUSTOM_RULE_2", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY);
-
- List<RuleKey> customRuleKeys = underTest.create(dbSession, Arrays.asList(firstRule, secondRule));
-
- List<RuleDto> rules = dbTester.getDbClient().ruleDao().selectByKeys(dbSession, customRuleKeys);
-
- assertThat(rules).hasSize(2);
- assertThat(rules).asList()
- .extracting("ruleKey")
- .containsOnly("CUSTOM_RULE_1", "CUSTOM_RULE_2");
- }
-
- @Test
- public void fail_to_create_custom_rules_when_wrong_rule_template() {
- // insert rule
- RuleDto rule = newRule(RuleKey.of("java", "S001")).setIsTemplate(false);
- dbTester.rules().insert(rule);
- dbSession.commit();
-
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", rule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*"));
-
- assertThatThrownBy(() -> underTest.create(dbSession, singletonList(newRule)))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("This rule is not a template rule: java:S001");
- }
-
- @Test
- public void fail_to_create_custom_rules_when_removed_rule_template() {
- // insert rule
- RuleDto rule = createTemplateRule();
- newRule(RuleKey.of("java", "S001")).setIsTemplate(false);
- rule.setStatus(RuleStatus.REMOVED);
- dbTester.rules().update(rule);
- dbSession.commit();
-
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", rule.getKey())
- .setName("My custom")
- .setMarkdownDescription("Some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY);
-
- List<NewCustomRule> newRules = singletonList(newRule);
- assertThatThrownBy(() -> underTest.create(dbSession, newRules))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("The template key doesn't exist: java:S001");
- }
-
- @Test
- public void fail_to_create_custom_rule_with_invalid_parameter() {
- // insert template rule
- RuleDto templateRule = createTemplateRuleWithIntArrayParam();
-
- assertThatThrownBy(() -> {
- // Create custom rule
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("Some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("myIntegers", "1,polop,2"));
- underTest.create(dbSession, newRule);
- })
- .isInstanceOf(BadRequestException.class)
- .hasMessage("Value 'polop' must be an integer.");
- }
-
- @Test
- public void fail_to_create_custom_rule_with_invalid_parameters() {
- // insert template rule
- RuleDto templateRule = createTemplateRuleWithTwoIntParams();
-
- // Create custom rule
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("Some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("first", "polop", "second", "palap"));
- try {
- underTest.create(dbSession, newRule);
- Fail.failBecauseExceptionWasNotThrown(BadRequestException.class);
- } catch (BadRequestException badRequest) {
- assertThat(badRequest.errors().toString()).contains("palap").contains("polop");
- }
- }
-
- @Test
- public void fail_to_create_custom_rule_with_empty_description() {
- // insert template rule
- RuleDto templateRule = createTemplateRuleWithTwoIntParams();
-
- // Create custom rule
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("Some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setMarkdownDescription("");
- assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> underTest.create(dbSession, newRule))
- .withMessage("The description is missing");
- }
-
- @Test
- public void reactivate_custom_rule_if_already_exists_in_removed_status() {
- String key = "CUSTOM_RULE";
-
- RuleDto templateRule = createTemplateRule();
-
- RuleDto rule = newCustomRule(templateRule, "Old description")
- .setRuleKey(key)
- .setStatus(RuleStatus.REMOVED)
- .setName("Old name")
- .setDescriptionFormat(Format.MARKDOWN)
- .setSeverity(Severity.INFO);
- dbTester.rules().insert(rule);
- dbTester.rules().insertRuleParam(rule, param -> param.setDefaultValue("a.*"));
- dbSession.commit();
-
- // Create custom rule with same key, but with different values
- NewCustomRule newRule = NewCustomRule.createForCustomRule(key, templateRule.getKey())
- .setName("New name")
- .setMarkdownDescription("New description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "c.*"));
- RuleKey customRuleKey = underTest.create(dbSession, newRule);
-
- RuleDto result = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
- assertThat(result.getKey()).isEqualTo(RuleKey.of("java", key));
- assertThat(result.getStatus()).isEqualTo(RuleStatus.READY);
-
- // These values should be the same than before
- assertThat(result.getName()).isEqualTo("Old name");
- assertThat(result.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Old description");
- assertThat(result.getSeverityString()).isEqualTo(Severity.INFO);
-
- List<RuleParamDto> params = dbTester.getDbClient().ruleDao().selectRuleParamsByRuleKey(dbSession, customRuleKey);
- assertThat(params).hasSize(1);
- assertThat(params.get(0).getDefaultValue()).isEqualTo("a.*");
- }
-
- @Test
- public void generate_reactivation_exception_when_rule_exists_in_removed_status_and_prevent_reactivation_parameter_is_true() {
- String key = "CUSTOM_RULE";
-
- RuleDto templateRule = createTemplateRule();
-
- RuleDto rule = newCustomRule(templateRule, "Old description")
- .setRuleKey(key)
- .setStatus(RuleStatus.REMOVED)
- .setName("Old name")
- .setSeverity(Severity.INFO);
- dbTester.rules().insert(rule);
- dbTester.rules().insertRuleParam(rule, param -> param.setDefaultValue("a.*"));
- dbSession.commit();
-
- // Create custom rule with same key, but with different values
- NewCustomRule newRule = NewCustomRule.createForCustomRule(key, templateRule.getKey())
- .setName("New name")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "c.*"))
- .setPreventReactivation(true);
-
- try {
- underTest.create(dbSession, newRule);
- fail();
- } catch (Exception e) {
- assertThat(e).isInstanceOf(ReactivationException.class);
- ReactivationException reactivationException = (ReactivationException) e;
- assertThat(reactivationException.ruleKey()).isEqualTo(rule.getKey());
- }
- }
-
- @Test
- public void fail_to_create_custom_rule_when_invalid_key() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
-
- NewCustomRule newRule = NewCustomRule.createForCustomRule("*INVALID*", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*"));
-
- assertThatThrownBy(() -> underTest.create(dbSession, newRule))
- .isInstanceOf(BadRequestException.class)
- .hasMessage("The rule key \"*INVALID*\" is invalid, it should only contain: a-z, 0-9, \"_\"");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_rule_key_already_exists() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
- // Create a custom rule
- AtomicReference<NewCustomRule> newRule = new AtomicReference<>(NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*")));
- underTest.create(dbSession, newRule.get());
-
- // Create another custom rule having same key
- newRule.set(NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My another custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*")));
-
- assertThatThrownBy(() -> underTest.create(dbSession, newRule.get()))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("A rule with the key 'CUSTOM_RULE' already exists");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_missing_name() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
-
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*"));
-
- assertThatThrownBy(() -> underTest.create(dbSession, newRule))
- .isInstanceOf(BadRequestException.class)
- .hasMessage("The name is missing");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_missing_description() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
-
- assertThatThrownBy(() -> {
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*"));
- underTest.create(dbSession, newRule);
- })
- .isInstanceOf(BadRequestException.class)
- .hasMessage("The description is missing");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_missing_severity() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
-
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*"));
-
- assertThatThrownBy(() -> underTest.create(dbSession, newRule))
- .isInstanceOf(BadRequestException.class)
- .hasMessage("The severity is missing");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_invalid_severity() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
-
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity("INVALID")
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*"));
-
- assertThatThrownBy(() -> underTest.create(dbSession, newRule))
- .isInstanceOf(BadRequestException.class)
- .hasMessage("Severity \"INVALID\" is invalid");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_missing_status() {
- // insert template rule
- RuleDto templateRule = createTemplateRule();
-
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setParameters(Map.of("regex", "a.*"));
-
- assertThatThrownBy(() -> underTest.create(dbSession, newRule))
- .isInstanceOf(BadRequestException.class)
- .hasMessage("The status is missing");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_wrong_rule_template() {
- // insert rule
- RuleDto rule = newRule(RuleKey.of("java", "S001")).setIsTemplate(false);
- dbTester.rules().insert(rule);
- dbSession.commit();
-
- // Create custom rule with unknown template rule
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", rule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setParameters(Map.of("regex", "a.*"));
-
- assertThatThrownBy(() -> underTest.create(dbSession, newRule))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("This rule is not a template rule: java:S001");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_null_template() {
- assertThatThrownBy(() -> {
- // Create custom rule
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", null)
- .setName("My custom")
- .setMarkdownDescription("Some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY);
- underTest.create(dbSession, newRule);
- })
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Template key should be set");
- }
-
- @Test
- public void fail_to_create_custom_rule_when_unknown_template() {
- assertThatThrownBy(() -> {
- // Create custom rule
- NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", RuleKey.of("java", "S001"))
- .setName("My custom")
- .setMarkdownDescription("Some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY);
- underTest.create(dbSession, newRule);
- })
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("The template key doesn't exist: java:S001");
- }
-
- @Test
- public void create_givenSecurityHotspotRule_doNotSetCleanCodeAttribute() {
- RuleDto templateRule = createTemplateRule();
-
- NewCustomRule newRule = NewCustomRule.createForCustomRule("security_hotspots_rule", templateRule.getKey())
- .setName("My custom")
- .setMarkdownDescription("some description")
- .setSeverity(Severity.MAJOR)
- .setStatus(RuleStatus.READY)
- .setType(RuleType.SECURITY_HOTSPOT);
-
- RuleKey customRuleKey = underTest.create(dbSession, newRule);
-
- RuleDto result = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
-
- assertThat(result.getCleanCodeAttribute()).isNull();
- }
-
- private RuleDto createTemplateRule() {
- RuleDto templateRule = RuleTesting.newRule(RuleKey.of("java", "S001"))
- .setIsTemplate(true)
- .setLanguage("java")
- .setPluginKey("sonarjava")
- .setConfigKey("S001")
- .setDefRemediationFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
- .setDefRemediationGapMultiplier("1h")
- .setDefRemediationBaseEffort("5min")
- .setGapDescription("desc")
- .setTags(Sets.newHashSet("usertag1", "usertag2"))
- .setSystemTags(Sets.newHashSet("tag1", "tag4"))
- .setSecurityStandards(Sets.newHashSet("owaspTop10:a1", "cwe:123"))
- .setCreatedAt(new Date().getTime())
- .setUpdatedAt(new Date().getTime());
- dbTester.rules().insert(templateRule);
- dbTester.rules().insertRuleParam(templateRule, param -> param.setName("regex").setType("STRING").setDescription("Reg ex").setDefaultValue(".*"));
- ruleIndexer.commitAndIndex(dbTester.getSession(), templateRule.getUuid());
- return templateRule;
- }
-
- private RuleDto createTemplateRuleWithIntArrayParam() {
- RuleDto templateRule = newRule(RuleKey.of("java", "S002"))
- .setIsTemplate(true)
- .setLanguage("java")
- .setConfigKey("S002")
- .setDefRemediationFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
- .setDefRemediationGapMultiplier("1h")
- .setDefRemediationBaseEffort("5min")
- .setGapDescription("desc")
- .setCreatedAt(new Date().getTime())
- .setUpdatedAt(new Date().getTime());
- dbTester.rules().insert(templateRule);
- dbTester.rules().insertRuleParam(templateRule,
- param -> param.setName("myIntegers").setType("INTEGER,multiple=true,values=1;2;3").setDescription("My Integers").setDefaultValue("1"));
- ruleIndexer.commitAndIndex(dbTester.getSession(), templateRule.getUuid());
- return templateRule;
- }
-
- private RuleDto createTemplateRuleWithTwoIntParams() {
- RuleDto templateRule = newRule(RuleKey.of("java", "S003"))
- .setIsTemplate(true)
- .setLanguage("java")
- .setConfigKey("S003")
- .setDefRemediationFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
- .setDefRemediationGapMultiplier("1h")
- .setDefRemediationBaseEffort("5min")
- .setGapDescription("desc")
- .setCreatedAt(new Date().getTime())
- .setUpdatedAt(new Date().getTime());
- dbTester.rules().insert(templateRule);
- dbTester.rules().insertRuleParam(templateRule, param -> param.setName("first").setType("INTEGER").setDescription("First integer").setDefaultValue("0"));
- dbTester.rules().insertRuleParam(templateRule, param -> param.setName("second").setType("INTEGER").setDescription("Second integer").setDefaultValue("0"));
- return templateRule;
- }
-
-}
import org.sonar.core.util.UuidFactory;
import org.sonar.db.DbTester;
import org.sonar.db.rule.RuleDto;
+import org.sonar.server.common.rule.service.RuleService;
import org.sonar.server.common.text.MacroInterpreter;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.rule.RuleCreator;
+import org.sonar.server.common.rule.RuleCreator;
import org.sonar.server.rule.RuleDescriptionFormatter;
import org.sonar.server.rule.index.RuleIndexer;
import org.sonar.server.tester.UserSessionRule;
private final UuidFactory uuidFactory = new SequenceUuidFactory();
private final WsActionTester ws = new WsActionTester(new CreateAction(db.getDbClient(),
- new RuleCreator(system2, new RuleIndexer(es.client(), db.getDbClient()), db.getDbClient(), newFullTypeValidations(), uuidFactory),
+ new RuleService(db.getDbClient(),
+ new RuleCreator(system2, new RuleIndexer(es.client(), db.getDbClient()), db.getDbClient(), newFullTypeValidations(), uuidFactory)),
new RuleMapper(new Languages(), createMacroInterpreter(), new RuleDescriptionFormatter()),
new RuleWsSupport(db.getDbClient(), userSession)));
.setParam("params", "regex=a.*")
.execute().getInput();
- String expetedResult = """
+ String expectedResult = """
{
"rule": {
"key": "java:MY_CUSTOM",
}
""";
- assertJson(result).isSimilarTo(expetedResult);
+ assertJson(result).isSimilarTo(expectedResult);
+ }
+
+ @Test
+ public void create_shouldSetCleanCodeAttributeAndImpacts() {
+ logInAsQProfileAdministrator();
+ // Template rule
+ RuleDto templateRule = newTemplateRule(RuleKey.of("java", "S001"))
+ .setType(BUG)
+ .setLanguage("js");
+ db.rules().insert(templateRule);
+
+ String result = ws.newRequest()
+ .setParam("customKey", "MY_CUSTOM")
+ .setParam("templateKey", templateRule.getKey().toString())
+ .setParam("name", "My custom rule")
+ .setParam("markdownDescription", "Description")
+ .setParam("status", "BETA")
+ .setParam("cleanCodeAttribute", "MODULAR")
+ .setParam("impacts", "RELIABILITY=HIGH;SECURITY=LOW")
+ .execute().getInput();
+
+ String expectedResult = """
+ {
+ "rule": {
+ "key": "java:MY_CUSTOM",
+ "repo": "java",
+ "name": "My custom rule",
+ "htmlDesc": "Description",
+ "severity": "MINOR",
+ "status": "BETA",
+ "type": "VULNERABILITY",
+ "internalKey": "configKey_S001",
+ "isTemplate": false,
+ "templateKey": "java:S001",
+ "lang": "js",
+ "cleanCodeAttribute": "MODULAR",
+ "cleanCodeAttributeCategory": "ADAPTABLE",
+ "impacts": [
+ {
+ "softwareQuality": "RELIABILITY",
+ "severity": "HIGH"
+ },
+ {
+ "softwareQuality": "SECURITY",
+ "severity": "LOW"
+ }
+ ]
+ }
+ }
+ """;
+
+ assertJson(result).isSimilarTo(expectedResult);
}
@Test
}
@Test
- public void status_set_to_default() {
+ public void severity_set_to_default() {
logInAsQProfileAdministrator();
RuleDto templateRule = newTemplateRule(RuleKey.of("java", "S001"));
import org.sonar.db.rule.DeprecatedRuleKeyDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.server.qualityprofile.builtin.QProfileName;
-import org.sonar.server.rule.NewCustomRule;
-import org.sonar.server.rule.RuleCreator;
+import org.sonar.server.common.rule.service.NewCustomRule;
+import org.sonar.server.common.rule.RuleCreator;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.function.Function.identity;
.toList();
if (!customRulesToCreate.isEmpty()) {
- return db.ruleDao().selectByKeys(dbSession, ruleCreator.create(dbSession, customRulesToCreate))
+ return db.ruleDao().selectByKeys(dbSession, ruleCreator.create(dbSession, customRulesToCreate).stream().map(RuleDto::getKey).toList())
.stream()
.collect(Collectors.toMap(RuleDto::getKey, identity()));
}
}
private static NewCustomRule importedRuleToNewCustomRule(ImportedRule r) {
- return NewCustomRule.createForCustomRule(r.getRuleKey().rule(), r.getTemplateKey())
+ return NewCustomRule.createForCustomRule(r.getRuleKey(), r.getTemplateKey())
.setName(r.getName())
.setSeverity(r.getSeverity())
.setStatus(RuleStatus.READY)
+++ /dev/null
-/*
- * 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;
-
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import java.util.HashMap;
-import java.util.Map;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rule.RuleStatus;
-import org.sonar.api.rules.RuleType;
-
-public class NewCustomRule {
-
- private String ruleKey;
- private RuleKey templateKey;
- private String name;
- private String markdownDescription;
- private String severity;
- private RuleStatus status;
- private RuleType type;
- private Map<String, String> parameters = new HashMap<>();
-
- private boolean preventReactivation = false;
-
- private NewCustomRule() {
- // No direct call to constructor
- }
-
- public String ruleKey() {
- return ruleKey;
- }
-
- public RuleKey templateKey() {
- return templateKey;
- }
-
- @CheckForNull
- public String name() {
- return name;
- }
-
- public NewCustomRule setName(@Nullable String name) {
- this.name = name;
- return this;
- }
-
- @CheckForNull
- public String markdownDescription() {
- return markdownDescription;
- }
-
- public NewCustomRule setMarkdownDescription(@Nullable String markdownDescription) {
- this.markdownDescription = markdownDescription;
- return this;
- }
-
- @CheckForNull
- public String severity() {
- return severity;
- }
-
- public NewCustomRule setSeverity(@Nullable String severity) {
- this.severity = severity;
- return this;
- }
-
- @CheckForNull
- public RuleStatus status() {
- return status;
- }
-
- public NewCustomRule setStatus(@Nullable RuleStatus status) {
- this.status = status;
- return this;
- }
-
- @CheckForNull
- public RuleType type() {
- return type;
- }
-
- public NewCustomRule setType(@Nullable RuleType type) {
- this.type = type;
- return this;
- }
-
- @CheckForNull
- public String parameter(final String paramKey) {
- return parameters.get(paramKey);
- }
-
- public NewCustomRule setParameters(Map<String, String> params) {
- this.parameters = params;
- return this;
- }
-
- public boolean isPreventReactivation() {
- return preventReactivation;
- }
-
- /**
- * When true, if the rule already exists in status REMOVED, an {@link ReactivationException} will be thrown
- */
- public NewCustomRule setPreventReactivation(boolean preventReactivation) {
- this.preventReactivation = preventReactivation;
- return this;
- }
-
- public static NewCustomRule createForCustomRule(String customKey, RuleKey templateKey) {
- Preconditions.checkArgument(!Strings.isNullOrEmpty(customKey), "Custom key should be set");
- Preconditions.checkArgument(templateKey != null, "Template key should be set");
- NewCustomRule newRule = new NewCustomRule();
- newRule.ruleKey = customKey;
- newRule.templateKey = templateKey;
- return newRule;
- }
-}
+++ /dev/null
-/*
- * 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;
-
-import org.sonar.api.rule.RuleKey;
-
-public class ReactivationException extends RuntimeException {
-
- private RuleKey ruleKey;
-
- public ReactivationException(String s, RuleKey ruleKey) {
- super(s);
- this.ruleKey = ruleKey;
- }
-
- public RuleKey ruleKey() {
- return ruleKey;
- }
-}
+++ /dev/null
-/*
- * 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;
-
-import com.google.common.base.Splitter;
-import com.google.common.base.Strings;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-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.issue.impact.SoftwareQuality;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rule.RuleStatus;
-import org.sonar.api.rule.Severity;
-import org.sonar.api.rules.CleanCodeAttribute;
-import org.sonar.api.rules.RuleType;
-import org.sonar.api.server.ServerSide;
-import org.sonar.api.server.rule.RuleParamType;
-import org.sonar.api.server.rule.internal.ImpactMapper;
-import org.sonar.api.utils.System2;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.issue.ImpactDto;
-import org.sonar.db.rule.RuleDescriptionSectionDto;
-import org.sonar.db.rule.RuleDto;
-import org.sonar.db.rule.RuleDto.Format;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.rule.index.RuleIndexer;
-import org.sonar.server.util.TypeValidations;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.Lists.newArrayList;
-import static java.lang.String.format;
-import static java.util.Objects.requireNonNull;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
-import static org.sonar.server.exceptions.BadRequestException.checkRequest;
-
-@ServerSide
-public class RuleCreator {
- private static final String TEMPLATE_KEY_NOT_EXIST_FORMAT = "The template key doesn't exist: %s";
-
- private final System2 system2;
- private final RuleIndexer ruleIndexer;
- private final DbClient dbClient;
- private final TypeValidations typeValidations;
- private final UuidFactory uuidFactory;
-
- public RuleCreator(System2 system2, RuleIndexer ruleIndexer, DbClient dbClient, TypeValidations typeValidations, UuidFactory uuidFactory) {
- this.system2 = system2;
- this.ruleIndexer = ruleIndexer;
- this.dbClient = dbClient;
- this.typeValidations = typeValidations;
- this.uuidFactory = uuidFactory;
- }
-
- public RuleKey create(DbSession dbSession, NewCustomRule newRule) {
- RuleKey templateKey = newRule.templateKey();
- RuleDto templateRule = dbClient.ruleDao().selectByKey(dbSession, templateKey)
- .orElseThrow(() -> new IllegalArgumentException(format(TEMPLATE_KEY_NOT_EXIST_FORMAT, templateKey)));
- checkArgument(templateRule.isTemplate(), "This rule is not a template rule: %s", templateKey.toString());
- checkArgument(templateRule.getStatus() != RuleStatus.REMOVED, TEMPLATE_KEY_NOT_EXIST_FORMAT, templateKey.toString());
- validateCustomRule(newRule, dbSession, templateKey);
-
- RuleKey customRuleKey = RuleKey.of(templateRule.getRepositoryKey(), newRule.ruleKey());
- Optional<RuleDto> definition = loadRule(dbSession, customRuleKey);
- String customRuleUuid = definition.map(d -> updateExistingRule(d, newRule, dbSession))
- .orElseGet(() -> createCustomRule(customRuleKey, newRule, templateRule, dbSession));
-
- ruleIndexer.commitAndIndex(dbSession, customRuleUuid);
- return customRuleKey;
- }
-
- public List<RuleKey> create(DbSession dbSession, List<NewCustomRule> newRules) {
- Set<RuleKey> templateKeys = newRules.stream().map(NewCustomRule::templateKey).collect(Collectors.toSet());
- Map<RuleKey, RuleDto> templateRules = dbClient.ruleDao().selectByKeys(dbSession, templateKeys)
- .stream()
- .collect(Collectors.toMap(
- RuleDto::getKey,
- Function.identity()));
-
- checkArgument(!templateRules.isEmpty() && templateKeys.size() == templateRules.size(), "Rule template keys should exists for each custom rule!");
- templateRules.values().forEach(ruleDto -> {
- checkArgument(ruleDto.isTemplate(), "This rule is not a template rule: %s", ruleDto.getKey().toString());
- checkArgument(ruleDto.getStatus() != RuleStatus.REMOVED, TEMPLATE_KEY_NOT_EXIST_FORMAT, ruleDto.getKey().toString());
- });
-
- List<String> customRuleUuids = newRules.stream()
- .map(newCustomRule -> {
- RuleDto templateRule = templateRules.get(newCustomRule.templateKey());
- validateCustomRule(newCustomRule, dbSession, templateRule.getKey());
- RuleKey customRuleKey = RuleKey.of(templateRule.getRepositoryKey(), newCustomRule.ruleKey());
- return createCustomRule(customRuleKey, newCustomRule, templateRule, dbSession);
- })
- .toList();
-
- ruleIndexer.commitAndIndex(dbSession, customRuleUuids);
- return newRules.stream()
- .map(newCustomRule -> {
- RuleDto templateRule = templateRules.get(newCustomRule.templateKey());
- return RuleKey.of(templateRule.getRepositoryKey(), newCustomRule.ruleKey());
- })
- .toList();
- }
-
- private void validateCustomRule(NewCustomRule newRule, DbSession dbSession, RuleKey templateKey) {
- List<String> errors = new ArrayList<>();
-
- validateRuleKey(errors, newRule.ruleKey());
- validateName(errors, newRule);
- validateDescription(errors, newRule);
-
- String severity = newRule.severity();
- if (Strings.isNullOrEmpty(severity)) {
- errors.add("The severity is missing");
- } else if (!Severity.ALL.contains(severity)) {
- errors.add(format("Severity \"%s\" is invalid", severity));
- }
- if (newRule.status() == null) {
- errors.add("The status is missing");
- }
-
- for (RuleParamDto ruleParam : dbClient.ruleDao().selectRuleParamsByRuleKey(dbSession, templateKey)) {
- try {
- validateParam(ruleParam, newRule.parameter(ruleParam.getName()));
- } catch (BadRequestException validationError) {
- errors.addAll(validationError.errors());
- }
- }
- checkRequest(errors.isEmpty(), errors);
- }
-
- private void validateParam(RuleParamDto ruleParam, @Nullable String value) {
- if (value != null) {
- RuleParamType ruleParamType = RuleParamType.parse(ruleParam.getType());
- if (ruleParamType.multiple()) {
- List<String> values = newArrayList(Splitter.on(",").split(value));
- typeValidations.validate(values, ruleParamType.type(), ruleParamType.values());
- } else {
- typeValidations.validate(value, ruleParamType.type(), ruleParamType.values());
- }
- }
- }
-
- private static void validateName(List<String> errors, NewCustomRule newRule) {
- if (Strings.isNullOrEmpty(newRule.name())) {
- errors.add("The name is missing");
- }
- }
-
- private static void validateDescription(List<String> errors, NewCustomRule newRule) {
- if (Strings.isNullOrEmpty(newRule.markdownDescription())) {
- errors.add("The description is missing");
- }
- }
-
- private static void validateRuleKey(List<String> errors, String ruleKey) {
- if (!ruleKey.matches("^[\\w]+$")) {
- errors.add(format("The rule key \"%s\" is invalid, it should only contain: a-z, 0-9, \"_\"", ruleKey));
- }
- }
-
- private Optional<RuleDto> loadRule(DbSession dbSession, RuleKey ruleKey) {
- return dbClient.ruleDao().selectByKey(dbSession, ruleKey);
- }
-
- private String createCustomRule(RuleKey ruleKey, NewCustomRule newRule, RuleDto templateRuleDto, DbSession dbSession) {
- RuleDescriptionSectionDto ruleDescriptionSectionDto = createDefaultRuleDescriptionSection(uuidFactory.create(), requireNonNull(newRule.markdownDescription()));
- int type = newRule.type() == null ? templateRuleDto.getType() : newRule.type().getDbConstant();
- String severity = newRule.severity();
-
- RuleDto ruleDto = new RuleDto()
- .setUuid(uuidFactory.create())
- .setRuleKey(ruleKey)
- .setPluginKey(templateRuleDto.getPluginKey())
- .setTemplateUuid(templateRuleDto.getUuid())
- .setConfigKey(templateRuleDto.getConfigKey())
- .setName(newRule.name())
- .setSeverity(severity)
- .setStatus(newRule.status())
- .setType(type)
- .setLanguage(templateRuleDto.getLanguage())
- .setDefRemediationFunction(templateRuleDto.getDefRemediationFunction())
- .setDefRemediationGapMultiplier(templateRuleDto.getDefRemediationGapMultiplier())
- .setDefRemediationBaseEffort(templateRuleDto.getDefRemediationBaseEffort())
- .setGapDescription(templateRuleDto.getGapDescription())
- .setScope(templateRuleDto.getScope())
- .setSystemTags(templateRuleDto.getSystemTags())
- .setSecurityStandards(templateRuleDto.getSecurityStandards())
- .setIsExternal(false)
- .setIsAdHoc(false)
- .setCreatedAt(system2.now())
- .setUpdatedAt(system2.now())
- .setDescriptionFormat(Format.MARKDOWN)
- .addRuleDescriptionSectionDto(ruleDescriptionSectionDto);
-
- if (type != RuleType.SECURITY_HOTSPOT.getDbConstant()) {
- SoftwareQuality softwareQuality = ImpactMapper.convertToSoftwareQuality(RuleType.valueOf(type));
- org.sonar.api.issue.impact.Severity impactSeverity = ImpactMapper.convertToImpactSeverity(severity);
- ruleDto = ruleDto.addDefaultImpact(new ImpactDto().setUuid(uuidFactory.create()).setSoftwareQuality(softwareQuality)
- .setSeverity(impactSeverity))
- .setCleanCodeAttribute(CleanCodeAttribute.CONVENTIONAL);
- }
-
- Set<String> tags = templateRuleDto.getTags();
- if (!tags.isEmpty()) {
- ruleDto.setTags(tags);
- }
- dbClient.ruleDao().insert(dbSession, ruleDto);
-
- for (RuleParamDto templateRuleParamDto : dbClient.ruleDao().selectRuleParamsByRuleKey(dbSession, templateRuleDto.getKey())) {
- String customRuleParamValue = Strings.emptyToNull(newRule.parameter(templateRuleParamDto.getName()));
- createCustomRuleParams(customRuleParamValue, ruleDto, templateRuleParamDto, dbSession);
- }
- return ruleDto.getUuid();
- }
-
- private void createCustomRuleParams(@Nullable String paramValue, RuleDto ruleDto, RuleParamDto templateRuleParam, DbSession dbSession) {
- RuleParamDto ruleParamDto = RuleParamDto.createFor(ruleDto)
- .setName(templateRuleParam.getName())
- .setType(templateRuleParam.getType())
- .setDescription(templateRuleParam.getDescription())
- .setDefaultValue(paramValue);
- dbClient.ruleDao().insertRuleParam(dbSession, ruleDto, ruleParamDto);
- }
-
- private String updateExistingRule(RuleDto ruleDto, NewCustomRule newRule, DbSession dbSession) {
- if (ruleDto.getStatus().equals(RuleStatus.REMOVED)) {
- if (newRule.isPreventReactivation()) {
- throw new ReactivationException(format("A removed rule with the key '%s' already exists", ruleDto.getKey().rule()), ruleDto.getKey());
- } else {
- ruleDto.setStatus(RuleStatus.READY)
- .setUpdatedAt(system2.now());
- dbClient.ruleDao().update(dbSession, ruleDto);
- }
- } else {
- throw new IllegalArgumentException(format("A rule with the key '%s' already exists", ruleDto.getKey().rule()));
- }
- return ruleDto.getUuid();
- }
-
-}
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.CleanCodeAttribute;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.db.DbSession;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.rule.NewCustomRule;
-import org.sonar.server.rule.ReactivationException;
-import org.sonar.server.rule.RuleCreator;
+import org.sonar.server.common.rule.ReactivationException;
+import org.sonar.server.common.rule.service.NewCustomRule;
+import org.sonar.server.common.rule.service.RuleService;
import org.sonarqube.ws.Rules;
import static com.google.common.base.Strings.isNullOrEmpty;
public static final String PARAM_STATUS = "status";
public static final String PARAM_TEMPLATE_KEY = "templateKey";
public static final String PARAM_TYPE = "type";
+ private static final String PARAM_IMPACTS = "impacts";
+ private static final String PARAM_CLEAN_CODE_ATTRIBUTE = "cleanCodeAttribute";
public static final String PARAMS = "params";
public static final String PARAM_PREVENT_REACTIVATION = "preventReactivation";
static final int NAME_MAXIMUM_LENGTH = 200;
private final DbClient dbClient;
- private final RuleCreator ruleCreator;
+ private final RuleService ruleService;
private final RuleMapper ruleMapper;
private final RuleWsSupport ruleWsSupport;
- public CreateAction(DbClient dbClient, RuleCreator ruleCreator, RuleMapper ruleMapper, RuleWsSupport ruleWsSupport) {
+ public CreateAction(DbClient dbClient, RuleService ruleService, RuleMapper ruleMapper, RuleWsSupport ruleWsSupport) {
this.dbClient = dbClient;
- this.ruleCreator = ruleCreator;
+ this.ruleService = ruleService;
this.ruleMapper = ruleMapper;
this.ruleWsSupport = ruleWsSupport;
}
new Change("5.5", "Creating manual rule is not more possible"),
new Change("10.0","Drop deprecated keys: 'custom_key', 'template_key', 'markdown_description', 'prevent_reactivation'"),
new Change("10.2", "Add 'impacts', 'cleanCodeAttribute', 'cleanCodeAttributeCategory' fields to the response"),
- new Change("10.2", "Fields 'type' and 'severity' are deprecated in the response. Use 'impacts' instead.")
- )
+ new Change("10.2", "Fields 'type' and 'severity' are deprecated in the response. Use 'impacts' instead."),
+ new Change("10.4", String.format("Add '%s' and '%s' parameters to the request", PARAM_IMPACTS, PARAM_CLEAN_CODE_ATTRIBUTE)),
+ new Change("10.4", String.format("Parameters '%s' and '%s' are deprecated. Use '%s' instead.", PARAM_TYPE, PARAM_SEVERITY, PARAM_IMPACTS)))
.setHandler(this);
action
action
.createParam(PARAM_TEMPLATE_KEY)
.setRequired(true)
- .setDescription("Key of the template rule in order to create a custom rule (mandatory for custom rule)")
+ .setDescription("Key of the template rule in order to create a custom rule")
.setExampleValue("java:XPath");
action
action
.createParam(PARAM_SEVERITY)
.setPossibleValues(Severity.ALL)
- .setDefaultValue(Severity.MAJOR)
- .setDescription("Rule severity");
+ .setDescription("Rule severity")
+ .setDeprecatedSince("10.4");
action
.createParam(PARAM_STATUS)
.setDescription("Rule status");
action.createParam(PARAMS)
- .setDescription("Parameters as semi-colon list of <key>=<value>, for example 'params=key1=v1;key2=v2' (Only for custom rule)");
+ .setDescription("Parameters as semi-colon list of <key>=<value>")
+ .setExampleValue("key1=v1;key2=v2");
action
.createParam(PARAM_PREVENT_REACTIVATION)
action.createParam(PARAM_TYPE)
.setPossibleValues(RuleType.names())
.setDescription("Rule type")
- .setSince("6.7");
+ .setSince("6.7")
+ .setDeprecatedSince("10.4");
+
+ action.createParam(PARAM_CLEAN_CODE_ATTRIBUTE)
+ .setDescription("Clean code attribute")
+ .setPossibleValues(CleanCodeAttribute.values())
+ .setSince("10.4");
+
+ action.createParam(PARAM_IMPACTS)
+ .setDescription("Impacts as semi-colon list of <software_quality>=<severity>")
+ .setExampleValue("SECURITY=HIGH;MAINTAINABILITY=LOW")
+ .setSince("10.4");
}
@Override
public void handle(Request request, Response response) throws Exception {
ruleWsSupport.checkQProfileAdminPermission();
- String customKey = request.mandatoryParam(PARAM_CUSTOM_KEY);
try (DbSession dbSession = dbClient.openSession(false)) {
try {
- NewCustomRule newRule = NewCustomRule.createForCustomRule(customKey, RuleKey.parse(request.mandatoryParam(PARAM_TEMPLATE_KEY)))
- .setName(request.mandatoryParam(PARAM_NAME))
- .setMarkdownDescription(request.mandatoryParam(PARAM_DESCRIPTION))
- .setSeverity(request.mandatoryParam(PARAM_SEVERITY))
- .setStatus(RuleStatus.valueOf(request.mandatoryParam(PARAM_STATUS)))
- .setPreventReactivation(request.mandatoryParamAsBoolean(PARAM_PREVENT_REACTIVATION));
- String params = request.param(PARAMS);
- if (!isNullOrEmpty(params)) {
- newRule.setParameters(KeyValueFormat.parse(params));
- }
- ofNullable(request.param(PARAM_TYPE)).ifPresent(t -> newRule.setType(RuleType.valueOf(t)));
- writeResponse(dbSession, request, response, ruleCreator.create(dbSession, newRule));
+ NewCustomRule newCustomRule = toNewCustomRule(request);
+ writeResponse(dbSession, request, response, ruleService.createCustomRule(newCustomRule, dbSession).ruleDto());
} catch (ReactivationException e) {
response.stream().setStatus(HTTP_CONFLICT);
writeResponse(dbSession, request, response, e.ruleKey());
}
}
- private void writeResponse(DbSession dbSession, Request request, Response response, RuleKey ruleKey) {
- writeProtobuf(createResponse(dbSession, ruleKey), request, response);
+ private static NewCustomRule toNewCustomRule(Request request) {
+ RuleKey templateKey = RuleKey.parse(request.mandatoryParam(PARAM_TEMPLATE_KEY));
+ NewCustomRule newRule = NewCustomRule.createForCustomRule(
+ RuleKey.of(templateKey.repository(), request.mandatoryParam(PARAM_CUSTOM_KEY)), templateKey)
+ .setName(request.mandatoryParam(PARAM_NAME))
+ .setMarkdownDescription(request.mandatoryParam(PARAM_DESCRIPTION))
+ .setStatus(RuleStatus.valueOf(request.mandatoryParam(PARAM_STATUS)))
+ .setPreventReactivation(request.mandatoryParamAsBoolean(PARAM_PREVENT_REACTIVATION))
+ .setSeverity(request.param(PARAM_SEVERITY))
+ .setType(ofNullable(request.param(PARAM_TYPE)).map(RuleType::valueOf).orElse(null))
+ .setCleanCodeAttribute(ofNullable(request.param(PARAM_CLEAN_CODE_ATTRIBUTE)).map(CleanCodeAttribute::valueOf).orElse(null));
+ String params = request.param(PARAMS);
+ if (!isNullOrEmpty(params)) {
+ newRule.setParameters(KeyValueFormat.parse(params));
+ }
+ String impacts = request.param(PARAM_IMPACTS);
+ if (!isNullOrEmpty(impacts)) {
+ newRule.setImpacts(KeyValueFormat.parse(impacts).entrySet().stream()
+ .map(e -> new NewCustomRule.Impact(SoftwareQuality.valueOf(e.getKey()), org.sonar.api.issue.impact.Severity.valueOf(e.getValue())))
+ .toList());
+ }
+ return newRule;
+ }
+
+ private void writeResponse(DbSession dbSession, Request request, Response response, RuleDto rule) {
+ writeProtobuf(createResponse(dbSession, rule), request, response);
}
- private Rules.CreateResponse createResponse(DbSession dbSession, RuleKey ruleKey) {
+ private void writeResponse(DbSession dbSession, Request request, Response response, RuleKey ruleKey) {
RuleDto rule = dbClient.ruleDao().selectByKey(dbSession, ruleKey)
.orElseThrow(() -> new IllegalStateException(String.format("Cannot load rule, that has just been created '%s'", ruleKey)));
+ writeProtobuf(createResponse(dbSession, rule), request, response);
+ }
+
+ private Rules.CreateResponse createResponse(DbSession dbSession, RuleDto rule) {
List<RuleDto> templateRules = new ArrayList<>();
if (rule.isCustomRule()) {
Optional<RuleDto> templateRule = dbClient.ruleDao().selectByUuid(rule.getTemplateUuid(), dbSession);
+++ /dev/null
-/*
- * 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.util;
-
-import java.util.Arrays;
-
-public class TypeValidationsTesting {
- private TypeValidationsTesting() {
- // utility class
- }
-
- public static TypeValidations newFullTypeValidations() {
- return new TypeValidations(Arrays.asList(
- new BooleanTypeValidation(),
- new IntegerTypeValidation(),
- new FloatTypeValidation(),
- new StringTypeValidation(),
- new StringListTypeValidation()
- ));
- }
-}
import org.sonar.server.qualityprofile.builtin.RuleActivator;
import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
import org.sonar.server.qualityprofile.ws.QProfilesWsModule;
-import org.sonar.server.rule.RuleCreator;
+import org.sonar.server.common.rule.RuleCreator;
import org.sonar.server.rule.RuleDefinitionsLoader;
import org.sonar.server.rule.RuleDescriptionFormatter;
import org.sonar.server.rule.RuleUpdater;