]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15919 added filtering rule set change events based on languages user subscribed to
authorLukasz Jarocki <lukasz.jarocki@sonarsource.com>
Tue, 8 Feb 2022 12:06:37 +0000 (13:06 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Feb 2022 15:48:04 +0000 (15:48 +0000)
106 files changed:
build.gradle
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotification.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationBuilder.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationHandler.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTemplate.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfile.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileInsert.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileInsertImpl.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileLoader.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileRepository.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryImpl.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdate.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdateImpl.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQualityProfilesUpdateListener.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DescendantProfilesSupplier.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DistributedRuleActivatorEventsDistributor.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QProfileName.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventService.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventServiceImpl.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivationContext.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivator.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivatorEventsDistributor.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/StandaloneRuleActivatorEventsDistributor.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/package-info.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationHandlerTest.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTemplateTest.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTest.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileInsertImplTest.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileLoaderTest.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryImplTest.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdateImplTest.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQualityProfilesUpdateListenerTest.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/RuleActivatorTest.java [deleted file]
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryRule.java [deleted file]
server/sonar-webserver-pushapi/build.gradle
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventService.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java
server/sonar-webserver-webapi/build.gradle
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileBackuperImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileCopier.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileFactory.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileFactoryImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileResetImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileRulesImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileTreeImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/RegisterQualityProfiles.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotification.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationBuilder.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationHandler.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTemplate.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfile.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsert.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsertImpl.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileLoader.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepository.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdate.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdateImpl.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQualityProfilesUpdateListener.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/DescendantProfilesSupplier.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/QProfileName.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/RuleActivationContext.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/RuleActivator.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/AddProjectAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/CreateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/RemoveProjectAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileBackuperImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileComparisonTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileCopierTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileFactoryImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileResetImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileRuleImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileRulesImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileTreeImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/RegisterQualityProfilesNotificationTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/RegisterQualityProfilesTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationHandlerTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTemplateTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsertImplTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileLoaderTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryRule.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdateImplTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQualityProfilesUpdateListenerTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/RuleActivatorTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/AddProjectActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ChangeParentActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/CreateActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/InheritanceActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/QProfilesWsMediumTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/RemoveProjectActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/SearchActionTest.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java
sonar-core/src/main/java/org/sonar/core/util/RuleChange.java
sonar-core/src/main/java/org/sonar/core/util/RuleSetChangeEvent.java
sonar-core/src/test/java/org/sonar/core/util/RuleSetChangeEventTest.java [new file with mode: 0644]

index fda070c2ccc59c52b8d56ec730c087253f8453af..e674806af4161e3bebe8705e3d13dd674a6e0af0 100644 (file)
@@ -337,6 +337,7 @@ subprojects {
         entry 'okhttp'
         entry 'mockwebserver'
       }
+      dependency 'org.json:json:20211205'
       dependency 'com.tngtech.java:junit-dataprovider:1.13.1'
       dependency 'info.picocli:picocli:3.6.1'
       dependencySet(group: 'io.jsonwebtoken', version: '0.11.2') {
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotification.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotification.java
deleted file mode 100644 (file)
index df8d85c..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import org.sonar.api.notifications.Notification;
-
-public class BuiltInQPChangeNotification extends Notification {
-  static final String TYPE = "built-in-quality-profiles";
-
-  public BuiltInQPChangeNotification() {
-    super(TYPE);
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationBuilder.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationBuilder.java
deleted file mode 100644 (file)
index 7de478f..0000000
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.IntStream;
-import org.sonar.api.notifications.Notification;
-
-import static com.google.common.base.Preconditions.checkState;
-import static java.lang.Integer.parseInt;
-import static java.lang.Long.parseLong;
-import static java.lang.String.format;
-import static java.util.Objects.requireNonNull;
-
-public class BuiltInQPChangeNotificationBuilder {
-
-  private static final String NUMBER_OF_PROFILES = "numberOfProfiles";
-  private static final String PROFILE_NAME = ".profileName";
-  private static final String LANGUAGE_KEY = ".languageKey";
-  private static final String LANGUAGE_NAME = ".languageName";
-  private static final String NEW_RULES = ".newRules";
-  private static final String UPDATED_RULES = ".updatedRules";
-  private static final String REMOVED_RULES = ".removedRules";
-  private static final String START_DATE = ".startDate";
-  private static final String END_DATE = ".endDate";
-
-  private final List<Profile> profiles = new ArrayList<>();
-
-  public BuiltInQPChangeNotificationBuilder addProfile(Profile profile) {
-    profiles.add(profile);
-    return this;
-  }
-
-  public BuiltInQPChangeNotification build() {
-    BuiltInQPChangeNotification notification = new BuiltInQPChangeNotification();
-    notification.setFieldValue(NUMBER_OF_PROFILES, String.valueOf(profiles.size()));
-    AtomicInteger count = new AtomicInteger();
-    profiles.forEach(profile -> {
-      int index = count.getAndIncrement();
-      notification.setFieldValue(index + PROFILE_NAME, profile.getProfileName());
-      notification.setFieldValue(index + LANGUAGE_KEY, profile.getLanguageKey());
-      notification.setFieldValue(index + LANGUAGE_NAME, profile.getLanguageName());
-      notification.setFieldValue(index + NEW_RULES, String.valueOf(profile.getNewRules()));
-      notification.setFieldValue(index + UPDATED_RULES, String.valueOf(profile.getUpdatedRules()));
-      notification.setFieldValue(index + REMOVED_RULES, String.valueOf(profile.getRemovedRules()));
-      notification.setFieldValue(index + START_DATE, String.valueOf(profile.getStartDate()));
-      notification.setFieldValue(index + END_DATE, String.valueOf(profile.getEndDate()));
-    });
-    return notification;
-  }
-
-  public static BuiltInQPChangeNotificationBuilder parse(Notification notification) {
-    checkState(BuiltInQPChangeNotification.TYPE.equals(notification.getType()),
-      "Expected notification of type %s but got %s", BuiltInQPChangeNotification.TYPE, notification.getType());
-    BuiltInQPChangeNotificationBuilder notif = new BuiltInQPChangeNotificationBuilder();
-    String numberOfProfilesText = notification.getFieldValue(NUMBER_OF_PROFILES);
-    checkState(numberOfProfilesText != null, "Could not read the built-in quality profile notification");
-    Integer numberOfProfiles = Integer.valueOf(numberOfProfilesText);
-    IntStream.rangeClosed(0, numberOfProfiles - 1)
-      .mapToObj(index -> Profile.newBuilder()
-        .setProfileName(getNonNullFieldValue(notification, index + PROFILE_NAME))
-        .setLanguageKey(getNonNullFieldValue(notification, index + LANGUAGE_KEY))
-        .setLanguageName(getNonNullFieldValue(notification, index + LANGUAGE_NAME))
-        .setNewRules(parseInt(getNonNullFieldValue(notification, index + NEW_RULES)))
-        .setUpdatedRules(parseInt(getNonNullFieldValue(notification, index + UPDATED_RULES)))
-        .setRemovedRules(parseInt(getNonNullFieldValue(notification, index + REMOVED_RULES)))
-        .setStartDate(parseLong(getNonNullFieldValue(notification, index + START_DATE)))
-        .setEndDate(parseLong(getNonNullFieldValue(notification, index + END_DATE)))
-        .build())
-      .forEach(notif::addProfile);
-    return notif;
-  }
-
-  private static String getNonNullFieldValue(Notification notification, String key) {
-    String value = notification.getFieldValue(key);
-    return requireNonNull(value, format("Notification field '%s' is null", key));
-  }
-
-  public List<Profile> getProfiles() {
-    return profiles;
-  }
-
-  public static class Profile {
-    private final String profileName;
-    private final String languageKey;
-    private final String languageName;
-    private final int newRules;
-    private final int updatedRules;
-    private final int removedRules;
-    private final long startDate;
-    private final long endDate;
-
-    public Profile(Builder builder) {
-      this.profileName = builder.profileName;
-      this.languageKey = builder.languageKey;
-      this.languageName = builder.languageName;
-      this.newRules = builder.newRules;
-      this.updatedRules = builder.updatedRules;
-      this.removedRules = builder.removedRules;
-      this.startDate = builder.startDate;
-      this.endDate = builder.endDate;
-    }
-
-    public String getProfileName() {
-      return profileName;
-    }
-
-    public String getLanguageKey() {
-      return languageKey;
-    }
-
-    public String getLanguageName() {
-      return languageName;
-    }
-
-    public int getNewRules() {
-      return newRules;
-    }
-
-    public int getUpdatedRules() {
-      return updatedRules;
-    }
-
-    public int getRemovedRules() {
-      return removedRules;
-    }
-
-    public long getStartDate() {
-      return startDate;
-    }
-
-    public long getEndDate() {
-      return endDate;
-    }
-
-    public static Builder newBuilder() {
-      return new Builder();
-    }
-
-    public static class Builder {
-      private String profileName;
-      private String languageKey;
-      private String languageName;
-      private int newRules;
-      private int updatedRules;
-      private int removedRules;
-      private long startDate;
-      private long endDate;
-
-      private Builder() {
-      }
-
-      public Builder setLanguageKey(String languageKey) {
-        this.languageKey = requireNonNull(languageKey, "languageKEy should not be null");
-        return this;
-      }
-
-      public Builder setLanguageName(String languageName) {
-        this.languageName = requireNonNull(languageName, "languageName should not be null");
-        return this;
-      }
-
-      public Builder setProfileName(String profileName) {
-        this.profileName = requireNonNull(profileName, "profileName should not be null");
-        return this;
-      }
-
-      public Builder setNewRules(int newRules) {
-        checkState(newRules >= 0, "newRules should not be negative");
-        this.newRules = newRules;
-        return this;
-      }
-
-      public Builder setUpdatedRules(int updatedRules) {
-        checkState(updatedRules >= 0, "updatedRules should not be negative");
-        this.updatedRules = updatedRules;
-        return this;
-      }
-
-      public Builder setRemovedRules(int removedRules) {
-        checkState(removedRules >= 0, "removedRules should not be negative");
-        this.removedRules = removedRules;
-        return this;
-      }
-
-      public Builder setStartDate(long startDate) {
-        this.startDate = startDate;
-        return this;
-      }
-
-      public Builder setEndDate(long endDate) {
-        this.endDate = endDate;
-        return this;
-      }
-
-      public Profile build() {
-        return new Profile(this);
-      }
-    }
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationHandler.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationHandler.java
deleted file mode 100644 (file)
index 5402ead..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.Collection;
-import java.util.Optional;
-import java.util.Set;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.server.notification.EmailNotificationHandler;
-import org.sonar.server.notification.NotificationDispatcherMetadata;
-import org.sonar.server.notification.email.EmailNotificationChannel;
-import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
-
-import static org.sonar.core.util.stream.MoreCollectors.toSet;
-
-public class BuiltInQPChangeNotificationHandler extends EmailNotificationHandler<BuiltInQPChangeNotification> {
-  private final DbClient dbClient;
-
-  public BuiltInQPChangeNotificationHandler(DbClient dbClient, EmailNotificationChannel emailNotificationChannel) {
-    super(emailNotificationChannel);
-    this.dbClient = dbClient;
-  }
-
-  @Override
-  public Optional<NotificationDispatcherMetadata> getMetadata() {
-    return Optional.empty();
-  }
-
-  @Override
-  public Class<BuiltInQPChangeNotification> getNotificationClass() {
-    return BuiltInQPChangeNotification.class;
-  }
-
-  @Override
-  public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<BuiltInQPChangeNotification> notifications) {
-    try (DbSession session = dbClient.openSession(false)) {
-      return dbClient.authorizationDao()
-        .selectQualityProfileAdministratorLogins(session)
-        .stream()
-        .flatMap(t -> notifications.stream().map(notification -> new EmailDeliveryRequest(t.getEmail(), notification)))
-        .collect(toSet());
-    }
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTemplate.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTemplate.java
deleted file mode 100644 (file)
index bd10966..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.Comparator;
-import java.util.Date;
-import javax.annotation.CheckForNull;
-import org.sonar.api.notifications.Notification;
-import org.sonar.api.platform.Server;
-import org.sonar.server.issue.notification.EmailMessage;
-import org.sonar.server.issue.notification.EmailTemplate;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.sonar.api.utils.DateUtils.formatDate;
-import static org.sonar.server.qualityprofile.BuiltInQPChangeNotificationBuilder.Profile;
-import static org.sonar.server.qualityprofile.BuiltInQPChangeNotificationBuilder.parse;
-
-public class BuiltInQPChangeNotificationTemplate implements EmailTemplate {
-
-  private final Server server;
-
-  public BuiltInQPChangeNotificationTemplate(Server server) {
-    this.server = server;
-  }
-
-  @Override
-  @CheckForNull
-  public EmailMessage format(Notification notification) {
-    if (!BuiltInQPChangeNotification.TYPE.equals(notification.getType())) {
-      return null;
-    }
-
-    BuiltInQPChangeNotificationBuilder profilesNotification = parse(notification);
-    StringBuilder message = new StringBuilder("The following built-in profiles have been updated:\n\n");
-    profilesNotification.getProfiles().stream()
-      .sorted(Comparator.comparing(Profile::getLanguageName).thenComparing(Profile::getProfileName))
-      .forEach(profile -> {
-        message.append("\"")
-          .append(profile.getProfileName())
-          .append("\" - ")
-          .append(profile.getLanguageName())
-          .append(": ")
-          .append(server.getPublicRootUrl()).append("/profiles/changelog?language=")
-          .append(profile.getLanguageKey())
-          .append("&name=")
-          .append(encode(profile.getProfileName()))
-          .append("&since=")
-          .append(formatDate(new Date(profile.getStartDate())))
-          .append("&to=")
-          .append(formatDate(new Date(profile.getEndDate())))
-          .append("\n");
-        int newRules = profile.getNewRules();
-        if (newRules > 0) {
-          message.append(" ").append(newRules).append(" new rule")
-            .append(plural(newRules))
-            .append('\n');
-        }
-        int updatedRules = profile.getUpdatedRules();
-        if (updatedRules > 0) {
-          message.append(" ").append(updatedRules).append(" rule")
-            .append(updatedRules > 1 ? "s have been updated" : " has been updated")
-            .append("\n");
-        }
-        int removedRules = profile.getRemovedRules();
-        if (removedRules > 0) {
-          message.append(" ").append(removedRules).append(" rule")
-            .append(plural(removedRules))
-            .append(" removed\n");
-        }
-        message.append("\n");
-      });
-
-    message.append("This is a good time to review your quality profiles and update them to benefit from the latest evolutions: ");
-    message.append(server.getPublicRootUrl()).append("/profiles");
-
-    // And finally return the email that will be sent
-    return new EmailMessage()
-      .setMessageId(BuiltInQPChangeNotification.TYPE)
-      .setSubject("Built-in quality profiles have been updated")
-      .setPlainTextMessage(message.toString());
-  }
-
-  private static String plural(int count) {
-    return count > 1 ? "s" : "";
-  }
-
-  public String encode(String text) {
-    try {
-      return URLEncoder.encode(text, UTF_8.name());
-    } catch (UnsupportedEncodingException e) {
-      throw new IllegalStateException(String.format("Cannot encode %s", text), e);
-    }
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfile.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfile.java
deleted file mode 100644 (file)
index 866c7dc..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
-import java.util.List;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.Immutable;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
-
-import static org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.BuiltInActiveRule;
-import static org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.OverriddenParam;
-
-/**
- * Represent a Quality Profile as computed from {@link BuiltInQualityProfilesDefinition} provided by installed plugins.
- */
-@Immutable
-public final class BuiltInQProfile {
-  private final QProfileName qProfileName;
-  private final boolean isDefault;
-  private final List<ActiveRule> activeRules;
-
-  private BuiltInQProfile(Builder builder) {
-    this.qProfileName = new QProfileName(builder.language, builder.name);
-    this.isDefault = builder.declaredDefault || builder.computedDefault;
-    this.activeRules = ImmutableList.copyOf(builder.activeRules);
-  }
-
-  public String getName() {
-    return qProfileName.getName();
-  }
-
-  public String getLanguage() {
-    return qProfileName.getLanguage();
-  }
-
-  public QProfileName getQProfileName() {
-    return qProfileName;
-  }
-
-  public boolean isDefault() {
-    return isDefault;
-  }
-
-  public List<ActiveRule> getActiveRules() {
-    return activeRules;
-  }
-
-  static final class ActiveRule {
-    private final String ruleUuid;
-    private final RuleKey ruleKey;
-    private final String severity;
-    private final List<OverriddenParam> params;
-
-    ActiveRule(String ruleUuid, BuiltInActiveRule builtIn) {
-      this(ruleUuid, RuleKey.of(builtIn.repoKey(), builtIn.ruleKey()), builtIn.overriddenSeverity(), builtIn.overriddenParams());
-    }
-
-    ActiveRule(String ruleUuid, RuleKey ruleKey, @Nullable String severity, List<OverriddenParam> params) {
-      this.ruleUuid = ruleUuid;
-      this.ruleKey = ruleKey;
-      this.severity = severity;
-      this.params = params;
-    }
-
-    public String getRuleUuid() {
-      return ruleUuid;
-    }
-
-    public RuleKey getRuleKey() {
-      return ruleKey;
-    }
-
-    @CheckForNull
-    public String getSeverity() {
-      return severity;
-    }
-
-    public List<OverriddenParam> getParams() {
-      return params;
-    }
-  }
-
-  static final class Builder {
-    private String language;
-    private String name;
-    private boolean declaredDefault;
-    private boolean computedDefault;
-    private final List<ActiveRule> activeRules = new ArrayList<>();
-
-    public Builder setLanguage(String language) {
-      this.language = language;
-      return this;
-    }
-
-    Builder setName(String name) {
-      this.name = name;
-      return this;
-    }
-
-    String getName() {
-      return name;
-    }
-
-    Builder setDeclaredDefault(boolean declaredDefault) {
-      this.declaredDefault = declaredDefault;
-      return this;
-    }
-
-    boolean isDeclaredDefault() {
-      return declaredDefault;
-    }
-
-    Builder setComputedDefault(boolean flag) {
-      computedDefault = flag;
-      return this;
-    }
-
-    Builder addRule(ActiveRule activeRule) {
-      this.activeRules.add(activeRule);
-      return this;
-    }
-
-    BuiltInQProfile build() {
-      return new BuiltInQProfile(this);
-    }
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileInsert.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileInsert.java
deleted file mode 100644 (file)
index 7cfd2d2..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import org.sonar.db.DbSession;
-
-public interface BuiltInQProfileInsert {
-  /**
-   * Persist a new built-in profile
-   * Db sessions are committed and Elasticsearch indices are updated
-   */
-  void create(DbSession batchSession, BuiltInQProfile builtInQProfile);
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileInsertImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileInsertImpl.java
deleted file mode 100644 (file)
index d4aad4f..0000000
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.base.Splitter;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
-import org.sonar.api.server.rule.RuleParamType;
-import org.sonar.api.utils.System2;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleKey;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.DefaultQProfileDto;
-import org.sonar.db.qualityprofile.OrgQProfileDto;
-import org.sonar.db.qualityprofile.RulesProfileDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
-import org.sonar.server.rule.ServerRuleFinder;
-import org.sonar.server.util.TypeValidations;
-
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.collect.Lists.newArrayList;
-import static java.util.Collections.emptySet;
-import static java.util.Objects.requireNonNull;
-
-public class BuiltInQProfileInsertImpl implements BuiltInQProfileInsert {
-  private final DbClient dbClient;
-  private final ServerRuleFinder ruleFinder;
-  private final System2 system2;
-  private final UuidFactory uuidFactory;
-  private final TypeValidations typeValidations;
-  private final ActiveRuleIndexer activeRuleIndexer;
-  private RuleRepository ruleRepository;
-
-  public BuiltInQProfileInsertImpl(DbClient dbClient, ServerRuleFinder ruleFinder, System2 system2, UuidFactory uuidFactory,
-    TypeValidations typeValidations, ActiveRuleIndexer activeRuleIndexer) {
-    this.dbClient = dbClient;
-    this.ruleFinder = ruleFinder;
-    this.system2 = system2;
-    this.uuidFactory = uuidFactory;
-    this.typeValidations = typeValidations;
-    this.activeRuleIndexer = activeRuleIndexer;
-  }
-
-  @Override
-  public void create(DbSession batchDbSession, BuiltInQProfile builtInQProfile) {
-    initRuleRepository(batchDbSession);
-
-    Date now = new Date(system2.now());
-    RulesProfileDto ruleProfile = insertRulesProfile(batchDbSession, builtInQProfile, now);
-
-    List<ActiveRuleChange> changes = builtInQProfile.getActiveRules().stream()
-      .map(activeRule -> insertActiveRule(batchDbSession, ruleProfile, activeRule, now.getTime()))
-      .collect(MoreCollectors.toList());
-
-    changes.forEach(change -> dbClient.qProfileChangeDao().insert(batchDbSession, change.toDto(null)));
-
-    createDefaultAndOrgQProfiles(batchDbSession, builtInQProfile, ruleProfile);
-
-    activeRuleIndexer.commitAndIndex(batchDbSession, changes);
-  }
-
-
-  private void createDefaultAndOrgQProfiles(DbSession batchDbSession, BuiltInQProfile builtIn, RulesProfileDto rulesProfileDto) {
-    Optional<String> qProfileUuid = dbClient.defaultQProfileDao().selectDefaultQProfileUuid(batchDbSession, builtIn.getLanguage());
-
-    OrgQProfileDto dto = new OrgQProfileDto()
-      .setRulesProfileUuid(rulesProfileDto.getUuid())
-      .setUuid(uuidFactory.create());
-
-    if (builtIn.isDefault() && qProfileUuid.isEmpty()) {
-      DefaultQProfileDto defaultQProfileDto = new DefaultQProfileDto()
-        .setQProfileUuid(dto.getUuid())
-        .setLanguage(builtIn.getLanguage());
-      dbClient.defaultQProfileDao().insert(batchDbSession, defaultQProfileDto);
-    }
-
-    dbClient.qualityProfileDao().insert(batchDbSession, dto);
-  }
-
-  private void initRuleRepository(DbSession dbSession) {
-    if (ruleRepository == null) {
-      ruleRepository = new RuleRepository(dbClient, dbSession, ruleFinder);
-    }
-  }
-
-  private RulesProfileDto insertRulesProfile(DbSession dbSession, BuiltInQProfile builtIn, Date now) {
-    RulesProfileDto dto = new RulesProfileDto()
-      .setUuid(uuidFactory.create())
-      .setName(builtIn.getName())
-      .setLanguage(builtIn.getLanguage())
-      .setIsBuiltIn(true)
-      .setRulesUpdatedAtAsDate(now);
-    dbClient.qualityProfileDao().insert(dbSession, dto);
-    return dto;
-  }
-
-  private ActiveRuleChange insertActiveRule(DbSession batchDbSession, RulesProfileDto rulesProfileDto, BuiltInQProfile.ActiveRule activeRule, long now) {
-    RuleKey ruleKey = activeRule.getRuleKey();
-    RuleDefinitionDto ruleDefinitionDto = ruleRepository.getDefinition(ruleKey)
-      .orElseThrow(() -> new IllegalStateException("RuleDefinition not found for key " + ruleKey));
-
-    ActiveRuleDto dto = new ActiveRuleDto();
-    dto.setProfileUuid(rulesProfileDto.getUuid());
-    dto.setRuleUuid(ruleDefinitionDto.getUuid());
-    dto.setKey(ActiveRuleKey.of(rulesProfileDto, ruleDefinitionDto.getKey()));
-    dto.setSeverity(firstNonNull(activeRule.getSeverity(), ruleDefinitionDto.getSeverityString()));
-    dto.setUpdatedAt(now);
-    dto.setCreatedAt(now);
-    dbClient.activeRuleDao().insert(batchDbSession, dto);
-
-    List<ActiveRuleParamDto> paramDtos = insertActiveRuleParams(batchDbSession, activeRule, dto);
-
-    ActiveRuleChange change = new ActiveRuleChange(ActiveRuleChange.Type.ACTIVATED, dto, ruleDefinitionDto);
-    change.setSeverity(dto.getSeverityString());
-    paramDtos.forEach(paramDto -> change.setParameter(paramDto.getKey(), paramDto.getValue()));
-    return change;
-  }
-
-  private List<ActiveRuleParamDto> insertActiveRuleParams(DbSession session, BuiltInQProfile.ActiveRule activeRule, ActiveRuleDto activeRuleDto) {
-    Map<String, String> valuesByParamKey = activeRule.getParams().stream()
-      .collect(MoreCollectors.uniqueIndex(BuiltInQualityProfilesDefinition.OverriddenParam::key, BuiltInQualityProfilesDefinition.OverriddenParam::overriddenValue));
-    List<ActiveRuleParamDto> rules = ruleRepository.getRuleParams(activeRule.getRuleKey()).stream()
-      .map(param -> createParamDto(param, Optional.ofNullable(valuesByParamKey.get(param.getName())).orElse(param.getDefaultValue())))
-      .filter(Objects::nonNull)
-      .collect(Collectors.toList());
-
-    rules.forEach(paramDto -> dbClient.activeRuleDao().insertParam(session, activeRuleDto, paramDto));
-    return rules;
-  }
-
-  @CheckForNull
-  private ActiveRuleParamDto createParamDto(RuleParamDto param, @Nullable String value) {
-    if (value == null) {
-      return null;
-    }
-    ActiveRuleParamDto paramDto = ActiveRuleParamDto.createFor(param);
-    paramDto.setValue(validateParam(param, value));
-    return paramDto;
-  }
-
-  private String validateParam(RuleParamDto ruleParam, String value) {
-    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());
-    }
-    return value;
-  }
-
-  private static class RuleRepository {
-    private final Map<RuleKey, Set<RuleParamDto>> params;
-    private final ServerRuleFinder ruleFinder;
-
-    private RuleRepository(DbClient dbClient, DbSession session, ServerRuleFinder ruleFinder) {
-      this.ruleFinder = ruleFinder;
-      this.params = new HashMap<>();
-
-      for (RuleParamDto ruleParam : dbClient.ruleDao().selectAllRuleParams(session)) {
-        Optional<RuleKey> ruleKey = ruleFinder.findDtoByUuid(ruleParam.getRuleUuid())
-          .map(r -> RuleKey.of(r.getRepositoryKey(), r.getRuleKey()));
-
-        if (ruleKey.isPresent()) {
-          params.computeIfAbsent(ruleKey.get(), r -> new HashSet<>()).add(ruleParam);
-        }
-      }
-    }
-
-    private Optional<RuleDefinitionDto> getDefinition(RuleKey ruleKey) {
-      return ruleFinder.findDtoByKey(requireNonNull(ruleKey, "RuleKey can't be null"));
-    }
-
-    private Set<RuleParamDto> getRuleParams(RuleKey ruleKey) {
-      return params.getOrDefault(requireNonNull(ruleKey, "RuleKey can't be null"), emptySet());
-    }
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileLoader.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileLoader.java
deleted file mode 100644 (file)
index b975716..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import org.picocontainer.Startable;
-
-/**
- * Startable added to {@link org.sonar.server.platform.platformlevel.PlatformLevelStartup} responsible for initializing
- * {@link BuiltInQProfileRepository}.
- */
-public class BuiltInQProfileLoader implements Startable {
-  private final BuiltInQProfileRepository builtInQProfileRepository;
-
-  public BuiltInQProfileLoader(BuiltInQProfileRepository builtInQProfileRepository) {
-    this.builtInQProfileRepository = builtInQProfileRepository;
-  }
-
-  @Override
-  public void start() {
-    builtInQProfileRepository.initialize();
-  }
-
-  @Override
-  public void stop() {
-    // nothing to do
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileRepository.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileRepository.java
deleted file mode 100644 (file)
index 72db5b9..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.List;
-
-public interface BuiltInQProfileRepository {
-  /**
-   * Initializes the Repository.
-   *
-   * This method is intended to be called from a startup task
-   * (see {@link org.sonar.server.platform.platformlevel.PlatformLevelStartup}).
-   *
-   * @throws IllegalStateException if called more then once
-   */
-  void initialize();
-
-  /**
-   * @return an immutable list
-   *
-   * @throws IllegalStateException if {@link #initialize()} has not been called
-   */
-  List<BuiltInQProfile> get();
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryImpl.java
deleted file mode 100644 (file)
index 57ca9b1..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Multimap;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import javax.annotation.Nullable;
-import org.sonar.api.profiles.RulesProfile;
-import org.sonar.api.resources.Language;
-import org.sonar.api.resources.Languages;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.BuiltInQualityProfile;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.api.utils.log.Profiler;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.rule.DeprecatedRuleKeyDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.server.rule.ServerRuleFinder;
-
-import static com.google.common.base.Preconditions.checkState;
-
-public class BuiltInQProfileRepositoryImpl implements BuiltInQProfileRepository {
-  private static final Logger LOGGER = Loggers.get(BuiltInQProfileRepositoryImpl.class);
-  private static final String DEFAULT_PROFILE_NAME = "Sonar way";
-
-  private final DbClient dbClient;
-  private final ServerRuleFinder ruleFinder;
-  private final Languages languages;
-  private final List<BuiltInQualityProfilesDefinition> definitions;
-  private List<BuiltInQProfile> qProfiles;
-
-  /**
-   * Requires for pico container when no {@link BuiltInQualityProfilesDefinition} is defined at all
-   */
-  public BuiltInQProfileRepositoryImpl(DbClient dbClient, ServerRuleFinder ruleFinder, Languages languages) {
-    this(dbClient, ruleFinder, languages, new BuiltInQualityProfilesDefinition[0]);
-  }
-
-  public BuiltInQProfileRepositoryImpl(DbClient dbClient, ServerRuleFinder ruleFinder, Languages languages, BuiltInQualityProfilesDefinition... definitions) {
-    this.dbClient = dbClient;
-    this.ruleFinder = ruleFinder;
-    this.languages = languages;
-    this.definitions = ImmutableList.copyOf(definitions);
-  }
-
-  @Override
-  public void initialize() {
-    checkState(qProfiles == null, "initialize must be called only once");
-
-    Profiler profiler = Profiler.create(LOGGER).startInfo("Load quality profiles");
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    for (BuiltInQualityProfilesDefinition definition : definitions) {
-      definition.define(context);
-    }
-    Map<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguage = validateAndClean(context);
-    this.qProfiles = toFlatList(rulesProfilesByLanguage);
-    ensureAllLanguagesHaveAtLeastOneBuiltInQP();
-    profiler.stopDebug();
-  }
-
-  @Override
-  public List<BuiltInQProfile> get() {
-    checkState(qProfiles != null, "initialize must be called first");
-
-    return qProfiles;
-  }
-
-  private void ensureAllLanguagesHaveAtLeastOneBuiltInQP() {
-    Set<String> languagesWithBuiltInQProfiles = qProfiles.stream().map(BuiltInQProfile::getLanguage).collect(Collectors.toSet());
-    Set<String> languagesWithoutBuiltInQProfiles = Arrays.stream(languages.all())
-      .map(Language::getKey)
-      .filter(key -> !languagesWithBuiltInQProfiles.contains(key))
-      .collect(Collectors.toSet());
-
-    checkState(languagesWithoutBuiltInQProfiles.isEmpty(), "The following languages have no built-in quality profiles: %s",
-      String.join("", languagesWithoutBuiltInQProfiles));
-  }
-
-  private Map<String, Map<String, BuiltInQualityProfile>> validateAndClean(BuiltInQualityProfilesDefinition.Context context) {
-    Map<String, Map<String, BuiltInQualityProfile>> profilesByLanguageAndName = context.profilesByLanguageAndName();
-    profilesByLanguageAndName.entrySet()
-      .removeIf(entry -> {
-        String language = entry.getKey();
-        if (languages.get(language) == null) {
-          LOGGER.info("Language {} is not installed, related quality profiles are ignored", language);
-          return true;
-        }
-        return false;
-      });
-
-    return profilesByLanguageAndName;
-  }
-
-  private List<BuiltInQProfile> toFlatList(Map<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguage) {
-    if (rulesProfilesByLanguage.isEmpty()) {
-      return Collections.emptyList();
-    }
-    Map<RuleKey, RuleDefinitionDto> rulesByRuleKey = loadRuleDefinitionsByRuleKey();
-    Map<String, List<BuiltInQProfile.Builder>> buildersByLanguage = rulesProfilesByLanguage
-      .entrySet()
-      .stream()
-      .collect(MoreCollectors.uniqueIndex(
-        Map.Entry::getKey,
-        rulesProfilesByLanguageAndName -> toQualityProfileBuilders(rulesProfilesByLanguageAndName, rulesByRuleKey)));
-    return buildersByLanguage
-      .entrySet()
-      .stream()
-      .filter(BuiltInQProfileRepositoryImpl::ensureAtMostOneDeclaredDefault)
-      .map(entry -> toQualityProfiles(entry.getValue()))
-      .flatMap(Collection::stream)
-      .collect(MoreCollectors.toList());
-  }
-
-  private Map<RuleKey, RuleDefinitionDto> loadRuleDefinitionsByRuleKey() {
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      Collection<RuleDefinitionDto> ruleDefinitions = ruleFinder.findAll();
-      Multimap<String, DeprecatedRuleKeyDto> deprecatedRuleKeysByRuleId = dbClient.ruleDao().selectAllDeprecatedRuleKeys(dbSession).stream()
-        .collect(MoreCollectors.index(DeprecatedRuleKeyDto::getRuleUuid));
-      Map<RuleKey, RuleDefinitionDto> rulesByRuleKey = new HashMap<>();
-      for (RuleDefinitionDto ruleDefinition : ruleDefinitions) {
-        rulesByRuleKey.put(ruleDefinition.getKey(), ruleDefinition);
-        deprecatedRuleKeysByRuleId.get(ruleDefinition.getUuid()).forEach(t -> rulesByRuleKey.put(RuleKey.of(t.getOldRepositoryKey(), t.getOldRuleKey()), ruleDefinition));
-      }
-      return rulesByRuleKey;
-    }
-  }
-
-  /**
-   * Creates {@link BuiltInQProfile.Builder} for each unique quality profile name for a given language.
-   * Builders will have the following properties populated:
-   * <ul>
-   *   <li>{@link BuiltInQProfile.Builder#language language}: key of the method's parameter</li>
-   *   <li>{@link BuiltInQProfile.Builder#name name}: {@link RulesProfile#getName()}</li>
-   *   <li>{@link BuiltInQProfile.Builder#declaredDefault declaredDefault}: {@code true} if at least one RulesProfile
-   *       with a given name has {@link RulesProfile#getDefaultProfile()} is {@code true}</li>
-   *   <li>{@link BuiltInQProfile.Builder#activeRules activeRules}: the concatenate of the active rules of all
-   *       RulesProfile with a given name</li>
-   * </ul>
-   */
-  private static List<BuiltInQProfile.Builder> toQualityProfileBuilders(Map.Entry<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguageAndName,
-    Map<RuleKey, RuleDefinitionDto> rulesByRuleKey) {
-    String language = rulesProfilesByLanguageAndName.getKey();
-    // use a LinkedHashMap to keep order of insertion of RulesProfiles
-    Map<String, BuiltInQProfile.Builder> qualityProfileBuildersByName = new LinkedHashMap<>();
-    for (BuiltInQualityProfile builtInProfile : rulesProfilesByLanguageAndName.getValue().values()) {
-      qualityProfileBuildersByName.compute(
-        builtInProfile.name(),
-        (name, existingBuilder) -> updateOrCreateBuilder(language, existingBuilder, builtInProfile, rulesByRuleKey));
-    }
-    return ImmutableList.copyOf(qualityProfileBuildersByName.values());
-  }
-
-  /**
-   * Fails if more than one {@link BuiltInQProfile.Builder#declaredDefault} is {@code true}, otherwise returns {@code true}.
-   */
-  private static boolean ensureAtMostOneDeclaredDefault(Map.Entry<String, List<BuiltInQProfile.Builder>> entry) {
-    Set<String> declaredDefaultProfileNames = entry.getValue().stream()
-      .filter(BuiltInQProfile.Builder::isDeclaredDefault)
-      .map(BuiltInQProfile.Builder::getName)
-      .collect(MoreCollectors.toSet());
-    checkState(declaredDefaultProfileNames.size() <= 1, "Several Quality profiles are flagged as default for the language %s: %s", entry.getKey(), declaredDefaultProfileNames);
-    return true;
-  }
-
-  private static BuiltInQProfile.Builder updateOrCreateBuilder(String language, @Nullable BuiltInQProfile.Builder existingBuilder, BuiltInQualityProfile builtInProfile,
-    Map<RuleKey, RuleDefinitionDto> rulesByRuleKey) {
-    BuiltInQProfile.Builder builder = createOrReuseBuilder(existingBuilder, language, builtInProfile);
-    builder.setDeclaredDefault(builtInProfile.isDefault());
-    builtInProfile.rules().forEach(builtInActiveRule -> {
-      RuleKey ruleKey = RuleKey.of(builtInActiveRule.repoKey(), builtInActiveRule.ruleKey());
-      RuleDefinitionDto ruleDefinition = rulesByRuleKey.get(ruleKey);
-      checkState(ruleDefinition != null, "Rule with key '%s' not found", ruleKey);
-      builder.addRule(new BuiltInQProfile.ActiveRule(ruleDefinition.getUuid(), ruleDefinition.getKey(),
-        builtInActiveRule.overriddenSeverity(), builtInActiveRule.overriddenParams()));
-    });
-    return builder;
-  }
-
-  private static BuiltInQProfile.Builder createOrReuseBuilder(@Nullable BuiltInQProfile.Builder existingBuilder, String language, BuiltInQualityProfile builtInProfile) {
-    if (existingBuilder == null) {
-      return new BuiltInQProfile.Builder()
-        .setLanguage(language)
-        .setName(builtInProfile.name());
-    }
-    return existingBuilder;
-  }
-
-  private static List<BuiltInQProfile> toQualityProfiles(List<BuiltInQProfile.Builder> builders) {
-    if (builders.stream().noneMatch(BuiltInQProfile.Builder::isDeclaredDefault)) {
-      Optional<BuiltInQProfile.Builder> sonarWayProfile = builders.stream().filter(builder -> builder.getName().equals(DEFAULT_PROFILE_NAME)).findFirst();
-      if (sonarWayProfile.isPresent()) {
-        sonarWayProfile.get().setComputedDefault(true);
-      } else {
-        builders.iterator().next().setComputedDefault(true);
-      }
-    }
-    return builders.stream()
-      .map(BuiltInQProfile.Builder::build)
-      .collect(MoreCollectors.toList(builders.size()));
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdate.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdate.java
deleted file mode 100644 (file)
index 44403a0..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.List;
-import org.sonar.db.DbSession;
-import org.sonar.db.qualityprofile.RulesProfileDto;
-
-public interface BuiltInQProfileUpdate {
-  /**
-   * Persist an existing built-in profile.
-   * Db session is committed and Elasticsearch indices are updated.
-   */
-  List<ActiveRuleChange> update(DbSession dbSession, BuiltInQProfile builtInQProfile, RulesProfileDto ruleProfile);
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdateImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdateImpl.java
deleted file mode 100644 (file)
index 9f0f059..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Stream;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.RulesProfileDto;
-import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
-
-import static org.sonar.core.util.stream.MoreCollectors.toSet;
-
-public class BuiltInQProfileUpdateImpl implements BuiltInQProfileUpdate {
-
-  private final DbClient dbClient;
-  private final RuleActivator ruleActivator;
-  private final ActiveRuleIndexer activeRuleIndexer;
-  private final QualityProfileChangeEventService qualityProfileChangeEventService;
-
-  public BuiltInQProfileUpdateImpl(DbClient dbClient, RuleActivator ruleActivator, ActiveRuleIndexer activeRuleIndexer,
-    QualityProfileChangeEventService qualityProfileChangeEventService) {
-    this.dbClient = dbClient;
-    this.ruleActivator = ruleActivator;
-    this.activeRuleIndexer = activeRuleIndexer;
-    this.qualityProfileChangeEventService = qualityProfileChangeEventService;
-  }
-
-  public List<ActiveRuleChange> update(DbSession dbSession, BuiltInQProfile builtInDefinition, RulesProfileDto initialRuleProfile) {
-    // Keep reference to all the activated rules before update
-    Set<String> deactivatedRuleUuids = dbClient.activeRuleDao().selectByRuleProfile(dbSession, initialRuleProfile)
-      .stream()
-      .map(ActiveRuleDto::getRuleUuid)
-      .collect(MoreCollectors.toHashSet());
-
-    // all rules, including those which are removed from built-in profile
-    Set<String> ruleUuids = Stream.concat(
-        deactivatedRuleUuids.stream(),
-        builtInDefinition.getActiveRules().stream().map(BuiltInQProfile.ActiveRule::getRuleUuid))
-      .collect(toSet());
-
-    Collection<RuleActivation> activations = new ArrayList<>();
-    for (BuiltInQProfile.ActiveRule ar : builtInDefinition.getActiveRules()) {
-      RuleActivation activation = convert(ar);
-      activations.add(activation);
-      deactivatedRuleUuids.remove(activation.getRuleUuid());
-    }
-
-    RuleActivationContext context = ruleActivator.createContextForBuiltInProfile(dbSession, initialRuleProfile, ruleUuids);
-    List<ActiveRuleChange> changes = new ArrayList<>();
-
-    changes.addAll(ruleActivator.activate(dbSession, activations, context));
-
-    // these rules are no longer part of the built-in profile
-    deactivatedRuleUuids.forEach(ruleUuid -> changes.addAll(ruleActivator.deactivate(dbSession, context, ruleUuid, false)));
-
-    if (!changes.isEmpty()) {
-      qualityProfileChangeEventService.distributeRuleChangeEvent(context.getProfiles(), changes, initialRuleProfile.getLanguage());
-    }
-
-    activeRuleIndexer.commitAndIndex(dbSession, changes);
-    return changes;
-  }
-
-  private static RuleActivation convert(BuiltInQProfile.ActiveRule ar) {
-    Map<String, String> params = ar.getParams().stream()
-      .collect(MoreCollectors.uniqueIndex(BuiltInQualityProfilesDefinition.OverriddenParam::key, BuiltInQualityProfilesDefinition.OverriddenParam::overriddenValue));
-    return RuleActivation.create(ar.getRuleUuid(), ar.getSeverity(), params);
-  }
-
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQualityProfilesUpdateListener.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQualityProfilesUpdateListener.java
deleted file mode 100644 (file)
index 0e9a2ae..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.collect.Multimap;
-import java.util.Collection;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.resources.Language;
-import org.sonar.api.resources.Languages;
-import org.sonar.server.notification.NotificationManager;
-import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationBuilder.Profile;
-
-import static org.sonar.core.config.CorePropertyDefinitions.DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED;
-
-public class BuiltInQualityProfilesUpdateListener {
-
-  private final NotificationManager notificationManager;
-  private final Languages languages;
-  private final Configuration config;
-
-  public BuiltInQualityProfilesUpdateListener(NotificationManager notificationManager, Languages languages, Configuration config) {
-    this.notificationManager = notificationManager;
-    this.languages = languages;
-    this.config = config;
-  }
-
-  void onChange(Multimap<QProfileName, ActiveRuleChange> changedProfiles, long startDate, long endDate) {
-    if (config.getBoolean(DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES).orElse(false)) {
-      return;
-    }
-
-    BuiltInQPChangeNotificationBuilder builder = new BuiltInQPChangeNotificationBuilder();
-    changedProfiles.keySet().stream()
-      .map(changedProfile -> {
-        String profileName = changedProfile.getName();
-        Language language = languages.get(changedProfile.getLanguage());
-        Collection<ActiveRuleChange> activeRuleChanges = changedProfiles.get(changedProfile);
-        int newRules = (int) activeRuleChanges.stream().map(ActiveRuleChange::getType).filter(ACTIVATED::equals).count();
-        int updatedRules = (int) activeRuleChanges.stream().map(ActiveRuleChange::getType).filter(UPDATED::equals).count();
-        int removedRules = (int) activeRuleChanges.stream().map(ActiveRuleChange::getType).filter(DEACTIVATED::equals).count();
-        return Profile.newBuilder()
-          .setProfileName(profileName)
-          .setLanguageKey(language.getKey())
-          .setLanguageName(language.getName())
-          .setNewRules(newRules)
-          .setUpdatedRules(updatedRules)
-          .setRemovedRules(removedRules)
-          .setStartDate(startDate)
-          .setEndDate(endDate)
-          .build();
-      })
-      .forEach(builder::addProfile);
-
-    notificationManager.scheduleForSending(builder.build());
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DescendantProfilesSupplier.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DescendantProfilesSupplier.java
deleted file mode 100644 (file)
index 10127dc..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.Collection;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-
-@FunctionalInterface
-public interface DescendantProfilesSupplier {
-
-  Result get(Collection<QProfileDto> profiles, Collection<String> ruleUuids);
-
-  final class Result {
-    private final Collection<QProfileDto> profiles;
-    private final Collection<ActiveRuleDto> activeRules;
-    private final Collection<ActiveRuleParamDto> activeRuleParams;
-
-    public Result(Collection<QProfileDto> profiles, Collection<ActiveRuleDto> activeRules, Collection<ActiveRuleParamDto> activeRuleParams) {
-      this.profiles = profiles;
-      this.activeRules = activeRules;
-      this.activeRuleParams = activeRuleParams;
-    }
-
-    public Collection<QProfileDto> getProfiles() {
-      return profiles;
-    }
-
-    public Collection<ActiveRuleDto> getActiveRules() {
-      return activeRules;
-    }
-
-    public Collection<ActiveRuleParamDto> getActiveRuleParams() {
-      return activeRuleParams;
-    }
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DistributedRuleActivatorEventsDistributor.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DistributedRuleActivatorEventsDistributor.java
deleted file mode 100644 (file)
index a6a3b84..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import org.sonar.api.server.ServerSide;
-import org.sonar.core.util.RuleActivationListener;
-import org.sonar.core.util.RuleSetChangeEvent;
-import org.sonar.process.cluster.hz.HazelcastMember;
-
-@ServerSide
-public class DistributedRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {
-
-  private HazelcastMember hazelcastMember;
-
-  public DistributedRuleActivatorEventsDistributor(HazelcastMember hazelcastMember) {
-    this.hazelcastMember = hazelcastMember;
-  }
-
-  @Override
-  public void subscribe(RuleActivationListener listener) {
-    hazelcastMember.subscribeRuleActivationTopic(listener);
-  }
-
-  @Override
-  public void pushEvent(RuleSetChangeEvent event) {
-    hazelcastMember.publishEvent(event);
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QProfileName.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QProfileName.java
deleted file mode 100644 (file)
index ad910ce..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import javax.annotation.Nullable;
-
-public class QProfileName {
-  private final String lang;
-  private final String name;
-
-  public QProfileName(String lang, String name) {
-    this.lang = lang;
-    this.name = name;
-  }
-
-  public String getLanguage() {
-    return lang;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public static QProfileName createFor(String lang, String name){
-    return new QProfileName(lang, name);
-  }
-
-  @Override
-  public boolean equals(@Nullable Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-    QProfileName that = (QProfileName) o;
-    if (!lang.equals(that.lang)) {
-      return false;
-    }
-    return name.equals(that.name);
-  }
-
-  @Override
-  public int hashCode() {
-    int result = lang.hashCode();
-    result = 31 * result + name.hashCode();
-    return result;
-  }
-
-  @Override
-  public String toString() {
-    return String.format("%s/%s", lang, name);
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventService.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventService.java
deleted file mode 100644 (file)
index 008afff..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import org.sonar.db.project.ProjectDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-
-public interface QualityProfileChangeEventService {
-
-  void publishRuleActivationToSonarLintClients(ProjectDto project, Optional<QProfileDto> activatedProfile, Optional<QProfileDto> deactivatedProfile);
-
-  void distributeRuleChangeEvent(Collection<QProfileDto> profiles, List<ActiveRuleChange> activeRuleChanges, String language);
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventServiceImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventServiceImpl.java
deleted file mode 100644 (file)
index 22c96e6..0000000
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.jetbrains.annotations.NotNull;
-import org.sonar.api.server.ServerSide;
-import org.sonar.core.util.ParamChange;
-import org.sonar.core.util.RuleChange;
-import org.sonar.core.util.RuleSetChangeEvent;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.project.ProjectDto;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.ProjectQprofileAssociationDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.rule.RuleDto;
-import org.sonar.server.rule.index.RuleIndex;
-import org.sonar.server.rule.index.RuleQuery;
-
-@ServerSide
-public class QualityProfileChangeEventServiceImpl implements QualityProfileChangeEventService {
-
-  private final DbClient dbClient;
-  private final RuleIndex ruleIndex;
-  private final RuleActivatorEventsDistributor eventsDistributor;
-
-  public QualityProfileChangeEventServiceImpl(DbClient dbClient, RuleIndex ruleIndex, RuleActivatorEventsDistributor eventsDistributor) {
-    this.dbClient = dbClient;
-    this.ruleIndex = ruleIndex;
-    this.eventsDistributor = eventsDistributor;
-  }
-
-  @Override
-  public void publishRuleActivationToSonarLintClients(ProjectDto project, Optional<QProfileDto> activatedProfile, Optional<QProfileDto> deactivatedProfile) {
-    List<RuleChange> activatedRules = new ArrayList<>();
-    List<RuleChange> deactivatedRules = new ArrayList<>();
-
-    try (DbSession dbSession = dbClient.openSession(false)) {
-
-      if (activatedProfile.isPresent()) {
-        RuleQuery query = new RuleQuery().setQProfile(activatedProfile.get()).setActivation(true).setIncludeExternal(true);
-        // .setLanguages() ?
-        Iterator<String> searchIdResult = ruleIndex.searchAll(query);
-        List<String> uuids = new ArrayList<>();
-        while (searchIdResult.hasNext()) {
-          uuids.add(searchIdResult.next());
-        }
-
-        List<RuleDto> ruleDtos = dbClient.ruleDao().selectByUuids(dbSession, uuids);
-        Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid = dbClient.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, uuids)
-          .stream().collect(Collectors.groupingBy(ActiveRuleParamDto::getActiveRuleUuid));
-
-        for (RuleDto ruleDto : ruleDtos) {
-          RuleChange ruleChange = toRuleChange(ruleDto, paramsByRuleUuid);
-          activatedRules.add(ruleChange);
-        }
-      }
-
-      if (deactivatedProfile.isPresent()) {
-        RuleQuery query = new RuleQuery().setQProfile(deactivatedProfile.get()).setActivation(true).setIncludeExternal(true);
-        // .setLanguages() ?
-        Iterator<String> searchIdResult = ruleIndex.searchAll(query);
-        List<String> uuids = new ArrayList<>();
-        while (searchIdResult.hasNext()) {
-          uuids.add(searchIdResult.next());
-        }
-
-        List<RuleDto> ruleDtos = dbClient.ruleDao().selectByUuids(dbSession, uuids);
-        Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid = dbClient.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, uuids)
-          .stream().collect(Collectors.groupingBy(ActiveRuleParamDto::getActiveRuleUuid));
-
-        for (RuleDto ruleDto : ruleDtos) {
-          RuleChange ruleChange = toRuleChange(ruleDto, paramsByRuleUuid);
-          deactivatedRules.add(ruleChange);
-        }
-      }
-
-    }
-
-    RuleSetChangeEvent event = new RuleSetChangeEvent(new String[]{project.getKey()}, activatedRules.toArray(new RuleChange[0]), deactivatedRules.toArray(new RuleChange[0]));
-    eventsDistributor.pushEvent(event);
-  }
-
-  @NotNull
-  private RuleChange toRuleChange(RuleDto ruleDto, Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid) {
-    RuleChange ruleChange = new RuleChange();
-    ruleChange.setKey(ruleDto.getRuleKey());
-    ruleChange.setLanguage(ruleDto.getLanguage());
-    ruleChange.setSeverity(ruleDto.getSeverityString());
-
-    List<ParamChange> paramChanges = new ArrayList<>();
-    List<ActiveRuleParamDto> activeRuleParamDtos = paramsByRuleUuid.getOrDefault(ruleDto.getUuid(), new ArrayList<>());
-    for (ActiveRuleParamDto activeRuleParam : activeRuleParamDtos) {
-      paramChanges.add(new ParamChange(activeRuleParam.getKey(), activeRuleParam.getValue()));
-    }
-    ruleChange.setParams(paramChanges.toArray(new ParamChange[0]));
-
-    String templateUuid = ruleDto.getTemplateUuid();
-    if (templateUuid != null && !"".equals(templateUuid)) {
-      try (DbSession dbSession = dbClient.openSession(false)) {
-        RuleDto templateRule = dbClient.ruleDao().selectByUuid(templateUuid, dbSession)
-          .orElseThrow(() -> new IllegalStateException(String.format("unknow Template Rule '%s'", templateUuid)));
-        ruleChange.setTemplateKey(templateRule.getRuleKey());
-      }
-    }
-
-    return ruleChange;
-  }
-
-  public void distributeRuleChangeEvent(Collection<QProfileDto> profiles, List<ActiveRuleChange> activeRuleChanges, String language) {
-    if (activeRuleChanges.isEmpty()) {
-      return;
-    }
-
-    Set<RuleChange> activatedRules = new HashSet<>();
-    Set<RuleChange> deactivatedRules = new HashSet<>();
-
-    for (ActiveRuleChange arc : activeRuleChanges) {
-
-      RuleChange ruleChange = new RuleChange();
-      ruleChange.setKey(arc.getActiveRule().getRuleKey().rule());
-      ruleChange.setSeverity(arc.getSeverity());
-      ruleChange.setLanguage(language);
-
-      Optional<String> templateKey = templateKey(arc);
-      templateKey.ifPresent(ruleChange::setTemplateKey);
-
-      // params
-      List<ParamChange> paramChanges = new ArrayList<>();
-      for (Map.Entry<String, String> entry : arc.getParameters().entrySet()) {
-        paramChanges.add(new ParamChange(entry.getKey(), entry.getValue()));
-      }
-      ruleChange.setParams(paramChanges.toArray(new ParamChange[0]));
-
-      switch (arc.getType()) {
-        case ACTIVATED:
-        case UPDATED:
-          activatedRules.add(ruleChange);
-          break;
-        case DEACTIVATED:
-          deactivatedRules.add(ruleChange);
-          break;
-      }
-    }
-
-    Set<String> projectKeys = getProjectKeys(profiles);
-
-    RuleSetChangeEvent event = new RuleSetChangeEvent(projectKeys.toArray(new String[0]), activatedRules.toArray(new RuleChange[0]), deactivatedRules.toArray(new RuleChange[0]));
-    eventsDistributor.pushEvent(event);
-
-  }
-
-  private Optional<String> templateKey(ActiveRuleChange arc) {
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      String ruleUuid = arc.getRuleUuid();
-      RuleDto rule = dbClient.ruleDao().selectByUuid(ruleUuid, dbSession).orElseThrow(() -> new IllegalStateException("unknow rule"));
-      String templateUuid = rule.getTemplateUuid();
-      if (templateUuid != null && !"".equals(templateUuid)) {
-        RuleDto templateRule = dbClient.ruleDao().selectByUuid(templateUuid, dbSession)
-          .orElseThrow(() -> new IllegalStateException(String.format("unknow Template Rule '%s'", templateUuid)));
-        return Optional.of(templateRule.getRuleKey());
-      }
-    }
-    return Optional.empty();
-  }
-
-  private Set<String> getProjectKeys(Collection<QProfileDto> profiles) {
-    Set<String> projectKeys = new HashSet<>();
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      for (QProfileDto profileDto : profiles) {
-        List<ProjectQprofileAssociationDto> associationDtos = dbClient.qualityProfileDao().selectSelectedProjects(dbSession, profileDto, null);
-        for (ProjectQprofileAssociationDto associationDto : associationDtos) {
-          projectKeys.add(associationDto.getProjectKey());
-        }
-      }
-      return projectKeys;
-    }
-  }
-
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivationContext.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivationContext.java
deleted file mode 100644 (file)
index 458926e..0000000
+++ /dev/null
@@ -1,366 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ListMultimap;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-import javax.annotation.CheckForNull;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleKey;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.qualityprofile.RulesProfileDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleParamDto;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-import static java.util.Objects.requireNonNull;
-import static org.sonar.core.util.stream.MoreCollectors.index;
-import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
-import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
-import static org.sonar.server.exceptions.BadRequestException.checkRequest;
-
-/**
- * Cache of the data required to activate/deactivate
- * multiple rules on a Quality profile, including
- * the rule definitions, the rule parameters, the tree
- * of profiles hierarchy and its related active rules.
- */
-class RuleActivationContext {
-
-  private final long date;
-
-  // The profile that is initially targeted by the operation
-  private final RulesProfileDto baseRulesProfile;
-
-  private final Map<String, QProfileDto> profilesByUuid = new HashMap<>();
-  private final ListMultimap<String, QProfileDto> profilesByParentUuid = ArrayListMultimap.create();
-
-  // The rules/active rules involved in the group of activations/de-activations
-  private final Map<String, RuleWrapper> rulesByUuid = new HashMap<>();
-  private final Map<ActiveRuleKey, ActiveRuleWrapper> activeRulesByKey = new HashMap<>();
-
-  // Cursors used to move in the rules and in the tree of profiles.
-
-  private RulesProfileDto currentRulesProfile;
-  // Cardinality is zero-to-many when cursor is on a built-in rules profile,
-  // otherwise it's always one, and only one (cursor on descendants or on non-built-in base profile).
-  private Collection<QProfileDto> currentProfiles;
-  private RuleWrapper currentRule;
-  private ActiveRuleWrapper currentActiveRule;
-  private ActiveRuleWrapper currentParentActiveRule;
-
-  private boolean descendantsLoaded = false;
-  private final DescendantProfilesSupplier descendantProfilesSupplier;
-
-  private RuleActivationContext(Builder builder) {
-    this.date = builder.date;
-    this.descendantProfilesSupplier = builder.descendantProfilesSupplier;
-
-    ListMultimap<String, RuleParamDto> paramsByRuleId = builder.ruleParams.stream().collect(index(RuleParamDto::getRuleUuid));
-    for (RuleDefinitionDto rule : builder.rules) {
-      RuleWrapper wrapper = new RuleWrapper(rule, paramsByRuleId.get(rule.getUuid()));
-      rulesByUuid.put(rule.getUuid(), wrapper);
-    }
-
-    this.baseRulesProfile = builder.baseRulesProfile;
-    register(builder.profiles);
-    register(builder.activeRules, builder.activeRuleParams);
-  }
-
-  private void register(Collection<QProfileDto> profiles) {
-    for (QProfileDto profile : profiles) {
-      profilesByUuid.put(profile.getKee(), profile);
-      if (profile.getParentKee() != null) {
-        profilesByParentUuid.put(profile.getParentKee(), profile);
-      }
-    }
-  }
-
-  private void register(Collection<ActiveRuleDto> activeRules, Collection<ActiveRuleParamDto> activeRuleParams) {
-    ListMultimap<String, ActiveRuleParamDto> paramsByActiveRuleUuid = activeRuleParams.stream().collect(index(ActiveRuleParamDto::getActiveRuleUuid));
-    for (ActiveRuleDto activeRule : activeRules) {
-      ActiveRuleWrapper wrapper = new ActiveRuleWrapper(activeRule, paramsByActiveRuleUuid.get(activeRule.getUuid()));
-      this.activeRulesByKey.put(activeRule.getKey(), wrapper);
-    }
-  }
-
-  long getDate() {
-    return date;
-  }
-
-  /**
-   * The rule currently selected.
-   */
-  RuleWrapper getRule() {
-    checkState(currentRule != null, "Rule has not been set yet");
-    return currentRule;
-  }
-
-  @CheckForNull
-  String getRequestedParamValue(RuleActivation request, String key) {
-    if (currentRule.rule.isCustomRule()) {
-      return null;
-    }
-    return request.getParameter(key);
-  }
-
-  boolean hasRequestedParamValue(RuleActivation request, String key) {
-    return request.hasParameter(key);
-  }
-
-  /**
-   * The rules profile being selected.
-   */
-  RulesProfileDto getRulesProfile() {
-    checkState(currentRulesProfile != null, "Rule profile has not been set yet");
-    return currentRulesProfile;
-  }
-
-  /**
-   * The active rule related to the selected profile and rule.
-   * @return null if the selected rule is not activated on the selected profile.
-   * @see #getRulesProfile()
-   * @see #getRule()
-   */
-  @CheckForNull
-  ActiveRuleWrapper getActiveRule() {
-    return currentActiveRule;
-  }
-
-  /**
-   * The active rule related to the rule and the parent of the selected profile.
-   * @return null if the selected rule is not activated on the parent profile.
-   * @see #getRule()
-   */
-  @CheckForNull
-  ActiveRuleWrapper getParentActiveRule() {
-    return currentParentActiveRule;
-  }
-
-  /**
-   * Whether the profile cursor is on the base profile or not.
-   */
-  boolean isCascading() {
-    return currentRulesProfile != null && !currentRulesProfile.getUuid().equals(baseRulesProfile.getUuid());
-  }
-
-  /**
-   * The profiles being selected. Can be zero or many if {@link #getRulesProfile()} is built-in.
-   * Else the collection always contains a single profile.
-   */
-  Collection<QProfileDto> getProfiles() {
-    checkState(currentProfiles != null, "Profiles have not been set yet");
-    return currentProfiles;
-  }
-
-  /**
-   * The children of {@link #getProfiles()}
-   */
-  Collection<QProfileDto> getChildProfiles() {
-    loadDescendants();
-    return getProfiles().stream()
-      .flatMap(p -> profilesByParentUuid.get(p.getKee()).stream())
-      .collect(Collectors.toList());
-  }
-
-  private void loadDescendants() {
-    if (descendantsLoaded) {
-      return;
-    }
-    Collection<QProfileDto> baseProfiles = profilesByUuid.values().stream()
-      .filter(p -> p.getRulesProfileUuid().equals(baseRulesProfile.getUuid()))
-      .collect(toArrayList(profilesByUuid.size()));
-    DescendantProfilesSupplier.Result result = descendantProfilesSupplier.get(baseProfiles, rulesByUuid.keySet());
-    register(result.getProfiles());
-    register(result.getActiveRules(), result.getActiveRuleParams());
-    descendantsLoaded = true;
-  }
-
-  /**
-   * Move the cursor to the given rule and back to the base profile.
-   */
-  public void reset(String ruleUuid) {
-    doSwitch(this.baseRulesProfile, ruleUuid);
-  }
-
-  /**
-   * Moves cursor to a child profile
-   */
-  void selectChild(QProfileDto to) {
-    checkState(!to.isBuiltIn());
-    QProfileDto qp = requireNonNull(this.profilesByUuid.get(to.getKee()), () -> "No profile with uuid " + to.getKee());
-
-    RulesProfileDto ruleProfile = RulesProfileDto.from(qp);
-    doSwitch(ruleProfile, getRule().get().getUuid());
-  }
-
-  private void doSwitch(RulesProfileDto ruleProfile, String ruleUuid) {
-    this.currentRule = rulesByUuid.get(ruleUuid);
-    checkRequest(this.currentRule != null, "Rule with UUID %s not found", ruleUuid);
-    RuleKey ruleKey = currentRule.get().getKey();
-
-    this.currentRulesProfile = ruleProfile;
-    this.currentProfiles = profilesByUuid.values().stream()
-      .filter(p -> p.getRulesProfileUuid().equals(ruleProfile.getUuid()))
-      .collect(Collectors.toList());
-    this.currentActiveRule = this.activeRulesByKey.get(ActiveRuleKey.of(ruleProfile, ruleKey));
-    this.currentParentActiveRule = this.currentProfiles.stream()
-      .map(QProfileDto::getParentKee)
-      .filter(Objects::nonNull)
-      .map(profilesByUuid::get)
-      .filter(Objects::nonNull)
-      .findFirst()
-      .map(profile -> activeRulesByKey.get(ActiveRuleKey.of(profile, ruleKey)))
-      .orElse(null);
-  }
-
-  static final class Builder {
-    private long date = System.currentTimeMillis();
-    private RulesProfileDto baseRulesProfile;
-    private Collection<RuleDefinitionDto> rules;
-    private Collection<RuleParamDto> ruleParams;
-    private Collection<QProfileDto> profiles;
-    private Collection<ActiveRuleDto> activeRules;
-    private Collection<ActiveRuleParamDto> activeRuleParams;
-    private DescendantProfilesSupplier descendantProfilesSupplier;
-
-    Builder setDate(long l) {
-      this.date = l;
-      return this;
-    }
-
-    Builder setBaseProfile(RulesProfileDto p) {
-      this.baseRulesProfile = p;
-      return this;
-    }
-
-    Builder setRules(Collection<RuleDefinitionDto> rules) {
-      this.rules = rules;
-      return this;
-    }
-
-    Builder setRuleParams(Collection<RuleParamDto> ruleParams) {
-      this.ruleParams = ruleParams;
-      return this;
-    }
-
-    /**
-     * All the profiles involved in the activation workflow, including the
-     * parent profile, even if it's not updated.
-     */
-    Builder setProfiles(Collection<QProfileDto> profiles) {
-      this.profiles = profiles;
-      return this;
-    }
-
-    Builder setActiveRules(Collection<ActiveRuleDto> activeRules) {
-      this.activeRules = activeRules;
-      return this;
-    }
-
-    Builder setActiveRuleParams(Collection<ActiveRuleParamDto> activeRuleParams) {
-      this.activeRuleParams = activeRuleParams;
-      return this;
-    }
-
-    Builder setDescendantProfilesSupplier(DescendantProfilesSupplier d) {
-      this.descendantProfilesSupplier = d;
-      return this;
-    }
-
-    RuleActivationContext build() {
-      checkArgument(date > 0, "date is not set");
-      requireNonNull(baseRulesProfile, "baseRulesProfile is null");
-      requireNonNull(rules, "rules is null");
-      requireNonNull(ruleParams, "ruleParams is null");
-      requireNonNull(profiles, "profiles is null");
-      requireNonNull(activeRules, "activeRules is null");
-      requireNonNull(activeRuleParams, "activeRuleParams is null");
-      requireNonNull(descendantProfilesSupplier, "descendantProfilesSupplier is null");
-      return new RuleActivationContext(this);
-    }
-  }
-
-  static final class RuleWrapper {
-    private final RuleDefinitionDto rule;
-    private final Map<String, RuleParamDto> paramsByKey;
-
-    private RuleWrapper(RuleDefinitionDto rule, Collection<RuleParamDto> params) {
-      this.rule = rule;
-      this.paramsByKey = params.stream().collect(uniqueIndex(RuleParamDto::getName));
-    }
-
-    RuleDefinitionDto get() {
-      return rule;
-    }
-
-    Collection<RuleParamDto> getParams() {
-      return paramsByKey.values();
-    }
-
-    @CheckForNull
-    RuleParamDto getParam(String key) {
-      return paramsByKey.get(key);
-    }
-
-    @CheckForNull
-    String getParamDefaultValue(String key) {
-      RuleParamDto param = getParam(key);
-      return param != null ? param.getDefaultValue() : null;
-    }
-  }
-
-  static final class ActiveRuleWrapper {
-    private final ActiveRuleDto activeRule;
-    private final Map<String, ActiveRuleParamDto> paramsByKey;
-
-    private ActiveRuleWrapper(ActiveRuleDto activeRule, Collection<ActiveRuleParamDto> params) {
-      this.activeRule = activeRule;
-      this.paramsByKey = params.stream().collect(uniqueIndex(ActiveRuleParamDto::getKey));
-    }
-
-    ActiveRuleDto get() {
-      return activeRule;
-    }
-
-    Collection<ActiveRuleParamDto> getParams() {
-      return paramsByKey.values();
-    }
-
-    @CheckForNull
-    ActiveRuleParamDto getParam(String key) {
-      return paramsByKey.get(key);
-    }
-
-    @CheckForNull
-    String getParamValue(String key) {
-      ActiveRuleParamDto param = paramsByKey.get(key);
-      return param != null ? param.getValue() : null;
-    }
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivator.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivator.java
deleted file mode 100644 (file)
index 713ae6c..0000000
+++ /dev/null
@@ -1,536 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.base.Splitter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Stream;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
-import org.sonar.api.rule.RuleStatus;
-import org.sonar.api.server.ServerSide;
-import org.sonar.api.server.rule.RuleParamType;
-import org.sonar.api.utils.System2;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.qualityprofile.ActiveRuleDao;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleKey;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.OrgQProfileDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.qualityprofile.RulesProfileDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.qualityprofile.RuleActivationContext.ActiveRuleWrapper;
-import org.sonar.server.qualityprofile.RuleActivationContext.RuleWrapper;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.util.TypeValidations;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static java.util.stream.Collectors.toList;
-import static org.sonar.server.exceptions.BadRequestException.checkRequest;
-
-/**
- * Activation and deactivation of rules in Quality profiles
- */
-@ServerSide
-public class RuleActivator {
-
-  private final System2 system2;
-  private final DbClient db;
-  private final TypeValidations typeValidations;
-  private final UserSession userSession;
-
-  public RuleActivator(System2 system2, DbClient db, TypeValidations typeValidations, UserSession userSession) {
-    this.system2 = system2;
-    this.db = db;
-    this.typeValidations = typeValidations;
-    this.userSession = userSession;
-  }
-
-
-  public List<ActiveRuleChange> activate(DbSession dbSession, Collection<RuleActivation> activations, RuleActivationContext context) {
-    return activations.stream().map(a -> activate(dbSession, a, context))
-      .flatMap(List::stream)
-      .collect(toList());
-  }
-
-  public List<ActiveRuleChange> activate(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
-    context.reset(activation.getRuleUuid());
-    List<ActiveRuleChange> activeRuleChanges = doActivate(dbSession, activation, context);
-    return activeRuleChanges;
-  }
-
-  private List<ActiveRuleChange> doActivate(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
-    RuleDefinitionDto rule = context.getRule().get();
-    checkRequest(RuleStatus.REMOVED != rule.getStatus(), "Rule was removed: %s", rule.getKey());
-    checkRequest(!rule.isTemplate(), "Rule template can't be activated on a Quality profile: %s", rule.getKey());
-    checkRequest(context.getRulesProfile().getLanguage().equals(rule.getLanguage()),
-      "%s rule %s cannot be activated on %s profile %s", rule.getLanguage(), rule.getKey(), context.getRulesProfile().getLanguage(),context.getRulesProfile().getName());
-    List<ActiveRuleChange> changes = new ArrayList<>();
-    ActiveRuleChange change;
-    boolean stopCascading = false;
-
-    ActiveRuleWrapper activeRule = context.getActiveRule();
-    ActiveRuleKey activeRuleKey = ActiveRuleKey.of(context.getRulesProfile(), rule.getKey());
-    if (activeRule == null) {
-      if (activation.isReset()) {
-        // ignore reset when rule is not activated
-        return changes;
-      }
-      // new activation
-      change = new ActiveRuleChange(ActiveRuleChange.Type.ACTIVATED, activeRuleKey, rule);
-      applySeverityAndParamToChange(activation, context, change);
-      if (context.isCascading() || isSameAsParent(change, context)) {
-        change.setInheritance(ActiveRuleInheritance.INHERITED);
-      }
-    } else {
-      // already activated
-      if (context.isCascading() && activeRule.get().doesOverride()) {
-        // propagating to descendants, but child profile already overrides rule -> stop propagation
-        return changes;
-      }
-      change = new ActiveRuleChange(ActiveRuleChange.Type.UPDATED, activeRuleKey, rule);
-      if (context.isCascading() && activeRule.get().getInheritance() == null) {
-        // activate on child, then on parent -> mark child as overriding parent
-        change.setInheritance(ActiveRuleInheritance.OVERRIDES);
-        change.setSeverity(activeRule.get().getSeverityString());
-        for (ActiveRuleParamDto activeParam : activeRule.getParams()) {
-          change.setParameter(activeParam.getKey(), activeParam.getValue());
-        }
-        stopCascading = true;
-      } else {
-        applySeverityAndParamToChange(activation, context, change);
-        if (!context.isCascading() && context.getParentActiveRule() != null) {
-          // override rule which is already declared on parents
-          change.setInheritance(isSameAsParent(change, context) ? ActiveRuleInheritance.INHERITED : ActiveRuleInheritance.OVERRIDES);
-        }
-      }
-      if (isSame(change, activeRule)) {
-        change = null;
-        stopCascading = true;
-      }
-    }
-
-    if (change != null) {
-      changes.add(change);
-      persist(change, context, dbSession);
-    }
-
-    if (!changes.isEmpty()) {
-      updateProfileDates(dbSession, context);
-    }
-
-    if (!stopCascading) {
-      changes.addAll(propagateActivationToDescendants(dbSession, activation, context));
-    }
-
-    return changes;
-  }
-
-  private void updateProfileDates(DbSession dbSession, RuleActivationContext context) {
-    RulesProfileDto ruleProfile = context.getRulesProfile();
-    ruleProfile.setRulesUpdatedAtAsDate(new Date(context.getDate()));
-    db.qualityProfileDao().update(dbSession, ruleProfile);
-
-    if (userSession.isLoggedIn()) {
-      context.getProfiles().forEach(p -> db.qualityProfileDao().update(dbSession, OrgQProfileDto.from(p).setUserUpdatedAt(context.getDate())));
-    }
-  }
-
-  /**
-   * Update severity and params
-   */
-  private void applySeverityAndParamToChange(RuleActivation request, RuleActivationContext context, ActiveRuleChange change) {
-    RuleWrapper rule = context.getRule();
-    ActiveRuleWrapper activeRule = context.getActiveRule();
-    ActiveRuleWrapper parentActiveRule = context.getParentActiveRule();
-
-    if (request.isReset()) {
-      applySeverityAndParamsWhenResetRequested(change, rule, parentActiveRule);
-    } else if (context.getRulesProfile().isBuiltIn()) {
-      applySeverityAndParamsWhenBuiltInProfile(request, context, change, rule);
-    } else {
-      applySeverityAndParamsWhenNonBuiltInProfile(request, context, change, rule, activeRule, parentActiveRule);
-    }
-  }
-
-  private void applySeverityAndParamsWhenResetRequested(ActiveRuleChange change, RuleWrapper rule, @Nullable ActiveRuleWrapper parentActiveRule) {
-    String severity = firstNonNull(
-      parentActiveRule != null ? parentActiveRule.get().getSeverityString() : null,
-      rule.get().getSeverityString());
-    change.setSeverity(severity);
-
-    for (RuleParamDto ruleParamDto : rule.getParams()) {
-      String paramKey = ruleParamDto.getName();
-      // load params from parent profile, else from default values
-      String paramValue = firstNonNull(
-        parentActiveRule != null ? parentActiveRule.getParamValue(paramKey) : null,
-        rule.getParamDefaultValue(paramKey));
-
-      change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
-    }
-  }
-
-  private void applySeverityAndParamsWhenBuiltInProfile(RuleActivation request, RuleActivationContext context, ActiveRuleChange change,
-    RuleWrapper rule) {
-    // for builtin quality profiles, the severity from profile, when null use the default severity of the rule
-    String severity = firstNonNull(request.getSeverity(), rule.get().getSeverityString());
-    change.setSeverity(severity);
-
-    for (RuleParamDto ruleParamDto : rule.getParams()) {
-      String paramKey = ruleParamDto.getName();
-      // use the value defined in the profile definition, else the rule default value
-      String paramValue = firstNonNull(
-        context.getRequestedParamValue(request, paramKey),
-        rule.getParamDefaultValue(paramKey));
-      change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
-    }
-  }
-
-  /**
-   * 1. apply requested severity and param
-   * 2. if rule activated and overridden - apply user value
-   * 3. apply parent value
-   * 4. apply defaults
-   */
-  private void applySeverityAndParamsWhenNonBuiltInProfile(RuleActivation request, RuleActivationContext context, ActiveRuleChange change,
-    RuleWrapper rule, @Nullable ActiveRuleWrapper activeRule, @Nullable ActiveRuleWrapper parentActiveRule) {
-    String severity = getSeverityForNonBuiltInProfile(request, rule, activeRule, parentActiveRule);
-    change.setSeverity(severity);
-
-    for (RuleParamDto ruleParamDto : rule.getParams()) {
-      String paramKey = ruleParamDto.getName();
-      String parentValue = parentActiveRule != null ? parentActiveRule.getParamValue(paramKey) : null;
-      String paramValue;
-      if (context.hasRequestedParamValue(request, paramKey)) {
-        // If the request contains the parameter then we're using either value from request, or parent value, or default value
-        paramValue = firstNonNull(
-          context.getRequestedParamValue(request, paramKey),
-          parentValue,
-          rule.getParamDefaultValue(paramKey));
-      } else if (activeRule != null) {
-        // If the request doesn't contain the parameter, then we're using either user value from db, or parent value if rule inherited, or default
-        // value
-        paramValue = firstNonNull(
-          activeRule.get().doesOverride() ? activeRule.getParamValue(paramKey) : null,
-          parentValue == null ? activeRule.getParamValue(paramKey) : parentValue,
-          rule.getParamDefaultValue(paramKey));
-      } else {
-        paramValue = firstNonNull(
-          parentValue,
-          rule.getParamDefaultValue(paramKey));
-      }
-      change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
-    }
-  }
-
-  private static String getSeverityForNonBuiltInProfile(RuleActivation request, RuleWrapper rule, @Nullable ActiveRuleWrapper activeRule,
-    @Nullable ActiveRuleWrapper parentActiveRule) {
-    String severity;
-    if (activeRule != null) {
-      ActiveRuleDto activeRuleDto = activeRule.get();
-      // load severity from request, else keep existing one (if overridden), else from parent if rule inherited, else from default
-      severity = firstNonNull(
-        request.getSeverity(),
-        activeRuleDto.doesOverride() ? activeRuleDto.getSeverityString() : null,
-          parentActiveRule != null ? parentActiveRule.get().getSeverityString() : activeRuleDto.getSeverityString(),
-        rule.get().getSeverityString());
-    } else {
-      // load severity from request, else from parent, else from default
-      severity = firstNonNull(
-        request.getSeverity(),
-        parentActiveRule != null ? parentActiveRule.get().getSeverityString() : null,
-        rule.get().getSeverityString());
-    }
-    return severity;
-  }
-
-  private List<ActiveRuleChange> propagateActivationToDescendants(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
-    List<ActiveRuleChange> changes = new ArrayList<>();
-
-    // get all inherited profiles
-    context.getChildProfiles().forEach(child -> {
-      context.selectChild(child);
-      changes.addAll(doActivate(dbSession, activation, context));
-    });
-    return changes;
-  }
-
-  private void persist(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
-    ActiveRuleDto activeRule = null;
-    if (change.getType() == ActiveRuleChange.Type.ACTIVATED) {
-      activeRule = doInsert(change, context, dbSession);
-    } else if (change.getType() == ActiveRuleChange.Type.DEACTIVATED) {
-      ActiveRuleDao dao = db.activeRuleDao();
-      activeRule = dao.delete(dbSession, change.getKey()).orElse(null);
-
-    } else if (change.getType() == ActiveRuleChange.Type.UPDATED) {
-      activeRule = doUpdate(change, context, dbSession);
-    }
-    change.setActiveRule(activeRule);
-    db.qProfileChangeDao().insert(dbSession, change.toDto(userSession.getUuid()));
-  }
-
-  private ActiveRuleDto doInsert(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
-    ActiveRuleDao dao = db.activeRuleDao();
-    RuleWrapper rule = context.getRule();
-
-    ActiveRuleDto activeRule = new ActiveRuleDto();
-    activeRule.setProfileUuid(context.getRulesProfile().getUuid());
-    activeRule.setRuleUuid(rule.get().getUuid());
-    activeRule.setKey(ActiveRuleKey.of(context.getRulesProfile(), rule.get().getKey()));
-    String severity = change.getSeverity();
-    if (severity != null) {
-      activeRule.setSeverity(severity);
-    }
-    ActiveRuleInheritance inheritance = change.getInheritance();
-    if (inheritance != null) {
-      activeRule.setInheritance(inheritance.name());
-    }
-    activeRule.setUpdatedAt(system2.now());
-    activeRule.setCreatedAt(system2.now());
-    dao.insert(dbSession, activeRule);
-    for (Map.Entry<String, String> param : change.getParameters().entrySet()) {
-      if (param.getValue() != null) {
-        ActiveRuleParamDto paramDto = ActiveRuleParamDto.createFor(rule.getParam(param.getKey()));
-        paramDto.setValue(param.getValue());
-        dao.insertParam(dbSession, activeRule, paramDto);
-      }
-    }
-    return activeRule;
-  }
-
-  private ActiveRuleDto doUpdate(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
-    ActiveRuleWrapper activeRule = context.getActiveRule();
-    if (activeRule == null) {
-      return null;
-    }
-    ActiveRuleDao dao = db.activeRuleDao();
-    String severity = change.getSeverity();
-    if (severity != null) {
-      activeRule.get().setSeverity(severity);
-    }
-    ActiveRuleInheritance inheritance = change.getInheritance();
-    if (inheritance != null) {
-      activeRule.get().setInheritance(inheritance.name());
-    }
-    activeRule.get().setUpdatedAt(system2.now());
-    dao.update(dbSession, activeRule.get());
-
-    for (Map.Entry<String, String> param : change.getParameters().entrySet()) {
-      ActiveRuleParamDto activeRuleParamDto = activeRule.getParam(param.getKey());
-      if (activeRuleParamDto == null) {
-        // did not exist
-        if (param.getValue() != null) {
-          activeRuleParamDto = ActiveRuleParamDto.createFor(context.getRule().getParam(param.getKey()));
-          activeRuleParamDto.setValue(param.getValue());
-          dao.insertParam(dbSession, activeRule.get(), activeRuleParamDto);
-        }
-      } else {
-        if (param.getValue() != null) {
-          activeRuleParamDto.setValue(param.getValue());
-          dao.updateParam(dbSession, activeRuleParamDto);
-        } else {
-          dao.deleteParam(dbSession, activeRuleParamDto);
-        }
-      }
-    }
-    return activeRule.get();
-  }
-
-  public List<ActiveRuleChange> deactivate(DbSession dbSession, RuleActivationContext context, String ruleUuid, boolean force) {
-    context.reset(ruleUuid);
-    List<ActiveRuleChange> activeRuleChanges = doDeactivate(dbSession, context, force);
-    return activeRuleChanges;
-  }
-
-  private List<ActiveRuleChange> doDeactivate(DbSession dbSession, RuleActivationContext context, boolean force) {
-    List<ActiveRuleChange> changes = new ArrayList<>();
-    ActiveRuleWrapper activeRule = context.getActiveRule();
-    if (activeRule == null) {
-      return changes;
-    }
-
-    ActiveRuleChange change;
-    checkRequest(force || context.isCascading() || activeRule.get().getInheritance() == null, "Cannot deactivate inherited rule '%s'", context.getRule().get().getKey());
-    change = new ActiveRuleChange(ActiveRuleChange.Type.DEACTIVATED, activeRule.get(), context.getRule().get());
-    changes.add(change);
-    persist(change, context, dbSession);
-
-    // get all inherited profiles (they are not built-in by design)
-    context.getChildProfiles().forEach(child -> {
-      context.selectChild(child);
-      changes.addAll(doDeactivate(dbSession, context, force));
-    });
-
-    if (!changes.isEmpty()) {
-      updateProfileDates(dbSession, context);
-    }
-
-    return changes;
-  }
-
-  @CheckForNull
-  private String validateParam(RuleParamDto ruleParam, @Nullable String value) {
-    if (value != null) {
-      RuleParamType ruleParamType = RuleParamType.parse(ruleParam.getType());
-      if (ruleParamType.multiple()) {
-        List<String> values = Splitter.on(",").splitToList(value);
-        typeValidations.validate(values, ruleParamType.type(), ruleParamType.values());
-      } else {
-        typeValidations.validate(value, ruleParamType.type(), ruleParamType.values());
-      }
-    }
-    return value;
-  }
-
-  public RuleActivationContext createContextForBuiltInProfile(DbSession dbSession, RulesProfileDto builtInProfile, Collection<String> ruleUuids) {
-    checkArgument(builtInProfile.isBuiltIn(), "Rules profile with UUID %s is not built-in", builtInProfile.getUuid());
-
-    RuleActivationContext.Builder builder = new RuleActivationContext.Builder();
-    builder.setDescendantProfilesSupplier(createDescendantProfilesSupplier(dbSession));
-
-    // load rules
-    completeWithRules(dbSession, builder, ruleUuids);
-
-    // load org profiles. Their parents are null by nature.
-    List<QProfileDto> profiles = db.qualityProfileDao().selectQProfilesByRuleProfile(dbSession, builtInProfile);
-    builder.setProfiles(profiles);
-    builder.setBaseProfile(builtInProfile);
-
-    // load active rules
-    Collection<String> ruleProfileUuids = Stream
-      .concat(Stream.of(builtInProfile.getUuid()), profiles.stream().map(QProfileDto::getRulesProfileUuid))
-      .collect(MoreCollectors.toHashSet(profiles.size() + 1));
-    completeWithActiveRules(dbSession, builder, ruleUuids, ruleProfileUuids);
-    return builder.build();
-  }
-
-  public RuleActivationContext createContextForUserProfile(DbSession dbSession, QProfileDto profile, Collection<String> ruleUuids) {
-    checkArgument(!profile.isBuiltIn(), "Profile with UUID %s is built-in", profile.getKee());
-    RuleActivationContext.Builder builder = new RuleActivationContext.Builder();
-    builder.setDescendantProfilesSupplier(createDescendantProfilesSupplier(dbSession));
-
-    // load rules
-    completeWithRules(dbSession, builder, ruleUuids);
-
-    // load profiles
-    List<QProfileDto> profiles = new ArrayList<>();
-    profiles.add(profile);
-    if (profile.getParentKee() != null) {
-      profiles.add(db.qualityProfileDao().selectByUuid(dbSession, profile.getParentKee()));
-    }
-    builder.setProfiles(profiles);
-    builder.setBaseProfile(RulesProfileDto.from(profile));
-
-    // load active rules
-    Collection<String> ruleProfileUuids = profiles.stream()
-      .map(QProfileDto::getRulesProfileUuid)
-      .collect(MoreCollectors.toHashSet(profiles.size()));
-    completeWithActiveRules(dbSession, builder, ruleUuids, ruleProfileUuids);
-
-    return builder.build();
-  }
-
-  DescendantProfilesSupplier createDescendantProfilesSupplier(DbSession dbSession) {
-    return (parents, ruleUuids) -> {
-      Collection<QProfileDto> profiles = db.qualityProfileDao().selectDescendants(dbSession, parents);
-      Set<String> ruleProfileUuids = profiles.stream()
-        .map(QProfileDto::getRulesProfileUuid)
-        .collect(MoreCollectors.toHashSet());
-      Collection<ActiveRuleDto> activeRules = db.activeRuleDao().selectByRulesAndRuleProfileUuids(dbSession, ruleUuids, ruleProfileUuids);
-      List<String> activeRuleUuids = activeRules.stream().map(ActiveRuleDto::getUuid).collect(MoreCollectors.toArrayList(activeRules.size()));
-      List<ActiveRuleParamDto> activeRuleParams = db.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, activeRuleUuids);
-      return new DescendantProfilesSupplier.Result(profiles, activeRules, activeRuleParams);
-    };
-  }
-
-  private void completeWithRules(DbSession dbSession, RuleActivationContext.Builder builder, Collection<String> ruleUuids) {
-    List<RuleDefinitionDto> rules = db.ruleDao().selectDefinitionByUuids(dbSession, ruleUuids);
-    builder.setRules(rules);
-    builder.setRuleParams(db.ruleDao().selectRuleParamsByRuleUuids(dbSession, ruleUuids));
-  }
-
-  private void completeWithActiveRules(DbSession dbSession, RuleActivationContext.Builder builder, Collection<String> ruleUuids, Collection<String> ruleProfileUuids) {
-    Collection<ActiveRuleDto> activeRules = db.activeRuleDao().selectByRulesAndRuleProfileUuids(dbSession, ruleUuids, ruleProfileUuids);
-    builder.setActiveRules(activeRules);
-    List<String> activeRuleUuids = activeRules.stream().map(ActiveRuleDto::getUuid).collect(MoreCollectors.toArrayList(activeRules.size()));
-    builder.setActiveRuleParams(db.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, activeRuleUuids));
-  }
-
-  private static boolean isSame(ActiveRuleChange change, ActiveRuleWrapper activeRule) {
-    ActiveRuleInheritance inheritance = change.getInheritance();
-    if (inheritance != null && !inheritance.name().equals(activeRule.get().getInheritance())) {
-      return false;
-    }
-    String severity = change.getSeverity();
-    if (severity != null && !severity.equals(activeRule.get().getSeverityString())) {
-      return false;
-    }
-    for (Map.Entry<String, String> changeParam : change.getParameters().entrySet()) {
-      String activeParamValue = activeRule.getParamValue(changeParam.getKey());
-      if (changeParam.getValue() == null && activeParamValue != null) {
-        return false;
-      }
-      if (changeParam.getValue() != null && (activeParamValue == null || !StringUtils.equals(changeParam.getValue(), activeParamValue))) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  /**
-   * True if trying to override an inherited rule but with exactly the same values
-   */
-  private static boolean isSameAsParent(ActiveRuleChange change, RuleActivationContext context) {
-    ActiveRuleWrapper parentActiveRule = context.getParentActiveRule();
-    if (parentActiveRule == null) {
-      return false;
-    }
-    if (!StringUtils.equals(change.getSeverity(), parentActiveRule.get().getSeverityString())) {
-      return false;
-    }
-    for (Map.Entry<String, String> entry : change.getParameters().entrySet()) {
-      if (entry.getValue() != null && !entry.getValue().equals(parentActiveRule.getParamValue(entry.getKey()))) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  @CheckForNull
-  private static String firstNonNull(String... strings) {
-    for (String s : strings) {
-      if (s != null) {
-        return s;
-      }
-    }
-    return null;
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivatorEventsDistributor.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivatorEventsDistributor.java
deleted file mode 100644 (file)
index d41f1a5..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import org.sonar.core.util.RuleActivationListener;
-import org.sonar.core.util.RuleSetChangeEvent;
-
-public interface RuleActivatorEventsDistributor {
-
-  void subscribe(RuleActivationListener listener);
-
-  void pushEvent(RuleSetChangeEvent event);
-}
\ No newline at end of file
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/StandaloneRuleActivatorEventsDistributor.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/StandaloneRuleActivatorEventsDistributor.java
deleted file mode 100644 (file)
index 43fc97d..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.ArrayList;
-import java.util.List;
-import org.sonar.api.server.ServerSide;
-import org.sonar.core.util.RuleActivationListener;
-import org.sonar.core.util.RuleSetChangeEvent;
-
-@ServerSide
-public class StandaloneRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {
-
-  private List<RuleActivationListener> listeners = new ArrayList<>();
-
-  @Override
-  public void subscribe(RuleActivationListener listener) {
-    listeners.add(listener);
-  }
-
-  @Override
-  public void pushEvent(RuleSetChangeEvent event) {
-    listeners.forEach(l -> l.listen(event));
-  }
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/package-info.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/package-info.java
deleted file mode 100644 (file)
index 0b1a410..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import javax.annotation.ParametersAreNonnullByDefault;
-
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationHandlerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationHandlerTest.java
deleted file mode 100644 (file)
index c178ab5..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.Random;
-import java.util.Set;
-import java.util.stream.IntStream;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.EmailSubscriberDto;
-import org.sonar.db.permission.AuthorizationDao;
-import org.sonar.server.notification.email.EmailNotificationChannel;
-
-import static java.util.Collections.emptySet;
-import static java.util.stream.Collectors.toSet;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class BuiltInQPChangeNotificationHandlerTest {
-  private DbClient dbClient = mock(DbClient.class);
-  private DbSession dbSession = mock(DbSession.class);
-  private AuthorizationDao authorizationDao = mock(AuthorizationDao.class);
-  private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
-
-  private BuiltInQPChangeNotificationHandler underTest = new BuiltInQPChangeNotificationHandler(dbClient, emailNotificationChannel);
-
-  @Before
-  public void wire_mocks() {
-    when(dbClient.openSession(false)).thenReturn(dbSession);
-    when(dbClient.authorizationDao()).thenReturn(authorizationDao);
-  }
-
-  @Test
-  public void getMetadata_returns_empty() {
-    assertThat(underTest.getMetadata()).isEmpty();
-  }
-
-  @Test
-  public void getNotificationClass_is_BuiltInQPChangeNotification() {
-    assertThat(underTest.getNotificationClass()).isEqualTo(BuiltInQPChangeNotification.class);
-  }
-
-  @Test
-  public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
-    when(emailNotificationChannel.isActivated()).thenReturn(false);
-    Set<BuiltInQPChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
-      .mapToObj(i -> mock(BuiltInQPChangeNotification.class))
-      .collect(toSet());
-
-    int deliver = underTest.deliver(notifications);
-
-    assertThat(deliver).isZero();
-    verify(emailNotificationChannel).isActivated();
-    verifyNoMoreInteractions(emailNotificationChannel);
-    verifyZeroInteractions(dbClient);
-    notifications.forEach(Mockito::verifyZeroInteractions);
-  }
-
-  @Test
-  public void deliver_has_no_effect_if_there_is_no_global_administer_email_subscriber() {
-    when(emailNotificationChannel.isActivated()).thenReturn(true);
-    Set<BuiltInQPChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
-      .mapToObj(i -> mock(BuiltInQPChangeNotification.class))
-      .collect(toSet());
-    when(authorizationDao.selectQualityProfileAdministratorLogins(dbSession))
-      .thenReturn(emptySet());
-
-    int deliver = underTest.deliver(notifications);
-
-    assertThat(deliver).isZero();
-    verify(emailNotificationChannel).isActivated();
-    verifyNoMoreInteractions(emailNotificationChannel);
-    verify(dbClient).openSession(false);
-    verify(dbClient).authorizationDao();
-    verifyNoMoreInteractions(dbClient);
-    verify(authorizationDao).selectQualityProfileAdministratorLogins(dbSession);
-    verifyNoMoreInteractions(authorizationDao);
-    notifications.forEach(Mockito::verifyZeroInteractions);
-  }
-
-  @Test
-  public void deliver_create_emailRequest_for_each_notification_and_for_each_global_administer_email_subscriber() {
-    when(emailNotificationChannel.isActivated()).thenReturn(true);
-    Set<BuiltInQPChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
-      .mapToObj(i -> mock(BuiltInQPChangeNotification.class))
-      .collect(toSet());
-    Set<EmailSubscriberDto> emailSubscribers = IntStream.range(0, 1 + new Random().nextInt(10))
-      .mapToObj(i -> EmailSubscriberDto.create("login_" + i, true, "login_" + i + "@foo"))
-      .collect(toSet());
-    when(authorizationDao.selectQualityProfileAdministratorLogins(dbSession))
-      .thenReturn(emailSubscribers);
-    Set<EmailNotificationChannel.EmailDeliveryRequest> expectedRequests = notifications.stream()
-      .flatMap(notification -> emailSubscribers.stream().map(subscriber -> new EmailNotificationChannel.EmailDeliveryRequest(subscriber.getEmail(), notification)))
-      .collect(toSet());
-    int deliveries = new Random().nextInt(expectedRequests.size());
-    when(emailNotificationChannel.deliverAll(expectedRequests)).thenReturn(deliveries);
-
-    int deliver = underTest.deliver(notifications);
-
-    assertThat(deliver).isEqualTo(deliveries);
-    verify(emailNotificationChannel).isActivated();
-    verify(emailNotificationChannel).deliverAll(expectedRequests);
-    verifyNoMoreInteractions(emailNotificationChannel);
-    verify(dbClient).openSession(false);
-    verify(dbClient).authorizationDao();
-    verifyNoMoreInteractions(dbClient);
-    verify(authorizationDao).selectQualityProfileAdministratorLogins(dbSession);
-    verifyNoMoreInteractions(authorizationDao);
-    notifications.forEach(Mockito::verifyZeroInteractions);
-  }
-
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTemplateTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTemplateTest.java
deleted file mode 100644 (file)
index 6cf1581..0000000
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.Date;
-import org.junit.Before;
-import org.junit.Test;
-import org.sonar.api.platform.Server;
-import org.sonar.server.issue.notification.EmailMessage;
-import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationBuilder.Profile;
-
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.utils.DateUtils.formatDate;
-
-public class BuiltInQPChangeNotificationTemplateTest {
-
-  private Server server = mock(Server.class);
-
-  private BuiltInQPChangeNotificationTemplate underTest = new BuiltInQPChangeNotificationTemplate(server);
-
-  @Before
-  public void setUp() {
-    when(server.getPublicRootUrl()).thenReturn("http://" + randomAlphanumeric(10));
-  }
-
-  @Test
-  public void notification_contains_a_subject() {
-    String profileName = newProfileName();
-    String languageKey = newLanguageKey();
-    String languageName = newLanguageName();
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName)
-        .setLanguageKey(languageKey)
-        .setLanguageName(languageName)
-        .setNewRules(2)
-        .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertThat(emailMessage.getSubject()).isEqualTo("Built-in quality profiles have been updated");
-  }
-
-  @Test
-  public void notification_contains_count_of_new_rules() {
-    String profileName = newProfileName();
-    String languageKey = newLanguageKey();
-    String languageName = newLanguageName();
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName)
-        .setLanguageKey(languageKey)
-        .setLanguageName(languageName)
-        .setNewRules(2)
-        .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertMessage(emailMessage, "\n 2 new rules\n");
-  }
-
-  @Test
-  public void notification_contains_count_of_updated_rules() {
-    String profileName = newProfileName();
-    String languageKey = newLanguageKey();
-    String languageName = newLanguageName();
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName)
-        .setLanguageKey(languageKey)
-        .setLanguageName(languageName)
-        .setUpdatedRules(2)
-        .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertMessage(emailMessage, "\n 2 rules have been updated\n");
-  }
-
-  @Test
-  public void notification_contains_count_of_removed_rules() {
-    String profileName = newProfileName();
-    String languageKey = newLanguageKey();
-    String languageName = newLanguageName();
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName)
-        .setLanguageKey(languageKey)
-        .setLanguageName(languageName)
-        .setRemovedRules(2)
-        .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertMessage(emailMessage, "\n 2 rules removed\n");
-  }
-
-  @Test
-  public void notification_supports_grammar_for_single_rule_added_removed_or_updated() {
-    String profileName = newProfileName();
-    String languageKey = newLanguageKey();
-    String languageName = newLanguageName();
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-        .addProfile(Profile.newBuilder()
-            .setProfileName(profileName)
-            .setLanguageKey(languageKey)
-            .setLanguageName(languageName)
-            .setNewRules(1)
-            .setUpdatedRules(1)
-            .setRemovedRules(1)
-            .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertThat(emailMessage.getMessage())
-        .contains("\n 1 new rule\n")
-        .contains("\n 1 rule has been updated\n")
-        .contains("\n 1 rule removed\n");
-  }
-
-  @Test
-  public void notification_contains_list_of_new_updated_and_removed_rules() {
-    String profileName = newProfileName();
-    String languageKey = newLanguageKey();
-    String languageName = newLanguageName();
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName)
-        .setLanguageKey(languageKey)
-        .setLanguageName(languageName)
-        .setNewRules(2)
-        .setUpdatedRules(3)
-        .setRemovedRules(4)
-        .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertMessage(emailMessage,
-      "\n" +
-        " 2 new rules\n" +
-        " 3 rules have been updated\n" +
-        " 4 rules removed\n");
-  }
-
-  @Test
-  public void notification_contains_many_profiles() {
-    String profileName1 = "profile1_" + randomAlphanumeric(20);
-    String languageKey1 = "langkey1_" + randomAlphanumeric(20);
-    String languageName1 = "langName1_" + randomAlphanumeric(20);
-    String profileName2 = "profile2_" + randomAlphanumeric(20);
-    String languageKey2 = "langkey2_" + randomAlphanumeric(20);
-    String languageName2 = "langName2_" + randomAlphanumeric(20);
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName1)
-        .setLanguageKey(languageKey1)
-        .setLanguageName(languageName1)
-        .setNewRules(2)
-        .build())
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName2)
-        .setLanguageKey(languageKey2)
-        .setLanguageName(languageName2)
-        .setNewRules(13)
-        .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertThat(emailMessage.getMessage()).containsSubsequence("The following built-in profiles have been updated:\n",
-      profileTitleText(profileName1, languageKey1, languageName1),
-      " 2 new rules\n",
-      profileTitleText(profileName2, languageKey2, languageName2),
-      " 13 new rules\n",
-      "This is a good time to review your quality profiles and update them to benefit from the latest evolutions: " + server.getPublicRootUrl() + "/profiles");
-  }
-
-  @Test
-  public void notification_contains_profiles_sorted_by_language_then_by_profile_name() {
-    String languageKey1 = "langkey1_" + randomAlphanumeric(20);
-    String languageName1 = "langName1_" + randomAlphanumeric(20);
-    String languageKey2 = "langKey2_" + randomAlphanumeric(20);
-    String languageName2 = "langName2_" + randomAlphanumeric(20);
-    String profileName1 = "profile1_" + randomAlphanumeric(20);
-    String profileName2 = "profile2_" + randomAlphanumeric(20);
-    String profileName3 = "profile3_" + randomAlphanumeric(20);
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder().setProfileName(profileName3).setLanguageKey(languageKey2).setLanguageName(languageName2).build())
-      .addProfile(Profile.newBuilder().setProfileName(profileName2).setLanguageKey(languageKey1).setLanguageName(languageName1).build())
-      .addProfile(Profile.newBuilder().setProfileName(profileName1).setLanguageKey(languageKey2).setLanguageName(languageName2).build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertThat(emailMessage.getMessage()).containsSubsequence(
-      "\"" + profileName2 + "\" - " + languageName1,
-      "\"" + profileName1 + "\" - " + languageName2,
-      "\"" + profileName3 + "\" - " + languageName2);
-  }
-
-  @Test
-  public void notification_contains_encoded_profile_name() {
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName("Sonar Way")
-        .setLanguageKey("java")
-        .setLanguageName(newLanguageName())
-        .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertThat(emailMessage.getMessage()).contains(server.getPublicRootUrl() + "/profiles/changelog?language=java&name=Sonar+Way");
-  }
-
-  @Test
-  public void notification_contains_from_and_to_date() {
-    String profileName = newProfileName();
-    String languageKey = newLanguageKey();
-    String languageName = newLanguageName();
-    long startDate = 1_000_000_000_000L;
-    long endDate = startDate + 1_100_000_000_000L;
-    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName)
-        .setLanguageKey(languageKey)
-        .setLanguageName(languageName)
-        .setStartDate(startDate)
-        .setEndDate(endDate)
-        .build());
-
-    EmailMessage emailMessage = underTest.format(notification.build());
-
-    assertMessage(emailMessage,
-      profileTitleText(profileName, languageKey, languageName, formatDate(new Date(startDate)), formatDate(new Date(endDate))));
-  }
-
-  private void assertMessage(EmailMessage emailMessage, String expectedProfileDetails) {
-    assertThat(emailMessage.getMessage())
-      .containsSubsequence(
-        "The following built-in profiles have been updated:\n\n",
-        expectedProfileDetails,
-        "\nThis is a good time to review your quality profiles and update them to benefit from the latest evolutions: " + server.getPublicRootUrl() + "/profiles");
-  }
-
-  private String profileTitleText(String profileName, String languageKey, String languageName) {
-    return "\"" + profileName + "\" - " + languageName + ": " + server.getPublicRootUrl() + "/profiles/changelog?language=" + languageKey + "&name=" + profileName;
-  }
-
-  private String profileTitleText(String profileName, String languageKey, String languageName, String startDate, String endDate) {
-    return "\"" + profileName + "\" - " + languageName + ": " + server.getPublicRootUrl() + "/profiles/changelog?language=" + languageKey + "&name=" + profileName +
-      "&since=" + startDate + "&to=" + endDate + "\n";
-  }
-
-  private static String newProfileName() {
-    return "profileName_" + randomAlphanumeric(20);
-  }
-
-  private static String newLanguageName() {
-    return "languageName_" + randomAlphanumeric(20);
-  }
-
-  private static String newLanguageKey() {
-    return "languageKey_" + randomAlphanumeric(20);
-  }
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQPChangeNotificationTest.java
deleted file mode 100644 (file)
index 6341e3a..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.Random;
-import org.junit.Test;
-import org.sonar.api.notifications.Notification;
-import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationBuilder.Profile;
-
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.Assertions.tuple;
-
-public class BuiltInQPChangeNotificationTest {
-
-  private static final Random RANDOM = new Random();
-
-
-  @Test
-  public void serialize_and_parse_no_profile() {
-    Notification notification = new BuiltInQPChangeNotificationBuilder().build();
-
-    BuiltInQPChangeNotificationBuilder result = BuiltInQPChangeNotificationBuilder.parse(notification);
-
-    assertThat(result.getProfiles()).isEmpty();
-  }
-
-  @Test
-  public void serialize_and_parse_single_profile() {
-    String profileName = randomAlphanumeric(20);
-    String languageKey = randomAlphanumeric(20);
-    String languageName = randomAlphanumeric(20);
-    int newRules = RANDOM.nextInt(5000);
-    int updatedRules = RANDOM.nextInt(5000);
-    int removedRules = RANDOM.nextInt(5000);
-    long startDate = RANDOM.nextInt(5000);
-    long endDate = startDate + RANDOM.nextInt(5000);
-
-    BuiltInQPChangeNotification notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName)
-        .setLanguageKey(languageKey)
-        .setLanguageName(languageName)
-        .setNewRules(newRules)
-        .setUpdatedRules(updatedRules)
-        .setRemovedRules(removedRules)
-        .setStartDate(startDate)
-        .setEndDate(endDate)
-        .build())
-      .build();
-    BuiltInQPChangeNotificationBuilder result = BuiltInQPChangeNotificationBuilder.parse(notification);
-
-    assertThat(result.getProfiles())
-      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getNewRules, Profile::getUpdatedRules, Profile::getRemovedRules,
-        Profile::getStartDate, Profile::getEndDate)
-      .containsExactlyInAnyOrder(tuple(profileName, languageKey, languageName, newRules, updatedRules, removedRules, startDate, endDate));
-  }
-
-  @Test
-  public void serialize_and_parse_multiple_profiles() {
-    String profileName1 = randomAlphanumeric(20);
-    String languageKey1 = randomAlphanumeric(20);
-    String languageName1 = randomAlphanumeric(20);
-    String profileName2 = randomAlphanumeric(20);
-    String languageKey2 = randomAlphanumeric(20);
-    String languageName2 = randomAlphanumeric(20);
-
-    BuiltInQPChangeNotification notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName1)
-        .setLanguageKey(languageKey1)
-        .setLanguageName(languageName1)
-        .build())
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName2)
-        .setLanguageKey(languageKey2)
-        .setLanguageName(languageName2)
-        .build())
-      .build();
-    BuiltInQPChangeNotificationBuilder result = BuiltInQPChangeNotificationBuilder.parse(notification);
-
-    assertThat(result.getProfiles()).extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName)
-      .containsExactlyInAnyOrder(tuple(profileName1, languageKey1, languageName1), tuple(profileName2, languageKey2, languageName2));
-  }
-
-  @Test
-  public void serialize_and_parse_max_values() {
-    String profileName = randomAlphanumeric(20);
-    String languageKey = randomAlphanumeric(20);
-    String languageName = randomAlphanumeric(20);
-    int newRules = Integer.MAX_VALUE;
-    int updatedRules = Integer.MAX_VALUE;
-    int removedRules = Integer.MAX_VALUE;
-    long startDate = Long.MAX_VALUE;
-    long endDate = Long.MAX_VALUE;
-
-    BuiltInQPChangeNotification notification = new BuiltInQPChangeNotificationBuilder()
-      .addProfile(Profile.newBuilder()
-        .setProfileName(profileName)
-        .setLanguageKey(languageKey)
-        .setLanguageName(languageName)
-        .setNewRules(newRules)
-        .setUpdatedRules(updatedRules)
-        .setRemovedRules(removedRules)
-        .setStartDate(startDate)
-        .setEndDate(endDate)
-        .build())
-      .build();
-    BuiltInQPChangeNotificationBuilder result = BuiltInQPChangeNotificationBuilder.parse(notification);
-
-    assertThat(result.getProfiles())
-      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getNewRules, Profile::getUpdatedRules, Profile::getRemovedRules,
-        Profile::getStartDate, Profile::getEndDate)
-      .containsExactlyInAnyOrder(tuple(profileName, languageKey, languageName, newRules, updatedRules, removedRules, startDate, endDate));
-  }
-
-  @Test
-  public void fail_with_ISE_when_parsing_empty_notification() {
-    assertThatThrownBy(() -> BuiltInQPChangeNotificationBuilder.parse(new Notification(BuiltInQPChangeNotification.TYPE)))
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Could not read the built-in quality profile notification");
-  }
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileInsertImplTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileInsertImplTest.java
deleted file mode 100644 (file)
index cfa001f..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.Arrays;
-import java.util.List;
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.PropertyType;
-import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
-import org.sonar.api.rule.Severity;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.NewBuiltInQualityProfile;
-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.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleKey;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.QProfileChangeDto;
-import org.sonar.db.qualityprofile.QProfileChangeQuery;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
-import org.sonar.server.rule.DefaultRuleFinder;
-import org.sonar.server.rule.ServerRuleFinder;
-import org.sonar.server.util.StringTypeValidation;
-import org.sonar.server.util.TypeValidations;
-
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-
-public class BuiltInQProfileInsertImplTest {
-
-  @Rule
-  public BuiltInQProfileRepositoryRule builtInQProfileRepository = new BuiltInQProfileRepositoryRule();
-  @Rule
-  public DbTester db = DbTester.create();
-
-  private final System2 system2 = new AlwaysIncreasingSystem2();
-  private final UuidFactory uuidFactory = new SequenceUuidFactory();
-  private final TypeValidations typeValidations = new TypeValidations(singletonList(new StringTypeValidation()));
-  private final DbSession dbSession = db.getSession();
-  private final DbSession batchDbSession = db.getDbClient().openSession(true);
-  private final ServerRuleFinder ruleFinder = new DefaultRuleFinder(db.getDbClient());
-  private final ActiveRuleIndexer activeRuleIndexer = mock(ActiveRuleIndexer.class);
-  private final BuiltInQProfileInsertImpl underTest = new BuiltInQProfileInsertImpl(db.getDbClient(), ruleFinder, system2, uuidFactory, typeValidations, activeRuleIndexer);
-
-  @After
-  public void tearDown() {
-    batchDbSession.close();
-  }
-
-  @Test
-  public void insert_active_rules_and_changelog() {
-    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
-    RuleDefinitionDto rule2 = db.rules().insert(r -> r.setLanguage("xoo"));
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo");
-
-    newQp.activateRule(rule1.getRepositoryKey(), rule1.getRuleKey()).overrideSeverity(Severity.CRITICAL);
-    newQp.activateRule(rule2.getRepositoryKey(), rule2.getRuleKey()).overrideSeverity(Severity.MAJOR);
-    newQp.done();
-
-    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"), rule1, rule2);
-    call(builtIn);
-
-    verifyTableSize("rules_profiles", 1);
-    verifyTableSize("org_qprofiles", 1);
-    verifyTableSize("active_rules", 2);
-    verifyTableSize("active_rule_parameters", 0);
-    verifyTableSize("qprofile_changes", 2);
-    verifyTableSize("default_qprofiles", 0);
-
-    QProfileDto profile = verifyProfileInDb(builtIn);
-    verifyActiveRuleInDb(profile, rule1, Severity.CRITICAL);
-    verifyActiveRuleInDb(profile, rule2, Severity.MAJOR);
-  }
-
-  @Test
-  public void insert_default_qp() {
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    context.createBuiltInQualityProfile("the name", "xoo")
-      .setDefault(true)
-      .done();
-
-    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"));
-    call(builtIn);
-
-    verifyTableSize("rules_profiles", 1);
-    verifyTableSize("org_qprofiles", 1);
-    verifyTableSize("active_rules", 0);
-    verifyTableSize("active_rule_parameters", 0);
-    verifyTableSize("qprofile_changes", 0);
-    verifyTableSize("default_qprofiles", 1);
-
-    verifyProfileInDb(builtIn);
-  }
-
-  @Test
-  public void insert_active_rules_with_params() {
-    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
-    RuleParamDto param1 = db.rules().insertRuleParam(rule1, p -> p.setType(PropertyType.STRING.name()));
-    RuleParamDto param2 = db.rules().insertRuleParam(rule1, p -> p.setType(PropertyType.STRING.name()));
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo");
-
-    newQp.activateRule(rule1.getRepositoryKey(), rule1.getRuleKey()).overrideSeverity(Severity.CRITICAL);
-    newQp.done();
-
-    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"), rule1);
-    call(builtIn);
-
-    verifyTableSize("rules_profiles", 1);
-    verifyTableSize("org_qprofiles", 1);
-    verifyTableSize("active_rules", 1);
-    verifyTableSize("active_rule_parameters", 2);
-    verifyTableSize("qprofile_changes", 1);
-
-    QProfileDto profile = verifyProfileInDb(builtIn);
-    verifyActiveRuleInDb(profile, rule1, Severity.CRITICAL, param1, param2);
-  }
-
-  @Test
-  public void flag_profile_as_default_if_declared_as_default_by_api() {
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo").setDefault(true);
-    newQp.done();
-
-    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"));
-
-    call(builtIn);
-
-    QProfileDto profile = verifyProfileInDb(builtIn);
-    QProfileDto defaultProfile = db.getDbClient().qualityProfileDao().selectDefaultProfile(dbSession, "xoo");
-    assertThat(defaultProfile.getKee()).isEqualTo(profile.getKee());
-  }
-
-  @Test
-  public void existing_default_profile_must_not_be_changed() {
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo").setDefault(true);
-    newQp.done();
-    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"));
-
-    QProfileDto currentDefault = db.qualityProfiles().insert(p -> p.setLanguage("xoo"));
-    db.qualityProfiles().setAsDefault(currentDefault);
-
-    call(builtIn);
-
-    QProfileDto defaultProfile = db.getDbClient().qualityProfileDao().selectDefaultProfile(dbSession, "xoo");
-    assertThat(defaultProfile.getKee()).isEqualTo(currentDefault.getKee());
-    verifyTableSize("rules_profiles", 2);
-  }
-
-  @Test
-  public void dont_flag_profile_as_default_if_not_declared_as_default_by_api() {
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo").setDefault(false);
-    newQp.done();
-    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"));
-
-    call(builtIn);
-
-    QProfileDto defaultProfile = db.getDbClient().qualityProfileDao().selectDefaultProfile(dbSession, "xoo");
-    assertThat(defaultProfile).isNull();
-  }
-
-  // TODO test lot of active_rules, params, orgas
-
-  private void verifyActiveRuleInDb(QProfileDto profile, RuleDefinitionDto rule, String expectedSeverity, RuleParamDto... paramDtos) {
-    ActiveRuleDto activeRule = db.getDbClient().activeRuleDao().selectByKey(dbSession, ActiveRuleKey.of(profile, rule.getKey())).get();
-    assertThat(activeRule.getUuid()).isNotNull();
-    assertThat(activeRule.getInheritance()).isNull();
-    assertThat(activeRule.doesOverride()).isFalse();
-    assertThat(activeRule.getRuleUuid()).isEqualTo(rule.getUuid());
-    assertThat(activeRule.getProfileUuid()).isEqualTo(profile.getRulesProfileUuid());
-    assertThat(activeRule.getSeverityString()).isEqualTo(expectedSeverity);
-    assertThat(activeRule.getCreatedAt()).isPositive();
-    assertThat(activeRule.getUpdatedAt()).isPositive();
-
-    List<ActiveRuleParamDto> params = db.getDbClient().activeRuleDao().selectParamsByActiveRuleUuid(dbSession, activeRule.getUuid());
-    assertThat(params).extracting(ActiveRuleParamDto::getKey).containsOnly(Arrays.stream(paramDtos).map(RuleParamDto::getName).toArray(String[]::new));
-
-    QProfileChangeQuery changeQuery = new QProfileChangeQuery(profile.getKee());
-    QProfileChangeDto change = db.getDbClient().qProfileChangeDao().selectByQuery(dbSession, changeQuery).stream()
-      .filter(c -> c.getDataAsMap().get("ruleUuid").equals(rule.getUuid()))
-      .findFirst()
-      .get();
-    assertThat(change.getChangeType()).isEqualTo(ActiveRuleChange.Type.ACTIVATED.name());
-    assertThat(change.getCreatedAt()).isPositive();
-    assertThat(change.getUuid()).isNotEmpty();
-    assertThat(change.getUserUuid()).isNull();
-    assertThat(change.getRulesProfileUuid()).isEqualTo(profile.getRulesProfileUuid());
-    assertThat(change.getDataAsMap()).containsEntry("severity", expectedSeverity);
-  }
-
-  private QProfileDto verifyProfileInDb(BuiltInQProfile builtIn) {
-    QProfileDto profileOnOrg1 = db.getDbClient().qualityProfileDao().selectByNameAndLanguage(dbSession, builtIn.getName(), builtIn.getLanguage());
-    assertThat(profileOnOrg1.getLanguage()).isEqualTo(builtIn.getLanguage());
-    assertThat(profileOnOrg1.getName()).isEqualTo(builtIn.getName());
-    assertThat(profileOnOrg1.getParentKee()).isNull();
-    assertThat(profileOnOrg1.getLastUsed()).isNull();
-    assertThat(profileOnOrg1.getUserUpdatedAt()).isNull();
-    assertThat(profileOnOrg1.getRulesUpdatedAt()).isNotEmpty();
-    assertThat(profileOnOrg1.getKee()).isNotEqualTo(profileOnOrg1.getRulesProfileUuid());
-    assertThat(profileOnOrg1.getRulesProfileUuid()).isNotNull();
-    return profileOnOrg1;
-  }
-
-  private void verifyTableSize(String table, int expectedSize) {
-    assertThat(db.countRowsOfTable(dbSession, table)).as("table " + table).isEqualTo(expectedSize);
-  }
-
-  private void call(BuiltInQProfile builtIn) {
-    underTest.create(dbSession, builtIn);
-    dbSession.commit();
-    batchDbSession.commit();
-  }
-
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileLoaderTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileLoaderTest.java
deleted file mode 100644 (file)
index 6e2ab84..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import org.junit.Rule;
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class BuiltInQProfileLoaderTest {
-  @Rule
-  public BuiltInQProfileRepositoryRule builtInQProfileRepositoryRule = new BuiltInQProfileRepositoryRule();
-
-  private BuiltInQProfileLoader underTest = new BuiltInQProfileLoader(builtInQProfileRepositoryRule);
-
-  @Test
-  public void start_initializes_DefinedQProfileRepository() {
-    underTest.start();
-
-    assertThat(builtInQProfileRepositoryRule.isInitialized()).isTrue();
-  }
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryImplTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryImplTest.java
deleted file mode 100644 (file)
index 4672e42..0000000
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.List;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.resources.Language;
-import org.sonar.api.resources.Languages;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbTester;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.server.language.LanguageTesting;
-import org.sonar.server.rule.DefaultRuleFinder;
-import org.sonar.server.rule.ServerRuleFinder;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.groups.Tuple.tuple;
-import static org.mockito.Mockito.mock;
-import static org.sonar.db.rule.RuleTesting.EXTERNAL_XOO;
-import static org.sonar.server.qualityprofile.BuiltInQProfile.ActiveRule;
-
-public class BuiltInQProfileRepositoryImplTest {
-  private static final Language FOO_LANGUAGE = LanguageTesting.newLanguage("foo", "foo", "foo");
-  private static final String SONAR_WAY_QP_NAME = "Sonar way";
-
-  @Rule
-  public DbTester db = DbTester.create();
-
-  private final DbClient dbClient = db.getDbClient();
-  private final ServerRuleFinder ruleFinder = new DefaultRuleFinder(dbClient);
-
-  @Test
-  public void create_qprofile_with_rule() {
-    RuleDefinitionDto rule1 = db.rules().insert();
-    RuleDefinitionDto rule2 = db.rules().insert();
-    db.rules().insert();
-    DummyProfileDefinition definition = new DummyProfileDefinition("foo", "foo", false, asList(rule1.getKey(), rule2.getKey()));
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE), definition);
-
-    underTest.initialize();
-
-    assertThat(underTest.get())
-      .extracting(BuiltInQProfile::getName)
-      .containsExactlyInAnyOrder("foo");
-    assertThat(underTest.get().get(0).getActiveRules())
-      .extracting(ActiveRule::getRuleUuid, ActiveRule::getRuleKey)
-      .containsExactlyInAnyOrder(
-        tuple(rule1.getUuid(), rule1.getKey()),
-        tuple(rule2.getUuid(), rule2.getKey()));
-  }
-
-  @Test
-  public void make_single_profile_of_a_language_default_even_if_not_flagged_as_so() {
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
-      new DummyProfileDefinition("foo", "foo1", false));
-
-    underTest.initialize();
-
-    assertThat(underTest.get())
-      .extracting(BuiltInQProfile::getLanguage, BuiltInQProfile::isDefault)
-      .containsExactly(tuple(FOO_LANGUAGE.getKey(), true));
-  }
-
-  @Test
-  public void make_single_profile_of_a_language_default_even_if_flagged_as_so() {
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
-      new DummyProfileDefinition("foo", "foo1", true));
-
-    underTest.initialize();
-
-    assertThat(underTest.get())
-      .extracting(BuiltInQProfile::getLanguage, BuiltInQProfile::isDefault)
-      .containsExactly(tuple(FOO_LANGUAGE.getKey(), true));
-  }
-
-  @Test
-  public void make_first_profile_of_a_language_default_when_none_flagged_as_so() {
-    DummyProfileDefinition[] definitions = new DummyProfileDefinition[] {new DummyProfileDefinition("foo", "foo1", false), new DummyProfileDefinition("foo", "foo2", false)};
-    String firstName = definitions[0].getName();
-    String secondName = definitions[1].getName();
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE), definitions);
-
-    underTest.initialize();
-
-    assertThat(underTest.get())
-      .extracting(BuiltInQProfile::getName, BuiltInQProfile::isDefault)
-      .containsExactlyInAnyOrder(tuple(firstName, true), tuple(secondName, false));
-  }
-
-  @Test
-  public void create_profile_Sonar_Way_as_default_if_none_other_is_defined_default_for_a_given_language() {
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(
-      dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
-      new DummyProfileDefinition("foo", "doh", false), new DummyProfileDefinition("foo", "boo", false),
-      new DummyProfileDefinition("foo", SONAR_WAY_QP_NAME, false), new DummyProfileDefinition("foo", "goo", false));
-
-    underTest.initialize();
-
-    assertThat(underTest.get())
-      .filteredOn(b -> FOO_LANGUAGE.getKey().equals(b.getLanguage()))
-      .filteredOn(BuiltInQProfile::isDefault)
-      .extracting(BuiltInQProfile::getName)
-      .containsExactly(SONAR_WAY_QP_NAME);
-  }
-
-  @Test
-  public void do_not_create_Sonar_Way_as_default_if_other_profile_is_defined_as_default() {
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(
-      dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
-      new DummyProfileDefinition("foo", SONAR_WAY_QP_NAME, false), new DummyProfileDefinition("foo", "goo", true));
-
-    underTest.initialize();
-
-    assertThat(underTest.get())
-      .filteredOn(b -> FOO_LANGUAGE.getKey().equals(b.getLanguage()))
-      .filteredOn(BuiltInQProfile::isDefault)
-      .extracting(BuiltInQProfile::getName)
-      .containsExactly("goo");
-  }
-
-  @Test
-  public void match_Sonar_Way_default_with_case_sensitivity() {
-    String sonarWayInOtherCase = SONAR_WAY_QP_NAME.toUpperCase();
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(
-      dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
-      new DummyProfileDefinition("foo", "goo", false), new DummyProfileDefinition("foo", sonarWayInOtherCase, false));
-
-    underTest.initialize();
-
-    assertThat(underTest.get())
-      .filteredOn(b -> FOO_LANGUAGE.getKey().equals(b.getLanguage()))
-      .filteredOn(BuiltInQProfile::isDefault)
-      .extracting(BuiltInQProfile::getName)
-      .containsExactly("goo");
-  }
-
-  @Test
-  public void create_no_BuiltInQProfile_when_all_definitions_apply_to_non_defined_languages() {
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(mock(DbClient.class), ruleFinder, new Languages(),
-      new DummyProfileDefinition("foo", "P1", false));
-
-    underTest.initialize();
-
-    assertThat(underTest.get()).isEmpty();
-  }
-
-  @Test
-  public void create_qprofile_with_deprecated_rule() {
-    RuleDefinitionDto rule1 = db.rules().insert();
-    db.rules().insertDeprecatedKey(d -> d.setRuleUuid(rule1.getUuid()).setOldRepositoryKey("oldRepo").setOldRuleKey("oldKey"));
-    RuleDefinitionDto rule2 = db.rules().insert();
-    DummyProfileDefinition definition = new DummyProfileDefinition("foo", "foo", false,
-      asList(RuleKey.of("oldRepo", "oldKey"), rule2.getKey()));
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE), definition);
-
-    underTest.initialize();
-
-    assertThat(underTest.get())
-      .extracting(BuiltInQProfile::getName)
-      .containsExactlyInAnyOrder("foo");
-    assertThat(underTest.get().get(0).getActiveRules())
-      .extracting(ActiveRule::getRuleUuid, ActiveRule::getRuleKey)
-      .containsExactlyInAnyOrder(
-        tuple(rule1.getUuid(), rule1.getKey()),
-        tuple(rule2.getUuid(), rule2.getKey()));
-  }
-
-  @Test
-  public void fail_with_ISE_when_rule_does_not_exist() {
-    DummyProfileDefinition[] definitions = new DummyProfileDefinition[] {new DummyProfileDefinition("foo", "foo", false, singletonList(EXTERNAL_XOO))};
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE), definitions);
-
-
-  }
-
-  @Test
-  public void fail_with_ISE_when_two_profiles_with_different_name_are_default_for_the_same_language() {
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
-      new DummyProfileDefinition("foo", "foo1", true), new DummyProfileDefinition("foo", "foo2", true));
-
-    assertThatThrownBy(underTest::initialize)
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Several Quality profiles are flagged as default for the language foo: [foo1, foo2]");
-  }
-
-  @Test
-  public void get_throws_ISE_if_called_before_initialize() {
-    BuiltInQProfileRepositoryImpl underTest = new BuiltInQProfileRepositoryImpl(mock(DbClient.class), ruleFinder, new Languages());
-
-    assertThatThrownBy(underTest::get)
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("initialize must be called first");
-  }
-
-  @Test
-  public void initialize_throws_ISE_if_called_twice() {
-    BuiltInQProfileRepositoryImpl underTest = new BuiltInQProfileRepositoryImpl(mock(DbClient.class), ruleFinder, new Languages());
-    underTest.initialize();
-
-    assertThatThrownBy(underTest::initialize)
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("initialize must be called only once");
-  }
-
-  @Test
-  public void initialize_throws_ISE_if_language_has_no_builtin_qp() {
-    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(mock(DbClient.class), ruleFinder, new Languages(FOO_LANGUAGE));
-
-    assertThatThrownBy(underTest::initialize)
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("The following languages have no built-in quality profiles: foo");
-  }
-
-  private static final class DummyProfileDefinition implements BuiltInQualityProfilesDefinition {
-    private final String language;
-    private final String name;
-    private final boolean defaultProfile;
-    private final List<RuleKey> activeRuleKeys;
-
-    private DummyProfileDefinition(String language, String name, boolean defaultProfile, List<RuleKey> activeRuleKeys) {
-      this.language = language;
-      this.name = name;
-      this.defaultProfile = defaultProfile;
-      this.activeRuleKeys = activeRuleKeys;
-    }
-
-    private DummyProfileDefinition(String language, String name, boolean defaultProfile) {
-      this(language, name, defaultProfile, emptyList());
-    }
-
-    @Override
-    public void define(Context context) {
-      NewBuiltInQualityProfile builtInQualityProfile = context.createBuiltInQualityProfile(name, language);
-      activeRuleKeys.forEach(activeRuleKey -> builtInQualityProfile.activateRule(activeRuleKey.repository(), activeRuleKey.rule()));
-      builtInQualityProfile.setDefault(defaultProfile);
-      builtInQualityProfile.done();
-    }
-
-    String getName() {
-      return name;
-    }
-
-  }
-
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdateImplTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdateImplTest.java
deleted file mode 100644 (file)
index 024de98..0000000
+++ /dev/null
@@ -1,554 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import javax.annotation.Nullable;
-import org.assertj.core.groups.Tuple;
-import org.junit.Before;
-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.Severity;
-import org.sonar.api.rules.RulePriority;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.NewBuiltInQualityProfile;
-import org.sonar.api.utils.System2;
-import org.sonar.db.DbSession;
-import org.sonar.db.DbTester;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleKey;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.OrgActiveRuleDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.qualityprofile.RulesProfileDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
-import org.sonar.server.tester.UserSessionRule;
-import org.sonar.server.util.IntegerTypeValidation;
-import org.sonar.server.util.StringTypeValidation;
-import org.sonar.server.util.TypeValidations;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.emptyMap;
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.groups.Tuple.tuple;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.sonar.api.rules.RulePriority.BLOCKER;
-import static org.sonar.api.rules.RulePriority.CRITICAL;
-import static org.sonar.api.rules.RulePriority.MAJOR;
-import static org.sonar.api.rules.RulePriority.MINOR;
-import static org.sonar.db.qualityprofile.QualityProfileTesting.newRuleProfileDto;
-import static org.sonar.server.qualityprofile.ActiveRuleInheritance.INHERITED;
-
-public class BuiltInQProfileUpdateImplTest {
-
-  private static final long NOW = 1_000;
-  private static final long PAST = NOW - 100;
-
-  @Rule
-  public BuiltInQProfileRepositoryRule builtInProfileRepository = new BuiltInQProfileRepositoryRule();
-  @Rule
-  public DbTester db = DbTester.create();
-  @Rule
-  public UserSessionRule userSession = UserSessionRule.standalone();
-  private System2 system2 = new TestSystem2().setNow(NOW);
-  private ActiveRuleIndexer activeRuleIndexer = mock(ActiveRuleIndexer.class);
-  private TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
-  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
-  private RuleActivator ruleActivator = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
-
-  private BuiltInQProfileUpdateImpl underTest = new BuiltInQProfileUpdateImpl(db.getDbClient(), ruleActivator, activeRuleIndexer,
-    qualityProfileChangeEventService);
-
-  private RulesProfileDto persistedProfile;
-
-  @Before
-  public void setUp() {
-    persistedProfile = newRuleProfileDto(rp -> rp
-      .setIsBuiltIn(true)
-      .setLanguage("xoo")
-      .setRulesUpdatedAt(null));
-    db.getDbClient().qualityProfileDao().insert(db.getSession(), persistedProfile);
-    db.commit();
-  }
-
-  @Test
-  public void activate_new_rules() {
-    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
-    RuleDefinitionDto rule2 = db.rules().insert(r -> r.setLanguage("xoo"));
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
-    newQp.activateRule(rule1.getRepositoryKey(), rule1.getRuleKey()).overrideSeverity(Severity.CRITICAL);
-    newQp.activateRule(rule2.getRepositoryKey(), rule2.getRuleKey()).overrideSeverity(Severity.MAJOR);
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule1, rule2);
-
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
-
-    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThat(activeRules).hasSize(2);
-    assertThatRuleIsNewlyActivated(activeRules, rule1, CRITICAL);
-    assertThatRuleIsNewlyActivated(activeRules, rule2, MAJOR);
-    assertThatProfileIsMarkedAsUpdated(persistedProfile);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  @Test
-  public void already_activated_rule_is_updated_in_case_of_differences() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey()).overrideSeverity(Severity.CRITICAL);
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule);
-
-    activateRuleInDb(persistedProfile, rule, BLOCKER);
-
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
-
-    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThat(activeRules).hasSize(1);
-    assertThatRuleIsUpdated(activeRules, rule, CRITICAL);
-    assertThatProfileIsMarkedAsUpdated(persistedProfile);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  @Test
-  public void already_activated_rule_is_not_touched_if_no_differences() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey()).overrideSeverity(Severity.CRITICAL);
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule);
-
-    activateRuleInDb(persistedProfile, rule, CRITICAL);
-
-    underTest.update(db.getSession(), builtIn, persistedProfile);
-
-    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThat(activeRules).hasSize(1);
-    assertThatRuleIsUntouched(activeRules, rule, CRITICAL);
-    assertThatProfileIsNotMarkedAsUpdated(persistedProfile);
-    verifyNoInteractions(qualityProfileChangeEventService);
-  }
-
-  @Test
-  public void deactivate_rule_that_is_not_in_built_in_definition_anymore() {
-    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
-    RuleDefinitionDto rule2 = db.rules().insert(r -> r.setLanguage("xoo"));
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
-    newQp.activateRule(rule2.getRepositoryKey(), rule2.getRuleKey()).overrideSeverity(Severity.MAJOR);
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule1, rule2);
-
-    // built-in definition contains only rule2
-    // so rule1 must be deactivated
-    activateRuleInDb(persistedProfile, rule1, CRITICAL);
-
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
-
-    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThat(activeRules).hasSize(1);
-    assertThatRuleIsDeactivated(activeRules, rule1);
-    assertThatProfileIsMarkedAsUpdated(persistedProfile);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  @Test
-  public void activate_deactivate_and_update_three_rules_at_the_same_time() {
-    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
-    RuleDefinitionDto rule2 = db.rules().insert(r -> r.setLanguage("xoo"));
-    RuleDefinitionDto rule3 = db.rules().insert(r -> r.setLanguage("xoo"));
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
-    newQp.activateRule(rule1.getRepositoryKey(), rule1.getRuleKey()).overrideSeverity(Severity.CRITICAL);
-    newQp.activateRule(rule2.getRepositoryKey(), rule2.getRuleKey()).overrideSeverity(Severity.MAJOR);
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule1, rule2);
-
-    // rule1 must be updated (blocker to critical)
-    // rule2 must be activated
-    // rule3 must be deactivated
-    activateRuleInDb(persistedProfile, rule1, BLOCKER);
-    activateRuleInDb(persistedProfile, rule3, BLOCKER);
-
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
-
-    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThat(activeRules).hasSize(2);
-    assertThatRuleIsUpdated(activeRules, rule1, CRITICAL);
-    assertThatRuleIsNewlyActivated(activeRules, rule2, MAJOR);
-    assertThatRuleIsDeactivated(activeRules, rule3);
-    assertThatProfileIsMarkedAsUpdated(persistedProfile);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  // SONAR-10473
-  @Test
-  public void activate_rule_on_built_in_profile_resets_severity_to_default_if_not_overridden() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setSeverity(Severity.MAJOR).setLanguage("xoo"));
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule);
-    underTest.update(db.getSession(), builtIn, persistedProfile);
-
-    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThatRuleIsNewlyActivated(activeRules, rule, MAJOR);
-
-    // emulate an upgrade of analyzer that changes the default severity of the rule
-    rule.setSeverity(Severity.MINOR);
-    db.rules().update(rule);
-
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
-    activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThatRuleIsNewlyActivated(activeRules, rule, MINOR);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  @Test
-  public void activate_rule_on_built_in_profile_resets_params_to_default_if_not_overridden() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
-    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", rule.getLanguage());
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(newQp.language(), newQp.name()), rule);
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
-
-    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThat(activeRules).hasSize(1);
-    assertThatRuleHasParams(db, activeRules.get(0), tuple("min", "10"));
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-
-    // emulate an upgrade of analyzer that changes the default value of parameter min
-    ruleParam.setDefaultValue("20");
-    db.getDbClient().ruleDao().updateRuleParam(db.getSession(), rule, ruleParam);
-
-    changes = underTest.update(db.getSession(), builtIn, persistedProfile);
-    activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
-    assertThat(activeRules).hasSize(1);
-    assertThatRuleHasParams(db, activeRules.get(0), tuple("min", "20"));
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  @Test
-  public void propagate_activation_to_descendant_profiles() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
-
-    QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
-    QProfileDto childProfile = createChildProfile(profile);
-    QProfileDto grandchildProfile = createChildProfile(childProfile);
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(profile.getName(), profile.getLanguage());
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
-
-    assertThat(changes).hasSize(3);
-    assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, emptyMap());
-    assertThatRuleIsActivated(childProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
-    assertThatRuleIsActivated(grandchildProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  // SONAR-14559
-  @Test
-  public void propagate_rule_update_to_descendant_active_rule() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
-
-    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
-    activateRuleInDb(RulesProfileDto.from(parentProfile), rule, RulePriority.valueOf(Severity.MINOR), null);
-
-    QProfileDto childProfile = createChildProfile(parentProfile);
-    activateRuleInDb(RulesProfileDto.from(childProfile), rule, RulePriority.valueOf(Severity.MINOR), INHERITED);
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(parentProfile.getName(), parentProfile.getLanguage());
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(parentProfile.getLanguage(), parentProfile.getName()), rule);
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(parentProfile));
-
-    assertThat(changes).hasSize(2);
-
-    List<ActiveRuleDto> parentActiveRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), RulesProfileDto.from(parentProfile));
-    assertThatRuleIsUpdated(parentActiveRules, rule, RulePriority.BLOCKER, null);
-
-    List<ActiveRuleDto> childActiveRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), RulesProfileDto.from(childProfile));
-    assertThatRuleIsUpdated(childActiveRules, rule, RulePriority.BLOCKER, INHERITED);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  @Test
-  public void propagate_rule_param_update_to_descendant_active_rule_params() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
-    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
-
-    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
-    ActiveRuleDto parentActiveRuleDto = activateRuleInDb(RulesProfileDto.from(parentProfile), rule,
-        RulePriority.valueOf(Severity.MINOR), null);
-    activateRuleParamInDb(parentActiveRuleDto, ruleParam, "20");
-
-    QProfileDto childProfile = createChildProfile(parentProfile);
-    ActiveRuleDto childActiveRuleDto = activateRuleInDb(RulesProfileDto.from(childProfile), rule,
-        RulePriority.valueOf(Severity.MINOR), INHERITED);
-    activateRuleParamInDb(childActiveRuleDto, ruleParam, "20");
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(parentProfile.getName(), parentProfile.getLanguage());
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
-    newQp.done();
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(parentProfile.getLanguage(), parentProfile.getName()), rule);
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(parentProfile));
-
-    assertThat(changes).hasSize(2);
-
-    assertThatRuleHasParams(db, parentActiveRuleDto, tuple("min", "10"));
-    assertThatRuleHasParams(db, childActiveRuleDto, tuple("min", "10"));
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-  }
-
-  @Test
-  public void do_not_load_descendants_if_no_changes() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
-
-    QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
-    QProfileDto childProfile = createChildProfile(profile);
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(profile.getName(), profile.getLanguage());
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
-    newQp.done();
-
-    // first run
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
-    assertThat(changes).hasSize(2).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.ACTIVATED);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-
-    // second run, without any input changes
-    RuleActivator ruleActivatorWithoutDescendants = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession) {
-      @Override
-      DescendantProfilesSupplier createDescendantProfilesSupplier(DbSession dbSession) {
-        return (profiles, ruleIds) -> {
-          throw new IllegalStateException("BOOM - descendants should not be loaded");
-        };
-      }
-    };
-    changes = new BuiltInQProfileUpdateImpl(db.getDbClient(), ruleActivatorWithoutDescendants, activeRuleIndexer, qualityProfileChangeEventService)
-      .update(db.getSession(), builtIn, RulesProfileDto.from(profile));
-    assertThat(changes).isEmpty();
-    verifyNoMoreInteractions(qualityProfileChangeEventService);
-  }
-
-  @Test
-  public void propagate_deactivation_to_descendant_profiles() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
-
-    QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
-    QProfileDto childProfile = createChildProfile(profile);
-    QProfileDto grandChildProfile = createChildProfile(childProfile);
-
-    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(profile.getName(), profile.getLanguage());
-    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
-    newQp.done();
-
-    // first run to activate the rule
-    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
-    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
-    assertThat(changes).hasSize(3).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.ACTIVATED);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-
-    // second run to deactivate the rule
-    context = new BuiltInQualityProfilesDefinition.Context();
-    NewBuiltInQualityProfile updatedQp = context.createBuiltInQualityProfile(profile.getName(), profile.getLanguage());
-    updatedQp.done();
-    builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
-    changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
-    assertThat(changes).hasSize(3).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.DEACTIVATED);
-    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
-
-    assertThatRuleIsDeactivated(profile, rule);
-    assertThatRuleIsDeactivated(childProfile, rule);
-    assertThatRuleIsDeactivated(grandChildProfile, rule);
-  }
-
-  private QProfileDto createChildProfile(QProfileDto parent) {
-    return db.qualityProfiles().insert(p -> p
-      .setLanguage(parent.getLanguage())
-      .setParentKee(parent.getKee())
-      .setName("Child of " + parent.getName()))
-      .setIsBuiltIn(false);
-  }
-
-  private void assertThatRuleIsActivated(QProfileDto profile, RuleDefinitionDto rule, @Nullable List<ActiveRuleChange> changes,
-    String expectedSeverity, @Nullable ActiveRuleInheritance expectedInheritance, Map<String, String> expectedParams) {
-    OrgActiveRuleDto activeRule = db.getDbClient().activeRuleDao().selectByProfile(db.getSession(), profile)
-      .stream()
-      .filter(ar -> ar.getRuleKey().equals(rule.getKey()))
-      .findFirst()
-      .orElseThrow(IllegalStateException::new);
-
-    assertThat(activeRule.getSeverityString()).isEqualTo(expectedSeverity);
-    assertThat(activeRule.getInheritance()).isEqualTo(expectedInheritance != null ? expectedInheritance.name() : null);
-
-    List<ActiveRuleParamDto> params = db.getDbClient().activeRuleDao().selectParamsByActiveRuleUuid(db.getSession(), activeRule.getUuid());
-    assertThat(params).hasSize(expectedParams.size());
-
-    if (changes != null) {
-      ActiveRuleChange change = changes.stream()
-        .filter(c -> c.getActiveRule().getUuid().equals(activeRule.getUuid()))
-        .findFirst().orElseThrow(IllegalStateException::new);
-      assertThat(change.getInheritance()).isEqualTo(expectedInheritance);
-      assertThat(change.getSeverity()).isEqualTo(expectedSeverity);
-      assertThat(change.getType()).isEqualTo(ActiveRuleChange.Type.ACTIVATED);
-    }
-  }
-
-  private static void assertThatRuleHasParams(DbTester db, ActiveRuleDto activeRule, Tuple... expectedParams) {
-    assertThat(db.getDbClient().activeRuleDao().selectParamsByActiveRuleUuid(db.getSession(), activeRule.getUuid()))
-      .extracting(ActiveRuleParamDto::getKey, ActiveRuleParamDto::getValue)
-      .containsExactlyInAnyOrder(expectedParams);
-  }
-
-  private static void assertThatRuleIsNewlyActivated(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule, RulePriority severity) {
-    ActiveRuleDto activeRule = findRule(activeRules, rule).get();
-
-    assertThat(activeRule.getInheritance()).isNull();
-    assertThat(activeRule.getSeverityString()).isEqualTo(severity.name());
-    assertThat(activeRule.getCreatedAt()).isEqualTo(NOW);
-    assertThat(activeRule.getUpdatedAt()).isEqualTo(NOW);
-  }
-
-  private static void assertThatRuleIsUpdated(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule, RulePriority severity, @Nullable ActiveRuleInheritance expectedInheritance) {
-    ActiveRuleDto activeRule = findRule(activeRules, rule).get();
-
-    if (expectedInheritance != null) {
-      assertThat(activeRule.getInheritance()).isEqualTo(expectedInheritance.name());
-    } else {
-      assertThat(activeRule.getInheritance()).isNull();
-    }
-    assertThat(activeRule.getSeverityString()).isEqualTo(severity.name());
-    assertThat(activeRule.getCreatedAt()).isEqualTo(PAST);
-    assertThat(activeRule.getUpdatedAt()).isEqualTo(NOW);
-  }
-
-  private static void assertThatRuleIsUpdated(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule, RulePriority severity) {
-    assertThatRuleIsUpdated(activeRules, rule, severity, null);
-  }
-
-  private static void assertThatRuleIsUpdated(ActiveRuleDto activeRules, RuleDefinitionDto rule, RulePriority severity, @Nullable ActiveRuleInheritance expectedInheritance) {
-    assertThatRuleIsUpdated(singletonList(activeRules), rule, severity, expectedInheritance);
-  }
-
-  private static void assertThatRuleIsUntouched(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule, RulePriority severity) {
-    ActiveRuleDto activeRule = findRule(activeRules, rule).get();
-
-    assertThat(activeRule.getInheritance()).isNull();
-    assertThat(activeRule.getSeverityString()).isEqualTo(severity.name());
-    assertThat(activeRule.getCreatedAt()).isEqualTo(PAST);
-    assertThat(activeRule.getUpdatedAt()).isEqualTo(PAST);
-  }
-
-  private void assertThatRuleIsDeactivated(QProfileDto profile, RuleDefinitionDto rule) {
-    Collection<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRulesAndRuleProfileUuids(
-      db.getSession(), singletonList(rule.getUuid()), singletonList(profile.getRulesProfileUuid()));
-    assertThat(activeRules).isEmpty();
-  }
-
-  private static void assertThatRuleIsDeactivated(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule) {
-    assertThat(findRule(activeRules, rule)).isEmpty();
-  }
-
-  private void assertThatProfileIsMarkedAsUpdated(RulesProfileDto dto) {
-    RulesProfileDto reloaded = db.getDbClient().qualityProfileDao().selectBuiltInRuleProfiles(db.getSession())
-      .stream()
-      .filter(p -> p.getUuid().equals(dto.getUuid()))
-      .findFirst()
-      .get();
-    assertThat(reloaded.getRulesUpdatedAt()).isNotEmpty();
-  }
-
-  private void assertThatProfileIsNotMarkedAsUpdated(RulesProfileDto dto) {
-    RulesProfileDto reloaded = db.getDbClient().qualityProfileDao().selectBuiltInRuleProfiles(db.getSession())
-      .stream()
-      .filter(p -> p.getUuid().equals(dto.getUuid()))
-      .findFirst()
-      .get();
-    assertThat(reloaded.getRulesUpdatedAt()).isNull();
-  }
-
-  private static Optional<ActiveRuleDto> findRule(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule) {
-    return activeRules.stream()
-      .filter(ar -> ar.getRuleKey().equals(rule.getKey()))
-      .findFirst();
-  }
-
-  private ActiveRuleDto activateRuleInDb(RulesProfileDto profile, RuleDefinitionDto rule, RulePriority severity) {
-    return activateRuleInDb(profile, rule, severity, null);
-  }
-
-  private ActiveRuleDto activateRuleInDb(RulesProfileDto ruleProfile, RuleDefinitionDto rule, RulePriority severity, @Nullable ActiveRuleInheritance inheritance) {
-    ActiveRuleDto dto = new ActiveRuleDto()
-      .setKey(ActiveRuleKey.of(ruleProfile, RuleKey.of(rule.getRepositoryKey(), rule.getRuleKey())))
-      .setProfileUuid(ruleProfile.getUuid())
-      .setSeverity(severity.name())
-      .setRuleUuid(rule.getUuid())
-      .setInheritance(inheritance != null ? inheritance.name() : null)
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST);
-    db.getDbClient().activeRuleDao().insert(db.getSession(), dto);
-    db.commit();
-    return dto;
-  }
-
-  private void activateRuleParamInDb(ActiveRuleDto activeRuleDto, RuleParamDto ruleParamDto, String value) {
-    ActiveRuleParamDto dto = new ActiveRuleParamDto()
-        .setActiveRuleUuid(activeRuleDto.getUuid())
-        .setRulesParameterUuid(ruleParamDto.getUuid())
-        .setKey(ruleParamDto.getName())
-        .setValue(value);
-    db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRuleDto, dto);
-    db.commit();
-  }
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQualityProfilesUpdateListenerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQualityProfilesUpdateListenerTest.java
deleted file mode 100644 (file)
index 2f6fc17..0000000
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Multimap;
-import org.assertj.core.groups.Tuple;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.notifications.Notification;
-import org.sonar.api.resources.Language;
-import org.sonar.api.resources.Languages;
-import org.sonar.core.util.Uuids;
-import org.sonar.db.qualityprofile.ActiveRuleKey;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.server.notification.NotificationManager;
-import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationBuilder.Profile;
-
-import static java.util.Arrays.asList;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.tuple;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.sonar.core.config.CorePropertyDefinitions.DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES;
-import static org.sonar.server.language.LanguageTesting.newLanguage;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED;
-
-public class BuiltInQualityProfilesUpdateListenerTest {
-
-  private NotificationManager notificationManager = mock(NotificationManager.class);
-  private MapSettings settings = new MapSettings();
-
-  @Test
-  public void add_profile_to_notification_for_added_rules() {
-    enableNotificationInGlobalSettings();
-    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
-    Languages languages = new Languages();
-    Tuple expectedTuple = addProfile(profiles, languages, ACTIVATED);
-
-    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
-    underTest.onChange(profiles, 0, 1);
-
-    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
-    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
-    verifyNoMoreInteractions(notificationManager);
-    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
-      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getNewRules)
-      .containsExactlyInAnyOrder(expectedTuple);
-  }
-
-  @Test
-  public void add_profile_to_notification_for_updated_rules() {
-    enableNotificationInGlobalSettings();
-    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
-    Languages languages = new Languages();
-    Tuple expectedTuple = addProfile(profiles, languages, UPDATED);
-
-    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
-    underTest.onChange(profiles, 0, 1);
-
-    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
-    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
-    verifyNoMoreInteractions(notificationManager);
-    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
-      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getUpdatedRules)
-      .containsExactlyInAnyOrder(expectedTuple);
-  }
-
-  @Test
-  public void add_profile_to_notification_for_removed_rules() {
-    enableNotificationInGlobalSettings();
-    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
-    Languages languages = new Languages();
-    Tuple expectedTuple = addProfile(profiles, languages, DEACTIVATED);
-
-    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
-    underTest.onChange(profiles, 0, 1);
-
-    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
-    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
-    verifyNoMoreInteractions(notificationManager);
-    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
-      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getRemovedRules)
-      .containsExactlyInAnyOrder(expectedTuple);
-  }
-
-  @Test
-  public void add_multiple_profiles_to_notification() {
-    enableNotificationInGlobalSettings();
-    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
-    Languages languages = new Languages();
-    Tuple expectedTuple1 = addProfile(profiles, languages, ACTIVATED);
-    Tuple expectedTuple2 = addProfile(profiles, languages, ACTIVATED);
-
-    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
-    underTest.onChange(profiles, 0, 1);
-
-    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
-    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
-    verifyNoMoreInteractions(notificationManager);
-    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
-      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getNewRules)
-      .containsExactlyInAnyOrder(expectedTuple1, expectedTuple2);
-  }
-
-  @Test
-  public void add_start_and_end_dates_to_notification() {
-    enableNotificationInGlobalSettings();
-    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
-    Languages languages = new Languages();
-    addProfile(profiles, languages, ACTIVATED);
-    long startDate = 10_000_000_000L;
-    long endDate = 15_000_000_000L;
-
-    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
-    underTest.onChange(profiles, startDate, endDate);
-
-    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
-    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
-    verifyNoMoreInteractions(notificationManager);
-    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
-      .extracting(Profile::getStartDate, Profile::getEndDate)
-      .containsExactlyInAnyOrder(tuple(startDate, endDate));
-  }
-
-  @Test
-  public void avoid_notification_if_configured_in_settings() {
-    settings.setProperty(DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES, true);
-    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
-    Languages languages = new Languages();
-    addProfile(profiles, languages, ACTIVATED);
-
-    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
-    underTest.onChange(profiles, 0, 1);
-
-    verifyZeroInteractions(notificationManager);
-  }
-
-  private Tuple addProfile(Multimap<QProfileName, ActiveRuleChange> profiles, Languages languages, ActiveRuleChange.Type type) {
-    String profileName = randomLowerCaseText();
-    String ruleUuid1 = Uuids.createFast();
-    String ruleUuid2 = Uuids.createFast();
-    Language language = newLanguage(randomLowerCaseText(), randomLowerCaseText());
-    languages.add(language);
-    profiles.putAll(new QProfileName(language.getKey(), profileName),
-      asList(new ActiveRuleChange(
-        type,
-        ActiveRuleKey.parse("qp:repo:rule1"), new RuleDefinitionDto().setUuid(ruleUuid1)),
-        new ActiveRuleChange(type, ActiveRuleKey.parse("qp:repo:rule2"), new RuleDefinitionDto().setUuid(ruleUuid2))));
-    return tuple(profileName, language.getKey(), language.getName(), 2);
-  }
-
-  private static String randomLowerCaseText() {
-    return randomAlphanumeric(20).toLowerCase();
-  }
-
-  private void enableNotificationInGlobalSettings() {
-    settings.setProperty(DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES, false);
-  }
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/RuleActivatorTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/RuleActivatorTest.java
deleted file mode 100644 (file)
index cdcd4fa..0000000
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.collect.ImmutableMap;
-import java.util.List;
-import javax.annotation.Nullable;
-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.Severity;
-import org.sonar.api.rules.RulePriority;
-import org.sonar.api.utils.System2;
-import org.sonar.db.DbSession;
-import org.sonar.db.DbTester;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleKey;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.qualityprofile.RulesProfileDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.qualityprofile.DescendantProfilesSupplier.Result;
-import org.sonar.server.tester.UserSessionRule;
-import org.sonar.server.util.IntegerTypeValidation;
-import org.sonar.server.util.StringTypeValidation;
-import org.sonar.server.util.TypeValidations;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.Mockito.mock;
-import static org.sonar.server.qualityprofile.ActiveRuleInheritance.INHERITED;
-import static org.sonar.server.qualityprofile.ActiveRuleInheritance.OVERRIDES;
-
-/**
- * Class org.sonar.server.qualityprofile.RuleActivator is mostly covered in 
- * org.sonar.server.qualityprofile.BuiltInQProfileUpdateImplTest
- */
-public class RuleActivatorTest {
-  @Rule
-  public final DbTester db = DbTester.create();
-
-  @Rule
-  public final UserSessionRule userSession = UserSessionRule.standalone();
-
-  private static final long NOW = 1_000;
-  private static final long PAST = NOW - 100;
-  private final System2 system2 = new TestSystem2().setNow(NOW);
-  private final TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
-
-  private final QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
-  private final RuleActivator underTest = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
-
-  @Test
-  public void reset_overridden_active_rule() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
-    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
-
-    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
-    ActiveRuleDto parentActiveRuleDto = activateRuleInDb(RulesProfileDto.from(parentProfile), rule,
-      RulePriority.valueOf(Severity.BLOCKER), null);
-    ActiveRuleParamDto parentActiveRuleParam = activateRuleParamInDb(parentActiveRuleDto, ruleParam, "10");
-
-    QProfileDto childProfile = createChildProfile(parentProfile);
-    ActiveRuleDto childActiveRuleDto = activateRuleInDb(RulesProfileDto.from(childProfile), rule,
-      RulePriority.valueOf(Severity.MINOR), OVERRIDES);
-    ActiveRuleParamDto childActiveRuleParam = activateRuleParamInDb(childActiveRuleDto, ruleParam, "15");
-
-    DbSession session = db.getSession();
-    RuleActivation resetRequest = RuleActivation.createReset(rule.getUuid());
-    RuleActivationContext context = new RuleActivationContext.Builder()
-      .setProfiles(asList(parentProfile, childProfile))
-      .setBaseProfile(RulesProfileDto.from(childProfile))
-      .setDate(NOW)
-      .setDescendantProfilesSupplier((profiles, ruleUuids) -> new Result(emptyList(), emptyList(), emptyList()))
-      .setRules(singletonList(rule))
-      .setRuleParams(singletonList(ruleParam))
-      .setActiveRules(asList(parentActiveRuleDto, childActiveRuleDto))
-      .setActiveRuleParams(asList(parentActiveRuleParam, childActiveRuleParam))
-      .build();
-
-    List<ActiveRuleChange> result = underTest.activate(session, resetRequest, context);
-
-    assertThat(result).hasSize(1);
-    assertThat(result.get(0).getParameters()).containsEntry("min", "10");
-    assertThat(result.get(0).getSeverity()).isEqualTo(Severity.BLOCKER);
-    assertThat(result.get(0).getInheritance()).isEqualTo(ActiveRuleInheritance.INHERITED);
-  }
-
-  @Test
-  public void request_new_severity_and_param_for_child_rule() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
-    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
-
-    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
-    ActiveRuleDto parentActiveRuleDto = activateRuleInDb(RulesProfileDto.from(parentProfile), rule,
-        RulePriority.valueOf(Severity.BLOCKER), null);
-    ActiveRuleParamDto parentActiveRuleParam = activateRuleParamInDb(parentActiveRuleDto, ruleParam, "10");
-
-    QProfileDto childProfile = createChildProfile(parentProfile);
-    ActiveRuleDto childActiveRuleDto = activateRuleInDb(RulesProfileDto.from(childProfile), rule,
-        RulePriority.valueOf(Severity.BLOCKER), INHERITED);
-    ActiveRuleParamDto childActiveRuleParam = activateRuleParamInDb(childActiveRuleDto, ruleParam, "10");
-
-    DbSession session = db.getSession();
-    RuleActivation resetRequest = RuleActivation.create(rule.getUuid(), Severity.MINOR, ImmutableMap.of("min", "15"));
-    RuleActivationContext context = new RuleActivationContext.Builder()
-        .setProfiles(asList(parentProfile, childProfile))
-        .setBaseProfile(RulesProfileDto.from(childProfile))
-        .setDate(NOW)
-        .setDescendantProfilesSupplier((profiles, ruleUuids) -> new Result(emptyList(), emptyList(), emptyList()))
-        .setRules(singletonList(rule))
-        .setRuleParams(singletonList(ruleParam))
-        .setActiveRules(asList(parentActiveRuleDto, childActiveRuleDto))
-        .setActiveRuleParams(asList(parentActiveRuleParam, childActiveRuleParam))
-        .build();
-
-    List<ActiveRuleChange> result = underTest.activate(session, resetRequest, context);
-
-    assertThat(result).hasSize(1);
-    assertThat(result.get(0).getParameters()).containsEntry("min", "15");
-    assertThat(result.get(0).getSeverity()).isEqualTo(Severity.MINOR);
-    assertThat(result.get(0).getInheritance()).isEqualTo(OVERRIDES);
-  }
-
-  @Test
-  public void set_severity_and_param_for_child_rule_when_activating() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
-    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
-
-    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
-
-    QProfileDto childProfile = createChildProfile(parentProfile);
-
-    DbSession session = db.getSession();
-    RuleActivation resetRequest = RuleActivation.create(rule.getUuid());
-    RuleActivationContext context = new RuleActivationContext.Builder()
-        .setProfiles(asList(parentProfile, childProfile))
-        .setBaseProfile(RulesProfileDto.from(childProfile))
-        .setDate(NOW)
-        .setDescendantProfilesSupplier((profiles, ruleUuids) -> new Result(emptyList(), emptyList(), emptyList()))
-        .setRules(singletonList(rule))
-        .setRuleParams(singletonList(ruleParam))
-        .setActiveRules(emptyList())
-        .setActiveRuleParams(emptyList())
-        .build();
-
-    List<ActiveRuleChange> result = underTest.activate(session, resetRequest, context);
-
-    assertThat(result).hasSize(1);
-    assertThat(result.get(0).getParameters()).containsEntry("min", "10");
-    assertThat(result.get(0).getSeverity()).isEqualTo(Severity.BLOCKER);
-    assertThat(result.get(0).getInheritance()).isNull();
-  }
-
-  @Test
-  public void fail_if_rule_language_doesnt_match_qp() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo")
-      .setRepositoryKey("repo")
-      .setRuleKey("rule")
-      .setSeverity(Severity.BLOCKER));
-    QProfileDto qp = db.qualityProfiles().insert(p -> p.setLanguage("xoo2").setKee("qp").setIsBuiltIn(true));
-
-    DbSession session = db.getSession();
-    RuleActivation resetRequest = RuleActivation.create(rule.getUuid());
-    RuleActivationContext context = new RuleActivationContext.Builder()
-      .setProfiles(singletonList(qp))
-      .setBaseProfile(RulesProfileDto.from(qp))
-      .setDate(NOW)
-      .setDescendantProfilesSupplier((profiles, ruleUuids) -> new Result(emptyList(), emptyList(), emptyList()))
-      .setRules(singletonList(rule))
-      .setRuleParams(emptyList())
-      .setActiveRules(emptyList())
-      .setActiveRuleParams(emptyList())
-      .build();
-
-
-    assertThrows("xoo rule repo:rule cannot be activated on xoo2 profile qp", BadRequestException.class,
-      () -> underTest.activate(session, resetRequest, context));
-  }
-
-
-  private ActiveRuleDto activateRuleInDb(RulesProfileDto ruleProfile, RuleDefinitionDto rule, RulePriority severity, @Nullable ActiveRuleInheritance inheritance) {
-    ActiveRuleDto dto = new ActiveRuleDto()
-      .setKey(ActiveRuleKey.of(ruleProfile, RuleKey.of(rule.getRepositoryKey(), rule.getRuleKey())))
-      .setProfileUuid(ruleProfile.getUuid())
-      .setSeverity(severity.name())
-      .setRuleUuid(rule.getUuid())
-      .setInheritance(inheritance != null ? inheritance.name() : null)
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST);
-    db.getDbClient().activeRuleDao().insert(db.getSession(), dto);
-    db.commit();
-    return dto;
-  }
-
-  private ActiveRuleParamDto activateRuleParamInDb(ActiveRuleDto activeRuleDto, RuleParamDto ruleParamDto, String value) {
-    ActiveRuleParamDto dto = new ActiveRuleParamDto()
-      .setActiveRuleUuid(activeRuleDto.getUuid())
-      .setRulesParameterUuid(ruleParamDto.getUuid())
-      .setKey(ruleParamDto.getName())
-      .setValue(value);
-    db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRuleDto, dto);
-    db.commit();
-    return dto;
-  }
-
-  private QProfileDto createChildProfile(QProfileDto parent) {
-    return db.qualityProfiles().insert(p -> p
-      .setLanguage(parent.getLanguage())
-      .setParentKee(parent.getKee())
-      .setName("Child of " + parent.getName()))
-      .setIsBuiltIn(false);
-  }
-}
diff --git a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryRule.java b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/qualityprofile/BuiltInQProfileRepositoryRule.java
deleted file mode 100644 (file)
index c7e2b2a..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.qualityprofile;
-
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import org.junit.rules.ExternalResource;
-import org.sonar.api.resources.Language;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.rule.RuleDefinitionDto;
-
-import static com.google.common.base.Preconditions.checkState;
-
-public class BuiltInQProfileRepositoryRule extends ExternalResource implements BuiltInQProfileRepository {
-  private boolean initializeCalled = false;
-  private List<BuiltInQProfile> profiles = new ArrayList<>();
-
-  @Override
-  protected void before() {
-    this.initializeCalled = false;
-    this.profiles.clear();
-  }
-
-  @Override
-  public void initialize() {
-    checkState(!initializeCalled, "initialize must be called only once");
-    this.initializeCalled = true;
-  }
-
-  @Override
-  public List<BuiltInQProfile> get() {
-    checkState(initializeCalled, "initialize must be called first");
-
-    return ImmutableList.copyOf(profiles);
-  }
-
-  public boolean isInitialized() {
-    return initializeCalled;
-  }
-
-  public BuiltInQProfile add(Language language, String profileName) {
-    return add(language, profileName, false);
-  }
-
-  public BuiltInQProfile add(Language language, String profileName, boolean isDefault) {
-    return add(language, profileName, isDefault, new BuiltInQProfile.ActiveRule[0]);
-  }
-
-  public BuiltInQProfile add(Language language, String profileName, boolean isDefault, BuiltInQProfile.ActiveRule... rules) {
-    BuiltInQProfile builtIn = create(language, profileName, isDefault, rules);
-    profiles.add(builtIn);
-    return builtIn;
-  }
-
-  public BuiltInQProfile create(Language language, String profileName, boolean isDefault, BuiltInQProfile.ActiveRule... rules) {
-    BuiltInQProfile.Builder builder = new BuiltInQProfile.Builder()
-      .setLanguage(language.getKey())
-      .setName(profileName)
-      .setDeclaredDefault(isDefault);
-    Arrays.stream(rules).forEach(builder::addRule);
-    return builder.build();
-  }
-
-  public BuiltInQProfile create(BuiltInQualityProfilesDefinition.BuiltInQualityProfile api, RuleDefinitionDto... rules) {
-    BuiltInQProfile.Builder builder = new BuiltInQProfile.Builder()
-      .setLanguage(api.language())
-      .setName(api.name())
-      .setDeclaredDefault(api.isDefault());
-    Map<RuleKey, RuleDefinitionDto> rulesByRuleKey = Arrays.stream(rules)
-      .collect(MoreCollectors.uniqueIndex(RuleDefinitionDto::getKey));
-    api.rules().forEach(rule -> {
-      RuleKey ruleKey = RuleKey.of(rule.repoKey(), rule.ruleKey());
-      RuleDefinitionDto ruleDefinition = rulesByRuleKey.get(ruleKey);
-      Preconditions.checkState(ruleDefinition != null, "Rule '%s' not found", ruleKey);
-      builder.addRule(new BuiltInQProfile.ActiveRule(ruleDefinition.getUuid(), rule));
-    });
-    return builder
-      .build();
-  }
-}
index f3f66fea49436cc83383bc6ebf3e4b814ecd9a23..7be19a379bcce51059c113040fed2cc67f570c0a 100644 (file)
@@ -6,9 +6,10 @@ sonarqube {
 dependencies {
 
     compile 'javax.servlet:javax.servlet-api'
-    compile project(':server:sonar-webserver-auth')
+    compile 'org.json:json'
+
     compile project(':server:sonar-webserver-ws')
-    compile project(':server:sonar-webserver-webapi')
+    compile project(':server:sonar-webserver-auth')
 
     testCompile 'junit:junit'
     testCompile 'org.assertj:assertj-core'
index cc0d3d7f0c71c4002e107f278a5f25222d70dd20..a726ee1197fed5e0a2129bcaa1e0e0cbd9211660 100644 (file)
@@ -35,17 +35,6 @@ public abstract class ServerPushClient {
   private static final Logger LOG = Loggers.get(ServerPushClient.class);
   private static final int DEFAULT_HEARTBEAT_PERIOD = 60;
 
-
-
-
-  private static final byte[] CRLF = new byte[] {'\r', '\n'};
-  private static final byte[] DATA_END = new byte[] {'\n', '\n'};
-  private static final byte[] DATA_FIELD = "data: ".getBytes(StandardCharsets.UTF_8);
-
-
-
-
-
   protected final AsyncContext asyncContext;
 
   private final ScheduledExecutorService executorService;
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java
new file mode 100644 (file)
index 0000000..7e63b06
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.pushapi.qualityprofile;
+
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
+import org.sonar.process.cluster.hz.HazelcastMember;
+
+@ServerSide
+public class DistributedRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {
+
+  private HazelcastMember hazelcastMember;
+
+  public DistributedRuleActivatorEventsDistributor(HazelcastMember hazelcastMember) {
+    this.hazelcastMember = hazelcastMember;
+  }
+
+  @Override
+  public void subscribe(RuleActivationListener listener) {
+    hazelcastMember.subscribeRuleActivationTopic(listener);
+  }
+
+  @Override
+  public void pushEvent(RuleSetChangeEvent event) {
+    hazelcastMember.publishEvent(event);
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventService.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventService.java
new file mode 100644 (file)
index 0000000..ccd546e
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.pushapi.qualityprofile;
+
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+
+public interface QualityProfileChangeEventService {
+
+  void publishRuleActivationToSonarLintClients(ProjectDto project, @Nullable QProfileDto activatedProfile, @Nullable QProfileDto deactivatedProfile);
+
+  void distributeRuleChangeEvent(Collection<QProfileDto> profiles, List<ActiveRuleChange> activeRuleChanges, String language);
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java
new file mode 100644 (file)
index 0000000..fbd8763
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.pushapi.qualityprofile;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.jetbrains.annotations.NotNull;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.ParamChange;
+import org.sonar.core.util.RuleChange;
+import org.sonar.core.util.RuleSetChangeEvent;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.ProjectQprofileAssociationDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.rule.index.RuleIndex;
+import org.sonar.server.rule.index.RuleQuery;
+
+@ServerSide
+public class QualityProfileChangeEventServiceImpl implements QualityProfileChangeEventService {
+
+  private final DbClient dbClient;
+  private final RuleIndex ruleIndex;
+  private final RuleActivatorEventsDistributor eventsDistributor;
+
+  public QualityProfileChangeEventServiceImpl(DbClient dbClient, RuleIndex ruleIndex, RuleActivatorEventsDistributor eventsDistributor) {
+    this.dbClient = dbClient;
+    this.ruleIndex = ruleIndex;
+    this.eventsDistributor = eventsDistributor;
+  }
+
+  @Override
+  public void publishRuleActivationToSonarLintClients(ProjectDto project, @Nullable QProfileDto activatedProfile, @Nullable QProfileDto deactivatedProfile) {
+    List<RuleChange> activatedRules = new ArrayList<>();
+    List<RuleChange> deactivatedRules = new ArrayList<>();
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+
+      if (activatedProfile != null) {
+        RuleQuery query = new RuleQuery().setQProfile(activatedProfile).setActivation(true).setIncludeExternal(true);
+        Iterator<String> searchIdResult = ruleIndex.searchAll(query);
+        List<String> uuids = new ArrayList<>();
+        while (searchIdResult.hasNext()) {
+          uuids.add(searchIdResult.next());
+        }
+
+        List<RuleDto> ruleDtos = dbClient.ruleDao().selectByUuids(dbSession, uuids);
+        Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid = dbClient.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, uuids)
+          .stream().collect(Collectors.groupingBy(ActiveRuleParamDto::getActiveRuleUuid));
+
+        for (RuleDto ruleDto : ruleDtos) {
+          RuleChange ruleChange = toRuleChange(ruleDto, paramsByRuleUuid);
+          activatedRules.add(ruleChange);
+        }
+      }
+
+      if (deactivatedProfile != null) {
+        RuleQuery query = new RuleQuery().setQProfile(deactivatedProfile).setActivation(true).setIncludeExternal(true);
+        // .setLanguages() ?
+        Iterator<String> searchIdResult = ruleIndex.searchAll(query);
+        List<String> uuids = new ArrayList<>();
+        while (searchIdResult.hasNext()) {
+          uuids.add(searchIdResult.next());
+        }
+
+        List<RuleDto> ruleDtos = dbClient.ruleDao().selectByUuids(dbSession, uuids);
+        Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid = dbClient.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, uuids)
+          .stream().collect(Collectors.groupingBy(ActiveRuleParamDto::getActiveRuleUuid));
+
+        for (RuleDto ruleDto : ruleDtos) {
+          RuleChange ruleChange = toRuleChange(ruleDto, paramsByRuleUuid);
+          deactivatedRules.add(ruleChange);
+        }
+      }
+
+    }
+
+    if (activatedRules.isEmpty() && deactivatedRules.isEmpty()) {
+      return;
+    }
+
+    RuleSetChangeEvent event = new RuleSetChangeEvent(new String[]{project.getKey()}, activatedRules.toArray(new RuleChange[0]), deactivatedRules.toArray(new RuleChange[0]));
+    eventsDistributor.pushEvent(event);
+  }
+
+  @NotNull
+  private RuleChange toRuleChange(RuleDto ruleDto, Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid) {
+    RuleChange ruleChange = new RuleChange();
+    ruleChange.setKey(ruleDto.getRuleKey());
+    ruleChange.setLanguage(ruleDto.getLanguage());
+    ruleChange.setSeverity(ruleDto.getSeverityString());
+
+    List<ParamChange> paramChanges = new ArrayList<>();
+    List<ActiveRuleParamDto> activeRuleParamDtos = paramsByRuleUuid.getOrDefault(ruleDto.getUuid(), new ArrayList<>());
+    for (ActiveRuleParamDto activeRuleParam : activeRuleParamDtos) {
+      paramChanges.add(new ParamChange(activeRuleParam.getKey(), activeRuleParam.getValue()));
+    }
+    ruleChange.setParams(paramChanges.toArray(new ParamChange[0]));
+
+    String templateUuid = ruleDto.getTemplateUuid();
+    if (templateUuid != null && !"".equals(templateUuid)) {
+      try (DbSession dbSession = dbClient.openSession(false)) {
+        RuleDto templateRule = dbClient.ruleDao().selectByUuid(templateUuid, dbSession)
+          .orElseThrow(() -> new IllegalStateException(String.format("unknow Template Rule '%s'", templateUuid)));
+        ruleChange.setTemplateKey(templateRule.getRuleKey());
+      }
+    }
+
+    return ruleChange;
+  }
+
+  public void distributeRuleChangeEvent(Collection<QProfileDto> profiles, List<ActiveRuleChange> activeRuleChanges, String language) {
+    if (activeRuleChanges.isEmpty()) {
+      return;
+    }
+
+    Set<RuleChange> activatedRules = new HashSet<>();
+    Set<RuleChange> deactivatedRules = new HashSet<>();
+
+    for (ActiveRuleChange arc : activeRuleChanges) {
+      if (arc.getActiveRule() == null) {
+        continue;
+      }
+
+      RuleChange ruleChange = new RuleChange();
+      ruleChange.setKey(arc.getActiveRule().getRuleKey().rule());
+      ruleChange.setSeverity(arc.getSeverity());
+      ruleChange.setLanguage(language);
+
+      Optional<String> templateKey = templateKey(arc);
+      templateKey.ifPresent(ruleChange::setTemplateKey);
+
+      // params
+      List<ParamChange> paramChanges = new ArrayList<>();
+      for (Map.Entry<String, String> entry : arc.getParameters().entrySet()) {
+        paramChanges.add(new ParamChange(entry.getKey(), entry.getValue()));
+      }
+      ruleChange.setParams(paramChanges.toArray(new ParamChange[0]));
+
+      switch (arc.getType()) {
+        case ACTIVATED:
+        case UPDATED:
+          activatedRules.add(ruleChange);
+          break;
+        case DEACTIVATED:
+          deactivatedRules.add(ruleChange);
+          break;
+      }
+    }
+
+    Set<String> projectKeys = getProjectKeys(profiles);
+
+    if (activatedRules.isEmpty() && deactivatedRules.isEmpty()) {
+      return;
+    }
+
+    RuleSetChangeEvent event = new RuleSetChangeEvent(projectKeys.toArray(new String[0]), activatedRules.toArray(new RuleChange[0]), deactivatedRules.toArray(new RuleChange[0]));
+    eventsDistributor.pushEvent(event);
+  }
+
+  private Optional<String> templateKey(ActiveRuleChange arc) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      String ruleUuid = arc.getRuleUuid();
+      RuleDto rule = dbClient.ruleDao().selectByUuid(ruleUuid, dbSession).orElseThrow(() -> new IllegalStateException("unknow rule"));
+      String templateUuid = rule.getTemplateUuid();
+      if (templateUuid != null && !"".equals(templateUuid)) {
+        RuleDto templateRule = dbClient.ruleDao().selectByUuid(templateUuid, dbSession)
+          .orElseThrow(() -> new IllegalStateException(String.format("unknow Template Rule '%s'", templateUuid)));
+        return Optional.of(templateRule.getRuleKey());
+      }
+    }
+    return Optional.empty();
+  }
+
+  private Set<String> getProjectKeys(Collection<QProfileDto> profiles) {
+    Set<String> projectKeys = new HashSet<>();
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      for (QProfileDto profileDto : profiles) {
+        List<ProjectQprofileAssociationDto> associationDtos = dbClient.qualityProfileDao().selectSelectedProjects(dbSession, profileDto, null);
+        for (ProjectQprofileAssociationDto associationDto : associationDtos) {
+          projectKeys.add(associationDto.getProjectKey());
+        }
+      }
+      return projectKeys;
+    }
+  }
+
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java
new file mode 100644 (file)
index 0000000..366ffc6
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.pushapi.qualityprofile;
+
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
+
+public interface RuleActivatorEventsDistributor {
+
+  void subscribe(RuleActivationListener listener);
+
+  void pushEvent(RuleSetChangeEvent event);
+}
\ No newline at end of file
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java
new file mode 100644 (file)
index 0000000..656ec75
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.pushapi.qualityprofile;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
+
+@ServerSide
+public class StandaloneRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {
+
+  private List<RuleActivationListener> listeners = new ArrayList<>();
+
+  @Override
+  public void subscribe(RuleActivationListener listener) {
+    listeners.add(listener);
+  }
+
+  @Override
+  public void pushEvent(RuleSetChangeEvent event) {
+    listeners.forEach(l -> l.listen(event));
+  }
+}
index 4b72f6de66d3e9846bf42677d7f8db7cdb09f922..51146f28510fdac96c3e58cd0f1525d8445e22fa 100644 (file)
@@ -35,7 +35,7 @@ import org.sonar.core.util.ParamChange;
 import org.sonar.core.util.RuleActivationListener;
 import org.sonar.core.util.RuleChange;
 import org.sonar.core.util.RuleSetChangeEvent;
-import org.sonar.server.qualityprofile.RuleActivatorEventsDistributor;
+import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor;
 
 import static java.util.Arrays.asList;
 
@@ -74,15 +74,19 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
   @Override
   public void listen(RuleSetChangeEvent ruleChangeEvent) {
     LOG.info("Generating a RuleSetChangeEvent");
-    // TODO filter on languages here as well
-    broadcastMessage(ruleChangeEvent, f -> f.getClientProjectKeys().isEmpty() || !Collections.disjoint(f.getClientProjectKeys(), asList(ruleChangeEvent.getProjects())));
+    broadcastMessage(ruleChangeEvent, getFilterForEvent(ruleChangeEvent));
+  }
+
+  private static Predicate<SonarLintClient> getFilterForEvent(RuleSetChangeEvent ruleChangeEvent) {
+    List<String> affectedProjects = asList(ruleChangeEvent.getProjects());
+    return f -> !Collections.disjoint(f.getClientProjectKeys(), affectedProjects) && f.getLanguages().contains(ruleChangeEvent.getLanguage());
   }
 
 
-  public void broadcastMessage(RuleSetChangeEvent message, Predicate<SonarLintClient> filter) {
-    String jsonString = getJSONString(message);
-    clients.stream().filter(filter).forEach(c -> {
+  public void broadcastMessage(RuleSetChangeEvent event, Predicate<SonarLintClient> projectsFilter) {
+    clients.stream().filter(projectsFilter).forEach(c -> {
       try {
+        String jsonString = getJSONString(event);
         c.writeAndFlush(jsonString);
       } catch (IOException e) {
         LOG.error("Unable to send message to a client: " + e.getMessage());
@@ -90,7 +94,6 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
     });
   }
 
-
   public String getJSONString(RuleSetChangeEvent ruleSetChangeEvent) {
     JSONObject result = new JSONObject();
     result.put("event", ruleSetChangeEvent.getEvent());
@@ -129,7 +132,7 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
     return ruleJson;
   }
 
-  private JSONObject toJson(ParamChange paramChange) {
+  private static JSONObject toJson(ParamChange paramChange) {
     JSONObject param = new JSONObject();
     param.put("key", paramChange.getKey());
     param.put("value", paramChange.getValue());
index 2c27c48cda12d8cc2cdbed8c6d75eafdd44617f4..ca30b37f478281a6c469754ae264b834ab32bacf 100644 (file)
@@ -84,7 +84,7 @@ public class SonarLintPushAction extends ServerPushAction {
 
     if (!isServerSideEventsRequest(servletRequest)) {
       servletResponse.stream().setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
-      return; // TODO fixme this is not closing the connexion properly
+      return;
     }
 
     setHeadersForResponse(servletResponse);
index 49076199685c6587e11cac6acc82a10a4085d457..c121bf82609ffd1843a3aad358ea665fd62e3a74 100644 (file)
  */
 package org.sonar.server.pushapi.sonarlint;
 
+import java.io.IOException;
 import java.util.Set;
 import javax.servlet.AsyncContext;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletResponse;
 import org.junit.Before;
 import org.junit.Test;
-import org.sonar.server.qualityprofile.StandaloneRuleActivatorEventsDistributor;
+import org.mockito.ArgumentCaptor;
+import org.sonar.core.util.RuleChange;
+import org.sonar.core.util.RuleSetChangeEvent;
+import org.sonar.server.pushapi.qualityprofile.StandaloneRuleActivatorEventsDistributor;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
 
 public class SonarLintClientsRegistryTest {
 
@@ -67,4 +76,56 @@ public class SonarLintClientsRegistryTest {
     assertThat(underTest.countConnectedClients()).isEqualTo(10);
   }
 
+  @Test
+  public void listen_givenOneClientInterestedInJavaEvents_sendOneJavaEvent() throws IOException {
+    Set<String> javaLanguageKey = Set.of("java");
+    ServletResponse response = mock(ServletResponse.class);
+    when(defaultAsyncContext.getResponse()).thenReturn(response);
+    ServletOutputStream outputStream = mock(ServletOutputStream.class);
+    when(response.getOutputStream()).thenReturn(outputStream);
+    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, javaLanguageKey);
+
+    underTest.registerClient(sonarLintClient);
+
+    RuleChange javaRule = createRuleChange("java");
+
+    RuleChange[] activatedRules = {};
+    RuleChange[] deactivatedRules = {javaRule};
+    RuleSetChangeEvent ruleChangeEvent = new RuleSetChangeEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules);
+    underTest.listen(ruleChangeEvent);
+
+    ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
+    verify(outputStream).write(captor.capture());
+    String message = new String(captor.getValue());
+    assertThat(message).contains("java");
+  }
+
+  @Test
+  public void listen_givenOneClientInterestedInJsEventsAndJavaEventGenerated_sendZeroEvents() throws IOException {
+    Set<String> jsLanguageKey = Set.of("js");
+    ServletResponse response = mock(ServletResponse.class);
+    when(defaultAsyncContext.getResponse()).thenReturn(response);
+    ServletOutputStream outputStream = mock(ServletOutputStream.class);
+    when(response.getOutputStream()).thenReturn(outputStream);
+    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, jsLanguageKey);
+
+    underTest.registerClient(sonarLintClient);
+
+    RuleChange javaRuleChange = createRuleChange("java");
+
+    RuleChange[] activatedRules = {};
+    RuleChange[] deactivatedRules = {javaRuleChange};
+    RuleSetChangeEvent ruleChangeEvent = new RuleSetChangeEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules);
+    underTest.listen(ruleChangeEvent);
+
+    verifyNoInteractions(outputStream);
+  }
+
+  private RuleChange createRuleChange(String language) {
+    RuleChange javaRule = new RuleChange();
+    javaRule.setLanguage(language);
+
+    return javaRule;
+  }
+
 }
index 0f9ac5823ef92d88ab6ad6fb402bad99361211fd..0cb227bd131e2244a2a6257e5660a1526744740e 100644 (file)
@@ -20,6 +20,7 @@ dependencies {
   compile project(':server:sonar-webserver-auth')
   compile project(':server:sonar-webserver-es')
   compile project(':server:sonar-webserver-ws')
+  compile project(':server:sonar-webserver-pushapi')
   compile project(':server:sonar-alm-client')
   compile project(':sonar-scanner-protocol')
 
index 71d432ae7f087aaf8f90b000dd8cdc47bb528e9f..30dffad218e064c35d597e8bcd88053d76f86f34 100644 (file)
@@ -43,6 +43,7 @@ import org.sonar.db.qualityprofile.ExportRuleParamDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.rule.DeprecatedRuleKeyDto;
 import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 import org.sonar.server.rule.NewCustomRule;
 import org.sonar.server.rule.RuleCreator;
 
index ecadc4ac9f47681550280c0bf5c69aa39526cd74..cb639786d0f86c5fb627eeac9ce6b5a56ffc9280 100644 (file)
@@ -23,6 +23,7 @@ import org.sonar.api.server.ServerSide;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 
 @ServerSide
 public class QProfileCopier {
index e2fb679ccdec22fc050978930b2fa36c0e211464..09ed138ea250a7fb74ab917b8a491ea25886e31c 100644 (file)
@@ -24,6 +24,7 @@ import javax.annotation.Nullable;
 import org.sonar.db.DbSession;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 
 /**
  * Create, delete and set as default profile.
index 5f10361148d5b539d8bd56c2777f0ba897a7ab62..1306fd4a6b62e69316af5f5f0e8f39e26260a897 100644 (file)
@@ -34,6 +34,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.qualityprofile.DefaultQProfileDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 
 import static com.google.common.base.Preconditions.checkArgument;
index 7deadcbbfeed370cd7fe51fbb5dae3277d8f8cce..ee02729090ea810aa7cf671bfb47a80dfeefa46a 100644 (file)
@@ -30,6 +30,9 @@ import org.sonar.db.DbSession;
 import org.sonar.db.qualityprofile.ActiveRuleDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.builtin.RuleActivationContext;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 
 import static com.google.common.base.Preconditions.checkArgument;
index db891a34eccf37be72bfe809d619e8cf480ee3a7..449d621a8ad1dd2cbebf9d6289b2389a26f42f25 100644 (file)
@@ -32,6 +32,9 @@ import org.sonar.db.DbSession;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.builtin.RuleActivationContext;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleQuery;
index d315e15b2cd53491e2058ccf15af217ce6666c29..aa491de41c9d21fb156b22066a1c86c5cc4d776b 100644 (file)
@@ -31,6 +31,9 @@ import org.sonar.db.qualityprofile.ActiveRuleDto;
 import org.sonar.db.qualityprofile.OrgActiveRuleDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.builtin.RuleActivationContext;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 
 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
@@ -43,7 +46,8 @@ public class QProfileTreeImpl implements QProfileTree {
   private final ActiveRuleIndexer activeRuleIndexer;
   private final QualityProfileChangeEventService qualityProfileChangeEventService;
 
-  public QProfileTreeImpl(DbClient db, RuleActivator ruleActivator, System2 system2, ActiveRuleIndexer activeRuleIndexer, QualityProfileChangeEventService qualityProfileChangeEventService) {
+  public QProfileTreeImpl(DbClient db, RuleActivator ruleActivator, System2 system2, ActiveRuleIndexer activeRuleIndexer,
+    QualityProfileChangeEventService qualityProfileChangeEventService) {
     this.db = db;
     this.ruleActivator = ruleActivator;
     this.system2 = system2;
index af93ddf4163f94f0ecbac5158890ade8832638da..8cc4762bd4ae4f3bf7528c4ef084f55fb779f922 100644 (file)
@@ -39,6 +39,12 @@ import org.sonar.db.DbSession;
 import org.sonar.db.qualityprofile.DefaultQProfileDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfile;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileInsert;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileRepository;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdate;
+import org.sonar.server.qualityprofile.builtin.BuiltInQualityProfilesUpdateListener;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 
 import static java.lang.String.format;
 import static java.util.stream.Collectors.toMap;
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotification.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotification.java
new file mode 100644 (file)
index 0000000..5401485
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import org.sonar.api.notifications.Notification;
+
+public class BuiltInQPChangeNotification extends Notification {
+  static final String TYPE = "built-in-quality-profiles";
+
+  public BuiltInQPChangeNotification() {
+    super(TYPE);
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationBuilder.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationBuilder.java
new file mode 100644 (file)
index 0000000..9eda796
--- /dev/null
@@ -0,0 +1,220 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.IntStream;
+import org.sonar.api.notifications.Notification;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.Integer.parseInt;
+import static java.lang.Long.parseLong;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+public class BuiltInQPChangeNotificationBuilder {
+
+  private static final String NUMBER_OF_PROFILES = "numberOfProfiles";
+  private static final String PROFILE_NAME = ".profileName";
+  private static final String LANGUAGE_KEY = ".languageKey";
+  private static final String LANGUAGE_NAME = ".languageName";
+  private static final String NEW_RULES = ".newRules";
+  private static final String UPDATED_RULES = ".updatedRules";
+  private static final String REMOVED_RULES = ".removedRules";
+  private static final String START_DATE = ".startDate";
+  private static final String END_DATE = ".endDate";
+
+  private final List<Profile> profiles = new ArrayList<>();
+
+  public BuiltInQPChangeNotificationBuilder addProfile(Profile profile) {
+    profiles.add(profile);
+    return this;
+  }
+
+  public BuiltInQPChangeNotification build() {
+    BuiltInQPChangeNotification notification = new BuiltInQPChangeNotification();
+    notification.setFieldValue(NUMBER_OF_PROFILES, String.valueOf(profiles.size()));
+    AtomicInteger count = new AtomicInteger();
+    profiles.forEach(profile -> {
+      int index = count.getAndIncrement();
+      notification.setFieldValue(index + PROFILE_NAME, profile.getProfileName());
+      notification.setFieldValue(index + LANGUAGE_KEY, profile.getLanguageKey());
+      notification.setFieldValue(index + LANGUAGE_NAME, profile.getLanguageName());
+      notification.setFieldValue(index + NEW_RULES, String.valueOf(profile.getNewRules()));
+      notification.setFieldValue(index + UPDATED_RULES, String.valueOf(profile.getUpdatedRules()));
+      notification.setFieldValue(index + REMOVED_RULES, String.valueOf(profile.getRemovedRules()));
+      notification.setFieldValue(index + START_DATE, String.valueOf(profile.getStartDate()));
+      notification.setFieldValue(index + END_DATE, String.valueOf(profile.getEndDate()));
+    });
+    return notification;
+  }
+
+  public static BuiltInQPChangeNotificationBuilder parse(Notification notification) {
+    checkState(BuiltInQPChangeNotification.TYPE.equals(notification.getType()),
+      "Expected notification of type %s but got %s", BuiltInQPChangeNotification.TYPE, notification.getType());
+    BuiltInQPChangeNotificationBuilder notif = new BuiltInQPChangeNotificationBuilder();
+    String numberOfProfilesText = notification.getFieldValue(NUMBER_OF_PROFILES);
+    checkState(numberOfProfilesText != null, "Could not read the built-in quality profile notification");
+    Integer numberOfProfiles = Integer.valueOf(numberOfProfilesText);
+    IntStream.rangeClosed(0, numberOfProfiles - 1)
+      .mapToObj(index -> Profile.newBuilder()
+        .setProfileName(getNonNullFieldValue(notification, index + PROFILE_NAME))
+        .setLanguageKey(getNonNullFieldValue(notification, index + LANGUAGE_KEY))
+        .setLanguageName(getNonNullFieldValue(notification, index + LANGUAGE_NAME))
+        .setNewRules(parseInt(getNonNullFieldValue(notification, index + NEW_RULES)))
+        .setUpdatedRules(parseInt(getNonNullFieldValue(notification, index + UPDATED_RULES)))
+        .setRemovedRules(parseInt(getNonNullFieldValue(notification, index + REMOVED_RULES)))
+        .setStartDate(parseLong(getNonNullFieldValue(notification, index + START_DATE)))
+        .setEndDate(parseLong(getNonNullFieldValue(notification, index + END_DATE)))
+        .build())
+      .forEach(notif::addProfile);
+    return notif;
+  }
+
+  private static String getNonNullFieldValue(Notification notification, String key) {
+    String value = notification.getFieldValue(key);
+    return requireNonNull(value, format("Notification field '%s' is null", key));
+  }
+
+  public List<Profile> getProfiles() {
+    return profiles;
+  }
+
+  public static class Profile {
+    private final String profileName;
+    private final String languageKey;
+    private final String languageName;
+    private final int newRules;
+    private final int updatedRules;
+    private final int removedRules;
+    private final long startDate;
+    private final long endDate;
+
+    public Profile(Builder builder) {
+      this.profileName = builder.profileName;
+      this.languageKey = builder.languageKey;
+      this.languageName = builder.languageName;
+      this.newRules = builder.newRules;
+      this.updatedRules = builder.updatedRules;
+      this.removedRules = builder.removedRules;
+      this.startDate = builder.startDate;
+      this.endDate = builder.endDate;
+    }
+
+    public String getProfileName() {
+      return profileName;
+    }
+
+    public String getLanguageKey() {
+      return languageKey;
+    }
+
+    public String getLanguageName() {
+      return languageName;
+    }
+
+    public int getNewRules() {
+      return newRules;
+    }
+
+    public int getUpdatedRules() {
+      return updatedRules;
+    }
+
+    public int getRemovedRules() {
+      return removedRules;
+    }
+
+    public long getStartDate() {
+      return startDate;
+    }
+
+    public long getEndDate() {
+      return endDate;
+    }
+
+    public static Builder newBuilder() {
+      return new Builder();
+    }
+
+    public static class Builder {
+      private String profileName;
+      private String languageKey;
+      private String languageName;
+      private int newRules;
+      private int updatedRules;
+      private int removedRules;
+      private long startDate;
+      private long endDate;
+
+      private Builder() {
+      }
+
+      public Builder setLanguageKey(String languageKey) {
+        this.languageKey = requireNonNull(languageKey, "languageKEy should not be null");
+        return this;
+      }
+
+      public Builder setLanguageName(String languageName) {
+        this.languageName = requireNonNull(languageName, "languageName should not be null");
+        return this;
+      }
+
+      public Builder setProfileName(String profileName) {
+        this.profileName = requireNonNull(profileName, "profileName should not be null");
+        return this;
+      }
+
+      public Builder setNewRules(int newRules) {
+        checkState(newRules >= 0, "newRules should not be negative");
+        this.newRules = newRules;
+        return this;
+      }
+
+      public Builder setUpdatedRules(int updatedRules) {
+        checkState(updatedRules >= 0, "updatedRules should not be negative");
+        this.updatedRules = updatedRules;
+        return this;
+      }
+
+      public Builder setRemovedRules(int removedRules) {
+        checkState(removedRules >= 0, "removedRules should not be negative");
+        this.removedRules = removedRules;
+        return this;
+      }
+
+      public Builder setStartDate(long startDate) {
+        this.startDate = startDate;
+        return this;
+      }
+
+      public Builder setEndDate(long endDate) {
+        this.endDate = endDate;
+        return this;
+      }
+
+      public Profile build() {
+        return new Profile(this);
+      }
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationHandler.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationHandler.java
new file mode 100644 (file)
index 0000000..371af1a
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.notification.EmailNotificationHandler;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
+
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+
+public class BuiltInQPChangeNotificationHandler extends EmailNotificationHandler<BuiltInQPChangeNotification> {
+  private final DbClient dbClient;
+
+  public BuiltInQPChangeNotificationHandler(DbClient dbClient, EmailNotificationChannel emailNotificationChannel) {
+    super(emailNotificationChannel);
+    this.dbClient = dbClient;
+  }
+
+  @Override
+  public Optional<NotificationDispatcherMetadata> getMetadata() {
+    return Optional.empty();
+  }
+
+  @Override
+  public Class<BuiltInQPChangeNotification> getNotificationClass() {
+    return BuiltInQPChangeNotification.class;
+  }
+
+  @Override
+  public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<BuiltInQPChangeNotification> notifications) {
+    try (DbSession session = dbClient.openSession(false)) {
+      return dbClient.authorizationDao()
+        .selectQualityProfileAdministratorLogins(session)
+        .stream()
+        .flatMap(t -> notifications.stream().map(notification -> new EmailDeliveryRequest(t.getEmail(), notification)))
+        .collect(toSet());
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTemplate.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTemplate.java
new file mode 100644 (file)
index 0000000..7b85bcf
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Comparator;
+import java.util.Date;
+import javax.annotation.CheckForNull;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.platform.Server;
+import org.sonar.server.issue.notification.EmailMessage;
+import org.sonar.server.issue.notification.EmailTemplate;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.sonar.api.utils.DateUtils.formatDate;
+import static org.sonar.server.qualityprofile.builtin.BuiltInQPChangeNotificationBuilder.Profile;
+import static org.sonar.server.qualityprofile.builtin.BuiltInQPChangeNotificationBuilder.parse;
+
+public class BuiltInQPChangeNotificationTemplate implements EmailTemplate {
+
+  private final Server server;
+
+  public BuiltInQPChangeNotificationTemplate(Server server) {
+    this.server = server;
+  }
+
+  @Override
+  @CheckForNull
+  public EmailMessage format(Notification notification) {
+    if (!BuiltInQPChangeNotification.TYPE.equals(notification.getType())) {
+      return null;
+    }
+
+    BuiltInQPChangeNotificationBuilder profilesNotification = parse(notification);
+    StringBuilder message = new StringBuilder("The following built-in profiles have been updated:\n\n");
+    profilesNotification.getProfiles().stream()
+      .sorted(Comparator.comparing(Profile::getLanguageName).thenComparing(Profile::getProfileName))
+      .forEach(profile -> {
+        message.append("\"")
+          .append(profile.getProfileName())
+          .append("\" - ")
+          .append(profile.getLanguageName())
+          .append(": ")
+          .append(server.getPublicRootUrl()).append("/profiles/changelog?language=")
+          .append(profile.getLanguageKey())
+          .append("&name=")
+          .append(encode(profile.getProfileName()))
+          .append("&since=")
+          .append(formatDate(new Date(profile.getStartDate())))
+          .append("&to=")
+          .append(formatDate(new Date(profile.getEndDate())))
+          .append("\n");
+        int newRules = profile.getNewRules();
+        if (newRules > 0) {
+          message.append(" ").append(newRules).append(" new rule")
+            .append(plural(newRules))
+            .append('\n');
+        }
+        int updatedRules = profile.getUpdatedRules();
+        if (updatedRules > 0) {
+          message.append(" ").append(updatedRules).append(" rule")
+            .append(updatedRules > 1 ? "s have been updated" : " has been updated")
+            .append("\n");
+        }
+        int removedRules = profile.getRemovedRules();
+        if (removedRules > 0) {
+          message.append(" ").append(removedRules).append(" rule")
+            .append(plural(removedRules))
+            .append(" removed\n");
+        }
+        message.append("\n");
+      });
+
+    message.append("This is a good time to review your quality profiles and update them to benefit from the latest evolutions: ");
+    message.append(server.getPublicRootUrl()).append("/profiles");
+
+    // And finally return the email that will be sent
+    return new EmailMessage()
+      .setMessageId(BuiltInQPChangeNotification.TYPE)
+      .setSubject("Built-in quality profiles have been updated")
+      .setPlainTextMessage(message.toString());
+  }
+
+  private static String plural(int count) {
+    return count > 1 ? "s" : "";
+  }
+
+  public String encode(String text) {
+    try {
+      return URLEncoder.encode(text, UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException(String.format("Cannot encode %s", text), e);
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfile.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfile.java
new file mode 100644 (file)
index 0000000..3f3945e
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+
+import static org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.BuiltInActiveRule;
+import static org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.OverriddenParam;
+
+/**
+ * Represent a Quality Profile as computed from {@link BuiltInQualityProfilesDefinition} provided by installed plugins.
+ */
+@Immutable
+public final class BuiltInQProfile {
+  private final QProfileName qProfileName;
+  private final boolean isDefault;
+  private final List<ActiveRule> activeRules;
+
+  private BuiltInQProfile(Builder builder) {
+    this.qProfileName = new QProfileName(builder.language, builder.name);
+    this.isDefault = builder.declaredDefault || builder.computedDefault;
+    this.activeRules = ImmutableList.copyOf(builder.activeRules);
+  }
+
+  public String getName() {
+    return qProfileName.getName();
+  }
+
+  public String getLanguage() {
+    return qProfileName.getLanguage();
+  }
+
+  public QProfileName getQProfileName() {
+    return qProfileName;
+  }
+
+  public boolean isDefault() {
+    return isDefault;
+  }
+
+  public List<ActiveRule> getActiveRules() {
+    return activeRules;
+  }
+
+  public static final class ActiveRule {
+    private final String ruleUuid;
+    private final RuleKey ruleKey;
+    private final String severity;
+    private final List<OverriddenParam> params;
+
+    public ActiveRule(String ruleUuid, BuiltInActiveRule builtIn) {
+      this(ruleUuid, RuleKey.of(builtIn.repoKey(), builtIn.ruleKey()), builtIn.overriddenSeverity(), builtIn.overriddenParams());
+    }
+
+    ActiveRule(String ruleUuid, RuleKey ruleKey, @Nullable String severity, List<OverriddenParam> params) {
+      this.ruleUuid = ruleUuid;
+      this.ruleKey = ruleKey;
+      this.severity = severity;
+      this.params = params;
+    }
+
+    public String getRuleUuid() {
+      return ruleUuid;
+    }
+
+    public RuleKey getRuleKey() {
+      return ruleKey;
+    }
+
+    @CheckForNull
+    public String getSeverity() {
+      return severity;
+    }
+
+    public List<OverriddenParam> getParams() {
+      return params;
+    }
+  }
+
+  static final class Builder {
+    private String language;
+    private String name;
+    private boolean declaredDefault;
+    private boolean computedDefault;
+    private final List<ActiveRule> activeRules = new ArrayList<>();
+
+    public Builder setLanguage(String language) {
+      this.language = language;
+      return this;
+    }
+
+    Builder setName(String name) {
+      this.name = name;
+      return this;
+    }
+
+    String getName() {
+      return name;
+    }
+
+    Builder setDeclaredDefault(boolean declaredDefault) {
+      this.declaredDefault = declaredDefault;
+      return this;
+    }
+
+    boolean isDeclaredDefault() {
+      return declaredDefault;
+    }
+
+    Builder setComputedDefault(boolean flag) {
+      computedDefault = flag;
+      return this;
+    }
+
+    Builder addRule(ActiveRule activeRule) {
+      this.activeRules.add(activeRule);
+      return this;
+    }
+
+    BuiltInQProfile build() {
+      return new BuiltInQProfile(this);
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsert.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsert.java
new file mode 100644 (file)
index 0000000..9d6d258
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import org.sonar.db.DbSession;
+
+public interface BuiltInQProfileInsert {
+  /**
+   * Persist a new built-in profile
+   * Db sessions are committed and Elasticsearch indices are updated
+   */
+  void create(DbSession batchSession, BuiltInQProfile builtInQProfile);
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsertImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsertImpl.java
new file mode 100644 (file)
index 0000000..bc6cc65
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.base.Splitter;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+import org.sonar.api.server.rule.RuleParamType;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleKey;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.DefaultQProfileDto;
+import org.sonar.db.qualityprofile.OrgQProfileDto;
+import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
+import org.sonar.server.rule.ServerRuleFinder;
+import org.sonar.server.util.TypeValidations;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Collections.emptySet;
+import static java.util.Objects.requireNonNull;
+
+public class BuiltInQProfileInsertImpl implements BuiltInQProfileInsert {
+  private final DbClient dbClient;
+  private final ServerRuleFinder ruleFinder;
+  private final System2 system2;
+  private final UuidFactory uuidFactory;
+  private final TypeValidations typeValidations;
+  private final ActiveRuleIndexer activeRuleIndexer;
+  private RuleRepository ruleRepository;
+
+  public BuiltInQProfileInsertImpl(DbClient dbClient, ServerRuleFinder ruleFinder, System2 system2, UuidFactory uuidFactory,
+    TypeValidations typeValidations, ActiveRuleIndexer activeRuleIndexer) {
+    this.dbClient = dbClient;
+    this.ruleFinder = ruleFinder;
+    this.system2 = system2;
+    this.uuidFactory = uuidFactory;
+    this.typeValidations = typeValidations;
+    this.activeRuleIndexer = activeRuleIndexer;
+  }
+
+  @Override
+  public void create(DbSession batchDbSession, BuiltInQProfile builtInQProfile) {
+    initRuleRepository(batchDbSession);
+
+    Date now = new Date(system2.now());
+    RulesProfileDto ruleProfile = insertRulesProfile(batchDbSession, builtInQProfile, now);
+
+    List<ActiveRuleChange> changes = builtInQProfile.getActiveRules().stream()
+      .map(activeRule -> insertActiveRule(batchDbSession, ruleProfile, activeRule, now.getTime()))
+      .collect(MoreCollectors.toList());
+
+    changes.forEach(change -> dbClient.qProfileChangeDao().insert(batchDbSession, change.toDto(null)));
+
+    createDefaultAndOrgQProfiles(batchDbSession, builtInQProfile, ruleProfile);
+
+    activeRuleIndexer.commitAndIndex(batchDbSession, changes);
+  }
+
+
+  private void createDefaultAndOrgQProfiles(DbSession batchDbSession, BuiltInQProfile builtIn, RulesProfileDto rulesProfileDto) {
+    Optional<String> qProfileUuid = dbClient.defaultQProfileDao().selectDefaultQProfileUuid(batchDbSession, builtIn.getLanguage());
+
+    OrgQProfileDto dto = new OrgQProfileDto()
+      .setRulesProfileUuid(rulesProfileDto.getUuid())
+      .setUuid(uuidFactory.create());
+
+    if (builtIn.isDefault() && qProfileUuid.isEmpty()) {
+      DefaultQProfileDto defaultQProfileDto = new DefaultQProfileDto()
+        .setQProfileUuid(dto.getUuid())
+        .setLanguage(builtIn.getLanguage());
+      dbClient.defaultQProfileDao().insert(batchDbSession, defaultQProfileDto);
+    }
+
+    dbClient.qualityProfileDao().insert(batchDbSession, dto);
+  }
+
+  private void initRuleRepository(DbSession dbSession) {
+    if (ruleRepository == null) {
+      ruleRepository = new RuleRepository(dbClient, dbSession, ruleFinder);
+    }
+  }
+
+  private RulesProfileDto insertRulesProfile(DbSession dbSession, BuiltInQProfile builtIn, Date now) {
+    RulesProfileDto dto = new RulesProfileDto()
+      .setUuid(uuidFactory.create())
+      .setName(builtIn.getName())
+      .setLanguage(builtIn.getLanguage())
+      .setIsBuiltIn(true)
+      .setRulesUpdatedAtAsDate(now);
+    dbClient.qualityProfileDao().insert(dbSession, dto);
+    return dto;
+  }
+
+  private ActiveRuleChange insertActiveRule(DbSession batchDbSession, RulesProfileDto rulesProfileDto, BuiltInQProfile.ActiveRule activeRule, long now) {
+    RuleKey ruleKey = activeRule.getRuleKey();
+    RuleDefinitionDto ruleDefinitionDto = ruleRepository.getDefinition(ruleKey)
+      .orElseThrow(() -> new IllegalStateException("RuleDefinition not found for key " + ruleKey));
+
+    ActiveRuleDto dto = new ActiveRuleDto();
+    dto.setProfileUuid(rulesProfileDto.getUuid());
+    dto.setRuleUuid(ruleDefinitionDto.getUuid());
+    dto.setKey(ActiveRuleKey.of(rulesProfileDto, ruleDefinitionDto.getKey()));
+    dto.setSeverity(firstNonNull(activeRule.getSeverity(), ruleDefinitionDto.getSeverityString()));
+    dto.setUpdatedAt(now);
+    dto.setCreatedAt(now);
+    dbClient.activeRuleDao().insert(batchDbSession, dto);
+
+    List<ActiveRuleParamDto> paramDtos = insertActiveRuleParams(batchDbSession, activeRule, dto);
+
+    ActiveRuleChange change = new ActiveRuleChange(ActiveRuleChange.Type.ACTIVATED, dto, ruleDefinitionDto);
+    change.setSeverity(dto.getSeverityString());
+    paramDtos.forEach(paramDto -> change.setParameter(paramDto.getKey(), paramDto.getValue()));
+    return change;
+  }
+
+  private List<ActiveRuleParamDto> insertActiveRuleParams(DbSession session, BuiltInQProfile.ActiveRule activeRule, ActiveRuleDto activeRuleDto) {
+    Map<String, String> valuesByParamKey = activeRule.getParams().stream()
+      .collect(MoreCollectors.uniqueIndex(BuiltInQualityProfilesDefinition.OverriddenParam::key, BuiltInQualityProfilesDefinition.OverriddenParam::overriddenValue));
+    List<ActiveRuleParamDto> rules = ruleRepository.getRuleParams(activeRule.getRuleKey()).stream()
+      .map(param -> createParamDto(param, Optional.ofNullable(valuesByParamKey.get(param.getName())).orElse(param.getDefaultValue())))
+      .filter(Objects::nonNull)
+      .collect(Collectors.toList());
+
+    rules.forEach(paramDto -> dbClient.activeRuleDao().insertParam(session, activeRuleDto, paramDto));
+    return rules;
+  }
+
+  @CheckForNull
+  private ActiveRuleParamDto createParamDto(RuleParamDto param, @Nullable String value) {
+    if (value == null) {
+      return null;
+    }
+    ActiveRuleParamDto paramDto = ActiveRuleParamDto.createFor(param);
+    paramDto.setValue(validateParam(param, value));
+    return paramDto;
+  }
+
+  private String validateParam(RuleParamDto ruleParam, String value) {
+    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());
+    }
+    return value;
+  }
+
+  private static class RuleRepository {
+    private final Map<RuleKey, Set<RuleParamDto>> params;
+    private final ServerRuleFinder ruleFinder;
+
+    private RuleRepository(DbClient dbClient, DbSession session, ServerRuleFinder ruleFinder) {
+      this.ruleFinder = ruleFinder;
+      this.params = new HashMap<>();
+
+      for (RuleParamDto ruleParam : dbClient.ruleDao().selectAllRuleParams(session)) {
+        Optional<RuleKey> ruleKey = ruleFinder.findDtoByUuid(ruleParam.getRuleUuid())
+          .map(r -> RuleKey.of(r.getRepositoryKey(), r.getRuleKey()));
+
+        if (ruleKey.isPresent()) {
+          params.computeIfAbsent(ruleKey.get(), r -> new HashSet<>()).add(ruleParam);
+        }
+      }
+    }
+
+    private Optional<RuleDefinitionDto> getDefinition(RuleKey ruleKey) {
+      return ruleFinder.findDtoByKey(requireNonNull(ruleKey, "RuleKey can't be null"));
+    }
+
+    private Set<RuleParamDto> getRuleParams(RuleKey ruleKey) {
+      return params.getOrDefault(requireNonNull(ruleKey, "RuleKey can't be null"), emptySet());
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileLoader.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileLoader.java
new file mode 100644 (file)
index 0000000..f6d1215
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import org.picocontainer.Startable;
+
+/**
+ * Startable added to {@link org.sonar.server.platform.platformlevel.PlatformLevelStartup} responsible for initializing
+ * {@link BuiltInQProfileRepository}.
+ */
+public class BuiltInQProfileLoader implements Startable {
+  private final BuiltInQProfileRepository builtInQProfileRepository;
+
+  public BuiltInQProfileLoader(BuiltInQProfileRepository builtInQProfileRepository) {
+    this.builtInQProfileRepository = builtInQProfileRepository;
+  }
+
+  @Override
+  public void start() {
+    builtInQProfileRepository.initialize();
+  }
+
+  @Override
+  public void stop() {
+    // nothing to do
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepository.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepository.java
new file mode 100644 (file)
index 0000000..f937724
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.List;
+
+public interface BuiltInQProfileRepository {
+  /**
+   * Initializes the Repository.
+   *
+   * This method is intended to be called from a startup task
+   * (see {@link org.sonar.server.platform.platformlevel.PlatformLevelStartup}).
+   *
+   * @throws IllegalStateException if called more then once
+   */
+  void initialize();
+
+  /**
+   * @return an immutable list
+   *
+   * @throws IllegalStateException if {@link #initialize()} has not been called
+   */
+  List<BuiltInQProfile> get();
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java
new file mode 100644 (file)
index 0000000..0da3b25
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.sonar.api.profiles.RulesProfile;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.BuiltInQualityProfile;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.rule.ServerRuleFinder;
+
+import static com.google.common.base.Preconditions.checkState;
+
+public class BuiltInQProfileRepositoryImpl implements BuiltInQProfileRepository {
+  private static final Logger LOGGER = Loggers.get(BuiltInQProfileRepositoryImpl.class);
+  private static final String DEFAULT_PROFILE_NAME = "Sonar way";
+
+  private final DbClient dbClient;
+  private final ServerRuleFinder ruleFinder;
+  private final Languages languages;
+  private final List<BuiltInQualityProfilesDefinition> definitions;
+  private List<BuiltInQProfile> qProfiles;
+
+  /**
+   * Requires for pico container when no {@link BuiltInQualityProfilesDefinition} is defined at all
+   */
+  public BuiltInQProfileRepositoryImpl(DbClient dbClient, ServerRuleFinder ruleFinder, Languages languages) {
+    this(dbClient, ruleFinder, languages, new BuiltInQualityProfilesDefinition[0]);
+  }
+
+  public BuiltInQProfileRepositoryImpl(DbClient dbClient, ServerRuleFinder ruleFinder, Languages languages, BuiltInQualityProfilesDefinition... definitions) {
+    this.dbClient = dbClient;
+    this.ruleFinder = ruleFinder;
+    this.languages = languages;
+    this.definitions = ImmutableList.copyOf(definitions);
+  }
+
+  @Override
+  public void initialize() {
+    checkState(qProfiles == null, "initialize must be called only once");
+
+    Profiler profiler = Profiler.create(LOGGER).startInfo("Load quality profiles");
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    for (BuiltInQualityProfilesDefinition definition : definitions) {
+      definition.define(context);
+    }
+    Map<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguage = validateAndClean(context);
+    this.qProfiles = toFlatList(rulesProfilesByLanguage);
+    ensureAllLanguagesHaveAtLeastOneBuiltInQP();
+    profiler.stopDebug();
+  }
+
+  @Override
+  public List<BuiltInQProfile> get() {
+    checkState(qProfiles != null, "initialize must be called first");
+
+    return qProfiles;
+  }
+
+  private void ensureAllLanguagesHaveAtLeastOneBuiltInQP() {
+    Set<String> languagesWithBuiltInQProfiles = qProfiles.stream().map(BuiltInQProfile::getLanguage).collect(Collectors.toSet());
+    Set<String> languagesWithoutBuiltInQProfiles = Arrays.stream(languages.all())
+      .map(Language::getKey)
+      .filter(key -> !languagesWithBuiltInQProfiles.contains(key))
+      .collect(Collectors.toSet());
+
+    checkState(languagesWithoutBuiltInQProfiles.isEmpty(), "The following languages have no built-in quality profiles: %s",
+      String.join("", languagesWithoutBuiltInQProfiles));
+  }
+
+  private Map<String, Map<String, BuiltInQualityProfile>> validateAndClean(BuiltInQualityProfilesDefinition.Context context) {
+    Map<String, Map<String, BuiltInQualityProfile>> profilesByLanguageAndName = context.profilesByLanguageAndName();
+    profilesByLanguageAndName.entrySet()
+      .removeIf(entry -> {
+        String language = entry.getKey();
+        if (languages.get(language) == null) {
+          LOGGER.info("Language {} is not installed, related quality profiles are ignored", language);
+          return true;
+        }
+        return false;
+      });
+
+    return profilesByLanguageAndName;
+  }
+
+  private List<BuiltInQProfile> toFlatList(Map<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguage) {
+    if (rulesProfilesByLanguage.isEmpty()) {
+      return Collections.emptyList();
+    }
+    Map<RuleKey, RuleDefinitionDto> rulesByRuleKey = loadRuleDefinitionsByRuleKey();
+    Map<String, List<BuiltInQProfile.Builder>> buildersByLanguage = rulesProfilesByLanguage
+      .entrySet()
+      .stream()
+      .collect(MoreCollectors.uniqueIndex(
+        Map.Entry::getKey,
+        rulesProfilesByLanguageAndName -> toQualityProfileBuilders(rulesProfilesByLanguageAndName, rulesByRuleKey)));
+    return buildersByLanguage
+      .entrySet()
+      .stream()
+      .filter(BuiltInQProfileRepositoryImpl::ensureAtMostOneDeclaredDefault)
+      .map(entry -> toQualityProfiles(entry.getValue()))
+      .flatMap(Collection::stream)
+      .collect(MoreCollectors.toList());
+  }
+
+  private Map<RuleKey, RuleDefinitionDto> loadRuleDefinitionsByRuleKey() {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      Collection<RuleDefinitionDto> ruleDefinitions = ruleFinder.findAll();
+      Multimap<String, DeprecatedRuleKeyDto> deprecatedRuleKeysByRuleId = dbClient.ruleDao().selectAllDeprecatedRuleKeys(dbSession).stream()
+        .collect(MoreCollectors.index(DeprecatedRuleKeyDto::getRuleUuid));
+      Map<RuleKey, RuleDefinitionDto> rulesByRuleKey = new HashMap<>();
+      for (RuleDefinitionDto ruleDefinition : ruleDefinitions) {
+        rulesByRuleKey.put(ruleDefinition.getKey(), ruleDefinition);
+        deprecatedRuleKeysByRuleId.get(ruleDefinition.getUuid()).forEach(t -> rulesByRuleKey.put(RuleKey.of(t.getOldRepositoryKey(), t.getOldRuleKey()), ruleDefinition));
+      }
+      return rulesByRuleKey;
+    }
+  }
+
+  /**
+   * Creates {@link BuiltInQProfile.Builder} for each unique quality profile name for a given language.
+   * Builders will have the following properties populated:
+   * <ul>
+   *   <li>{@link BuiltInQProfile.Builder#language language}: key of the method's parameter</li>
+   *   <li>{@link BuiltInQProfile.Builder#name name}: {@link RulesProfile#getName()}</li>
+   *   <li>{@link BuiltInQProfile.Builder#declaredDefault declaredDefault}: {@code true} if at least one RulesProfile
+   *       with a given name has {@link RulesProfile#getDefaultProfile()} is {@code true}</li>
+   *   <li>{@link BuiltInQProfile.Builder#activeRules activeRules}: the concatenate of the active rules of all
+   *       RulesProfile with a given name</li>
+   * </ul>
+   */
+  private static List<BuiltInQProfile.Builder> toQualityProfileBuilders(Map.Entry<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguageAndName,
+    Map<RuleKey, RuleDefinitionDto> rulesByRuleKey) {
+    String language = rulesProfilesByLanguageAndName.getKey();
+    // use a LinkedHashMap to keep order of insertion of RulesProfiles
+    Map<String, BuiltInQProfile.Builder> qualityProfileBuildersByName = new LinkedHashMap<>();
+    for (BuiltInQualityProfile builtInProfile : rulesProfilesByLanguageAndName.getValue().values()) {
+      qualityProfileBuildersByName.compute(
+        builtInProfile.name(),
+        (name, existingBuilder) -> updateOrCreateBuilder(language, existingBuilder, builtInProfile, rulesByRuleKey));
+    }
+    return ImmutableList.copyOf(qualityProfileBuildersByName.values());
+  }
+
+  /**
+   * Fails if more than one {@link BuiltInQProfile.Builder#declaredDefault} is {@code true}, otherwise returns {@code true}.
+   */
+  private static boolean ensureAtMostOneDeclaredDefault(Map.Entry<String, List<BuiltInQProfile.Builder>> entry) {
+    Set<String> declaredDefaultProfileNames = entry.getValue().stream()
+      .filter(BuiltInQProfile.Builder::isDeclaredDefault)
+      .map(BuiltInQProfile.Builder::getName)
+      .collect(MoreCollectors.toSet());
+    checkState(declaredDefaultProfileNames.size() <= 1, "Several Quality profiles are flagged as default for the language %s: %s", entry.getKey(), declaredDefaultProfileNames);
+    return true;
+  }
+
+  private static BuiltInQProfile.Builder updateOrCreateBuilder(String language, @Nullable BuiltInQProfile.Builder existingBuilder, BuiltInQualityProfile builtInProfile,
+    Map<RuleKey, RuleDefinitionDto> rulesByRuleKey) {
+    BuiltInQProfile.Builder builder = createOrReuseBuilder(existingBuilder, language, builtInProfile);
+    builder.setDeclaredDefault(builtInProfile.isDefault());
+    builtInProfile.rules().forEach(builtInActiveRule -> {
+      RuleKey ruleKey = RuleKey.of(builtInActiveRule.repoKey(), builtInActiveRule.ruleKey());
+      RuleDefinitionDto ruleDefinition = rulesByRuleKey.get(ruleKey);
+      checkState(ruleDefinition != null, "Rule with key '%s' not found", ruleKey);
+      builder.addRule(new BuiltInQProfile.ActiveRule(ruleDefinition.getUuid(), ruleDefinition.getKey(),
+        builtInActiveRule.overriddenSeverity(), builtInActiveRule.overriddenParams()));
+    });
+    return builder;
+  }
+
+  private static BuiltInQProfile.Builder createOrReuseBuilder(@Nullable BuiltInQProfile.Builder existingBuilder, String language, BuiltInQualityProfile builtInProfile) {
+    if (existingBuilder == null) {
+      return new BuiltInQProfile.Builder()
+        .setLanguage(language)
+        .setName(builtInProfile.name());
+    }
+    return existingBuilder;
+  }
+
+  private static List<BuiltInQProfile> toQualityProfiles(List<BuiltInQProfile.Builder> builders) {
+    if (builders.stream().noneMatch(BuiltInQProfile.Builder::isDeclaredDefault)) {
+      Optional<BuiltInQProfile.Builder> sonarWayProfile = builders.stream().filter(builder -> builder.getName().equals(DEFAULT_PROFILE_NAME)).findFirst();
+      if (sonarWayProfile.isPresent()) {
+        sonarWayProfile.get().setComputedDefault(true);
+      } else {
+        builders.iterator().next().setComputedDefault(true);
+      }
+    }
+    return builders.stream()
+      .map(BuiltInQProfile.Builder::build)
+      .collect(MoreCollectors.toList(builders.size()));
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdate.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdate.java
new file mode 100644 (file)
index 0000000..3f97544
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.List;
+import org.sonar.db.DbSession;
+import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+
+public interface BuiltInQProfileUpdate {
+  /**
+   * Persist an existing built-in profile.
+   * Db session is committed and Elasticsearch indices are updated.
+   */
+  List<ActiveRuleChange> update(DbSession dbSession, BuiltInQProfile builtInQProfile, RulesProfileDto ruleProfile);
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdateImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdateImpl.java
new file mode 100644 (file)
index 0000000..7e1e90e
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.RuleActivation;
+import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
+
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+
+public class BuiltInQProfileUpdateImpl implements BuiltInQProfileUpdate {
+
+  private final DbClient dbClient;
+  private final RuleActivator ruleActivator;
+  private final ActiveRuleIndexer activeRuleIndexer;
+  private final QualityProfileChangeEventService qualityProfileChangeEventService;
+
+  public BuiltInQProfileUpdateImpl(DbClient dbClient, RuleActivator ruleActivator, ActiveRuleIndexer activeRuleIndexer,
+    QualityProfileChangeEventService qualityProfileChangeEventService) {
+    this.dbClient = dbClient;
+    this.ruleActivator = ruleActivator;
+    this.activeRuleIndexer = activeRuleIndexer;
+    this.qualityProfileChangeEventService = qualityProfileChangeEventService;
+  }
+
+  public List<ActiveRuleChange> update(DbSession dbSession, BuiltInQProfile builtInDefinition, RulesProfileDto initialRuleProfile) {
+    // Keep reference to all the activated rules before update
+    Set<String> deactivatedRuleUuids = dbClient.activeRuleDao().selectByRuleProfile(dbSession, initialRuleProfile)
+      .stream()
+      .map(ActiveRuleDto::getRuleUuid)
+      .collect(MoreCollectors.toHashSet());
+
+    // all rules, including those which are removed from built-in profile
+    Set<String> ruleUuids = Stream.concat(
+        deactivatedRuleUuids.stream(),
+        builtInDefinition.getActiveRules().stream().map(BuiltInQProfile.ActiveRule::getRuleUuid))
+      .collect(toSet());
+
+    Collection<RuleActivation> activations = new ArrayList<>();
+    for (BuiltInQProfile.ActiveRule ar : builtInDefinition.getActiveRules()) {
+      RuleActivation activation = convert(ar);
+      activations.add(activation);
+      deactivatedRuleUuids.remove(activation.getRuleUuid());
+    }
+
+    RuleActivationContext context = ruleActivator.createContextForBuiltInProfile(dbSession, initialRuleProfile, ruleUuids);
+    List<ActiveRuleChange> changes = new ArrayList<>();
+
+    changes.addAll(ruleActivator.activate(dbSession, activations, context));
+
+    // these rules are no longer part of the built-in profile
+    deactivatedRuleUuids.forEach(ruleUuid -> changes.addAll(ruleActivator.deactivate(dbSession, context, ruleUuid, false)));
+
+    if (!changes.isEmpty()) {
+      qualityProfileChangeEventService.distributeRuleChangeEvent(context.getProfiles(), changes, initialRuleProfile.getLanguage());
+    }
+
+    activeRuleIndexer.commitAndIndex(dbSession, changes);
+    return changes;
+  }
+
+  private static RuleActivation convert(BuiltInQProfile.ActiveRule ar) {
+    Map<String, String> params = ar.getParams().stream()
+      .collect(MoreCollectors.uniqueIndex(BuiltInQualityProfilesDefinition.OverriddenParam::key, BuiltInQualityProfilesDefinition.OverriddenParam::overriddenValue));
+    return RuleActivation.create(ar.getRuleUuid(), ar.getSeverity(), params);
+  }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQualityProfilesUpdateListener.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQualityProfilesUpdateListener.java
new file mode 100644 (file)
index 0000000..098e217
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.collect.Multimap;
+import java.util.Collection;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.builtin.BuiltInQPChangeNotificationBuilder.Profile;
+
+import static org.sonar.core.config.CorePropertyDefinitions.DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED;
+
+public class BuiltInQualityProfilesUpdateListener {
+
+  private final NotificationManager notificationManager;
+  private final Languages languages;
+  private final Configuration config;
+
+  public BuiltInQualityProfilesUpdateListener(NotificationManager notificationManager, Languages languages, Configuration config) {
+    this.notificationManager = notificationManager;
+    this.languages = languages;
+    this.config = config;
+  }
+
+  public void onChange(Multimap<QProfileName, ActiveRuleChange> changedProfiles, long startDate, long endDate) {
+    if (config.getBoolean(DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES).orElse(false)) {
+      return;
+    }
+
+    BuiltInQPChangeNotificationBuilder builder = new BuiltInQPChangeNotificationBuilder();
+    changedProfiles.keySet().stream()
+      .map(changedProfile -> {
+        String profileName = changedProfile.getName();
+        Language language = languages.get(changedProfile.getLanguage());
+        Collection<ActiveRuleChange> activeRuleChanges = changedProfiles.get(changedProfile);
+        int newRules = (int) activeRuleChanges.stream().map(ActiveRuleChange::getType).filter(ACTIVATED::equals).count();
+        int updatedRules = (int) activeRuleChanges.stream().map(ActiveRuleChange::getType).filter(UPDATED::equals).count();
+        int removedRules = (int) activeRuleChanges.stream().map(ActiveRuleChange::getType).filter(DEACTIVATED::equals).count();
+        return Profile.newBuilder()
+          .setProfileName(profileName)
+          .setLanguageKey(language.getKey())
+          .setLanguageName(language.getName())
+          .setNewRules(newRules)
+          .setUpdatedRules(updatedRules)
+          .setRemovedRules(removedRules)
+          .setStartDate(startDate)
+          .setEndDate(endDate)
+          .build();
+      })
+      .forEach(builder::addProfile);
+
+    notificationManager.scheduleForSending(builder.build());
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/DescendantProfilesSupplier.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/DescendantProfilesSupplier.java
new file mode 100644 (file)
index 0000000..862c9f7
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.Collection;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+
+@FunctionalInterface
+public interface DescendantProfilesSupplier {
+
+  Result get(Collection<QProfileDto> profiles, Collection<String> ruleUuids);
+
+  final class Result {
+    private final Collection<QProfileDto> profiles;
+    private final Collection<ActiveRuleDto> activeRules;
+    private final Collection<ActiveRuleParamDto> activeRuleParams;
+
+    public Result(Collection<QProfileDto> profiles, Collection<ActiveRuleDto> activeRules, Collection<ActiveRuleParamDto> activeRuleParams) {
+      this.profiles = profiles;
+      this.activeRules = activeRules;
+      this.activeRuleParams = activeRuleParams;
+    }
+
+    public Collection<QProfileDto> getProfiles() {
+      return profiles;
+    }
+
+    public Collection<ActiveRuleDto> getActiveRules() {
+      return activeRules;
+    }
+
+    public Collection<ActiveRuleParamDto> getActiveRuleParams() {
+      return activeRuleParams;
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/QProfileName.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/QProfileName.java
new file mode 100644 (file)
index 0000000..bc59503
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import javax.annotation.Nullable;
+
+public class QProfileName {
+  private final String lang;
+  private final String name;
+
+  public QProfileName(String lang, String name) {
+    this.lang = lang;
+    this.name = name;
+  }
+
+  public String getLanguage() {
+    return lang;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public static QProfileName createFor(String lang, String name){
+    return new QProfileName(lang, name);
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    QProfileName that = (QProfileName) o;
+    if (!lang.equals(that.lang)) {
+      return false;
+    }
+    return name.equals(that.name);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = lang.hashCode();
+    result = 31 * result + name.hashCode();
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return String.format("%s/%s", lang, name);
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/RuleActivationContext.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/RuleActivationContext.java
new file mode 100644 (file)
index 0000000..c1aacdd
--- /dev/null
@@ -0,0 +1,367 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleKey;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.qualityprofile.RuleActivation;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.core.util.stream.MoreCollectors.index;
+import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+/**
+ * Cache of the data required to activate/deactivate
+ * multiple rules on a Quality profile, including
+ * the rule definitions, the rule parameters, the tree
+ * of profiles hierarchy and its related active rules.
+ */
+public class RuleActivationContext {
+
+  private final long date;
+
+  // The profile that is initially targeted by the operation
+  private final RulesProfileDto baseRulesProfile;
+
+  private final Map<String, QProfileDto> profilesByUuid = new HashMap<>();
+  private final ListMultimap<String, QProfileDto> profilesByParentUuid = ArrayListMultimap.create();
+
+  // The rules/active rules involved in the group of activations/de-activations
+  private final Map<String, RuleWrapper> rulesByUuid = new HashMap<>();
+  private final Map<ActiveRuleKey, ActiveRuleWrapper> activeRulesByKey = new HashMap<>();
+
+  // Cursors used to move in the rules and in the tree of profiles.
+
+  private RulesProfileDto currentRulesProfile;
+  // Cardinality is zero-to-many when cursor is on a built-in rules profile,
+  // otherwise it's always one, and only one (cursor on descendants or on non-built-in base profile).
+  private Collection<QProfileDto> currentProfiles;
+  private RuleWrapper currentRule;
+  private ActiveRuleWrapper currentActiveRule;
+  private ActiveRuleWrapper currentParentActiveRule;
+
+  private boolean descendantsLoaded = false;
+  private final DescendantProfilesSupplier descendantProfilesSupplier;
+
+  private RuleActivationContext(Builder builder) {
+    this.date = builder.date;
+    this.descendantProfilesSupplier = builder.descendantProfilesSupplier;
+
+    ListMultimap<String, RuleParamDto> paramsByRuleId = builder.ruleParams.stream().collect(index(RuleParamDto::getRuleUuid));
+    for (RuleDefinitionDto rule : builder.rules) {
+      RuleWrapper wrapper = new RuleWrapper(rule, paramsByRuleId.get(rule.getUuid()));
+      rulesByUuid.put(rule.getUuid(), wrapper);
+    }
+
+    this.baseRulesProfile = builder.baseRulesProfile;
+    register(builder.profiles);
+    register(builder.activeRules, builder.activeRuleParams);
+  }
+
+  private void register(Collection<QProfileDto> profiles) {
+    for (QProfileDto profile : profiles) {
+      profilesByUuid.put(profile.getKee(), profile);
+      if (profile.getParentKee() != null) {
+        profilesByParentUuid.put(profile.getParentKee(), profile);
+      }
+    }
+  }
+
+  private void register(Collection<ActiveRuleDto> activeRules, Collection<ActiveRuleParamDto> activeRuleParams) {
+    ListMultimap<String, ActiveRuleParamDto> paramsByActiveRuleUuid = activeRuleParams.stream().collect(index(ActiveRuleParamDto::getActiveRuleUuid));
+    for (ActiveRuleDto activeRule : activeRules) {
+      ActiveRuleWrapper wrapper = new ActiveRuleWrapper(activeRule, paramsByActiveRuleUuid.get(activeRule.getUuid()));
+      this.activeRulesByKey.put(activeRule.getKey(), wrapper);
+    }
+  }
+
+  long getDate() {
+    return date;
+  }
+
+  /**
+   * The rule currently selected.
+   */
+  public RuleWrapper getRule() {
+    checkState(currentRule != null, "Rule has not been set yet");
+    return currentRule;
+  }
+
+  @CheckForNull
+  String getRequestedParamValue(RuleActivation request, String key) {
+    if (currentRule.rule.isCustomRule()) {
+      return null;
+    }
+    return request.getParameter(key);
+  }
+
+  boolean hasRequestedParamValue(RuleActivation request, String key) {
+    return request.hasParameter(key);
+  }
+
+  /**
+   * The rules profile being selected.
+   */
+  RulesProfileDto getRulesProfile() {
+    checkState(currentRulesProfile != null, "Rule profile has not been set yet");
+    return currentRulesProfile;
+  }
+
+  /**
+   * The active rule related to the selected profile and rule.
+   * @return null if the selected rule is not activated on the selected profile.
+   * @see #getRulesProfile()
+   * @see #getRule()
+   */
+  @CheckForNull
+  ActiveRuleWrapper getActiveRule() {
+    return currentActiveRule;
+  }
+
+  /**
+   * The active rule related to the rule and the parent of the selected profile.
+   * @return null if the selected rule is not activated on the parent profile.
+   * @see #getRule()
+   */
+  @CheckForNull
+  ActiveRuleWrapper getParentActiveRule() {
+    return currentParentActiveRule;
+  }
+
+  /**
+   * Whether the profile cursor is on the base profile or not.
+   */
+  boolean isCascading() {
+    return currentRulesProfile != null && !currentRulesProfile.getUuid().equals(baseRulesProfile.getUuid());
+  }
+
+  /**
+   * The profiles being selected. Can be zero or many if {@link #getRulesProfile()} is built-in.
+   * Else the collection always contains a single profile.
+   */
+  Collection<QProfileDto> getProfiles() {
+    checkState(currentProfiles != null, "Profiles have not been set yet");
+    return currentProfiles;
+  }
+
+  /**
+   * The children of {@link #getProfiles()}
+   */
+  Collection<QProfileDto> getChildProfiles() {
+    loadDescendants();
+    return getProfiles().stream()
+      .flatMap(p -> profilesByParentUuid.get(p.getKee()).stream())
+      .collect(Collectors.toList());
+  }
+
+  private void loadDescendants() {
+    if (descendantsLoaded) {
+      return;
+    }
+    Collection<QProfileDto> baseProfiles = profilesByUuid.values().stream()
+      .filter(p -> p.getRulesProfileUuid().equals(baseRulesProfile.getUuid()))
+      .collect(toArrayList(profilesByUuid.size()));
+    DescendantProfilesSupplier.Result result = descendantProfilesSupplier.get(baseProfiles, rulesByUuid.keySet());
+    register(result.getProfiles());
+    register(result.getActiveRules(), result.getActiveRuleParams());
+    descendantsLoaded = true;
+  }
+
+  /**
+   * Move the cursor to the given rule and back to the base profile.
+   */
+  public void reset(String ruleUuid) {
+    doSwitch(this.baseRulesProfile, ruleUuid);
+  }
+
+  /**
+   * Moves cursor to a child profile
+   */
+  void selectChild(QProfileDto to) {
+    checkState(!to.isBuiltIn());
+    QProfileDto qp = requireNonNull(this.profilesByUuid.get(to.getKee()), () -> "No profile with uuid " + to.getKee());
+
+    RulesProfileDto ruleProfile = RulesProfileDto.from(qp);
+    doSwitch(ruleProfile, getRule().get().getUuid());
+  }
+
+  private void doSwitch(RulesProfileDto ruleProfile, String ruleUuid) {
+    this.currentRule = rulesByUuid.get(ruleUuid);
+    checkRequest(this.currentRule != null, "Rule with UUID %s not found", ruleUuid);
+    RuleKey ruleKey = currentRule.get().getKey();
+
+    this.currentRulesProfile = ruleProfile;
+    this.currentProfiles = profilesByUuid.values().stream()
+      .filter(p -> p.getRulesProfileUuid().equals(ruleProfile.getUuid()))
+      .collect(Collectors.toList());
+    this.currentActiveRule = this.activeRulesByKey.get(ActiveRuleKey.of(ruleProfile, ruleKey));
+    this.currentParentActiveRule = this.currentProfiles.stream()
+      .map(QProfileDto::getParentKee)
+      .filter(Objects::nonNull)
+      .map(profilesByUuid::get)
+      .filter(Objects::nonNull)
+      .findFirst()
+      .map(profile -> activeRulesByKey.get(ActiveRuleKey.of(profile, ruleKey)))
+      .orElse(null);
+  }
+
+  static final class Builder {
+    private long date = System.currentTimeMillis();
+    private RulesProfileDto baseRulesProfile;
+    private Collection<RuleDefinitionDto> rules;
+    private Collection<RuleParamDto> ruleParams;
+    private Collection<QProfileDto> profiles;
+    private Collection<ActiveRuleDto> activeRules;
+    private Collection<ActiveRuleParamDto> activeRuleParams;
+    private DescendantProfilesSupplier descendantProfilesSupplier;
+
+    Builder setDate(long l) {
+      this.date = l;
+      return this;
+    }
+
+    Builder setBaseProfile(RulesProfileDto p) {
+      this.baseRulesProfile = p;
+      return this;
+    }
+
+    Builder setRules(Collection<RuleDefinitionDto> rules) {
+      this.rules = rules;
+      return this;
+    }
+
+    Builder setRuleParams(Collection<RuleParamDto> ruleParams) {
+      this.ruleParams = ruleParams;
+      return this;
+    }
+
+    /**
+     * All the profiles involved in the activation workflow, including the
+     * parent profile, even if it's not updated.
+     */
+    Builder setProfiles(Collection<QProfileDto> profiles) {
+      this.profiles = profiles;
+      return this;
+    }
+
+    Builder setActiveRules(Collection<ActiveRuleDto> activeRules) {
+      this.activeRules = activeRules;
+      return this;
+    }
+
+    Builder setActiveRuleParams(Collection<ActiveRuleParamDto> activeRuleParams) {
+      this.activeRuleParams = activeRuleParams;
+      return this;
+    }
+
+    Builder setDescendantProfilesSupplier(DescendantProfilesSupplier d) {
+      this.descendantProfilesSupplier = d;
+      return this;
+    }
+
+    RuleActivationContext build() {
+      checkArgument(date > 0, "date is not set");
+      requireNonNull(baseRulesProfile, "baseRulesProfile is null");
+      requireNonNull(rules, "rules is null");
+      requireNonNull(ruleParams, "ruleParams is null");
+      requireNonNull(profiles, "profiles is null");
+      requireNonNull(activeRules, "activeRules is null");
+      requireNonNull(activeRuleParams, "activeRuleParams is null");
+      requireNonNull(descendantProfilesSupplier, "descendantProfilesSupplier is null");
+      return new RuleActivationContext(this);
+    }
+  }
+
+  public static final class RuleWrapper {
+    private final RuleDefinitionDto rule;
+    private final Map<String, RuleParamDto> paramsByKey;
+
+    private RuleWrapper(RuleDefinitionDto rule, Collection<RuleParamDto> params) {
+      this.rule = rule;
+      this.paramsByKey = params.stream().collect(uniqueIndex(RuleParamDto::getName));
+    }
+
+    public RuleDefinitionDto get() {
+      return rule;
+    }
+
+    Collection<RuleParamDto> getParams() {
+      return paramsByKey.values();
+    }
+
+    @CheckForNull
+    RuleParamDto getParam(String key) {
+      return paramsByKey.get(key);
+    }
+
+    @CheckForNull
+    String getParamDefaultValue(String key) {
+      RuleParamDto param = getParam(key);
+      return param != null ? param.getDefaultValue() : null;
+    }
+  }
+
+  static final class ActiveRuleWrapper {
+    private final ActiveRuleDto activeRule;
+    private final Map<String, ActiveRuleParamDto> paramsByKey;
+
+    private ActiveRuleWrapper(ActiveRuleDto activeRule, Collection<ActiveRuleParamDto> params) {
+      this.activeRule = activeRule;
+      this.paramsByKey = params.stream().collect(uniqueIndex(ActiveRuleParamDto::getKey));
+    }
+
+    ActiveRuleDto get() {
+      return activeRule;
+    }
+
+    Collection<ActiveRuleParamDto> getParams() {
+      return paramsByKey.values();
+    }
+
+    @CheckForNull
+    ActiveRuleParamDto getParam(String key) {
+      return paramsByKey.get(key);
+    }
+
+    @CheckForNull
+    String getParamValue(String key) {
+      ActiveRuleParamDto param = paramsByKey.get(key);
+      return param != null ? param.getValue() : null;
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/RuleActivator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/RuleActivator.java
new file mode 100644 (file)
index 0000000..b6da63d
--- /dev/null
@@ -0,0 +1,548 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.base.Splitter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.server.rule.RuleParamType;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.qualityprofile.ActiveRuleDao;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleKey;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.OrgQProfileDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.ActiveRuleInheritance;
+import org.sonar.server.qualityprofile.RuleActivation;
+import org.sonar.server.qualityprofile.builtin.RuleActivationContext.ActiveRuleWrapper;
+import org.sonar.server.qualityprofile.builtin.RuleActivationContext.RuleWrapper;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.util.TypeValidations;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.stream.Collectors.toList;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+/**
+ * Activation and deactivation of rules in Quality profiles
+ */
+@ServerSide
+public class RuleActivator {
+
+  private final System2 system2;
+  private final DbClient db;
+  private final TypeValidations typeValidations;
+  private final UserSession userSession;
+
+  public RuleActivator(System2 system2, DbClient db, TypeValidations typeValidations, UserSession userSession) {
+    this.system2 = system2;
+    this.db = db;
+    this.typeValidations = typeValidations;
+    this.userSession = userSession;
+  }
+
+
+  public List<ActiveRuleChange> activate(DbSession dbSession, Collection<RuleActivation> activations, RuleActivationContext context) {
+    return activations.stream().map(a -> activate(dbSession, a, context))
+      .flatMap(List::stream)
+      .collect(toList());
+  }
+
+  public List<ActiveRuleChange> activate(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
+    context.reset(activation.getRuleUuid());
+    return doActivate(dbSession, activation, context);
+  }
+
+  private List<ActiveRuleChange> doActivate(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
+    RuleDefinitionDto rule = context.getRule().get();
+    checkRequest(RuleStatus.REMOVED != rule.getStatus(), "Rule was removed: %s", rule.getKey());
+    checkRequest(!rule.isTemplate(), "Rule template can't be activated on a Quality profile: %s", rule.getKey());
+    checkRequest(context.getRulesProfile().getLanguage().equals(rule.getLanguage()),
+      "%s rule %s cannot be activated on %s profile %s", rule.getLanguage(), rule.getKey(), context.getRulesProfile().getLanguage(), context.getRulesProfile().getName());
+    List<ActiveRuleChange> changes = new ArrayList<>();
+    ActiveRuleChange change;
+    boolean stopCascading = false;
+
+    ActiveRuleWrapper activeRule = context.getActiveRule();
+    ActiveRuleKey activeRuleKey = ActiveRuleKey.of(context.getRulesProfile(), rule.getKey());
+    if (activeRule == null) {
+      if (activation.isReset()) {
+        // ignore reset when rule is not activated
+        return changes;
+      }
+      change = handleNewRuleActivation(activation, context, rule, activeRuleKey);
+    } else {
+      // already activated
+      if (context.isCascading() && activeRule.get().doesOverride()) {
+        // propagating to descendants, but child profile already overrides rule -> stop propagation
+        return changes;
+      }
+      change = new ActiveRuleChange(ActiveRuleChange.Type.UPDATED, activeRuleKey, rule);
+      stopCascading = handleUpdatedRuleActivation(activation, context, change, stopCascading, activeRule);
+
+      if (isSame(change, activeRule)) {
+        change = null;
+        stopCascading = true;
+      }
+    }
+
+    if (change != null) {
+      changes.add(change);
+      persist(change, context, dbSession);
+    }
+
+    if (!changes.isEmpty()) {
+      updateProfileDates(dbSession, context);
+    }
+
+    if (!stopCascading) {
+      changes.addAll(propagateActivationToDescendants(dbSession, activation, context));
+    }
+
+    return changes;
+  }
+
+  private boolean handleUpdatedRuleActivation(RuleActivation activation, RuleActivationContext context, ActiveRuleChange change,
+    boolean stopCascading, ActiveRuleWrapper activeRule) {
+    if (context.isCascading() && activeRule.get().getInheritance() == null) {
+      // activate on child, then on parent -> mark child as overriding parent
+      change.setInheritance(ActiveRuleInheritance.OVERRIDES);
+      change.setSeverity(activeRule.get().getSeverityString());
+      for (ActiveRuleParamDto activeParam : activeRule.getParams()) {
+        change.setParameter(activeParam.getKey(), activeParam.getValue());
+      }
+      stopCascading = true;
+    } else {
+      applySeverityAndParamToChange(activation, context, change);
+      if (!context.isCascading() && context.getParentActiveRule() != null) {
+        // override rule which is already declared on parents
+        change.setInheritance(isSameAsParent(change, context) ? ActiveRuleInheritance.INHERITED : ActiveRuleInheritance.OVERRIDES);
+      }
+    }
+    return stopCascading;
+  }
+
+  private ActiveRuleChange handleNewRuleActivation(RuleActivation activation, RuleActivationContext context, RuleDefinitionDto rule, ActiveRuleKey activeRuleKey) {
+    ActiveRuleChange change = new ActiveRuleChange(ActiveRuleChange.Type.ACTIVATED, activeRuleKey, rule);
+    applySeverityAndParamToChange(activation, context, change);
+    if (context.isCascading() || isSameAsParent(change, context)) {
+      change.setInheritance(ActiveRuleInheritance.INHERITED);
+    }
+    return change;
+  }
+
+  private void updateProfileDates(DbSession dbSession, RuleActivationContext context) {
+    RulesProfileDto ruleProfile = context.getRulesProfile();
+    ruleProfile.setRulesUpdatedAtAsDate(new Date(context.getDate()));
+    db.qualityProfileDao().update(dbSession, ruleProfile);
+
+    if (userSession.isLoggedIn()) {
+      context.getProfiles().forEach(p -> db.qualityProfileDao().update(dbSession, OrgQProfileDto.from(p).setUserUpdatedAt(context.getDate())));
+    }
+  }
+
+  /**
+   * Update severity and params
+   */
+  private void applySeverityAndParamToChange(RuleActivation request, RuleActivationContext context, ActiveRuleChange change) {
+    RuleWrapper rule = context.getRule();
+    ActiveRuleWrapper activeRule = context.getActiveRule();
+    ActiveRuleWrapper parentActiveRule = context.getParentActiveRule();
+
+    if (request.isReset()) {
+      applySeverityAndParamsWhenResetRequested(change, rule, parentActiveRule);
+    } else if (context.getRulesProfile().isBuiltIn()) {
+      applySeverityAndParamsWhenBuiltInProfile(request, context, change, rule);
+    } else {
+      applySeverityAndParamsWhenNonBuiltInProfile(request, context, change, rule, activeRule, parentActiveRule);
+    }
+  }
+
+  private void applySeverityAndParamsWhenResetRequested(ActiveRuleChange change, RuleWrapper rule, @Nullable ActiveRuleWrapper parentActiveRule) {
+    String severity = firstNonNull(
+      parentActiveRule != null ? parentActiveRule.get().getSeverityString() : null,
+      rule.get().getSeverityString());
+    change.setSeverity(severity);
+
+    for (RuleParamDto ruleParamDto : rule.getParams()) {
+      String paramKey = ruleParamDto.getName();
+      // load params from parent profile, else from default values
+      String paramValue = firstNonNull(
+        parentActiveRule != null ? parentActiveRule.getParamValue(paramKey) : null,
+        rule.getParamDefaultValue(paramKey));
+
+      change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
+    }
+  }
+
+  private void applySeverityAndParamsWhenBuiltInProfile(RuleActivation request, RuleActivationContext context, ActiveRuleChange change,
+    RuleWrapper rule) {
+    // for builtin quality profiles, the severity from profile, when null use the default severity of the rule
+    String severity = firstNonNull(request.getSeverity(), rule.get().getSeverityString());
+    change.setSeverity(severity);
+
+    for (RuleParamDto ruleParamDto : rule.getParams()) {
+      String paramKey = ruleParamDto.getName();
+      // use the value defined in the profile definition, else the rule default value
+      String paramValue = firstNonNull(
+        context.getRequestedParamValue(request, paramKey),
+        rule.getParamDefaultValue(paramKey));
+      change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
+    }
+  }
+
+  /**
+   * 1. apply requested severity and param
+   * 2. if rule activated and overridden - apply user value
+   * 3. apply parent value
+   * 4. apply defaults
+   */
+  private void applySeverityAndParamsWhenNonBuiltInProfile(RuleActivation request, RuleActivationContext context, ActiveRuleChange change,
+    RuleWrapper rule, @Nullable ActiveRuleWrapper activeRule, @Nullable ActiveRuleWrapper parentActiveRule) {
+    String severity = getSeverityForNonBuiltInProfile(request, rule, activeRule, parentActiveRule);
+    change.setSeverity(severity);
+
+    for (RuleParamDto ruleParamDto : rule.getParams()) {
+      String paramKey = ruleParamDto.getName();
+      String parentValue = parentActiveRule != null ? parentActiveRule.getParamValue(paramKey) : null;
+      String paramValue;
+      if (context.hasRequestedParamValue(request, paramKey)) {
+        // If the request contains the parameter then we're using either value from request, or parent value, or default value
+        paramValue = firstNonNull(
+          context.getRequestedParamValue(request, paramKey),
+          parentValue,
+          rule.getParamDefaultValue(paramKey));
+      } else if (activeRule != null) {
+        // If the request doesn't contain the parameter, then we're using either user value from db, or parent value if rule inherited, or default
+        // value
+        paramValue = firstNonNull(
+          activeRule.get().doesOverride() ? activeRule.getParamValue(paramKey) : null,
+          parentValue == null ? activeRule.getParamValue(paramKey) : parentValue,
+          rule.getParamDefaultValue(paramKey));
+      } else {
+        paramValue = firstNonNull(
+          parentValue,
+          rule.getParamDefaultValue(paramKey));
+      }
+      change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
+    }
+  }
+
+  private static String getSeverityForNonBuiltInProfile(RuleActivation request, RuleWrapper rule, @Nullable ActiveRuleWrapper activeRule,
+    @Nullable ActiveRuleWrapper parentActiveRule) {
+    String severity;
+    if (activeRule != null) {
+      ActiveRuleDto activeRuleDto = activeRule.get();
+      // load severity from request, else keep existing one (if overridden), else from parent if rule inherited, else from default
+      severity = firstNonNull(
+        request.getSeverity(),
+        activeRuleDto.doesOverride() ? activeRuleDto.getSeverityString() : null,
+        parentActiveRule != null ? parentActiveRule.get().getSeverityString() : activeRuleDto.getSeverityString(),
+        rule.get().getSeverityString());
+    } else {
+      // load severity from request, else from parent, else from default
+      severity = firstNonNull(
+        request.getSeverity(),
+        parentActiveRule != null ? parentActiveRule.get().getSeverityString() : null,
+        rule.get().getSeverityString());
+    }
+    return severity;
+  }
+
+  private List<ActiveRuleChange> propagateActivationToDescendants(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
+    List<ActiveRuleChange> changes = new ArrayList<>();
+
+    // get all inherited profiles
+    context.getChildProfiles().forEach(child -> {
+      context.selectChild(child);
+      changes.addAll(doActivate(dbSession, activation, context));
+    });
+    return changes;
+  }
+
+  private void persist(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
+    ActiveRuleDto activeRule = null;
+    if (change.getType() == ActiveRuleChange.Type.ACTIVATED) {
+      activeRule = doInsert(change, context, dbSession);
+    } else if (change.getType() == ActiveRuleChange.Type.DEACTIVATED) {
+      ActiveRuleDao dao = db.activeRuleDao();
+      activeRule = dao.delete(dbSession, change.getKey()).orElse(null);
+
+    } else if (change.getType() == ActiveRuleChange.Type.UPDATED) {
+      activeRule = doUpdate(change, context, dbSession);
+    }
+    change.setActiveRule(activeRule);
+    db.qProfileChangeDao().insert(dbSession, change.toDto(userSession.getUuid()));
+  }
+
+  private ActiveRuleDto doInsert(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
+    ActiveRuleDao dao = db.activeRuleDao();
+    RuleWrapper rule = context.getRule();
+
+    ActiveRuleDto activeRule = new ActiveRuleDto();
+    activeRule.setProfileUuid(context.getRulesProfile().getUuid());
+    activeRule.setRuleUuid(rule.get().getUuid());
+    activeRule.setKey(ActiveRuleKey.of(context.getRulesProfile(), rule.get().getKey()));
+    String severity = change.getSeverity();
+    if (severity != null) {
+      activeRule.setSeverity(severity);
+    }
+    ActiveRuleInheritance inheritance = change.getInheritance();
+    if (inheritance != null) {
+      activeRule.setInheritance(inheritance.name());
+    }
+    activeRule.setUpdatedAt(system2.now());
+    activeRule.setCreatedAt(system2.now());
+    dao.insert(dbSession, activeRule);
+    for (Map.Entry<String, String> param : change.getParameters().entrySet()) {
+      if (param.getValue() != null) {
+        ActiveRuleParamDto paramDto = ActiveRuleParamDto.createFor(rule.getParam(param.getKey()));
+        paramDto.setValue(param.getValue());
+        dao.insertParam(dbSession, activeRule, paramDto);
+      }
+    }
+    return activeRule;
+  }
+
+  private ActiveRuleDto doUpdate(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
+    ActiveRuleWrapper activeRule = context.getActiveRule();
+    if (activeRule == null) {
+      return null;
+    }
+    ActiveRuleDao dao = db.activeRuleDao();
+    String severity = change.getSeverity();
+    if (severity != null) {
+      activeRule.get().setSeverity(severity);
+    }
+    ActiveRuleInheritance inheritance = change.getInheritance();
+    if (inheritance != null) {
+      activeRule.get().setInheritance(inheritance.name());
+    }
+    activeRule.get().setUpdatedAt(system2.now());
+    dao.update(dbSession, activeRule.get());
+
+    for (Map.Entry<String, String> param : change.getParameters().entrySet()) {
+      ActiveRuleParamDto activeRuleParamDto = activeRule.getParam(param.getKey());
+      if (activeRuleParamDto == null) {
+        // did not exist
+        if (param.getValue() != null) {
+          activeRuleParamDto = ActiveRuleParamDto.createFor(context.getRule().getParam(param.getKey()));
+          activeRuleParamDto.setValue(param.getValue());
+          dao.insertParam(dbSession, activeRule.get(), activeRuleParamDto);
+        }
+      } else {
+        if (param.getValue() != null) {
+          activeRuleParamDto.setValue(param.getValue());
+          dao.updateParam(dbSession, activeRuleParamDto);
+        } else {
+          dao.deleteParam(dbSession, activeRuleParamDto);
+        }
+      }
+    }
+    return activeRule.get();
+  }
+
+  public List<ActiveRuleChange> deactivate(DbSession dbSession, RuleActivationContext context, String ruleUuid, boolean force) {
+    context.reset(ruleUuid);
+    return doDeactivate(dbSession, context, force);
+  }
+
+  private List<ActiveRuleChange> doDeactivate(DbSession dbSession, RuleActivationContext context, boolean force) {
+    List<ActiveRuleChange> changes = new ArrayList<>();
+    ActiveRuleWrapper activeRule = context.getActiveRule();
+    if (activeRule == null) {
+      return changes;
+    }
+
+    ActiveRuleChange change;
+    checkRequest(force || context.isCascading() || activeRule.get().getInheritance() == null, "Cannot deactivate inherited rule '%s'", context.getRule().get().getKey());
+    change = new ActiveRuleChange(ActiveRuleChange.Type.DEACTIVATED, activeRule.get(), context.getRule().get());
+    changes.add(change);
+    persist(change, context, dbSession);
+
+    // get all inherited profiles (they are not built-in by design)
+    context.getChildProfiles().forEach(child -> {
+      context.selectChild(child);
+      changes.addAll(doDeactivate(dbSession, context, force));
+    });
+
+    if (!changes.isEmpty()) {
+      updateProfileDates(dbSession, context);
+    }
+
+    return changes;
+  }
+
+  @CheckForNull
+  private String validateParam(RuleParamDto ruleParam, @Nullable String value) {
+    if (value != null) {
+      RuleParamType ruleParamType = RuleParamType.parse(ruleParam.getType());
+      if (ruleParamType.multiple()) {
+        List<String> values = Splitter.on(",").splitToList(value);
+        typeValidations.validate(values, ruleParamType.type(), ruleParamType.values());
+      } else {
+        typeValidations.validate(value, ruleParamType.type(), ruleParamType.values());
+      }
+    }
+    return value;
+  }
+
+  public RuleActivationContext createContextForBuiltInProfile(DbSession dbSession, RulesProfileDto builtInProfile, Collection<String> ruleUuids) {
+    checkArgument(builtInProfile.isBuiltIn(), "Rules profile with UUID %s is not built-in", builtInProfile.getUuid());
+
+    RuleActivationContext.Builder builder = new RuleActivationContext.Builder();
+    builder.setDescendantProfilesSupplier(createDescendantProfilesSupplier(dbSession));
+
+    // load rules
+    completeWithRules(dbSession, builder, ruleUuids);
+
+    // load org profiles. Their parents are null by nature.
+    List<QProfileDto> profiles = db.qualityProfileDao().selectQProfilesByRuleProfile(dbSession, builtInProfile);
+    builder.setProfiles(profiles);
+    builder.setBaseProfile(builtInProfile);
+
+    // load active rules
+    Collection<String> ruleProfileUuids = Stream
+      .concat(Stream.of(builtInProfile.getUuid()), profiles.stream().map(QProfileDto::getRulesProfileUuid))
+      .collect(MoreCollectors.toHashSet(profiles.size() + 1));
+    completeWithActiveRules(dbSession, builder, ruleUuids, ruleProfileUuids);
+    return builder.build();
+  }
+
+  public RuleActivationContext createContextForUserProfile(DbSession dbSession, QProfileDto profile, Collection<String> ruleUuids) {
+    checkArgument(!profile.isBuiltIn(), "Profile with UUID %s is built-in", profile.getKee());
+    RuleActivationContext.Builder builder = new RuleActivationContext.Builder();
+    builder.setDescendantProfilesSupplier(createDescendantProfilesSupplier(dbSession));
+
+    // load rules
+    completeWithRules(dbSession, builder, ruleUuids);
+
+    // load profiles
+    List<QProfileDto> profiles = new ArrayList<>();
+    profiles.add(profile);
+    if (profile.getParentKee() != null) {
+      profiles.add(db.qualityProfileDao().selectByUuid(dbSession, profile.getParentKee()));
+    }
+    builder.setProfiles(profiles);
+    builder.setBaseProfile(RulesProfileDto.from(profile));
+
+    // load active rules
+    Collection<String> ruleProfileUuids = profiles.stream()
+      .map(QProfileDto::getRulesProfileUuid)
+      .collect(MoreCollectors.toHashSet(profiles.size()));
+    completeWithActiveRules(dbSession, builder, ruleUuids, ruleProfileUuids);
+
+    return builder.build();
+  }
+
+  DescendantProfilesSupplier createDescendantProfilesSupplier(DbSession dbSession) {
+    return (parents, ruleUuids) -> {
+      Collection<QProfileDto> profiles = db.qualityProfileDao().selectDescendants(dbSession, parents);
+      Set<String> ruleProfileUuids = profiles.stream()
+        .map(QProfileDto::getRulesProfileUuid)
+        .collect(MoreCollectors.toHashSet());
+      Collection<ActiveRuleDto> activeRules = db.activeRuleDao().selectByRulesAndRuleProfileUuids(dbSession, ruleUuids, ruleProfileUuids);
+      List<String> activeRuleUuids = activeRules.stream().map(ActiveRuleDto::getUuid).collect(MoreCollectors.toArrayList(activeRules.size()));
+      List<ActiveRuleParamDto> activeRuleParams = db.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, activeRuleUuids);
+      return new DescendantProfilesSupplier.Result(profiles, activeRules, activeRuleParams);
+    };
+  }
+
+  private void completeWithRules(DbSession dbSession, RuleActivationContext.Builder builder, Collection<String> ruleUuids) {
+    List<RuleDefinitionDto> rules = db.ruleDao().selectDefinitionByUuids(dbSession, ruleUuids);
+    builder.setRules(rules);
+    builder.setRuleParams(db.ruleDao().selectRuleParamsByRuleUuids(dbSession, ruleUuids));
+  }
+
+  private void completeWithActiveRules(DbSession dbSession, RuleActivationContext.Builder builder, Collection<String> ruleUuids, Collection<String> ruleProfileUuids) {
+    Collection<ActiveRuleDto> activeRules = db.activeRuleDao().selectByRulesAndRuleProfileUuids(dbSession, ruleUuids, ruleProfileUuids);
+    builder.setActiveRules(activeRules);
+    List<String> activeRuleUuids = activeRules.stream().map(ActiveRuleDto::getUuid).collect(MoreCollectors.toArrayList(activeRules.size()));
+    builder.setActiveRuleParams(db.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, activeRuleUuids));
+  }
+
+  private static boolean isSame(ActiveRuleChange change, ActiveRuleWrapper activeRule) {
+    ActiveRuleInheritance inheritance = change.getInheritance();
+    if (inheritance != null && !inheritance.name().equals(activeRule.get().getInheritance())) {
+      return false;
+    }
+    String severity = change.getSeverity();
+    if (severity != null && !severity.equals(activeRule.get().getSeverityString())) {
+      return false;
+    }
+    for (Map.Entry<String, String> changeParam : change.getParameters().entrySet()) {
+      String activeParamValue = activeRule.getParamValue(changeParam.getKey());
+      if (changeParam.getValue() == null && activeParamValue != null) {
+        return false;
+      }
+      if (changeParam.getValue() != null && (activeParamValue == null || !StringUtils.equals(changeParam.getValue(), activeParamValue))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * True if trying to override an inherited rule but with exactly the same values
+   */
+  private static boolean isSameAsParent(ActiveRuleChange change, RuleActivationContext context) {
+    ActiveRuleWrapper parentActiveRule = context.getParentActiveRule();
+    if (parentActiveRule == null) {
+      return false;
+    }
+    if (!StringUtils.equals(change.getSeverity(), parentActiveRule.get().getSeverityString())) {
+      return false;
+    }
+    for (Map.Entry<String, String> entry : change.getParameters().entrySet()) {
+      if (entry.getValue() != null && !entry.getValue().equals(parentActiveRule.getParamValue(entry.getKey()))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @CheckForNull
+  private static String firstNonNull(String... strings) {
+    for (String s : strings) {
+      if (s != null) {
+        return s;
+      }
+    }
+    return null;
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/package-info.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/package-info.java
new file mode 100644 (file)
index 0000000..d26d865
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
index 202758dc4463a1f5a715b3da7e1a310deeb37610..d5366352372696a78883d2875194625826dc8e64 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.qualityprofile.ws;
 
-import java.util.Optional;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -31,11 +30,9 @@ import org.sonar.db.DbSession;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.component.ComponentFinder;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.user.UserSession;
 
-import static java.util.Optional.empty;
-import static java.util.Optional.of;
 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_ADD_PROJECT;
@@ -93,24 +90,23 @@ public class AddProjectAction implements QProfileWsAction {
       checkPermissions(dbSession, profile, project);
       QProfileDto currentProfile = dbClient.qualityProfileDao().selectAssociatedToProjectAndLanguage(dbSession, project, profile.getLanguage());
 
-      Optional<QProfileDto> deactivatedProfile = empty();
+      QProfileDto deactivatedProfile = null;
 
       if (currentProfile == null) {
         QProfileDto defaultProfile = dbClient.qualityProfileDao().selectDefaultProfile(dbSession, profile.getLanguage());
         if (defaultProfile != null) {
-          deactivatedProfile = of(defaultProfile);
+          deactivatedProfile = defaultProfile;
         }
 
         // project uses the default profile
         dbClient.qualityProfileDao().insertProjectProfileAssociation(dbSession, project, profile);
         dbSession.commit();
       } else if (!profile.getKee().equals(currentProfile.getKee())) {
-        deactivatedProfile = of(currentProfile);
+        deactivatedProfile = currentProfile;
         dbClient.qualityProfileDao().updateProjectProfileAssociation(dbSession, project, profile.getKee(), currentProfile.getKee());
         dbSession.commit();
       }
-      Optional<QProfileDto> activatedProfile = of(profile);
-      qualityProfileChangeEventService.publishRuleActivationToSonarLintClients(project, activatedProfile, deactivatedProfile);
+      qualityProfileChangeEventService.publishRuleActivationToSonarLintClients(project, profile, deactivatedProfile);
     }
 
     response.noContent();
index 12b91a065ad1665155e8bf187aab8cf76d788262..16f1be265bbadd96b57f3f702b4bcc8ec28c1aad 100644 (file)
@@ -32,7 +32,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.qualityprofile.QProfileExporters;
 import org.sonar.server.qualityprofile.QProfileFactory;
-import org.sonar.server.qualityprofile.QProfileName;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 import org.sonar.server.qualityprofile.QProfileResult;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.user.UserSession;
index 81c7987dc5b9f4d252d50162ecb3b26c55370a33..4ab8d7cf5272e651e079e78d46a27b56c44c8ece 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.qualityprofile.ws;
 
-import java.util.Optional;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -31,11 +30,9 @@ import org.sonar.db.DbSession;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.component.ComponentFinder;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.user.UserSession;
 
-import static java.util.Optional.empty;
-import static java.util.Optional.of;
 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_REMOVE_PROJECT;
@@ -93,16 +90,15 @@ public class RemoveProjectAction implements QProfileWsAction {
       dbClient.qualityProfileDao().deleteProjectProfileAssociation(dbSession, project, profile);
       dbSession.commit();
 
-      Optional<QProfileDto> deactivatedProfile = of(profile);
-      Optional<QProfileDto> activatedProfile = empty();
+      QProfileDto activatedProfile = null;
 
       // publish change for rules in the default quality profile
       QProfileDto defaultProfile = dbClient.qualityProfileDao().selectDefaultProfile(dbSession, profile.getLanguage());
       if (defaultProfile != null) {
-        activatedProfile = of(defaultProfile);
+        activatedProfile = defaultProfile;
       }
 
-      qualityProfileChangeEventService.publishRuleActivationToSonarLintClients(project, activatedProfile, deactivatedProfile);
+      qualityProfileChangeEventService.publishRuleActivationToSonarLintClients(project, activatedProfile, profile);
 
       response.noContent();
     }
index 2c9574f04d55ee453cf21bc6788b08f0ecab6ee3..800c5623d594139039a859f9aa7bf109eb873d99 100644 (file)
@@ -43,6 +43,7 @@ import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.qualityprofile.QualityProfileTesting;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 import org.sonar.server.rule.RuleCreator;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
index 3254844f7be3c5e4ae0201172b40254c926d8871..4282ef2abba417894898e6000c117934eb54f131 100644 (file)
@@ -38,8 +38,10 @@ import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.db.rule.RuleParamDto;
 import org.sonar.db.rule.RuleTesting;
 import org.sonar.server.es.EsTester;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.QProfileComparison.ActiveRuleDiff;
 import org.sonar.server.qualityprofile.QProfileComparison.QProfileComparisonResult;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.tester.UserSessionRule;
index c1515ec7f7ce20df1edbed4a2c5449ab0e5d33b3..7422ae3ca1537ea13ee1e44ea1148db997cfbb34 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.qualityprofile.QualityProfileTesting;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.fail;
index 8ed02021e5faacf0f29e2813b590a4cb459b57ec..a9137a756ed8ea82c29f73216aafa0a7cfa60327 100644 (file)
@@ -46,6 +46,7 @@ import org.sonar.db.rule.RuleParamDto;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 
 import static java.util.Arrays.asList;
index 08f2b22255a8fbfa3e887d6ce9b4755fc2f91fc2..031316d8b07f7557a8c8ad5683a5ee979f4d73f9 100644 (file)
@@ -29,6 +29,8 @@ import org.sonar.db.qualityprofile.OrgActiveRuleDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.qualityprofile.QualityProfileTesting;
 import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.util.IntegerTypeValidation;
index e1479fcb1e4a5a51153f9ac4257d55ca4a2fc521..1b84a8198006aeff4acb7b76a5ad5700bb58196c 100644 (file)
@@ -43,6 +43,8 @@ import org.sonar.db.rule.RuleParamDto;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleIndexer;
index 1384f1d6705bf1ff5aec9575ebdd9d0afbafb02c..3d07cbd70a62a75c89e36c01cdc45604d0e20a43 100644 (file)
@@ -33,6 +33,8 @@ import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.es.EsTester;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.tester.UserSessionRule;
index ad5d09abcef487735aa2bff62bd945ef210e7538..803118990717e0642e0326c8a5f721fd412ca94f 100644 (file)
@@ -36,6 +36,8 @@ import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.util.IntegerTypeValidation;
index 5a49e3e5b79764f8171abb43e01663b84f8c2b0d..7beb4f45b5de349c4826624dd054649b2dd08fb3 100644 (file)
@@ -45,6 +45,16 @@ import org.sonar.db.qualityprofile.ActiveRuleDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.qualityprofile.RulesProfileDto;
 import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfile;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileInsert;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileInsertImpl;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileRepositoryRule;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdate;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdateImpl;
+import org.sonar.server.qualityprofile.builtin.BuiltInQualityProfilesUpdateListener;
+import org.sonar.server.qualityprofile.builtin.QProfileName;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.DefaultRuleFinder;
 import org.sonar.server.rule.ServerRuleFinder;
index 2257d164d4a7dcca7f5d5a4739b7572762f21407..277c675070ebbfd61603b2f40422cb82091de47e 100644 (file)
@@ -37,6 +37,11 @@ import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.db.qualityprofile.RulesProfileDto;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.language.LanguageTesting;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfile;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileInsert;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileRepositoryRule;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdate;
+import org.sonar.server.qualityprofile.builtin.BuiltInQualityProfilesUpdateListener;
 import org.sonar.server.tester.UserSessionRule;
 
 import static java.lang.String.format;
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationHandlerTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationHandlerTest.java
new file mode 100644 (file)
index 0000000..3669e86
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.IntStream;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.EmailSubscriberDto;
+import org.sonar.db.permission.AuthorizationDao;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+
+import static java.util.Collections.emptySet;
+import static java.util.stream.Collectors.toSet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class BuiltInQPChangeNotificationHandlerTest {
+  private DbClient dbClient = mock(DbClient.class);
+  private DbSession dbSession = mock(DbSession.class);
+  private AuthorizationDao authorizationDao = mock(AuthorizationDao.class);
+  private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
+
+  private BuiltInQPChangeNotificationHandler underTest = new BuiltInQPChangeNotificationHandler(dbClient, emailNotificationChannel);
+
+  @Before
+  public void wire_mocks() {
+    when(dbClient.openSession(false)).thenReturn(dbSession);
+    when(dbClient.authorizationDao()).thenReturn(authorizationDao);
+  }
+
+  @Test
+  public void getMetadata_returns_empty() {
+    assertThat(underTest.getMetadata()).isEmpty();
+  }
+
+  @Test
+  public void getNotificationClass_is_BuiltInQPChangeNotification() {
+    assertThat(underTest.getNotificationClass()).isEqualTo(BuiltInQPChangeNotification.class);
+  }
+
+  @Test
+  public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
+    when(emailNotificationChannel.isActivated()).thenReturn(false);
+    Set<BuiltInQPChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
+      .mapToObj(i -> mock(BuiltInQPChangeNotification.class))
+      .collect(toSet());
+
+    int deliver = underTest.deliver(notifications);
+
+    assertThat(deliver).isZero();
+    verify(emailNotificationChannel).isActivated();
+    verifyNoMoreInteractions(emailNotificationChannel);
+    verifyZeroInteractions(dbClient);
+    notifications.forEach(Mockito::verifyZeroInteractions);
+  }
+
+  @Test
+  public void deliver_has_no_effect_if_there_is_no_global_administer_email_subscriber() {
+    when(emailNotificationChannel.isActivated()).thenReturn(true);
+    Set<BuiltInQPChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
+      .mapToObj(i -> mock(BuiltInQPChangeNotification.class))
+      .collect(toSet());
+    when(authorizationDao.selectQualityProfileAdministratorLogins(dbSession))
+      .thenReturn(emptySet());
+
+    int deliver = underTest.deliver(notifications);
+
+    assertThat(deliver).isZero();
+    verify(emailNotificationChannel).isActivated();
+    verifyNoMoreInteractions(emailNotificationChannel);
+    verify(dbClient).openSession(false);
+    verify(dbClient).authorizationDao();
+    verifyNoMoreInteractions(dbClient);
+    verify(authorizationDao).selectQualityProfileAdministratorLogins(dbSession);
+    verifyNoMoreInteractions(authorizationDao);
+    notifications.forEach(Mockito::verifyZeroInteractions);
+  }
+
+  @Test
+  public void deliver_create_emailRequest_for_each_notification_and_for_each_global_administer_email_subscriber() {
+    when(emailNotificationChannel.isActivated()).thenReturn(true);
+    Set<BuiltInQPChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
+      .mapToObj(i -> mock(BuiltInQPChangeNotification.class))
+      .collect(toSet());
+    Set<EmailSubscriberDto> emailSubscribers = IntStream.range(0, 1 + new Random().nextInt(10))
+      .mapToObj(i -> EmailSubscriberDto.create("login_" + i, true, "login_" + i + "@foo"))
+      .collect(toSet());
+    when(authorizationDao.selectQualityProfileAdministratorLogins(dbSession))
+      .thenReturn(emailSubscribers);
+    Set<EmailNotificationChannel.EmailDeliveryRequest> expectedRequests = notifications.stream()
+      .flatMap(notification -> emailSubscribers.stream().map(subscriber -> new EmailNotificationChannel.EmailDeliveryRequest(subscriber.getEmail(), notification)))
+      .collect(toSet());
+    int deliveries = new Random().nextInt(expectedRequests.size());
+    when(emailNotificationChannel.deliverAll(expectedRequests)).thenReturn(deliveries);
+
+    int deliver = underTest.deliver(notifications);
+
+    assertThat(deliver).isEqualTo(deliveries);
+    verify(emailNotificationChannel).isActivated();
+    verify(emailNotificationChannel).deliverAll(expectedRequests);
+    verifyNoMoreInteractions(emailNotificationChannel);
+    verify(dbClient).openSession(false);
+    verify(dbClient).authorizationDao();
+    verifyNoMoreInteractions(dbClient);
+    verify(authorizationDao).selectQualityProfileAdministratorLogins(dbSession);
+    verifyNoMoreInteractions(authorizationDao);
+    notifications.forEach(Mockito::verifyZeroInteractions);
+  }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTemplateTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTemplateTest.java
new file mode 100644 (file)
index 0000000..c009d36
--- /dev/null
@@ -0,0 +1,283 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.Date;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.platform.Server;
+import org.sonar.server.issue.notification.EmailMessage;
+import org.sonar.server.qualityprofile.builtin.BuiltInQPChangeNotificationBuilder.Profile;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.utils.DateUtils.formatDate;
+
+public class BuiltInQPChangeNotificationTemplateTest {
+
+  private Server server = mock(Server.class);
+
+  private BuiltInQPChangeNotificationTemplate underTest = new BuiltInQPChangeNotificationTemplate(server);
+
+  @Before
+  public void setUp() {
+    when(server.getPublicRootUrl()).thenReturn("http://" + randomAlphanumeric(10));
+  }
+
+  @Test
+  public void notification_contains_a_subject() {
+    String profileName = newProfileName();
+    String languageKey = newLanguageKey();
+    String languageName = newLanguageName();
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName)
+        .setLanguageKey(languageKey)
+        .setLanguageName(languageName)
+        .setNewRules(2)
+        .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertThat(emailMessage.getSubject()).isEqualTo("Built-in quality profiles have been updated");
+  }
+
+  @Test
+  public void notification_contains_count_of_new_rules() {
+    String profileName = newProfileName();
+    String languageKey = newLanguageKey();
+    String languageName = newLanguageName();
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName)
+        .setLanguageKey(languageKey)
+        .setLanguageName(languageName)
+        .setNewRules(2)
+        .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertMessage(emailMessage, "\n 2 new rules\n");
+  }
+
+  @Test
+  public void notification_contains_count_of_updated_rules() {
+    String profileName = newProfileName();
+    String languageKey = newLanguageKey();
+    String languageName = newLanguageName();
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName)
+        .setLanguageKey(languageKey)
+        .setLanguageName(languageName)
+        .setUpdatedRules(2)
+        .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertMessage(emailMessage, "\n 2 rules have been updated\n");
+  }
+
+  @Test
+  public void notification_contains_count_of_removed_rules() {
+    String profileName = newProfileName();
+    String languageKey = newLanguageKey();
+    String languageName = newLanguageName();
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName)
+        .setLanguageKey(languageKey)
+        .setLanguageName(languageName)
+        .setRemovedRules(2)
+        .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertMessage(emailMessage, "\n 2 rules removed\n");
+  }
+
+  @Test
+  public void notification_supports_grammar_for_single_rule_added_removed_or_updated() {
+    String profileName = newProfileName();
+    String languageKey = newLanguageKey();
+    String languageName = newLanguageName();
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+        .addProfile(Profile.newBuilder()
+            .setProfileName(profileName)
+            .setLanguageKey(languageKey)
+            .setLanguageName(languageName)
+            .setNewRules(1)
+            .setUpdatedRules(1)
+            .setRemovedRules(1)
+            .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertThat(emailMessage.getMessage())
+        .contains("\n 1 new rule\n")
+        .contains("\n 1 rule has been updated\n")
+        .contains("\n 1 rule removed\n");
+  }
+
+  @Test
+  public void notification_contains_list_of_new_updated_and_removed_rules() {
+    String profileName = newProfileName();
+    String languageKey = newLanguageKey();
+    String languageName = newLanguageName();
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName)
+        .setLanguageKey(languageKey)
+        .setLanguageName(languageName)
+        .setNewRules(2)
+        .setUpdatedRules(3)
+        .setRemovedRules(4)
+        .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertMessage(emailMessage,
+      "\n" +
+        " 2 new rules\n" +
+        " 3 rules have been updated\n" +
+        " 4 rules removed\n");
+  }
+
+  @Test
+  public void notification_contains_many_profiles() {
+    String profileName1 = "profile1_" + randomAlphanumeric(20);
+    String languageKey1 = "langkey1_" + randomAlphanumeric(20);
+    String languageName1 = "langName1_" + randomAlphanumeric(20);
+    String profileName2 = "profile2_" + randomAlphanumeric(20);
+    String languageKey2 = "langkey2_" + randomAlphanumeric(20);
+    String languageName2 = "langName2_" + randomAlphanumeric(20);
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName1)
+        .setLanguageKey(languageKey1)
+        .setLanguageName(languageName1)
+        .setNewRules(2)
+        .build())
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName2)
+        .setLanguageKey(languageKey2)
+        .setLanguageName(languageName2)
+        .setNewRules(13)
+        .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertThat(emailMessage.getMessage()).containsSubsequence("The following built-in profiles have been updated:\n",
+      profileTitleText(profileName1, languageKey1, languageName1),
+      " 2 new rules\n",
+      profileTitleText(profileName2, languageKey2, languageName2),
+      " 13 new rules\n",
+      "This is a good time to review your quality profiles and update them to benefit from the latest evolutions: " + server.getPublicRootUrl() + "/profiles");
+  }
+
+  @Test
+  public void notification_contains_profiles_sorted_by_language_then_by_profile_name() {
+    String languageKey1 = "langkey1_" + randomAlphanumeric(20);
+    String languageName1 = "langName1_" + randomAlphanumeric(20);
+    String languageKey2 = "langKey2_" + randomAlphanumeric(20);
+    String languageName2 = "langName2_" + randomAlphanumeric(20);
+    String profileName1 = "profile1_" + randomAlphanumeric(20);
+    String profileName2 = "profile2_" + randomAlphanumeric(20);
+    String profileName3 = "profile3_" + randomAlphanumeric(20);
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder().setProfileName(profileName3).setLanguageKey(languageKey2).setLanguageName(languageName2).build())
+      .addProfile(Profile.newBuilder().setProfileName(profileName2).setLanguageKey(languageKey1).setLanguageName(languageName1).build())
+      .addProfile(Profile.newBuilder().setProfileName(profileName1).setLanguageKey(languageKey2).setLanguageName(languageName2).build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertThat(emailMessage.getMessage()).containsSubsequence(
+      "\"" + profileName2 + "\" - " + languageName1,
+      "\"" + profileName1 + "\" - " + languageName2,
+      "\"" + profileName3 + "\" - " + languageName2);
+  }
+
+  @Test
+  public void notification_contains_encoded_profile_name() {
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName("Sonar Way")
+        .setLanguageKey("java")
+        .setLanguageName(newLanguageName())
+        .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertThat(emailMessage.getMessage()).contains(server.getPublicRootUrl() + "/profiles/changelog?language=java&name=Sonar+Way");
+  }
+
+  @Test
+  public void notification_contains_from_and_to_date() {
+    String profileName = newProfileName();
+    String languageKey = newLanguageKey();
+    String languageName = newLanguageName();
+    long startDate = 1_000_000_000_000L;
+    long endDate = startDate + 1_100_000_000_000L;
+    BuiltInQPChangeNotificationBuilder notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName)
+        .setLanguageKey(languageKey)
+        .setLanguageName(languageName)
+        .setStartDate(startDate)
+        .setEndDate(endDate)
+        .build());
+
+    EmailMessage emailMessage = underTest.format(notification.build());
+
+    assertMessage(emailMessage,
+      profileTitleText(profileName, languageKey, languageName, formatDate(new Date(startDate)), formatDate(new Date(endDate))));
+  }
+
+  private void assertMessage(EmailMessage emailMessage, String expectedProfileDetails) {
+    assertThat(emailMessage.getMessage())
+      .containsSubsequence(
+        "The following built-in profiles have been updated:\n\n",
+        expectedProfileDetails,
+        "\nThis is a good time to review your quality profiles and update them to benefit from the latest evolutions: " + server.getPublicRootUrl() + "/profiles");
+  }
+
+  private String profileTitleText(String profileName, String languageKey, String languageName) {
+    return "\"" + profileName + "\" - " + languageName + ": " + server.getPublicRootUrl() + "/profiles/changelog?language=" + languageKey + "&name=" + profileName;
+  }
+
+  private String profileTitleText(String profileName, String languageKey, String languageName, String startDate, String endDate) {
+    return "\"" + profileName + "\" - " + languageName + ": " + server.getPublicRootUrl() + "/profiles/changelog?language=" + languageKey + "&name=" + profileName +
+      "&since=" + startDate + "&to=" + endDate + "\n";
+  }
+
+  private static String newProfileName() {
+    return "profileName_" + randomAlphanumeric(20);
+  }
+
+  private static String newLanguageName() {
+    return "languageName_" + randomAlphanumeric(20);
+  }
+
+  private static String newLanguageKey() {
+    return "languageKey_" + randomAlphanumeric(20);
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQPChangeNotificationTest.java
new file mode 100644 (file)
index 0000000..0b65d4e
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.Random;
+import org.junit.Test;
+import org.sonar.api.notifications.Notification;
+import org.sonar.server.qualityprofile.builtin.BuiltInQPChangeNotificationBuilder.Profile;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class BuiltInQPChangeNotificationTest {
+
+  private static final Random RANDOM = new Random();
+
+
+  @Test
+  public void serialize_and_parse_no_profile() {
+    Notification notification = new BuiltInQPChangeNotificationBuilder().build();
+
+    BuiltInQPChangeNotificationBuilder result = BuiltInQPChangeNotificationBuilder.parse(notification);
+
+    assertThat(result.getProfiles()).isEmpty();
+  }
+
+  @Test
+  public void serialize_and_parse_single_profile() {
+    String profileName = randomAlphanumeric(20);
+    String languageKey = randomAlphanumeric(20);
+    String languageName = randomAlphanumeric(20);
+    int newRules = RANDOM.nextInt(5000);
+    int updatedRules = RANDOM.nextInt(5000);
+    int removedRules = RANDOM.nextInt(5000);
+    long startDate = RANDOM.nextInt(5000);
+    long endDate = startDate + RANDOM.nextInt(5000);
+
+    BuiltInQPChangeNotification notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName)
+        .setLanguageKey(languageKey)
+        .setLanguageName(languageName)
+        .setNewRules(newRules)
+        .setUpdatedRules(updatedRules)
+        .setRemovedRules(removedRules)
+        .setStartDate(startDate)
+        .setEndDate(endDate)
+        .build())
+      .build();
+    BuiltInQPChangeNotificationBuilder result = BuiltInQPChangeNotificationBuilder.parse(notification);
+
+    assertThat(result.getProfiles())
+      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getNewRules, Profile::getUpdatedRules, Profile::getRemovedRules,
+        Profile::getStartDate, Profile::getEndDate)
+      .containsExactlyInAnyOrder(tuple(profileName, languageKey, languageName, newRules, updatedRules, removedRules, startDate, endDate));
+  }
+
+  @Test
+  public void serialize_and_parse_multiple_profiles() {
+    String profileName1 = randomAlphanumeric(20);
+    String languageKey1 = randomAlphanumeric(20);
+    String languageName1 = randomAlphanumeric(20);
+    String profileName2 = randomAlphanumeric(20);
+    String languageKey2 = randomAlphanumeric(20);
+    String languageName2 = randomAlphanumeric(20);
+
+    BuiltInQPChangeNotification notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName1)
+        .setLanguageKey(languageKey1)
+        .setLanguageName(languageName1)
+        .build())
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName2)
+        .setLanguageKey(languageKey2)
+        .setLanguageName(languageName2)
+        .build())
+      .build();
+    BuiltInQPChangeNotificationBuilder result = BuiltInQPChangeNotificationBuilder.parse(notification);
+
+    assertThat(result.getProfiles()).extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName)
+      .containsExactlyInAnyOrder(tuple(profileName1, languageKey1, languageName1), tuple(profileName2, languageKey2, languageName2));
+  }
+
+  @Test
+  public void serialize_and_parse_max_values() {
+    String profileName = randomAlphanumeric(20);
+    String languageKey = randomAlphanumeric(20);
+    String languageName = randomAlphanumeric(20);
+    int newRules = Integer.MAX_VALUE;
+    int updatedRules = Integer.MAX_VALUE;
+    int removedRules = Integer.MAX_VALUE;
+    long startDate = Long.MAX_VALUE;
+    long endDate = Long.MAX_VALUE;
+
+    BuiltInQPChangeNotification notification = new BuiltInQPChangeNotificationBuilder()
+      .addProfile(Profile.newBuilder()
+        .setProfileName(profileName)
+        .setLanguageKey(languageKey)
+        .setLanguageName(languageName)
+        .setNewRules(newRules)
+        .setUpdatedRules(updatedRules)
+        .setRemovedRules(removedRules)
+        .setStartDate(startDate)
+        .setEndDate(endDate)
+        .build())
+      .build();
+    BuiltInQPChangeNotificationBuilder result = BuiltInQPChangeNotificationBuilder.parse(notification);
+
+    assertThat(result.getProfiles())
+      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getNewRules, Profile::getUpdatedRules, Profile::getRemovedRules,
+        Profile::getStartDate, Profile::getEndDate)
+      .containsExactlyInAnyOrder(tuple(profileName, languageKey, languageName, newRules, updatedRules, removedRules, startDate, endDate));
+  }
+
+  @Test
+  public void fail_with_ISE_when_parsing_empty_notification() {
+    assertThatThrownBy(() -> BuiltInQPChangeNotificationBuilder.parse(new Notification(BuiltInQPChangeNotification.TYPE)))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Could not read the built-in quality profile notification");
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsertImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileInsertImplTest.java
new file mode 100644 (file)
index 0000000..2b012c6
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.PropertyType;
+import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.NewBuiltInQualityProfile;
+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.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleKey;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.QProfileChangeDto;
+import org.sonar.db.qualityprofile.QProfileChangeQuery;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
+import org.sonar.server.rule.DefaultRuleFinder;
+import org.sonar.server.rule.ServerRuleFinder;
+import org.sonar.server.util.StringTypeValidation;
+import org.sonar.server.util.TypeValidations;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class BuiltInQProfileInsertImplTest {
+
+  @Rule
+  public BuiltInQProfileRepositoryRule builtInQProfileRepository = new BuiltInQProfileRepositoryRule();
+  @Rule
+  public DbTester db = DbTester.create();
+
+  private final System2 system2 = new AlwaysIncreasingSystem2();
+  private final UuidFactory uuidFactory = new SequenceUuidFactory();
+  private final TypeValidations typeValidations = new TypeValidations(singletonList(new StringTypeValidation()));
+  private final DbSession dbSession = db.getSession();
+  private final DbSession batchDbSession = db.getDbClient().openSession(true);
+  private final ServerRuleFinder ruleFinder = new DefaultRuleFinder(db.getDbClient());
+  private final ActiveRuleIndexer activeRuleIndexer = mock(ActiveRuleIndexer.class);
+  private final BuiltInQProfileInsertImpl underTest = new BuiltInQProfileInsertImpl(db.getDbClient(), ruleFinder, system2, uuidFactory, typeValidations, activeRuleIndexer);
+
+  @After
+  public void tearDown() {
+    batchDbSession.close();
+  }
+
+  @Test
+  public void insert_active_rules_and_changelog() {
+    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
+    RuleDefinitionDto rule2 = db.rules().insert(r -> r.setLanguage("xoo"));
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo");
+
+    newQp.activateRule(rule1.getRepositoryKey(), rule1.getRuleKey()).overrideSeverity(Severity.CRITICAL);
+    newQp.activateRule(rule2.getRepositoryKey(), rule2.getRuleKey()).overrideSeverity(Severity.MAJOR);
+    newQp.done();
+
+    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"), rule1, rule2);
+    call(builtIn);
+
+    verifyTableSize("rules_profiles", 1);
+    verifyTableSize("org_qprofiles", 1);
+    verifyTableSize("active_rules", 2);
+    verifyTableSize("active_rule_parameters", 0);
+    verifyTableSize("qprofile_changes", 2);
+    verifyTableSize("default_qprofiles", 0);
+
+    QProfileDto profile = verifyProfileInDb(builtIn);
+    verifyActiveRuleInDb(profile, rule1, Severity.CRITICAL);
+    verifyActiveRuleInDb(profile, rule2, Severity.MAJOR);
+  }
+
+  @Test
+  public void insert_default_qp() {
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    context.createBuiltInQualityProfile("the name", "xoo")
+      .setDefault(true)
+      .done();
+
+    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"));
+    call(builtIn);
+
+    verifyTableSize("rules_profiles", 1);
+    verifyTableSize("org_qprofiles", 1);
+    verifyTableSize("active_rules", 0);
+    verifyTableSize("active_rule_parameters", 0);
+    verifyTableSize("qprofile_changes", 0);
+    verifyTableSize("default_qprofiles", 1);
+
+    verifyProfileInDb(builtIn);
+  }
+
+  @Test
+  public void insert_active_rules_with_params() {
+    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
+    RuleParamDto param1 = db.rules().insertRuleParam(rule1, p -> p.setType(PropertyType.STRING.name()));
+    RuleParamDto param2 = db.rules().insertRuleParam(rule1, p -> p.setType(PropertyType.STRING.name()));
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo");
+
+    newQp.activateRule(rule1.getRepositoryKey(), rule1.getRuleKey()).overrideSeverity(Severity.CRITICAL);
+    newQp.done();
+
+    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"), rule1);
+    call(builtIn);
+
+    verifyTableSize("rules_profiles", 1);
+    verifyTableSize("org_qprofiles", 1);
+    verifyTableSize("active_rules", 1);
+    verifyTableSize("active_rule_parameters", 2);
+    verifyTableSize("qprofile_changes", 1);
+
+    QProfileDto profile = verifyProfileInDb(builtIn);
+    verifyActiveRuleInDb(profile, rule1, Severity.CRITICAL, param1, param2);
+  }
+
+  @Test
+  public void flag_profile_as_default_if_declared_as_default_by_api() {
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo").setDefault(true);
+    newQp.done();
+
+    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"));
+
+    call(builtIn);
+
+    QProfileDto profile = verifyProfileInDb(builtIn);
+    QProfileDto defaultProfile = db.getDbClient().qualityProfileDao().selectDefaultProfile(dbSession, "xoo");
+    assertThat(defaultProfile.getKee()).isEqualTo(profile.getKee());
+  }
+
+  @Test
+  public void existing_default_profile_must_not_be_changed() {
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo").setDefault(true);
+    newQp.done();
+    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"));
+
+    QProfileDto currentDefault = db.qualityProfiles().insert(p -> p.setLanguage("xoo"));
+    db.qualityProfiles().setAsDefault(currentDefault);
+
+    call(builtIn);
+
+    QProfileDto defaultProfile = db.getDbClient().qualityProfileDao().selectDefaultProfile(dbSession, "xoo");
+    assertThat(defaultProfile.getKee()).isEqualTo(currentDefault.getKee());
+    verifyTableSize("rules_profiles", 2);
+  }
+
+  @Test
+  public void dont_flag_profile_as_default_if_not_declared_as_default_by_api() {
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("the name", "xoo").setDefault(false);
+    newQp.done();
+    BuiltInQProfile builtIn = builtInQProfileRepository.create(context.profile("xoo", "the name"));
+
+    call(builtIn);
+
+    QProfileDto defaultProfile = db.getDbClient().qualityProfileDao().selectDefaultProfile(dbSession, "xoo");
+    assertThat(defaultProfile).isNull();
+  }
+
+  // TODO test lot of active_rules, params, orgas
+
+  private void verifyActiveRuleInDb(QProfileDto profile, RuleDefinitionDto rule, String expectedSeverity, RuleParamDto... paramDtos) {
+    ActiveRuleDto activeRule = db.getDbClient().activeRuleDao().selectByKey(dbSession, ActiveRuleKey.of(profile, rule.getKey())).get();
+    assertThat(activeRule.getUuid()).isNotNull();
+    assertThat(activeRule.getInheritance()).isNull();
+    assertThat(activeRule.doesOverride()).isFalse();
+    assertThat(activeRule.getRuleUuid()).isEqualTo(rule.getUuid());
+    assertThat(activeRule.getProfileUuid()).isEqualTo(profile.getRulesProfileUuid());
+    assertThat(activeRule.getSeverityString()).isEqualTo(expectedSeverity);
+    assertThat(activeRule.getCreatedAt()).isPositive();
+    assertThat(activeRule.getUpdatedAt()).isPositive();
+
+    List<ActiveRuleParamDto> params = db.getDbClient().activeRuleDao().selectParamsByActiveRuleUuid(dbSession, activeRule.getUuid());
+    assertThat(params).extracting(ActiveRuleParamDto::getKey).containsOnly(Arrays.stream(paramDtos).map(RuleParamDto::getName).toArray(String[]::new));
+
+    QProfileChangeQuery changeQuery = new QProfileChangeQuery(profile.getKee());
+    QProfileChangeDto change = db.getDbClient().qProfileChangeDao().selectByQuery(dbSession, changeQuery).stream()
+      .filter(c -> c.getDataAsMap().get("ruleUuid").equals(rule.getUuid()))
+      .findFirst()
+      .get();
+    assertThat(change.getChangeType()).isEqualTo(ActiveRuleChange.Type.ACTIVATED.name());
+    assertThat(change.getCreatedAt()).isPositive();
+    assertThat(change.getUuid()).isNotEmpty();
+    assertThat(change.getUserUuid()).isNull();
+    assertThat(change.getRulesProfileUuid()).isEqualTo(profile.getRulesProfileUuid());
+    assertThat(change.getDataAsMap()).containsEntry("severity", expectedSeverity);
+  }
+
+  private QProfileDto verifyProfileInDb(BuiltInQProfile builtIn) {
+    QProfileDto profileOnOrg1 = db.getDbClient().qualityProfileDao().selectByNameAndLanguage(dbSession, builtIn.getName(), builtIn.getLanguage());
+    assertThat(profileOnOrg1.getLanguage()).isEqualTo(builtIn.getLanguage());
+    assertThat(profileOnOrg1.getName()).isEqualTo(builtIn.getName());
+    assertThat(profileOnOrg1.getParentKee()).isNull();
+    assertThat(profileOnOrg1.getLastUsed()).isNull();
+    assertThat(profileOnOrg1.getUserUpdatedAt()).isNull();
+    assertThat(profileOnOrg1.getRulesUpdatedAt()).isNotEmpty();
+    assertThat(profileOnOrg1.getKee()).isNotEqualTo(profileOnOrg1.getRulesProfileUuid());
+    assertThat(profileOnOrg1.getRulesProfileUuid()).isNotNull();
+    return profileOnOrg1;
+  }
+
+  private void verifyTableSize(String table, int expectedSize) {
+    assertThat(db.countRowsOfTable(dbSession, table)).as("table " + table).isEqualTo(expectedSize);
+  }
+
+  private void call(BuiltInQProfile builtIn) {
+    underTest.create(dbSession, builtIn);
+    dbSession.commit();
+    batchDbSession.commit();
+  }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileLoaderTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileLoaderTest.java
new file mode 100644 (file)
index 0000000..9944614
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BuiltInQProfileLoaderTest {
+  @Rule
+  public BuiltInQProfileRepositoryRule builtInQProfileRepositoryRule = new BuiltInQProfileRepositoryRule();
+
+  private BuiltInQProfileLoader underTest = new BuiltInQProfileLoader(builtInQProfileRepositoryRule);
+
+  @Test
+  public void start_initializes_DefinedQProfileRepository() {
+    underTest.start();
+
+    assertThat(builtInQProfileRepositoryRule.isInitialized()).isTrue();
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java
new file mode 100644 (file)
index 0000000..1587b95
--- /dev/null
@@ -0,0 +1,272 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.language.LanguageTesting;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfile.ActiveRule;
+import org.sonar.server.rule.DefaultRuleFinder;
+import org.sonar.server.rule.ServerRuleFinder;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.Mockito.mock;
+import static org.sonar.db.rule.RuleTesting.EXTERNAL_XOO;
+
+
+public class BuiltInQProfileRepositoryImplTest {
+  private static final Language FOO_LANGUAGE = LanguageTesting.newLanguage("foo", "foo", "foo");
+  private static final String SONAR_WAY_QP_NAME = "Sonar way";
+
+  @Rule
+  public DbTester db = DbTester.create();
+
+  private final DbClient dbClient = db.getDbClient();
+  private final ServerRuleFinder ruleFinder = new DefaultRuleFinder(dbClient);
+
+  @Test
+  public void create_qprofile_with_rule() {
+    RuleDefinitionDto rule1 = db.rules().insert();
+    RuleDefinitionDto rule2 = db.rules().insert();
+    db.rules().insert();
+    DummyProfileDefinition definition = new DummyProfileDefinition("foo", "foo", false, asList(rule1.getKey(), rule2.getKey()));
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE), definition);
+
+    underTest.initialize();
+
+    assertThat(underTest.get())
+      .extracting(BuiltInQProfile::getName)
+      .containsExactlyInAnyOrder("foo");
+    assertThat(underTest.get().get(0).getActiveRules())
+      .extracting(ActiveRule::getRuleUuid, ActiveRule::getRuleKey)
+      .containsExactlyInAnyOrder(
+        tuple(rule1.getUuid(), rule1.getKey()),
+        tuple(rule2.getUuid(), rule2.getKey()));
+  }
+
+  @Test
+  public void make_single_profile_of_a_language_default_even_if_not_flagged_as_so() {
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
+      new DummyProfileDefinition("foo", "foo1", false));
+
+    underTest.initialize();
+
+    assertThat(underTest.get())
+      .extracting(BuiltInQProfile::getLanguage, BuiltInQProfile::isDefault)
+      .containsExactly(tuple(FOO_LANGUAGE.getKey(), true));
+  }
+
+  @Test
+  public void make_single_profile_of_a_language_default_even_if_flagged_as_so() {
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
+      new DummyProfileDefinition("foo", "foo1", true));
+
+    underTest.initialize();
+
+    assertThat(underTest.get())
+      .extracting(BuiltInQProfile::getLanguage, BuiltInQProfile::isDefault)
+      .containsExactly(tuple(FOO_LANGUAGE.getKey(), true));
+  }
+
+  @Test
+  public void make_first_profile_of_a_language_default_when_none_flagged_as_so() {
+    DummyProfileDefinition[] definitions = new DummyProfileDefinition[] {new DummyProfileDefinition("foo", "foo1", false), new DummyProfileDefinition("foo", "foo2", false)};
+    String firstName = definitions[0].getName();
+    String secondName = definitions[1].getName();
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE), definitions);
+
+    underTest.initialize();
+
+    assertThat(underTest.get())
+      .extracting(BuiltInQProfile::getName, BuiltInQProfile::isDefault)
+      .containsExactlyInAnyOrder(tuple(firstName, true), tuple(secondName, false));
+  }
+
+  @Test
+  public void create_profile_Sonar_Way_as_default_if_none_other_is_defined_default_for_a_given_language() {
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(
+      dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
+      new DummyProfileDefinition("foo", "doh", false), new DummyProfileDefinition("foo", "boo", false),
+      new DummyProfileDefinition("foo", SONAR_WAY_QP_NAME, false), new DummyProfileDefinition("foo", "goo", false));
+
+    underTest.initialize();
+
+    assertThat(underTest.get())
+      .filteredOn(b -> FOO_LANGUAGE.getKey().equals(b.getLanguage()))
+      .filteredOn(BuiltInQProfile::isDefault)
+      .extracting(BuiltInQProfile::getName)
+      .containsExactly(SONAR_WAY_QP_NAME);
+  }
+
+  @Test
+  public void do_not_create_Sonar_Way_as_default_if_other_profile_is_defined_as_default() {
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(
+      dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
+      new DummyProfileDefinition("foo", SONAR_WAY_QP_NAME, false), new DummyProfileDefinition("foo", "goo", true));
+
+    underTest.initialize();
+
+    assertThat(underTest.get())
+      .filteredOn(b -> FOO_LANGUAGE.getKey().equals(b.getLanguage()))
+      .filteredOn(BuiltInQProfile::isDefault)
+      .extracting(BuiltInQProfile::getName)
+      .containsExactly("goo");
+  }
+
+  @Test
+  public void match_Sonar_Way_default_with_case_sensitivity() {
+    String sonarWayInOtherCase = SONAR_WAY_QP_NAME.toUpperCase();
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(
+      dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
+      new DummyProfileDefinition("foo", "goo", false), new DummyProfileDefinition("foo", sonarWayInOtherCase, false));
+
+    underTest.initialize();
+
+    assertThat(underTest.get())
+      .filteredOn(b -> FOO_LANGUAGE.getKey().equals(b.getLanguage()))
+      .filteredOn(BuiltInQProfile::isDefault)
+      .extracting(BuiltInQProfile::getName)
+      .containsExactly("goo");
+  }
+
+  @Test
+  public void create_no_BuiltInQProfile_when_all_definitions_apply_to_non_defined_languages() {
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(mock(DbClient.class), ruleFinder, new Languages(),
+      new DummyProfileDefinition("foo", "P1", false));
+
+    underTest.initialize();
+
+    assertThat(underTest.get()).isEmpty();
+  }
+
+  @Test
+  public void create_qprofile_with_deprecated_rule() {
+    RuleDefinitionDto rule1 = db.rules().insert();
+    db.rules().insertDeprecatedKey(d -> d.setRuleUuid(rule1.getUuid()).setOldRepositoryKey("oldRepo").setOldRuleKey("oldKey"));
+    RuleDefinitionDto rule2 = db.rules().insert();
+    DummyProfileDefinition definition = new DummyProfileDefinition("foo", "foo", false,
+      asList(RuleKey.of("oldRepo", "oldKey"), rule2.getKey()));
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE), definition);
+
+    underTest.initialize();
+
+    assertThat(underTest.get())
+      .extracting(BuiltInQProfile::getName)
+      .containsExactlyInAnyOrder("foo");
+    assertThat(underTest.get().get(0).getActiveRules())
+      .extracting(ActiveRule::getRuleUuid, ActiveRule::getRuleKey)
+      .containsExactlyInAnyOrder(
+        tuple(rule1.getUuid(), rule1.getKey()),
+        tuple(rule2.getUuid(), rule2.getKey()));
+  }
+
+  @Test
+  public void fail_with_ISE_when_rule_does_not_exist() {
+    DummyProfileDefinition[] definitions = new DummyProfileDefinition[] {new DummyProfileDefinition("foo", "foo", false, singletonList(EXTERNAL_XOO))};
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE), definitions);
+
+    assertThatThrownBy(underTest::initialize)
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage(String.format("Rule with key '%s' not found", EXTERNAL_XOO));
+  }
+
+  @Test
+  public void fail_with_ISE_when_two_profiles_with_different_name_are_default_for_the_same_language() {
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, new Languages(FOO_LANGUAGE),
+      new DummyProfileDefinition("foo", "foo1", true), new DummyProfileDefinition("foo", "foo2", true));
+
+    assertThatThrownBy(underTest::initialize)
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Several Quality profiles are flagged as default for the language foo: [foo1, foo2]");
+  }
+
+  @Test
+  public void get_throws_ISE_if_called_before_initialize() {
+    BuiltInQProfileRepositoryImpl underTest = new BuiltInQProfileRepositoryImpl(mock(DbClient.class), ruleFinder, new Languages());
+
+    assertThatThrownBy(underTest::get)
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("initialize must be called first");
+  }
+
+  @Test
+  public void initialize_throws_ISE_if_called_twice() {
+    BuiltInQProfileRepositoryImpl underTest = new BuiltInQProfileRepositoryImpl(mock(DbClient.class), ruleFinder, new Languages());
+    underTest.initialize();
+
+    assertThatThrownBy(underTest::initialize)
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("initialize must be called only once");
+  }
+
+  @Test
+  public void initialize_throws_ISE_if_language_has_no_builtin_qp() {
+    BuiltInQProfileRepository underTest = new BuiltInQProfileRepositoryImpl(mock(DbClient.class), ruleFinder, new Languages(FOO_LANGUAGE));
+
+    assertThatThrownBy(underTest::initialize)
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("The following languages have no built-in quality profiles: foo");
+  }
+
+  private static final class DummyProfileDefinition implements BuiltInQualityProfilesDefinition {
+    private final String language;
+    private final String name;
+    private final boolean defaultProfile;
+    private final List<RuleKey> activeRuleKeys;
+
+    private DummyProfileDefinition(String language, String name, boolean defaultProfile, List<RuleKey> activeRuleKeys) {
+      this.language = language;
+      this.name = name;
+      this.defaultProfile = defaultProfile;
+      this.activeRuleKeys = activeRuleKeys;
+    }
+
+    private DummyProfileDefinition(String language, String name, boolean defaultProfile) {
+      this(language, name, defaultProfile, emptyList());
+    }
+
+    @Override
+    public void define(Context context) {
+      NewBuiltInQualityProfile builtInQualityProfile = context.createBuiltInQualityProfile(name, language);
+      activeRuleKeys.forEach(activeRuleKey -> builtInQualityProfile.activateRule(activeRuleKey.repository(), activeRuleKey.rule()));
+      builtInQualityProfile.setDefault(defaultProfile);
+      builtInQualityProfile.done();
+    }
+
+    String getName() {
+      return name;
+    }
+
+  }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryRule.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryRule.java
new file mode 100644 (file)
index 0000000..2797a0b
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.rules.ExternalResource;
+import org.sonar.api.resources.Language;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.rule.RuleDefinitionDto;
+
+import static com.google.common.base.Preconditions.checkState;
+
+public class BuiltInQProfileRepositoryRule extends ExternalResource implements BuiltInQProfileRepository {
+  private boolean initializeCalled = false;
+  private List<BuiltInQProfile> profiles = new ArrayList<>();
+
+  @Override
+  protected void before() {
+    this.initializeCalled = false;
+    this.profiles.clear();
+  }
+
+  @Override
+  public void initialize() {
+    checkState(!initializeCalled, "initialize must be called only once");
+    this.initializeCalled = true;
+  }
+
+  @Override
+  public List<BuiltInQProfile> get() {
+    checkState(initializeCalled, "initialize must be called first");
+
+    return ImmutableList.copyOf(profiles);
+  }
+
+  public boolean isInitialized() {
+    return initializeCalled;
+  }
+
+  public BuiltInQProfile add(Language language, String profileName) {
+    return add(language, profileName, false);
+  }
+
+  public BuiltInQProfile add(Language language, String profileName, boolean isDefault) {
+    return add(language, profileName, isDefault, new BuiltInQProfile.ActiveRule[0]);
+  }
+
+  public BuiltInQProfile add(Language language, String profileName, boolean isDefault, BuiltInQProfile.ActiveRule... rules) {
+    BuiltInQProfile builtIn = create(language, profileName, isDefault, rules);
+    profiles.add(builtIn);
+    return builtIn;
+  }
+
+  public BuiltInQProfile create(Language language, String profileName, boolean isDefault, BuiltInQProfile.ActiveRule... rules) {
+    BuiltInQProfile.Builder builder = new BuiltInQProfile.Builder()
+      .setLanguage(language.getKey())
+      .setName(profileName)
+      .setDeclaredDefault(isDefault);
+    Arrays.stream(rules).forEach(builder::addRule);
+    return builder.build();
+  }
+
+  public BuiltInQProfile create(BuiltInQualityProfilesDefinition.BuiltInQualityProfile api, RuleDefinitionDto... rules) {
+    BuiltInQProfile.Builder builder = new BuiltInQProfile.Builder()
+      .setLanguage(api.language())
+      .setName(api.name())
+      .setDeclaredDefault(api.isDefault());
+    Map<RuleKey, RuleDefinitionDto> rulesByRuleKey = Arrays.stream(rules)
+      .collect(MoreCollectors.uniqueIndex(RuleDefinitionDto::getKey));
+    api.rules().forEach(rule -> {
+      RuleKey ruleKey = RuleKey.of(rule.repoKey(), rule.ruleKey());
+      RuleDefinitionDto ruleDefinition = rulesByRuleKey.get(ruleKey);
+      Preconditions.checkState(ruleDefinition != null, "Rule '%s' not found", ruleKey);
+      builder.addRule(new BuiltInQProfile.ActiveRule(ruleDefinition.getUuid(), rule));
+    });
+    return builder
+      .build();
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdateImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileUpdateImplTest.java
new file mode 100644 (file)
index 0000000..4dfc4c5
--- /dev/null
@@ -0,0 +1,557 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.assertj.core.groups.Tuple;
+import org.junit.Before;
+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.Severity;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.NewBuiltInQualityProfile;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleKey;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.OrgActiveRuleDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.ActiveRuleInheritance;
+import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.util.IntegerTypeValidation;
+import org.sonar.server.util.StringTypeValidation;
+import org.sonar.server.util.TypeValidations;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.sonar.api.rules.RulePriority.BLOCKER;
+import static org.sonar.api.rules.RulePriority.CRITICAL;
+import static org.sonar.api.rules.RulePriority.MAJOR;
+import static org.sonar.api.rules.RulePriority.MINOR;
+import static org.sonar.db.qualityprofile.QualityProfileTesting.newRuleProfileDto;
+import static org.sonar.server.qualityprofile.ActiveRuleInheritance.INHERITED;
+
+public class BuiltInQProfileUpdateImplTest {
+
+  private static final long NOW = 1_000;
+  private static final long PAST = NOW - 100;
+
+  @Rule
+  public BuiltInQProfileRepositoryRule builtInProfileRepository = new BuiltInQProfileRepositoryRule();
+  @Rule
+  public DbTester db = DbTester.create();
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  private System2 system2 = new TestSystem2().setNow(NOW);
+  private ActiveRuleIndexer activeRuleIndexer = mock(ActiveRuleIndexer.class);
+  private TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
+  private RuleActivator ruleActivator = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
+
+  private BuiltInQProfileUpdateImpl underTest = new BuiltInQProfileUpdateImpl(db.getDbClient(), ruleActivator, activeRuleIndexer,
+    qualityProfileChangeEventService);
+
+  private RulesProfileDto persistedProfile;
+
+  @Before
+  public void setUp() {
+    persistedProfile = newRuleProfileDto(rp -> rp
+      .setIsBuiltIn(true)
+      .setLanguage("xoo")
+      .setRulesUpdatedAt(null));
+    db.getDbClient().qualityProfileDao().insert(db.getSession(), persistedProfile);
+    db.commit();
+  }
+
+  @Test
+  public void activate_new_rules() {
+    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
+    RuleDefinitionDto rule2 = db.rules().insert(r -> r.setLanguage("xoo"));
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
+    newQp.activateRule(rule1.getRepositoryKey(), rule1.getRuleKey()).overrideSeverity(Severity.CRITICAL);
+    newQp.activateRule(rule2.getRepositoryKey(), rule2.getRuleKey()).overrideSeverity(Severity.MAJOR);
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule1, rule2);
+
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
+
+    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThat(activeRules).hasSize(2);
+    assertThatRuleIsNewlyActivated(activeRules, rule1, CRITICAL);
+    assertThatRuleIsNewlyActivated(activeRules, rule2, MAJOR);
+    assertThatProfileIsMarkedAsUpdated(persistedProfile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  @Test
+  public void already_activated_rule_is_updated_in_case_of_differences() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey()).overrideSeverity(Severity.CRITICAL);
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule);
+
+    activateRuleInDb(persistedProfile, rule, BLOCKER);
+
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
+
+    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThat(activeRules).hasSize(1);
+    assertThatRuleIsUpdated(activeRules, rule, CRITICAL);
+    assertThatProfileIsMarkedAsUpdated(persistedProfile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  @Test
+  public void already_activated_rule_is_not_touched_if_no_differences() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey()).overrideSeverity(Severity.CRITICAL);
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule);
+
+    activateRuleInDb(persistedProfile, rule, CRITICAL);
+
+    underTest.update(db.getSession(), builtIn, persistedProfile);
+
+    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThat(activeRules).hasSize(1);
+    assertThatRuleIsUntouched(activeRules, rule, CRITICAL);
+    assertThatProfileIsNotMarkedAsUpdated(persistedProfile);
+    verifyNoInteractions(qualityProfileChangeEventService);
+  }
+
+  @Test
+  public void deactivate_rule_that_is_not_in_built_in_definition_anymore() {
+    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
+    RuleDefinitionDto rule2 = db.rules().insert(r -> r.setLanguage("xoo"));
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
+    newQp.activateRule(rule2.getRepositoryKey(), rule2.getRuleKey()).overrideSeverity(Severity.MAJOR);
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule1, rule2);
+
+    // built-in definition contains only rule2
+    // so rule1 must be deactivated
+    activateRuleInDb(persistedProfile, rule1, CRITICAL);
+
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
+
+    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThat(activeRules).hasSize(1);
+    assertThatRuleIsDeactivated(activeRules, rule1);
+    assertThatProfileIsMarkedAsUpdated(persistedProfile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  @Test
+  public void activate_deactivate_and_update_three_rules_at_the_same_time() {
+    RuleDefinitionDto rule1 = db.rules().insert(r -> r.setLanguage("xoo"));
+    RuleDefinitionDto rule2 = db.rules().insert(r -> r.setLanguage("xoo"));
+    RuleDefinitionDto rule3 = db.rules().insert(r -> r.setLanguage("xoo"));
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
+    newQp.activateRule(rule1.getRepositoryKey(), rule1.getRuleKey()).overrideSeverity(Severity.CRITICAL);
+    newQp.activateRule(rule2.getRepositoryKey(), rule2.getRuleKey()).overrideSeverity(Severity.MAJOR);
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule1, rule2);
+
+    // rule1 must be updated (blocker to critical)
+    // rule2 must be activated
+    // rule3 must be deactivated
+    activateRuleInDb(persistedProfile, rule1, BLOCKER);
+    activateRuleInDb(persistedProfile, rule3, BLOCKER);
+
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
+
+    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThat(activeRules).hasSize(2);
+    assertThatRuleIsUpdated(activeRules, rule1, CRITICAL);
+    assertThatRuleIsNewlyActivated(activeRules, rule2, MAJOR);
+    assertThatRuleIsDeactivated(activeRules, rule3);
+    assertThatProfileIsMarkedAsUpdated(persistedProfile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  // SONAR-10473
+  @Test
+  public void activate_rule_on_built_in_profile_resets_severity_to_default_if_not_overridden() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setSeverity(Severity.MAJOR).setLanguage("xoo"));
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", "xoo");
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule);
+    underTest.update(db.getSession(), builtIn, persistedProfile);
+
+    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThatRuleIsNewlyActivated(activeRules, rule, MAJOR);
+
+    // emulate an upgrade of analyzer that changes the default severity of the rule
+    rule.setSeverity(Severity.MINOR);
+    db.rules().update(rule);
+
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
+    activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThatRuleIsNewlyActivated(activeRules, rule, MINOR);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  @Test
+  public void activate_rule_on_built_in_profile_resets_params_to_default_if_not_overridden() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
+    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile("Sonar way", rule.getLanguage());
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(newQp.language(), newQp.name()), rule);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
+
+    List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThat(activeRules).hasSize(1);
+    assertThatRuleHasParams(db, activeRules.get(0), tuple("min", "10"));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+
+    // emulate an upgrade of analyzer that changes the default value of parameter min
+    ruleParam.setDefaultValue("20");
+    db.getDbClient().ruleDao().updateRuleParam(db.getSession(), rule, ruleParam);
+
+    changes = underTest.update(db.getSession(), builtIn, persistedProfile);
+    activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
+    assertThat(activeRules).hasSize(1);
+    assertThatRuleHasParams(db, activeRules.get(0), tuple("min", "20"));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  @Test
+  public void propagate_activation_to_descendant_profiles() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
+
+    QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
+    QProfileDto childProfile = createChildProfile(profile);
+    QProfileDto grandchildProfile = createChildProfile(childProfile);
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(profile.getName(), profile.getLanguage());
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
+
+    assertThat(changes).hasSize(3);
+    assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, emptyMap());
+    assertThatRuleIsActivated(childProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
+    assertThatRuleIsActivated(grandchildProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  // SONAR-14559
+  @Test
+  public void propagate_rule_update_to_descendant_active_rule() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
+
+    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
+    activateRuleInDb(RulesProfileDto.from(parentProfile), rule, RulePriority.valueOf(Severity.MINOR), null);
+
+    QProfileDto childProfile = createChildProfile(parentProfile);
+    activateRuleInDb(RulesProfileDto.from(childProfile), rule, RulePriority.valueOf(Severity.MINOR), INHERITED);
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(parentProfile.getName(), parentProfile.getLanguage());
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(parentProfile.getLanguage(), parentProfile.getName()), rule);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(parentProfile));
+
+    assertThat(changes).hasSize(2);
+
+    List<ActiveRuleDto> parentActiveRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), RulesProfileDto.from(parentProfile));
+    assertThatRuleIsUpdated(parentActiveRules, rule, RulePriority.BLOCKER, null);
+
+    List<ActiveRuleDto> childActiveRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), RulesProfileDto.from(childProfile));
+    assertThatRuleIsUpdated(childActiveRules, rule, RulePriority.BLOCKER, INHERITED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  @Test
+  public void propagate_rule_param_update_to_descendant_active_rule_params() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
+    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
+
+    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
+    ActiveRuleDto parentActiveRuleDto = activateRuleInDb(RulesProfileDto.from(parentProfile), rule,
+        RulePriority.valueOf(Severity.MINOR), null);
+    activateRuleParamInDb(parentActiveRuleDto, ruleParam, "20");
+
+    QProfileDto childProfile = createChildProfile(parentProfile);
+    ActiveRuleDto childActiveRuleDto = activateRuleInDb(RulesProfileDto.from(childProfile), rule,
+        RulePriority.valueOf(Severity.MINOR), INHERITED);
+    activateRuleParamInDb(childActiveRuleDto, ruleParam, "20");
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(parentProfile.getName(), parentProfile.getLanguage());
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
+    newQp.done();
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(parentProfile.getLanguage(), parentProfile.getName()), rule);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(parentProfile));
+
+    assertThat(changes).hasSize(2);
+
+    assertThatRuleHasParams(db, parentActiveRuleDto, tuple("min", "10"));
+    assertThatRuleHasParams(db, childActiveRuleDto, tuple("min", "10"));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+  }
+
+  @Test
+  public void do_not_load_descendants_if_no_changes() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
+
+    QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
+    QProfileDto childProfile = createChildProfile(profile);
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(profile.getName(), profile.getLanguage());
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
+    newQp.done();
+
+    // first run
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
+    assertThat(changes).hasSize(2).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.ACTIVATED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+
+    // second run, without any input changes
+    RuleActivator ruleActivatorWithoutDescendants = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession) {
+      @Override
+      DescendantProfilesSupplier createDescendantProfilesSupplier(DbSession dbSession) {
+        return (profiles, ruleIds) -> {
+          throw new IllegalStateException("BOOM - descendants should not be loaded");
+        };
+      }
+    };
+    changes = new BuiltInQProfileUpdateImpl(db.getDbClient(), ruleActivatorWithoutDescendants, activeRuleIndexer, qualityProfileChangeEventService)
+      .update(db.getSession(), builtIn, RulesProfileDto.from(profile));
+    assertThat(changes).isEmpty();
+    verifyNoMoreInteractions(qualityProfileChangeEventService);
+  }
+
+  @Test
+  public void propagate_deactivation_to_descendant_profiles() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo"));
+
+    QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
+    QProfileDto childProfile = createChildProfile(profile);
+    QProfileDto grandChildProfile = createChildProfile(childProfile);
+
+    BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile newQp = context.createBuiltInQualityProfile(profile.getName(), profile.getLanguage());
+    newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
+    newQp.done();
+
+    // first run to activate the rule
+    BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
+    assertThat(changes).hasSize(3).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.ACTIVATED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+
+    // second run to deactivate the rule
+    context = new BuiltInQualityProfilesDefinition.Context();
+    NewBuiltInQualityProfile updatedQp = context.createBuiltInQualityProfile(profile.getName(), profile.getLanguage());
+    updatedQp.done();
+    builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
+    changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
+    assertThat(changes).hasSize(3).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.DEACTIVATED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
+
+    assertThatRuleIsDeactivated(profile, rule);
+    assertThatRuleIsDeactivated(childProfile, rule);
+    assertThatRuleIsDeactivated(grandChildProfile, rule);
+  }
+
+  private QProfileDto createChildProfile(QProfileDto parent) {
+    return db.qualityProfiles().insert(p -> p
+      .setLanguage(parent.getLanguage())
+      .setParentKee(parent.getKee())
+      .setName("Child of " + parent.getName()))
+      .setIsBuiltIn(false);
+  }
+
+  private void assertThatRuleIsActivated(QProfileDto profile, RuleDefinitionDto rule, @Nullable List<ActiveRuleChange> changes,
+    String expectedSeverity, @Nullable ActiveRuleInheritance expectedInheritance, Map<String, String> expectedParams) {
+    OrgActiveRuleDto activeRule = db.getDbClient().activeRuleDao().selectByProfile(db.getSession(), profile)
+      .stream()
+      .filter(ar -> ar.getRuleKey().equals(rule.getKey()))
+      .findFirst()
+      .orElseThrow(IllegalStateException::new);
+
+    assertThat(activeRule.getSeverityString()).isEqualTo(expectedSeverity);
+    assertThat(activeRule.getInheritance()).isEqualTo(expectedInheritance != null ? expectedInheritance.name() : null);
+
+    List<ActiveRuleParamDto> params = db.getDbClient().activeRuleDao().selectParamsByActiveRuleUuid(db.getSession(), activeRule.getUuid());
+    assertThat(params).hasSize(expectedParams.size());
+
+    if (changes != null) {
+      ActiveRuleChange change = changes.stream()
+        .filter(c -> c.getActiveRule().getUuid().equals(activeRule.getUuid()))
+        .findFirst().orElseThrow(IllegalStateException::new);
+      assertThat(change.getInheritance()).isEqualTo(expectedInheritance);
+      assertThat(change.getSeverity()).isEqualTo(expectedSeverity);
+      assertThat(change.getType()).isEqualTo(ActiveRuleChange.Type.ACTIVATED);
+    }
+  }
+
+  private static void assertThatRuleHasParams(DbTester db, ActiveRuleDto activeRule, Tuple... expectedParams) {
+    assertThat(db.getDbClient().activeRuleDao().selectParamsByActiveRuleUuid(db.getSession(), activeRule.getUuid()))
+      .extracting(ActiveRuleParamDto::getKey, ActiveRuleParamDto::getValue)
+      .containsExactlyInAnyOrder(expectedParams);
+  }
+
+  private static void assertThatRuleIsNewlyActivated(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule, RulePriority severity) {
+    ActiveRuleDto activeRule = findRule(activeRules, rule).get();
+
+    assertThat(activeRule.getInheritance()).isNull();
+    assertThat(activeRule.getSeverityString()).isEqualTo(severity.name());
+    assertThat(activeRule.getCreatedAt()).isEqualTo(NOW);
+    assertThat(activeRule.getUpdatedAt()).isEqualTo(NOW);
+  }
+
+  private static void assertThatRuleIsUpdated(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule, RulePriority severity, @Nullable ActiveRuleInheritance expectedInheritance) {
+    ActiveRuleDto activeRule = findRule(activeRules, rule).get();
+
+    if (expectedInheritance != null) {
+      assertThat(activeRule.getInheritance()).isEqualTo(expectedInheritance.name());
+    } else {
+      assertThat(activeRule.getInheritance()).isNull();
+    }
+    assertThat(activeRule.getSeverityString()).isEqualTo(severity.name());
+    assertThat(activeRule.getCreatedAt()).isEqualTo(PAST);
+    assertThat(activeRule.getUpdatedAt()).isEqualTo(NOW);
+  }
+
+  private static void assertThatRuleIsUpdated(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule, RulePriority severity) {
+    assertThatRuleIsUpdated(activeRules, rule, severity, null);
+  }
+
+  private static void assertThatRuleIsUpdated(ActiveRuleDto activeRules, RuleDefinitionDto rule, RulePriority severity, @Nullable ActiveRuleInheritance expectedInheritance) {
+    assertThatRuleIsUpdated(singletonList(activeRules), rule, severity, expectedInheritance);
+  }
+
+  private static void assertThatRuleIsUntouched(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule, RulePriority severity) {
+    ActiveRuleDto activeRule = findRule(activeRules, rule).get();
+
+    assertThat(activeRule.getInheritance()).isNull();
+    assertThat(activeRule.getSeverityString()).isEqualTo(severity.name());
+    assertThat(activeRule.getCreatedAt()).isEqualTo(PAST);
+    assertThat(activeRule.getUpdatedAt()).isEqualTo(PAST);
+  }
+
+  private void assertThatRuleIsDeactivated(QProfileDto profile, RuleDefinitionDto rule) {
+    Collection<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRulesAndRuleProfileUuids(
+      db.getSession(), singletonList(rule.getUuid()), singletonList(profile.getRulesProfileUuid()));
+    assertThat(activeRules).isEmpty();
+  }
+
+  private static void assertThatRuleIsDeactivated(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule) {
+    assertThat(findRule(activeRules, rule)).isEmpty();
+  }
+
+  private void assertThatProfileIsMarkedAsUpdated(RulesProfileDto dto) {
+    RulesProfileDto reloaded = db.getDbClient().qualityProfileDao().selectBuiltInRuleProfiles(db.getSession())
+      .stream()
+      .filter(p -> p.getUuid().equals(dto.getUuid()))
+      .findFirst()
+      .get();
+    assertThat(reloaded.getRulesUpdatedAt()).isNotEmpty();
+  }
+
+  private void assertThatProfileIsNotMarkedAsUpdated(RulesProfileDto dto) {
+    RulesProfileDto reloaded = db.getDbClient().qualityProfileDao().selectBuiltInRuleProfiles(db.getSession())
+      .stream()
+      .filter(p -> p.getUuid().equals(dto.getUuid()))
+      .findFirst()
+      .get();
+    assertThat(reloaded.getRulesUpdatedAt()).isNull();
+  }
+
+  private static Optional<ActiveRuleDto> findRule(List<ActiveRuleDto> activeRules, RuleDefinitionDto rule) {
+    return activeRules.stream()
+      .filter(ar -> ar.getRuleKey().equals(rule.getKey()))
+      .findFirst();
+  }
+
+  private ActiveRuleDto activateRuleInDb(RulesProfileDto profile, RuleDefinitionDto rule, RulePriority severity) {
+    return activateRuleInDb(profile, rule, severity, null);
+  }
+
+  private ActiveRuleDto activateRuleInDb(RulesProfileDto ruleProfile, RuleDefinitionDto rule, RulePriority severity, @Nullable ActiveRuleInheritance inheritance) {
+    ActiveRuleDto dto = new ActiveRuleDto()
+      .setKey(ActiveRuleKey.of(ruleProfile, RuleKey.of(rule.getRepositoryKey(), rule.getRuleKey())))
+      .setProfileUuid(ruleProfile.getUuid())
+      .setSeverity(severity.name())
+      .setRuleUuid(rule.getUuid())
+      .setInheritance(inheritance != null ? inheritance.name() : null)
+      .setCreatedAt(PAST)
+      .setUpdatedAt(PAST);
+    db.getDbClient().activeRuleDao().insert(db.getSession(), dto);
+    db.commit();
+    return dto;
+  }
+
+  private void activateRuleParamInDb(ActiveRuleDto activeRuleDto, RuleParamDto ruleParamDto, String value) {
+    ActiveRuleParamDto dto = new ActiveRuleParamDto()
+        .setActiveRuleUuid(activeRuleDto.getUuid())
+        .setRulesParameterUuid(ruleParamDto.getUuid())
+        .setKey(ruleParamDto.getName())
+        .setValue(value);
+    db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRuleDto, dto);
+    db.commit();
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQualityProfilesUpdateListenerTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQualityProfilesUpdateListenerTest.java
new file mode 100644 (file)
index 0000000..74c5555
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import org.assertj.core.groups.Tuple;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.core.util.Uuids;
+import org.sonar.db.qualityprofile.ActiveRuleKey;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.builtin.BuiltInQPChangeNotificationBuilder.Profile;
+
+import static java.util.Arrays.asList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.sonar.core.config.CorePropertyDefinitions.DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES;
+import static org.sonar.server.language.LanguageTesting.newLanguage;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED;
+
+public class BuiltInQualityProfilesUpdateListenerTest {
+
+  private NotificationManager notificationManager = mock(NotificationManager.class);
+  private MapSettings settings = new MapSettings();
+
+  @Test
+  public void add_profile_to_notification_for_added_rules() {
+    enableNotificationInGlobalSettings();
+    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
+    Languages languages = new Languages();
+    Tuple expectedTuple = addProfile(profiles, languages, ACTIVATED);
+
+    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
+    underTest.onChange(profiles, 0, 1);
+
+    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
+    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
+    verifyNoMoreInteractions(notificationManager);
+    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
+      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getNewRules)
+      .containsExactlyInAnyOrder(expectedTuple);
+  }
+
+  @Test
+  public void add_profile_to_notification_for_updated_rules() {
+    enableNotificationInGlobalSettings();
+    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
+    Languages languages = new Languages();
+    Tuple expectedTuple = addProfile(profiles, languages, UPDATED);
+
+    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
+    underTest.onChange(profiles, 0, 1);
+
+    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
+    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
+    verifyNoMoreInteractions(notificationManager);
+    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
+      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getUpdatedRules)
+      .containsExactlyInAnyOrder(expectedTuple);
+  }
+
+  @Test
+  public void add_profile_to_notification_for_removed_rules() {
+    enableNotificationInGlobalSettings();
+    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
+    Languages languages = new Languages();
+    Tuple expectedTuple = addProfile(profiles, languages, DEACTIVATED);
+
+    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
+    underTest.onChange(profiles, 0, 1);
+
+    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
+    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
+    verifyNoMoreInteractions(notificationManager);
+    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
+      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getRemovedRules)
+      .containsExactlyInAnyOrder(expectedTuple);
+  }
+
+  @Test
+  public void add_multiple_profiles_to_notification() {
+    enableNotificationInGlobalSettings();
+    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
+    Languages languages = new Languages();
+    Tuple expectedTuple1 = addProfile(profiles, languages, ACTIVATED);
+    Tuple expectedTuple2 = addProfile(profiles, languages, ACTIVATED);
+
+    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
+    underTest.onChange(profiles, 0, 1);
+
+    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
+    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
+    verifyNoMoreInteractions(notificationManager);
+    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
+      .extracting(Profile::getProfileName, Profile::getLanguageKey, Profile::getLanguageName, Profile::getNewRules)
+      .containsExactlyInAnyOrder(expectedTuple1, expectedTuple2);
+  }
+
+  @Test
+  public void add_start_and_end_dates_to_notification() {
+    enableNotificationInGlobalSettings();
+    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
+    Languages languages = new Languages();
+    addProfile(profiles, languages, ACTIVATED);
+    long startDate = 10_000_000_000L;
+    long endDate = 15_000_000_000L;
+
+    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
+    underTest.onChange(profiles, startDate, endDate);
+
+    ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
+    verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
+    verifyNoMoreInteractions(notificationManager);
+    assertThat(BuiltInQPChangeNotificationBuilder.parse(notificationArgumentCaptor.getValue()).getProfiles())
+      .extracting(Profile::getStartDate, Profile::getEndDate)
+      .containsExactlyInAnyOrder(tuple(startDate, endDate));
+  }
+
+  @Test
+  public void avoid_notification_if_configured_in_settings() {
+    settings.setProperty(DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES, true);
+    Multimap<QProfileName, ActiveRuleChange> profiles = ArrayListMultimap.create();
+    Languages languages = new Languages();
+    addProfile(profiles, languages, ACTIVATED);
+
+    BuiltInQualityProfilesUpdateListener underTest = new BuiltInQualityProfilesUpdateListener(notificationManager, languages, settings.asConfig());
+    underTest.onChange(profiles, 0, 1);
+
+    verifyZeroInteractions(notificationManager);
+  }
+
+  private Tuple addProfile(Multimap<QProfileName, ActiveRuleChange> profiles, Languages languages, ActiveRuleChange.Type type) {
+    String profileName = randomLowerCaseText();
+    String ruleUuid1 = Uuids.createFast();
+    String ruleUuid2 = Uuids.createFast();
+    Language language = newLanguage(randomLowerCaseText(), randomLowerCaseText());
+    languages.add(language);
+    profiles.putAll(new QProfileName(language.getKey(), profileName),
+      asList(new ActiveRuleChange(
+        type,
+        ActiveRuleKey.parse("qp:repo:rule1"), new RuleDefinitionDto().setUuid(ruleUuid1)),
+        new ActiveRuleChange(type, ActiveRuleKey.parse("qp:repo:rule2"), new RuleDefinitionDto().setUuid(ruleUuid2))));
+    return tuple(profileName, language.getKey(), language.getName(), 2);
+  }
+
+  private static String randomLowerCaseText() {
+    return randomAlphanumeric(20).toLowerCase();
+  }
+
+  private void enableNotificationInGlobalSettings() {
+    settings.setProperty(DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES, false);
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/RuleActivatorTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/RuleActivatorTest.java
new file mode 100644 (file)
index 0000000..a4feaed
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.qualityprofile.builtin;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import javax.annotation.Nullable;
+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.Severity;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleKey;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.qualityprofile.RulesProfileDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.ActiveRuleInheritance;
+import org.sonar.server.qualityprofile.RuleActivation;
+import org.sonar.server.qualityprofile.builtin.DescendantProfilesSupplier.Result;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.util.IntegerTypeValidation;
+import org.sonar.server.util.StringTypeValidation;
+import org.sonar.server.util.TypeValidations;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.sonar.server.qualityprofile.ActiveRuleInheritance.INHERITED;
+import static org.sonar.server.qualityprofile.ActiveRuleInheritance.OVERRIDES;
+
+/**
+ * Class org.sonar.server.qualityprofile.builtin.RuleActivator is mostly covered in
+ * org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdateImplTest
+ */
+public class RuleActivatorTest {
+  @Rule
+  public final DbTester db = DbTester.create();
+
+  @Rule
+  public final UserSessionRule userSession = UserSessionRule.standalone();
+
+  private static final long NOW = 1_000;
+  private static final long PAST = NOW - 100;
+  private final System2 system2 = new TestSystem2().setNow(NOW);
+  private final TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
+
+  private final QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
+  private final RuleActivator underTest = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
+
+  @Test
+  public void reset_overridden_active_rule() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
+    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
+
+    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
+    ActiveRuleDto parentActiveRuleDto = activateRuleInDb(RulesProfileDto.from(parentProfile), rule,
+      RulePriority.valueOf(Severity.BLOCKER), null);
+    ActiveRuleParamDto parentActiveRuleParam = activateRuleParamInDb(parentActiveRuleDto, ruleParam, "10");
+
+    QProfileDto childProfile = createChildProfile(parentProfile);
+    ActiveRuleDto childActiveRuleDto = activateRuleInDb(RulesProfileDto.from(childProfile), rule,
+      RulePriority.valueOf(Severity.MINOR), OVERRIDES);
+    ActiveRuleParamDto childActiveRuleParam = activateRuleParamInDb(childActiveRuleDto, ruleParam, "15");
+
+    DbSession session = db.getSession();
+    RuleActivation resetRequest = RuleActivation.createReset(rule.getUuid());
+    RuleActivationContext context = new RuleActivationContext.Builder()
+      .setProfiles(asList(parentProfile, childProfile))
+      .setBaseProfile(RulesProfileDto.from(childProfile))
+      .setDate(NOW)
+      .setDescendantProfilesSupplier((profiles, ruleUuids) -> new Result(emptyList(), emptyList(), emptyList()))
+      .setRules(singletonList(rule))
+      .setRuleParams(singletonList(ruleParam))
+      .setActiveRules(asList(parentActiveRuleDto, childActiveRuleDto))
+      .setActiveRuleParams(asList(parentActiveRuleParam, childActiveRuleParam))
+      .build();
+
+    List<ActiveRuleChange> result = underTest.activate(session, resetRequest, context);
+
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).getParameters()).containsEntry("min", "10");
+    assertThat(result.get(0).getSeverity()).isEqualTo(Severity.BLOCKER);
+    assertThat(result.get(0).getInheritance()).isEqualTo(ActiveRuleInheritance.INHERITED);
+  }
+
+  @Test
+  public void request_new_severity_and_param_for_child_rule() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
+    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
+
+    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
+    ActiveRuleDto parentActiveRuleDto = activateRuleInDb(RulesProfileDto.from(parentProfile), rule,
+        RulePriority.valueOf(Severity.BLOCKER), null);
+    ActiveRuleParamDto parentActiveRuleParam = activateRuleParamInDb(parentActiveRuleDto, ruleParam, "10");
+
+    QProfileDto childProfile = createChildProfile(parentProfile);
+    ActiveRuleDto childActiveRuleDto = activateRuleInDb(RulesProfileDto.from(childProfile), rule,
+        RulePriority.valueOf(Severity.BLOCKER), INHERITED);
+    ActiveRuleParamDto childActiveRuleParam = activateRuleParamInDb(childActiveRuleDto, ruleParam, "10");
+
+    DbSession session = db.getSession();
+    RuleActivation resetRequest = RuleActivation.create(rule.getUuid(), Severity.MINOR, ImmutableMap.of("min", "15"));
+    RuleActivationContext context = new RuleActivationContext.Builder()
+        .setProfiles(asList(parentProfile, childProfile))
+        .setBaseProfile(RulesProfileDto.from(childProfile))
+        .setDate(NOW)
+        .setDescendantProfilesSupplier((profiles, ruleUuids) -> new Result(emptyList(), emptyList(), emptyList()))
+        .setRules(singletonList(rule))
+        .setRuleParams(singletonList(ruleParam))
+        .setActiveRules(asList(parentActiveRuleDto, childActiveRuleDto))
+        .setActiveRuleParams(asList(parentActiveRuleParam, childActiveRuleParam))
+        .build();
+
+    List<ActiveRuleChange> result = underTest.activate(session, resetRequest, context);
+
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).getParameters()).containsEntry("min", "15");
+    assertThat(result.get(0).getSeverity()).isEqualTo(Severity.MINOR);
+    assertThat(result.get(0).getInheritance()).isEqualTo(OVERRIDES);
+  }
+
+  @Test
+  public void set_severity_and_param_for_child_rule_when_activating() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo").setSeverity(Severity.BLOCKER));
+    RuleParamDto ruleParam = db.rules().insertRuleParam(rule, p -> p.setName("min").setDefaultValue("10"));
+
+    QProfileDto parentProfile = db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage()).setIsBuiltIn(true));
+
+    QProfileDto childProfile = createChildProfile(parentProfile);
+
+    DbSession session = db.getSession();
+    RuleActivation resetRequest = RuleActivation.create(rule.getUuid());
+    RuleActivationContext context = new RuleActivationContext.Builder()
+        .setProfiles(asList(parentProfile, childProfile))
+        .setBaseProfile(RulesProfileDto.from(childProfile))
+        .setDate(NOW)
+        .setDescendantProfilesSupplier((profiles, ruleUuids) -> new Result(emptyList(), emptyList(), emptyList()))
+        .setRules(singletonList(rule))
+        .setRuleParams(singletonList(ruleParam))
+        .setActiveRules(emptyList())
+        .setActiveRuleParams(emptyList())
+        .build();
+
+    List<ActiveRuleChange> result = underTest.activate(session, resetRequest, context);
+
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).getParameters()).containsEntry("min", "10");
+    assertThat(result.get(0).getSeverity()).isEqualTo(Severity.BLOCKER);
+    assertThat(result.get(0).getInheritance()).isNull();
+  }
+
+  @Test
+  public void fail_if_rule_language_doesnt_match_qp() {
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setLanguage("xoo")
+      .setRepositoryKey("repo")
+      .setRuleKey("rule")
+      .setSeverity(Severity.BLOCKER));
+    QProfileDto qp = db.qualityProfiles().insert(p -> p.setLanguage("xoo2").setKee("qp").setIsBuiltIn(true));
+
+    DbSession session = db.getSession();
+    RuleActivation resetRequest = RuleActivation.create(rule.getUuid());
+    RuleActivationContext context = new RuleActivationContext.Builder()
+      .setProfiles(singletonList(qp))
+      .setBaseProfile(RulesProfileDto.from(qp))
+      .setDate(NOW)
+      .setDescendantProfilesSupplier((profiles, ruleUuids) -> new Result(emptyList(), emptyList(), emptyList()))
+      .setRules(singletonList(rule))
+      .setRuleParams(emptyList())
+      .setActiveRules(emptyList())
+      .setActiveRuleParams(emptyList())
+      .build();
+
+
+    assertThrows("xoo rule repo:rule cannot be activated on xoo2 profile qp", BadRequestException.class,
+      () -> underTest.activate(session, resetRequest, context));
+  }
+
+
+  private ActiveRuleDto activateRuleInDb(RulesProfileDto ruleProfile, RuleDefinitionDto rule, RulePriority severity, @Nullable ActiveRuleInheritance inheritance) {
+    ActiveRuleDto dto = new ActiveRuleDto()
+      .setKey(ActiveRuleKey.of(ruleProfile, RuleKey.of(rule.getRepositoryKey(), rule.getRuleKey())))
+      .setProfileUuid(ruleProfile.getUuid())
+      .setSeverity(severity.name())
+      .setRuleUuid(rule.getUuid())
+      .setInheritance(inheritance != null ? inheritance.name() : null)
+      .setCreatedAt(PAST)
+      .setUpdatedAt(PAST);
+    db.getDbClient().activeRuleDao().insert(db.getSession(), dto);
+    db.commit();
+    return dto;
+  }
+
+  private ActiveRuleParamDto activateRuleParamInDb(ActiveRuleDto activeRuleDto, RuleParamDto ruleParamDto, String value) {
+    ActiveRuleParamDto dto = new ActiveRuleParamDto()
+      .setActiveRuleUuid(activeRuleDto.getUuid())
+      .setRulesParameterUuid(ruleParamDto.getUuid())
+      .setKey(ruleParamDto.getName())
+      .setValue(value);
+    db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRuleDto, dto);
+    db.commit();
+    return dto;
+  }
+
+  private QProfileDto createChildProfile(QProfileDto parent) {
+    return db.qualityProfiles().insert(p -> p
+      .setLanguage(parent.getLanguage())
+      .setParentKee(parent.getKee())
+      .setName("Child of " + parent.getName()))
+      .setIsBuiltIn(false);
+  }
+}
index ffed401c241ddb3fdf934c58e5ace522c77517e8..52a39342278fdeed085cf8ff56d7b7e701480552 100644 (file)
@@ -36,15 +36,13 @@ import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.language.LanguageTesting;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
 
 import static java.lang.String.format;
-import static java.util.Optional.empty;
-import static java.util.Optional.of;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
@@ -94,7 +92,7 @@ public class AddProjectActionTest {
     assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT);
 
     assertProjectIsAssociatedToProfile(project, profile);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(profile), empty());
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, profile, null);
   }
 
   @Test
@@ -108,7 +106,7 @@ public class AddProjectActionTest {
     call(project, qualityProfile);
 
     assertProjectIsAssociatedToProfile(project, qualityProfile);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(qualityProfile), empty());
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, qualityProfile, null);
   }
 
   @Test
@@ -125,7 +123,7 @@ public class AddProjectActionTest {
 
     assertProjectIsNotAssociatedToProfile(project, profile1);
     assertProjectIsAssociatedToProfile(project, profile2);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(profile2), of(profile1));
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, profile2, profile1);
   }
 
   @Test
@@ -141,7 +139,7 @@ public class AddProjectActionTest {
 
     assertProjectIsAssociatedToProfile(project, profile3Language1);
     assertProjectIsAssociatedToProfile(project, profile2Language2);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(profile3Language1), of(profile1Language1));
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, profile3Language1, profile1Language1);
   }
 
   @Test
@@ -153,7 +151,7 @@ public class AddProjectActionTest {
     call(project, profile);
 
     assertProjectIsAssociatedToProfile(project, profile);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(profile), empty());
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, profile, null);
   }
 
   @Test
index f2c2046719dcb254a851c41f6bd3f5bc6eeaaab4..e09e88d9179368c80fe34d6d8ab555a888cd3418 100644 (file)
@@ -48,9 +48,9 @@ import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.language.LanguageTesting;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.QProfileTreeImpl;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
-import org.sonar.server.qualityprofile.RuleActivator;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleIndexer;
index 51800d9ed99e58c4d7d1721468e66cb8e17cbdd2..3cd0742ec30061e54f102b034c6a5566b2e3fc39 100644 (file)
@@ -42,12 +42,12 @@ import org.sonar.db.rule.RuleTesting;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.QProfileExporters;
 import org.sonar.server.qualityprofile.QProfileFactoryImpl;
 import org.sonar.server.qualityprofile.QProfileRules;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
-import org.sonar.server.qualityprofile.RuleActivator;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleIndexer;
index 8204ffa75637ec9497f9ec034c4dd78cb1868e70..f8579c35740097e263d0fa2fb45a237db56d88bf 100644 (file)
@@ -41,13 +41,13 @@ import org.sonar.db.rule.RuleTesting;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.QProfileRules;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
 import org.sonar.server.qualityprofile.QProfileTree;
 import org.sonar.server.qualityprofile.QProfileTreeImpl;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.RuleActivation;
-import org.sonar.server.qualityprofile.RuleActivator;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleIndexer;
index fc1a2256dc7edeb4af1245f31eeb202b692da11e..7159997ddc11056564cffdad782b6b55b4757181 100644 (file)
@@ -41,10 +41,10 @@ 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.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.QProfileRules;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
-import org.sonar.server.qualityprofile.RuleActivator;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleIndexer;
index 48d36e29b858e7cdc370894180e5edefa67f6c06..0fd260ed919b8f259a399c1baf92cdbb3a71e031 100644 (file)
@@ -39,15 +39,13 @@ import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.language.LanguageTesting;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
 
 import static java.lang.String.format;
-import static java.util.Optional.empty;
-import static java.util.Optional.of;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.verify;
@@ -103,7 +101,7 @@ public class RemoveProjectActionTest {
 
     assertProjectIsNotAssociatedToProfile(project, profileLang1);
     assertProjectIsAssociatedToProfile(project, profileLang2);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, empty(), of(profileLang1));
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, null, profileLang1);
   }
 
   @Test
@@ -117,7 +115,7 @@ public class RemoveProjectActionTest {
     assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT);
 
     assertProjectIsNotAssociatedToProfile(project, profile);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, empty(), of(profile));
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, null, profile);
   }
 
   @Test
@@ -130,7 +128,7 @@ public class RemoveProjectActionTest {
     call(project, profile);
 
     assertProjectIsNotAssociatedToProfile(project, profile);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, empty(), of(profile));
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, null, profile);
   }
 
   @Test
@@ -145,7 +143,7 @@ public class RemoveProjectActionTest {
     call(project, profile);
 
     assertProjectIsNotAssociatedToProfile(project, profile);
-    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, empty(), of(profile));
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, null, profile);
   }
 
   @Test
index d0026ee115f46adcdef8714b33ab685d4ac7419d..f236266c8d1fbfbad760d8149733d55dc77aa88b 100644 (file)
@@ -45,12 +45,12 @@ import org.sonar.db.user.UserDto;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.language.LanguageTesting;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.ActiveRuleChange;
 import org.sonar.server.qualityprofile.QProfileRules;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.RuleActivation;
-import org.sonar.server.qualityprofile.RuleActivator;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleIndexer;
index c4504d656c9296304f5a60fec73cc62cfced3e92..9a89c83b8e0b348fe80f57b272e1e29f5b1e6982 100644 (file)
@@ -186,10 +186,10 @@ import org.sonar.server.qualitygate.ProjectsInWarningModule;
 import org.sonar.server.qualitygate.QualityGateModule;
 import org.sonar.server.qualitygate.notification.QGChangeNotificationHandler;
 import org.sonar.server.qualitygate.ws.QualityGateWsModule;
-import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationHandler;
-import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationTemplate;
-import org.sonar.server.qualityprofile.BuiltInQProfileRepositoryImpl;
-import org.sonar.server.qualityprofile.DistributedRuleActivatorEventsDistributor;
+import org.sonar.server.qualityprofile.builtin.BuiltInQPChangeNotificationHandler;
+import org.sonar.server.qualityprofile.builtin.BuiltInQPChangeNotificationTemplate;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileRepositoryImpl;
+import org.sonar.server.pushapi.qualityprofile.DistributedRuleActivatorEventsDistributor;
 import org.sonar.server.qualityprofile.QProfileBackuperImpl;
 import org.sonar.server.qualityprofile.QProfileComparison;
 import org.sonar.server.qualityprofile.QProfileCopier;
@@ -199,9 +199,9 @@ import org.sonar.server.qualityprofile.QProfileParser;
 import org.sonar.server.qualityprofile.QProfileResetImpl;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
 import org.sonar.server.qualityprofile.QProfileTreeImpl;
-import org.sonar.server.qualityprofile.QualityProfileChangeEventServiceImpl;
-import org.sonar.server.qualityprofile.RuleActivator;
-import org.sonar.server.qualityprofile.StandaloneRuleActivatorEventsDistributor;
+import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventServiceImpl;
+import org.sonar.server.qualityprofile.builtin.RuleActivator;
+import org.sonar.server.pushapi.qualityprofile.StandaloneRuleActivatorEventsDistributor;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.qualityprofile.ws.QProfilesWsModule;
 import org.sonar.server.root.ws.RootWsModule;
index 04819b8373eb8941e3695c8b7a039edbb3cd8f0d..7aec25a435449fdb79ced7d41e71cfd7dc2cf222 100644 (file)
@@ -32,10 +32,10 @@ import org.sonar.server.plugins.DetectPluginChange;
 import org.sonar.server.plugins.PluginConsentVerifier;
 import org.sonar.server.qualitygate.ProjectsInWarningDaemon;
 import org.sonar.server.qualitygate.RegisterQualityGates;
-import org.sonar.server.qualityprofile.BuiltInQProfileInsertImpl;
-import org.sonar.server.qualityprofile.BuiltInQProfileLoader;
-import org.sonar.server.qualityprofile.BuiltInQProfileUpdateImpl;
-import org.sonar.server.qualityprofile.BuiltInQualityProfilesUpdateListener;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileInsertImpl;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileLoader;
+import org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdateImpl;
+import org.sonar.server.qualityprofile.builtin.BuiltInQualityProfilesUpdateListener;
 import org.sonar.server.qualityprofile.RegisterQualityProfiles;
 import org.sonar.server.rule.RegisterRules;
 import org.sonar.server.rule.WebServerRuleFinder;
index 016a08ec488d9f4148b9d22439c88b5fb2dbfe9d..60de8040b9763b95a24974aeb3e1d49441139972 100644 (file)
  */
 package org.sonar.core.util;
 
-public class RuleChange {
+import java.io.Serializable;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class RuleChange implements Serializable {
   String key;
   String language;
   String templateKey;
@@ -34,11 +38,12 @@ public class RuleChange {
     this.key = key;
   }
 
+  @CheckForNull
   public String getLanguage() {
     return language;
   }
 
-  public void setLanguage(String language) {
+  public void setLanguage(@Nullable String language) {
     this.language = language;
   }
 
@@ -50,11 +55,12 @@ public class RuleChange {
     this.templateKey = templateKey;
   }
 
+  @CheckForNull
   public String getSeverity() {
     return severity;
   }
 
-  public void setSeverity(String severity) {
+  public void setSeverity(@Nullable String severity) {
     this.severity = severity;
   }
 
index 2a7d4873836055beb341fdd3b3148184677f77e4..5c9825a0522ec7206c9f014d2b4afe58348dd9ec 100644 (file)
@@ -23,9 +23,10 @@ import java.io.Serializable;
 
 public class RuleSetChangeEvent implements Serializable {
 
-  private final String event = "RuleSetChange";
+  private static final String EVENT = "RuleSetChange";
 
   private String[] projects;
+  private String language;
   private RuleChange[] activatedRules;
   private RuleChange[] deactivatedRules;
 
@@ -33,6 +34,10 @@ public class RuleSetChangeEvent implements Serializable {
     this.projects = projects;
     this.activatedRules = activatedRules;
     this.deactivatedRules = deactivatedRules;
+    if (activatedRules.length == 0 && deactivatedRules.length == 0) {
+      throw new IllegalArgumentException("Can't create RuleSetChangeEvent without any rules that have changed");
+    }
+    this.language = activatedRules.length > 0 ? activatedRules[0].getLanguage() : deactivatedRules[0].getLanguage();
   }
 
   public void setProjects(String[] projects) {
@@ -48,13 +53,17 @@ public class RuleSetChangeEvent implements Serializable {
   }
 
   public String getEvent() {
-    return event;
+    return EVENT;
   }
 
   public String[] getProjects() {
     return projects;
   }
 
+  public String getLanguage() {
+    return language;
+  }
+
   public RuleChange[] getActivatedRules() {
     return activatedRules;
   }
diff --git a/sonar-core/src/test/java/org/sonar/core/util/RuleSetChangeEventTest.java b/sonar-core/src/test/java/org/sonar/core/util/RuleSetChangeEventTest.java
new file mode 100644 (file)
index 0000000..7116658
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.core.util;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class RuleSetChangeEventTest {
+
+  @Test
+  public void getLanguage_givenNoDeactivatedRules_languageIsCorrectlyIdentified() {
+    String[] projects = {"sonarqube"};
+    RuleChange[] activatedRules = {createRuleChange("java")};
+    RuleChange[] deactivatedRules = {};
+    RuleSetChangeEvent event = new RuleSetChangeEvent(projects, activatedRules, deactivatedRules);
+
+    String language = event.getLanguage();
+
+    assertThat(language).isEqualTo("java");
+  }
+
+  @Test
+  public void getLanguage_givenNoActivatedRules_languageIsCorrectlyIdentified() {
+    String[] projects = {"sonarqube"};
+    RuleChange[] activatedRules = {};
+    RuleChange[] deactivatedRules = {createRuleChange("java")};
+    RuleSetChangeEvent event = new RuleSetChangeEvent(projects, activatedRules, deactivatedRules);
+
+    String language = event.getLanguage();
+
+    assertThat(language).isEqualTo("java");
+  }
+
+  @Test
+  public void getLanguage_givenBothArraysEmpty_throwException() {
+    String[] projects = {"sonarqube"};
+    RuleChange[] activatedRules = {};
+    RuleChange[] deactivatedRules = {};
+
+    assertThatThrownBy(() -> new RuleSetChangeEvent(projects, activatedRules, deactivatedRules))
+      .isInstanceOf(IllegalArgumentException.class);
+  }
+
+  private RuleChange createRuleChange(String language) {
+    RuleChange ruleChange = new RuleChange();
+    ruleChange.setLanguage(language);
+    return ruleChange;
+  }
+}