]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23593 UpdateAction accepts impacts
authorOrlovAlexander <alexander.orlov@sonarsource.com>
Wed, 13 Nov 2024 16:56:07 +0000 (17:56 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 15 Nov 2024 20:02:42 +0000 (20:02 +0000)
server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/UpdateActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/common/ParamParsingUtils.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/common/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/RuleUpdate.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/RuleUpdater.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/UpdateAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/common/ParamParsingUtilsTest.java [new file with mode: 0644]

index 7835dec89dfba71e4ee3bab61b4bc55c40986ce8..5c4197ddb81a6c33eda94e6e264d6c8da2b7f459 100644 (file)
  */
 package org.sonar.server.rule.ws;
 
-import org.junit.Rule;
-import org.junit.Test;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.api.issue.impact.SoftwareQuality;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.System2;
 import org.sonar.core.util.UuidFactoryFast;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbTester;
+import org.sonar.db.issue.ImpactDto;
 import org.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.db.user.UserDto;
@@ -49,6 +53,7 @@ import org.sonarqube.ws.Rules;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.tuple;
 import static org.mockito.AdditionalAnswers.returnsFirstArg;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
@@ -68,17 +73,17 @@ import static org.sonar.server.rule.ws.UpdateAction.PARAM_REMEDIATION_FN_TYPE;
 import static org.sonar.server.rule.ws.UpdateAction.PARAM_TAGS;
 import static org.sonar.test.JsonAssert.assertJson;
 
-public class UpdateActionIT {
+class UpdateActionIT {
 
   private static final long PAST = 10000L;
 
-  @Rule
+  @RegisterExtension
   public DbTester db = DbTester.create();
 
-  @Rule
+  @RegisterExtension
   public EsTester es = EsTester.create();
 
-  @Rule
+  @RegisterExtension
   public UserSessionRule userSession = UserSessionRule.standalone();
 
   private final DbClient dbClient = db.getDbClient();
@@ -95,7 +100,7 @@ public class UpdateActionIT {
   private final WsActionTester ws = new WsActionTester(underTest);
 
   @Test
-  public void check_definition() {
+  void check_definition() {
     assertThat(ws.getDef().isPost()).isTrue();
     assertThat(ws.getDef().isInternal()).isFalse();
     assertThat(ws.getDef().responseExampleAsString()).isNotNull();
@@ -103,7 +108,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void update_custom_rule() {
+  void update_custom_rule() {
     logInAsQProfileAdministrator();
     RuleDto templateRule = db.rules().insert(
       r -> r.setRuleKey(RuleKey.of("java", "S001")),
@@ -156,7 +161,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void update_tags() {
+  void update_tags() {
     logInAsQProfileAdministrator();
 
     RuleDto rule = db.rules().insert(setSystemTags("stag1", "stag2"), setTags("tag1", "tag2"), r -> r.setNoteData(null).setNoteUserUuid(null));
@@ -175,7 +180,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void update_rule_remediation_function() {
+  void update_rule_remediation_function() {
     logInAsQProfileAdministrator();
 
     RuleDto rule = db.rules().insert(
@@ -217,7 +222,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void update_note() {
+  void update_note() {
     UserDto userHavingUpdatingNote = db.users().insertUser();
     RuleDto rule = db.rules().insert(m -> m.setNoteData("old data").setNoteUserUuid(userHavingUpdatingNote.getUuid()));
     UserDto userAuthenticated = db.users().insertUser();
@@ -241,7 +246,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void fail_to_update_custom_when_description_is_empty() {
+  void fail_to_update_custom_when_description_is_empty() {
     logInAsQProfileAdministrator();
     RuleDto templateRule = db.rules().insert(
       r -> r.setRuleKey(RuleKey.of("java", "S001")),
@@ -269,7 +274,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void throw_IllegalArgumentException_if_trying_to_update_builtin_rule_description() {
+  void throw_IllegalArgumentException_if_trying_to_update_builtin_rule_description() {
     logInAsQProfileAdministrator();
     RuleDto rule = db.rules().insert();
 
@@ -285,7 +290,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void throw_ForbiddenException_if_not_profile_administrator() {
+  void throw_ForbiddenException_if_not_profile_administrator() {
     userSession.logIn();
 
     assertThatThrownBy(() -> {
@@ -295,7 +300,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void throw_UnauthorizedException_if_not_logged_in() {
+  void throw_UnauthorizedException_if_not_logged_in() {
     assertThatThrownBy(() -> {
       ws.newRequest().setMethod("POST").execute();
     })
@@ -303,7 +308,7 @@ public class UpdateActionIT {
   }
 
   @Test
-  public void returnRuleCleanCodeFields_whenEndpointIsCalled() {
+  void returnRuleCleanCodeFields_whenEndpointIsCalled() {
     UserDto userAuthenticated = db.users().insertUser();
     userSession.logIn(userAuthenticated).addPermission(ADMINISTER_QUALITY_PROFILES);
 
@@ -325,6 +330,98 @@ public class UpdateActionIT {
     assertThat(updateResponse.getRule()).extracting(Rules.Rule::getCleanCodeAttributeCategory).isEqualTo(Common.CleanCodeAttributeCategory.INTENTIONAL);
   }
 
+  @Test
+  void update_whenImpactProvided_shouldAlsoUpdateStandardSeverity() {
+    logInAsQProfileAdministrator();
+
+    RuleDto templateRule = db.rules().insert(
+      r -> r.setRuleKey(RuleKey.of("java", "S001")),
+      r -> r.setIsTemplate(true),
+      r -> r.setCreatedAt(PAST),
+      r -> r.setUpdatedAt(PAST));
+
+    RuleDto rule = db.rules().insert(ruleDto -> ruleDto
+      .setTemplateUuid(templateRule.getUuid())
+      .setType(RuleType.BUG)
+      .setSeverity(Severity.MAJOR)
+      .replaceAllDefaultImpacts(List.of(new ImpactDto(SoftwareQuality.RELIABILITY, org.sonar.api.issue.impact.Severity.HIGH))));
+
+    Rules.UpdateResponse updateResponse = ws.newRequest().setMethod("POST")
+      .setParam("key", rule.getKey().toString())
+      .setParam("impacts", "RELIABILITY=BLOCKER")
+      .executeProtobuf(Rules.UpdateResponse.class);
+
+    Rules.Rule ruleResponse = updateResponse.getRule();
+    assertThat(ruleResponse)
+      .extracting(r -> r.getImpacts().getImpactsList().stream().findFirst()
+        .orElseThrow(() -> new IllegalStateException("Impact is a mandatory field in the response.")))
+      .extracting(Common.Impact::getSoftwareQuality, Common.Impact::getSeverity)
+      .containsExactly(Common.SoftwareQuality.RELIABILITY, Common.ImpactSeverity.ImpactSeverity_BLOCKER);
+    assertThat(ruleResponse.getSeverity()).isEqualTo("BLOCKER");
+  }
+
+  @Test
+  void update_whenMultipleImpactsProvided_shouldOnlyUpdateExisting() {
+    logInAsQProfileAdministrator();
+
+    RuleDto templateRule = db.rules().insert(
+      r -> r.setRuleKey(RuleKey.of("java", "S001")),
+      r -> r.setIsTemplate(true),
+      r -> r.setCreatedAt(PAST),
+      r -> r.setUpdatedAt(PAST));
+
+    RuleDto rule = db.rules().insert(ruleDto -> ruleDto
+      .setTemplateUuid(templateRule.getUuid())
+      .setType(RuleType.BUG)
+      .setSeverity(Severity.MAJOR)
+      .replaceAllDefaultImpacts(List.of(
+        new ImpactDto(SoftwareQuality.RELIABILITY, org.sonar.api.issue.impact.Severity.MEDIUM),
+        new ImpactDto(SoftwareQuality.SECURITY, org.sonar.api.issue.impact.Severity.MEDIUM))));
+
+    Rules.UpdateResponse updateResponse = ws.newRequest().setMethod("POST")
+      .setParam("key", rule.getKey().toString())
+      .setParam("impacts", "RELIABILITY=BLOCKER;SECURITY=BLOCKER;MAINTAINABILITY=BLOCKER")
+      .executeProtobuf(Rules.UpdateResponse.class);
+
+    Rules.Rule ruleResponse = updateResponse.getRule();
+    assertThat(ruleResponse.getImpacts().getImpactsList())
+      .extracting(Common.Impact::getSoftwareQuality, Common.Impact::getSeverity)
+      .containsExactlyInAnyOrder(
+        tuple(Common.SoftwareQuality.RELIABILITY, Common.ImpactSeverity.ImpactSeverity_BLOCKER),
+        tuple(Common.SoftwareQuality.SECURITY, Common.ImpactSeverity.ImpactSeverity_BLOCKER));
+    assertThat(ruleResponse.getSeverity()).isEqualTo("BLOCKER");
+  }
+
+  @Test
+  void update_whenImpactsDontMatch_shouldNotUpdateAnything() {
+    logInAsQProfileAdministrator();
+
+    RuleDto templateRule = db.rules().insert(
+      r -> r.setRuleKey(RuleKey.of("java", "S001")),
+      r -> r.setIsTemplate(true),
+      r -> r.setCreatedAt(PAST),
+      r -> r.setUpdatedAt(PAST));
+
+    RuleDto rule = db.rules().insert(ruleDto -> ruleDto
+      .setTemplateUuid(templateRule.getUuid())
+      .setType(RuleType.BUG)
+      .setSeverity(Severity.MAJOR)
+      .replaceAllDefaultImpacts(List.of(
+        new ImpactDto(SoftwareQuality.RELIABILITY, org.sonar.api.issue.impact.Severity.MEDIUM))));
+
+    Rules.UpdateResponse updateResponse = ws.newRequest().setMethod("POST")
+      .setParam("key", rule.getKey().toString())
+      .setParam("impacts", "SECURITY=BLOCKER")
+      .executeProtobuf(Rules.UpdateResponse.class);
+
+    Rules.Rule ruleResponse = updateResponse.getRule();
+    assertThat(ruleResponse.getImpacts().getImpactsList())
+      .extracting(Common.Impact::getSoftwareQuality, Common.Impact::getSeverity)
+      .containsExactlyInAnyOrder(
+        tuple(Common.SoftwareQuality.RELIABILITY, Common.ImpactSeverity.MEDIUM));
+    assertThat(ruleResponse.getSeverity()).isEqualTo("MAJOR");
+  }
+
   private void logInAsQProfileAdministrator() {
     userSession
       .logIn()
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/common/ParamParsingUtils.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/common/ParamParsingUtils.java
new file mode 100644 (file)
index 0000000..80b957f
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.common;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.sonar.api.issue.impact.Severity;
+import org.sonar.api.issue.impact.SoftwareQuality;
+
+public class ParamParsingUtils {
+  private ParamParsingUtils() {
+    // utility class
+  }
+
+  public static Pair<SoftwareQuality, Severity> parseImpact(String impact) {
+    String[] parts = impact.split("=");
+    if (parts.length != 2) {
+      throw new IllegalArgumentException("Invalid impact format: " + impact);
+    }
+    return Pair.of(SoftwareQuality.valueOf(parts[0]),
+      Severity.valueOf(parts[1]));
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/common/package-info.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/common/package-info.java
new file mode 100644 (file)
index 0000000..fb8006c
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.common;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 83fea43fa584447f728a7d58ed32babcab982fd7..1916e6681dc2d99672b9fc33a6d5118220e68c1d 100644 (file)
@@ -50,6 +50,7 @@ import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
 import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByUserBuilder;
 import static org.sonar.core.rule.ImpactSeverityMapper.mapImpactSeverity;
 import static org.sonar.db.component.BranchType.BRANCH;
+import static org.sonar.server.common.ParamParsingUtils.parseImpact;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_SEVERITY;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IMPACT;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
@@ -219,13 +220,4 @@ public class SetSeverityAction implements IssuesWsAction {
       throw new IllegalArgumentException("One of the parameters 'severity' or 'impact' must be provided");
     }
   }
-
-  private static Pair<SoftwareQuality, org.sonar.api.issue.impact.Severity> parseImpact(String impact) {
-    String[] parts = impact.split("=");
-    if (parts.length != 2) {
-      throw new IllegalArgumentException("Invalid impact format: " + impact);
-    }
-    return Pair.of(SoftwareQuality.valueOf(parts[0]),
-      org.sonar.api.issue.impact.Severity.valueOf(parts[1]));
-  }
 }
index 3cd5c21b45c942ce510d84d8989a2665ca6bace2..e95c76ab8f77edcc23c9a49e454bbf657ee5af3b 100644 (file)
  */
 package org.sonar.server.rule;
 
+import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
+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.server.debt.DebtRemediationFunction;
@@ -43,6 +46,7 @@ public class RuleUpdate {
   private boolean changeName = false;
   private boolean changeDescription = false;
   private boolean changeSeverity = false;
+  private boolean changeImpacts = false;
   private boolean changeStatus = false;
   private boolean changeParameters = false;
   private final RuleUpdateUseCase useCase;
@@ -53,6 +57,7 @@ public class RuleUpdate {
   private String name;
   private String markdownDescription;
   private String severity;
+  private final Map<SoftwareQuality, Severity> impactSeverities = new EnumMap<>(SoftwareQuality.class);
   private RuleStatus status;
   private final Map<String, String> parameters = new HashMap<>();
 
@@ -140,6 +145,21 @@ public class RuleUpdate {
     return this;
   }
 
+  public Map<SoftwareQuality, Severity> getImpactSeverities() {
+    return impactSeverities;
+  }
+
+  /**
+   * Impacts to be updated (only for custom rules)
+   */
+  public RuleUpdate setImpactSeverities(Map<SoftwareQuality, Severity> impactSeverities) {
+    checkCustomRule();
+    this.impactSeverities.clear();
+    this.impactSeverities.putAll(impactSeverities);
+    changeImpacts = true;
+    return this;
+  }
+
   @CheckForNull
   public RuleStatus getStatus() {
     return status;
@@ -200,6 +220,10 @@ public class RuleUpdate {
     return changeSeverity;
   }
 
+  public boolean isChangeImpacts() {
+    return changeImpacts;
+  }
+
   public boolean isChangeStatus() {
     return changeStatus;
   }
@@ -213,7 +237,7 @@ public class RuleUpdate {
   }
 
   private boolean isCustomRuleFieldsEmpty() {
-    return !changeName && !changeDescription && !changeSeverity && !changeStatus && !changeParameters;
+    return !changeName && !changeDescription && !changeSeverity && !changeStatus && !changeParameters && !changeImpacts;
   }
 
   private void checkCustomRule() {
index 5fa23689c0c4c25e6ea70cd5a9e14d6b734e7918..357d57bde9f5ca54ff892bea26142b154863037e 100644 (file)
@@ -27,13 +27,16 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
 import javax.annotation.Nonnull;
 import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.sonar.api.issue.impact.SoftwareQuality;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.server.debt.DebtRemediationFunction;
 import org.sonar.api.server.rule.internal.ImpactMapper;
@@ -55,6 +58,9 @@ import static com.google.common.base.Strings.isNullOrEmpty;
 import static com.google.common.collect.FluentIterable.from;
 import static com.google.common.collect.Lists.newArrayList;
 import static org.apache.commons.lang3.StringUtils.isBlank;
+import static org.sonar.api.server.rule.internal.ImpactMapper.convertToDeprecatedSeverity;
+import static org.sonar.api.server.rule.internal.ImpactMapper.convertToRuleType;
+import static org.sonar.core.rule.ImpactSeverityMapper.mapImpactSeverity;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
 
 @ServerSide
@@ -109,6 +115,9 @@ public class RuleUpdater {
     if (update.isChangeSeverity()) {
       updateSeverity(update, rule);
     }
+    if (update.isChangeImpacts()) {
+      updateImpactSeverityAndStandardSeverityIfTypeMatch(update, rule);
+    }
     if (update.isChangeStatus()) {
       updateStatus(update, rule);
     }
@@ -129,7 +138,24 @@ public class RuleUpdater {
       .stream()
       .filter(i -> i.getSoftwareQuality().equals(ImpactMapper.convertToSoftwareQuality(rule.getEnumType())))
       .findFirst()
-      .ifPresent(i -> i.setSeverity(ImpactMapper.convertToImpactSeverity(severity)));
+      .ifPresent(i -> i.setSeverity(mapImpactSeverity(severity)));
+  }
+
+  private static void updateImpactSeverityAndStandardSeverityIfTypeMatch(RuleUpdate update, RuleDto rule) {
+    Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> impacts = update.getImpactSeverities();
+    if (impacts.isEmpty()) {
+      throw new IllegalArgumentException("Impacts are is missing");
+    }
+    impacts.forEach((key, value) -> rule.getDefaultImpacts()
+      .stream()
+      .filter(i -> i.getSoftwareQuality().equals(key))
+      .findFirst()
+      .ifPresent(i -> {
+        i.setSeverity(value);
+        if (Objects.equals(convertToRuleType(key), RuleType.valueOf(rule.getType()))) {
+          rule.setSeverity(convertToDeprecatedSeverity(value));
+        }
+      }));
   }
 
   private static void updateName(RuleUpdate update, RuleDto rule) {
index 1f289d2e169e72e261becac789c1a215dfcdad5e..4097700b240b54e1d7832b355c067e21e36422e2 100644 (file)
@@ -23,8 +23,12 @@ import com.google.common.base.Splitter;
 import com.google.common.io.Resources;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.EnumMap;
 import java.util.List;
+import java.util.Map;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+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;
@@ -50,6 +54,7 @@ import static java.lang.String.format;
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
 import static java.util.Optional.ofNullable;
+import static org.sonar.server.common.ParamParsingUtils.parseImpact;
 import static org.sonar.server.rule.ws.CreateAction.KEY_MAXIMUM_LENGTH;
 import static org.sonar.server.rule.ws.CreateAction.NAME_MAXIMUM_LENGTH;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
@@ -65,6 +70,7 @@ public class UpdateAction implements RulesWsAction {
   public static final String PARAM_NAME = "name";
   public static final String PARAM_DESCRIPTION = "markdownDescription";
   public static final String PARAM_SEVERITY = "severity";
+  public static final String PARAM_IMPACTS = "impacts";
   public static final String PARAM_STATUS = "status";
   public static final String PARAMS = "params";
 
@@ -91,13 +97,14 @@ public class UpdateAction implements RulesWsAction {
       .setDescription("Update an existing rule.<br>" +
         "Requires the 'Administer Quality Profiles' permission")
       .setChangelog(
-        new Change("10.2", "The field 'severity' and 'type' in the response have been deprecated, use 'impacts' instead."),
+        new Change("10.8", String.format("Parameter %s was added.", PARAM_IMPACTS)),
+        new Change("10.8", String.format("The parameter '%s' is not deprecated anymore.", PARAM_SEVERITY)),
+        new Change("10.8", "The field 'severity' and 'type' in the response are not deprecated anymore."),
         new Change("10.4", String.format("The parameter '%s' is deprecated.", PARAM_SEVERITY)),
         new Change("10.4", "Updating a removed rule is now possible."),
-        new Change("10.8", String.format("The parameter '%s' is not deprecated anymore.", PARAM_SEVERITY)))
-      .setSince("4.4")
-      .setChangelog(
+        new Change("10.2", "The field 'severity' and 'type' in the response have been deprecated, use 'impacts' instead."),
         new Change("10.2", "Add 'impacts', 'cleanCodeAttribute', 'cleanCodeAttributeCategory' fields to the response"))
+      .setSince("4.4")
       .setHandler(this);
 
     action.createParam(PARAM_KEY)
@@ -148,6 +155,11 @@ public class UpdateAction implements RulesWsAction {
       .setDescription("Rule severity (Only when updating a custom rule)")
       .setPossibleValues(Severity.ALL);
 
+    action
+      .createParam(PARAM_IMPACTS)
+      .setDescription("Rule impacts, semicolon-separated (Only when updating a custom rule impact severity)")
+      .setExampleValue("MAINTAINABILITY=HIGH;SECURITY=LOW");
+
     action
       .createParam(PARAM_STATUS)
       .setPossibleValues(RuleStatus.values())
@@ -186,6 +198,14 @@ public class UpdateAction implements RulesWsAction {
       update.setMarkdownDescription(description);
     }
     String severity = request.param(PARAM_SEVERITY);
+    String impacts = request.param(PARAM_IMPACTS);
+    if (impacts != null && severity != null) {
+      throw new IllegalArgumentException("Both 'severity' and 'impacts' parameters cannot be set at the same time");
+    }
+    if (impacts != null) {
+      Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> parsedImpact = parseImpacts(impacts);
+      update.setImpactSeverities(parsedImpact);
+    }
     if (severity != null) {
       update.setSeverity(severity);
     }
@@ -263,4 +283,13 @@ public class UpdateAction implements RulesWsAction {
 
     return responseBuilder.build();
   }
+
+  private static Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> parseImpacts(String impacts) {
+    Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> parsedImpacts = new EnumMap<>(SoftwareQuality.class);
+    for (String impact : impacts.split(";")) {
+      Pair<SoftwareQuality, org.sonar.api.issue.impact.Severity> pair = parseImpact(impact);
+      parsedImpacts.put(pair.getKey(), pair.getValue());
+    }
+    return parsedImpacts;
+  }
 }
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/common/ParamParsingUtilsTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/common/ParamParsingUtilsTest.java
new file mode 100644 (file)
index 0000000..494cd34
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.common;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.jupiter.api.Test;
+import org.sonar.api.issue.impact.Severity;
+import org.sonar.api.issue.impact.SoftwareQuality;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ParamParsingUtilsTest {
+
+  @Test
+  void parseImpact_whenCorrectParam_ShouldReturnExpectedResult() {
+    Pair<SoftwareQuality, Severity> result = ParamParsingUtils.parseImpact("MAINTAINABILITY=BLOCKER");
+    assertEquals(SoftwareQuality.MAINTAINABILITY, result.getKey());
+    assertEquals(Severity.BLOCKER, result.getValue());
+  }
+
+  @Test
+  void parseImpact_whenInvalidParam_ShouldThrowException() {
+    assertThatThrownBy(() -> ParamParsingUtils.parseImpact("MAINTAINABILITY"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Invalid impact format: MAINTAINABILITY");
+  }
+
+  @Test
+  void parseImpact_whenInvalidValues_ShouldThrowException() {
+    assertThatThrownBy(() -> ParamParsingUtils.parseImpact("MAINTAINABILITY=MAJOR"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("No enum constant org.sonar.api.issue.impact.Severity.MAJOR");
+    assertThatThrownBy(() -> ParamParsingUtils.parseImpact("BUG=LOW"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("No enum constant org.sonar.api.issue.impact.SoftwareQuality.BUG");
+  }
+}
+