]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20021 reducing the number of responsiblities and renaming of RegisterRules...
authorlukasz-jarocki-sonarsource <lukasz.jarocki@sonarsource.com>
Thu, 3 Aug 2023 07:53:26 +0000 (09:53 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Aug 2023 20:02:48 +0000 (20:02 +0000)
14 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java [deleted file]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolver.java
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java [deleted file]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesKeyVerifier.java [new file with mode: 0644]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrant.java [new file with mode: 0644]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java [new file with mode: 0644]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/SingleDeprecatedRuleKey.java [new file with mode: 0644]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/StartupRuleUpdater.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java [deleted file]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java [deleted file]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrantIT.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/SingleDeprecatedRuleKeyTest.java [new file with mode: 0644]
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java

index b01a42201692ff0ebc6adfd37f07c8513d558eb5..ca6e20244c1ca069f8c581ac1f06ca4ee2c84b72 100644 (file)
@@ -37,6 +37,11 @@ import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.rules.CleanCodeAttribute;
 import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbSession;
 import org.sonar.db.issue.ImpactDto;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -44,6 +49,7 @@ import static java.lang.String.format;
 import static java.util.Arrays.asList;
 import static java.util.Collections.emptySet;
 import static java.util.Optional.ofNullable;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
 
 public class RuleDto {
@@ -652,6 +658,48 @@ public class RuleDto {
     return strings == null || strings.isEmpty() ? null : String.join(",", strings);
   }
 
+  public static RuleDto from(RulesDefinition.Rule ruleDef, Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos, UuidFactory uuidFactory,
+    long now) {
+    RuleDto ruleDto = new RuleDto()
+      .setUuid(uuidFactory.create())
+      .setRuleKey(RuleKey.of(ruleDef.repository().key(), ruleDef.key()))
+      .setPluginKey(ruleDef.pluginKey())
+      .setIsTemplate(ruleDef.template())
+      .setConfigKey(ruleDef.internalKey())
+      .setLanguage(ruleDef.repository().language())
+      .setName(ruleDef.name())
+      .setSeverity(ruleDef.severity())
+      .setStatus(ruleDef.status())
+      .setGapDescription(ruleDef.gapDescription())
+      .setSystemTags(ruleDef.tags())
+      .setSecurityStandards(ruleDef.securityStandards())
+      .setType(RuleType.valueOf(ruleDef.type().name()))
+      .setScope(Scope.valueOf(ruleDef.scope().name()))
+      .setIsExternal(ruleDef.repository().isExternal())
+      .setIsAdHoc(false)
+      .setCreatedAt(now)
+      .setUpdatedAt(now)
+      .setEducationPrinciples(ruleDef.educationPrincipleKeys());
+
+    if (isNotEmpty(ruleDef.htmlDescription())) {
+      ruleDto.setDescriptionFormat(Format.HTML);
+    } else if (isNotEmpty(ruleDef.markdownDescription())) {
+      ruleDto.setDescriptionFormat(Format.MARKDOWN);
+    }
+
+    ruleDescriptionSectionDtos.forEach(ruleDto::addRuleDescriptionSectionDto);
+
+    DebtRemediationFunction debtRemediationFunction = ruleDef.debtRemediationFunction();
+    if (debtRemediationFunction != null) {
+      ruleDto.setDefRemediationFunction(debtRemediationFunction.type().name());
+      ruleDto.setDefRemediationGapMultiplier(debtRemediationFunction.gapMultiplier());
+      ruleDto.setDefRemediationBaseEffort(debtRemediationFunction.baseEffort());
+      ruleDto.setGapDescription(ruleDef.gapDescription());
+    }
+
+    return ruleDto;
+  }
+
   @Override
   public boolean equals(Object obj) {
     if (!(obj instanceof RuleDto)) {
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java
deleted file mode 100644 (file)
index 81bc584..0000000
+++ /dev/null
@@ -1,903 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.rule;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
-import org.sonar.api.Startable;
-import org.sonar.api.resources.Languages;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rule.RuleScope;
-import org.sonar.api.rule.RuleStatus;
-import org.sonar.api.rules.RuleType;
-import org.sonar.api.server.debt.DebtRemediationFunction;
-import org.sonar.api.server.rule.RulesDefinition;
-import org.sonar.api.utils.System2;
-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.UuidFactory;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.rule.DeprecatedRuleKeyDto;
-import org.sonar.db.rule.RuleDescriptionSectionDto;
-import org.sonar.db.rule.RuleDto;
-import org.sonar.db.rule.RuleDto.Format;
-import org.sonar.db.rule.RuleDto.Scope;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.db.rule.RuleRepositoryDto;
-import org.sonar.server.es.metadata.MetadataIndex;
-import org.sonar.server.qualityprofile.ActiveRuleChange;
-import org.sonar.server.qualityprofile.QProfileRules;
-import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
-import org.sonar.server.rule.index.RuleIndexer;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.collect.Sets.difference;
-import static com.google.common.collect.Sets.intersection;
-import static java.lang.String.format;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.emptySet;
-import static java.util.Collections.unmodifiableMap;
-import static org.apache.commons.lang.StringUtils.isNotEmpty;
-
-/**
- * Register rules at server startup
- */
-public class RegisterRules implements Startable {
-
-  private static final Logger LOG = Loggers.get(RegisterRules.class);
-
-  private final RuleDefinitionsLoader defLoader;
-  private final QProfileRules qProfileRules;
-  private final DbClient dbClient;
-  private final RuleIndexer ruleIndexer;
-  private final ActiveRuleIndexer activeRuleIndexer;
-  private final Languages languages;
-  private final System2 system2;
-  private final WebServerRuleFinder webServerRuleFinder;
-  private final UuidFactory uuidFactory;
-  private final MetadataIndex metadataIndex;
-  private final RuleDescriptionSectionsGeneratorResolver ruleDescriptionSectionsGeneratorResolver;
-
-
-  public RegisterRules(RuleDefinitionsLoader defLoader, QProfileRules qProfileRules, DbClient dbClient, RuleIndexer ruleIndexer,
-    ActiveRuleIndexer activeRuleIndexer, Languages languages, System2 system2,
-    WebServerRuleFinder webServerRuleFinder, UuidFactory uuidFactory, MetadataIndex metadataIndex,
-    RuleDescriptionSectionsGeneratorResolver ruleDescriptionSectionsGeneratorResolver) {
-    this.defLoader = defLoader;
-    this.qProfileRules = qProfileRules;
-    this.dbClient = dbClient;
-    this.ruleIndexer = ruleIndexer;
-    this.activeRuleIndexer = activeRuleIndexer;
-    this.languages = languages;
-    this.system2 = system2;
-    this.webServerRuleFinder = webServerRuleFinder;
-    this.uuidFactory = uuidFactory;
-    this.metadataIndex = metadataIndex;
-    this.ruleDescriptionSectionsGeneratorResolver = ruleDescriptionSectionsGeneratorResolver;
-  }
-
-  @Override
-  public void start() {
-    Profiler profiler = Profiler.create(LOG).startInfo("Register rules");
-    try (DbSession dbSession = dbClient.openSession(true)) {
-      RulesDefinition.Context ruleDefinitionContext = defLoader.load();
-      List<RulesDefinition.Repository> repositories = ruleDefinitionContext.repositories();
-      RegisterRulesContext registerRulesContext = createRegisterRulesContext(dbSession);
-
-      verifyRuleKeyConsistency(repositories, registerRulesContext);
-
-      for (RulesDefinition.ExtendedRepository repoDef : repositories) {
-        if (languages.get(repoDef.language()) != null) {
-          registerRules(registerRulesContext, repoDef.rules(), dbSession);
-          dbSession.commit();
-        }
-      }
-      processRemainingDbRules(registerRulesContext, dbSession);
-      List<ActiveRuleChange> changes = removeActiveRulesOnStillExistingRepositories(dbSession, registerRulesContext, repositories);
-      dbSession.commit();
-
-      persistRepositories(dbSession, ruleDefinitionContext.repositories());
-      // FIXME lack of resiliency, active rules index is corrupted if rule index fails
-      // to be updated. Only a single DB commit should be executed.
-      ruleIndexer.commitAndIndex(dbSession, registerRulesContext.getAllModified().map(RuleDto::getUuid).collect(Collectors.toSet()));
-      changes.forEach(arChange -> dbClient.qProfileChangeDao().insert(dbSession, arChange.toDto(null)));
-      activeRuleIndexer.commitAndIndex(dbSession, changes);
-      registerRulesContext.getRenamed().forEach(e -> LOG.info("Rule {} re-keyed to {}", e.getValue(), e.getKey().getKey()));
-      profiler.stopDebug();
-
-      if (!registerRulesContext.hasDbRules()) {
-        Stream.concat(ruleIndexer.getIndexTypes().stream(), activeRuleIndexer.getIndexTypes().stream())
-          .forEach(t -> metadataIndex.setInitialized(t, true));
-      }
-
-      webServerRuleFinder.startCaching();
-    }
-  }
-
-  private RegisterRulesContext createRegisterRulesContext(DbSession dbSession) {
-    Map<RuleKey, RuleDto> allRules = dbClient.ruleDao().selectAll(dbSession).stream()
-      .collect(Collectors.toMap(RuleDto::getKey, Function.identity()));
-    Map<String, Set<SingleDeprecatedRuleKey>> existingDeprecatedKeysById = loadDeprecatedRuleKeys(dbSession);
-    Map<String, List<RuleParamDto>> ruleParamsByRuleUuid = loadAllRuleParameters(dbSession);
-    return new RegisterRulesContext(allRules, existingDeprecatedKeysById, ruleParamsByRuleUuid);
-  }
-
-  private Map<String, List<RuleParamDto>> loadAllRuleParameters(DbSession dbSession) {
-    return dbClient.ruleDao().selectAllRuleParams(dbSession).stream()
-      .collect(Collectors.groupingBy(RuleParamDto::getRuleUuid));
-  }
-
-  private Map<String, Set<SingleDeprecatedRuleKey>> loadDeprecatedRuleKeys(DbSession dbSession) {
-    return dbClient.ruleDao().selectAllDeprecatedRuleKeys(dbSession).stream()
-      .map(SingleDeprecatedRuleKey::from)
-      .collect(Collectors.groupingBy(SingleDeprecatedRuleKey::getRuleUuid, Collectors.toSet()));
-  }
-
-  private static class RegisterRulesContext {
-    // initial immutable data
-    private final Map<RuleKey, RuleDto> dbRules;
-    private final Set<RuleDto> known;
-    private final Map<String, Set<SingleDeprecatedRuleKey>> dbDeprecatedKeysByUuid;
-    private final Map<String, List<RuleParamDto>> ruleParamsByRuleUuid;
-    private final Map<RuleKey, RuleDto> dbRulesByDbDeprecatedKey;
-    // mutable data
-    private final Set<RuleDto> created = new HashSet<>();
-    private final Map<RuleDto, RuleKey> renamed = new HashMap<>();
-    private final Set<RuleDto> updated = new HashSet<>();
-    private final Set<RuleDto> unchanged = new HashSet<>();
-    private final Set<RuleDto> removed = new HashSet<>();
-
-    private RegisterRulesContext(Map<RuleKey, RuleDto> dbRules, Map<String, Set<SingleDeprecatedRuleKey>> dbDeprecatedKeysByUuid,
-      Map<String, List<RuleParamDto>> ruleParamsByRuleUuid) {
-      this.dbRules = ImmutableMap.copyOf(dbRules);
-      this.known = ImmutableSet.copyOf(dbRules.values());
-      this.dbDeprecatedKeysByUuid = dbDeprecatedKeysByUuid;
-      this.ruleParamsByRuleUuid = ruleParamsByRuleUuid;
-      this.dbRulesByDbDeprecatedKey = buildDbRulesByDbDeprecatedKey(dbDeprecatedKeysByUuid, dbRules);
-    }
-
-    private static Map<RuleKey, RuleDto> buildDbRulesByDbDeprecatedKey(Map<String, Set<SingleDeprecatedRuleKey>> dbDeprecatedKeysByUuid,
-      Map<RuleKey, RuleDto> dbRules) {
-      Map<String, RuleDto> dbRulesByRuleUuid = dbRules.values().stream()
-        .collect(Collectors.toMap(RuleDto::getUuid, Function.identity()));
-
-      Map<RuleKey, RuleDto> rulesByKey = new LinkedHashMap<>();
-      for (Map.Entry<String, Set<SingleDeprecatedRuleKey>> entry : dbDeprecatedKeysByUuid.entrySet()) {
-        String ruleUuid = entry.getKey();
-        RuleDto rule = dbRulesByRuleUuid.get(ruleUuid);
-        if (rule == null) {
-          LOG.warn("Could not retrieve rule with uuid %s referenced by a deprecated rule key. " +
-              "The following deprecated rule keys seem to be referencing a non-existing rule",
-            ruleUuid, entry.getValue());
-        } else {
-          entry.getValue().forEach(d -> rulesByKey.put(d.getOldRuleKeyAsRuleKey(), rule));
-        }
-      }
-      return unmodifiableMap(rulesByKey);
-    }
-
-    private boolean hasDbRules() {
-      return !dbRules.isEmpty();
-    }
-
-    private Optional<RuleDto> getDbRuleFor(RulesDefinition.Rule ruleDef) {
-      RuleKey ruleKey = RuleKey.of(ruleDef.repository().key(), ruleDef.key());
-      Optional<RuleDto> res = Stream.concat(Stream.of(ruleKey), ruleDef.deprecatedRuleKeys().stream())
-        .map(dbRules::get)
-        .filter(Objects::nonNull)
-        .findFirst();
-      // may occur in case of plugin downgrade
-      if (res.isEmpty()) {
-        return Optional.ofNullable(dbRulesByDbDeprecatedKey.get(ruleKey));
-      }
-      return res;
-    }
-
-    private Map<RuleKey, SingleDeprecatedRuleKey> getDbDeprecatedKeysByOldRuleKey() {
-      return dbDeprecatedKeysByUuid.values().stream()
-        .flatMap(Collection::stream)
-        .collect(Collectors.toMap(SingleDeprecatedRuleKey::getOldRuleKeyAsRuleKey, Function.identity()));
-    }
-
-    private Set<SingleDeprecatedRuleKey> getDBDeprecatedKeysFor(RuleDto rule) {
-      return dbDeprecatedKeysByUuid.getOrDefault(rule.getUuid(), emptySet());
-    }
-
-    private List<RuleParamDto> getRuleParametersFor(String ruleUuid) {
-      return ruleParamsByRuleUuid.getOrDefault(ruleUuid, emptyList());
-    }
-
-    private Stream<RuleDto> getRemaining() {
-      Set<RuleDto> res = new HashSet<>(dbRules.values());
-      res.removeAll(unchanged);
-      res.removeAll(renamed.keySet());
-      res.removeAll(updated);
-      res.removeAll(removed);
-      return res.stream();
-    }
-
-    private Stream<RuleDto> getRemoved() {
-      return removed.stream();
-    }
-
-    public Stream<Map.Entry<RuleDto, RuleKey>> getRenamed() {
-      return renamed.entrySet().stream();
-    }
-
-    private Stream<RuleDto> getAllModified() {
-      return Stream.of(
-          created.stream(),
-          updated.stream(),
-          removed.stream(),
-          renamed.keySet().stream())
-        .flatMap(s -> s);
-    }
-
-    private boolean isCreated(RuleDto ruleDto) {
-      return created.contains(ruleDto);
-    }
-
-    private boolean isRenamed(RuleDto ruleDto) {
-      return renamed.containsKey(ruleDto);
-    }
-
-    private boolean isUpdated(RuleDto ruleDto) {
-      return updated.contains(ruleDto);
-    }
-
-    private void created(RuleDto ruleDto) {
-      checkState(!known.contains(ruleDto), "known RuleDto can't be created");
-      created.add(ruleDto);
-    }
-
-    private void renamed(RuleDto ruleDto) {
-      ensureKnown(ruleDto);
-      renamed.put(ruleDto, ruleDto.getKey());
-    }
-
-    private void updated(RuleDto ruleDto) {
-      ensureKnown(ruleDto);
-      updated.add(ruleDto);
-    }
-
-    private void removed(RuleDto ruleDto) {
-      ensureKnown(ruleDto);
-      removed.add(ruleDto);
-    }
-
-    private void unchanged(RuleDto ruleDto) {
-      ensureKnown(ruleDto);
-      unchanged.add(ruleDto);
-    }
-
-    private void ensureKnown(RuleDto ruleDto) {
-      checkState(known.contains(ruleDto), "unknown RuleDto");
-    }
-  }
-
-  private void persistRepositories(DbSession dbSession, List<RulesDefinition.Repository> repositories) {
-    List<String> keys = repositories.stream().map(RulesDefinition.Repository::key).toList();
-    Set<String> existingKeys = dbClient.ruleRepositoryDao().selectAllKeys(dbSession);
-
-    Map<Boolean, List<RuleRepositoryDto>> dtos = repositories.stream()
-      .map(r -> new RuleRepositoryDto(r.key(), r.language(), r.name()))
-      .collect(Collectors.groupingBy(i -> existingKeys.contains(i.getKey())));
-
-    dbClient.ruleRepositoryDao().update(dbSession, dtos.getOrDefault(true, emptyList()));
-    dbClient.ruleRepositoryDao().insert(dbSession, dtos.getOrDefault(false, emptyList()));
-    dbClient.ruleRepositoryDao().deleteIfKeyNotIn(dbSession, keys);
-    dbSession.commit();
-  }
-
-  @Override
-  public void stop() {
-    // nothing
-  }
-
-  private void registerRules(RegisterRulesContext context, List<RulesDefinition.Rule> ruleDefs, DbSession session) {
-    Map<RulesDefinition.Rule, RuleDto> dtos = new LinkedHashMap<>(ruleDefs.size());
-
-    for (RulesDefinition.Rule ruleDef : ruleDefs) {
-      RuleKey ruleKey = RuleKey.of(ruleDef.repository().key(), ruleDef.key());
-      RuleDto ruleDto = findOrCreateRuleDto(context, session, ruleDef);
-      dtos.put(ruleDef, ruleDto);
-
-      // we must detect renaming __before__ we modify the DTO
-      if (!ruleDto.getKey().equals(ruleKey)) {
-        context.renamed(ruleDto);
-        ruleDto.setRuleKey(ruleKey);
-      }
-
-      if (anyMerge(ruleDef, ruleDto)) {
-        context.updated(ruleDto);
-      }
-
-      if (context.isUpdated(ruleDto) || context.isRenamed(ruleDto)) {
-        update(session, ruleDto);
-      } else if (!context.isCreated(ruleDto)) {
-        context.unchanged(ruleDto);
-      }
-    }
-
-    for (Map.Entry<RulesDefinition.Rule, RuleDto> e : dtos.entrySet()) {
-      mergeParams(context, e.getKey(), e.getValue(), session);
-      updateDeprecatedKeys(context, e.getKey(), e.getValue(), session);
-    }
-  }
-
-  @Nonnull
-  private RuleDto findOrCreateRuleDto(RegisterRulesContext context, DbSession session, RulesDefinition.Rule ruleDef) {
-    return context.getDbRuleFor(ruleDef)
-      .orElseGet(() -> {
-        RuleDto newRule = createRuleDto(ruleDef, session);
-        context.created(newRule);
-        return newRule;
-      });
-  }
-
-  private RuleDto createRuleDto(RulesDefinition.Rule ruleDef, DbSession session) {
-    RuleDto ruleDto = new RuleDto()
-      .setUuid(uuidFactory.create())
-      .setRuleKey(RuleKey.of(ruleDef.repository().key(), ruleDef.key()))
-      .setPluginKey(ruleDef.pluginKey())
-      .setIsTemplate(ruleDef.template())
-      .setConfigKey(ruleDef.internalKey())
-      .setLanguage(ruleDef.repository().language())
-      .setName(ruleDef.name())
-      .setSeverity(ruleDef.severity())
-      .setStatus(ruleDef.status())
-      .setGapDescription(ruleDef.gapDescription())
-      .setSystemTags(ruleDef.tags())
-      .setSecurityStandards(ruleDef.securityStandards())
-      .setType(RuleType.valueOf(ruleDef.type().name()))
-      .setScope(toDtoScope(ruleDef.scope()))
-      .setIsExternal(ruleDef.repository().isExternal())
-      .setIsAdHoc(false)
-      .setCreatedAt(system2.now())
-      .setUpdatedAt(system2.now())
-      .setEducationPrinciples(ruleDef.educationPrincipleKeys());
-
-    if (isNotEmpty(ruleDef.htmlDescription())) {
-      ruleDto.setDescriptionFormat(Format.HTML);
-    } else if (isNotEmpty(ruleDef.markdownDescription())) {
-      ruleDto.setDescriptionFormat(Format.MARKDOWN);
-    }
-
-    generateRuleDescriptionSections(ruleDef)
-      .forEach(ruleDto::addRuleDescriptionSectionDto);
-
-    DebtRemediationFunction debtRemediationFunction = ruleDef.debtRemediationFunction();
-    if (debtRemediationFunction != null) {
-      ruleDto.setDefRemediationFunction(debtRemediationFunction.type().name());
-      ruleDto.setDefRemediationGapMultiplier(debtRemediationFunction.gapMultiplier());
-      ruleDto.setDefRemediationBaseEffort(debtRemediationFunction.baseEffort());
-      ruleDto.setGapDescription(ruleDef.gapDescription());
-    }
-
-    dbClient.ruleDao().insert(session, ruleDto);
-    return ruleDto;
-  }
-
-  private Set<RuleDescriptionSectionDto> generateRuleDescriptionSections(RulesDefinition.Rule ruleDef) {
-    RuleDescriptionSectionsGenerator descriptionSectionGenerator = ruleDescriptionSectionsGeneratorResolver.getRuleDescriptionSectionsGenerator(ruleDef);
-    return descriptionSectionGenerator.generateSections(ruleDef);
-  }
-
-  private static Scope toDtoScope(RuleScope scope) {
-    switch (scope) {
-      case ALL:
-        return Scope.ALL;
-      case MAIN:
-        return Scope.MAIN;
-      case TEST:
-        return Scope.TEST;
-      default:
-        throw new IllegalArgumentException("Unknown rule scope: " + scope);
-    }
-  }
-
-  private boolean anyMerge(RulesDefinition.Rule ruleDef, RuleDto ruleDto) {
-    boolean ruleMerged = mergeRule(ruleDef, ruleDto);
-    boolean debtDefinitionsMerged = mergeDebtDefinitions(ruleDef, ruleDto);
-    boolean tagsMerged = mergeTags(ruleDef, ruleDto);
-    boolean securityStandardsMerged = mergeSecurityStandards(ruleDef, ruleDto);
-    boolean educationPrinciplesMerged = mergeEducationPrinciples(ruleDef, ruleDto);
-    return ruleMerged || debtDefinitionsMerged || tagsMerged || securityStandardsMerged || educationPrinciplesMerged;
-  }
-
-  private boolean mergeRule(RulesDefinition.Rule def, RuleDto dto) {
-    boolean changed = false;
-    if (!Objects.equals(dto.getName(), def.name())) {
-      dto.setName(def.name());
-      changed = true;
-    }
-    if (mergeDescription(def, dto)) {
-      changed = true;
-    }
-    if (!Objects.equals(dto.getPluginKey(), def.pluginKey())) {
-      dto.setPluginKey(def.pluginKey());
-      changed = true;
-    }
-    if (!Objects.equals(dto.getConfigKey(), def.internalKey())) {
-      dto.setConfigKey(def.internalKey());
-      changed = true;
-    }
-    String severity = def.severity();
-    if (!Objects.equals(dto.getSeverityString(), severity)) {
-      dto.setSeverity(severity);
-      changed = true;
-    }
-    boolean isTemplate = def.template();
-    if (isTemplate != dto.isTemplate()) {
-      dto.setIsTemplate(isTemplate);
-      changed = true;
-    }
-    if (def.status() != dto.getStatus()) {
-      dto.setStatus(def.status());
-      changed = true;
-    }
-    if (!Objects.equals(dto.getScope().name(), def.scope().name())) {
-      dto.setScope(toDtoScope(def.scope()));
-      changed = true;
-    }
-    if (!Objects.equals(dto.getLanguage(), def.repository().language())) {
-      dto.setLanguage(def.repository().language());
-      changed = true;
-    }
-    RuleType type = RuleType.valueOf(def.type().name());
-    if (!Objects.equals(dto.getType(), type.getDbConstant())) {
-      dto.setType(type);
-      changed = true;
-    }
-    if (dto.isAdHoc()) {
-      dto.setIsAdHoc(false);
-      changed = true;
-    }
-    return changed;
-  }
-
-  private boolean mergeDescription(RulesDefinition.Rule rule, RuleDto ruleDto) {
-    Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos = generateRuleDescriptionSections(rule);
-    if (ruleDescriptionSectionsUnchanged(ruleDto, newRuleDescriptionSectionDtos)) {
-      return false;
-    }
-    ruleDto.replaceRuleDescriptionSectionDtos(newRuleDescriptionSectionDtos);
-    if (containsHtmlDescription(rule)) {
-      ruleDto.setDescriptionFormat(Format.HTML);
-      return true;
-    } else if (isNotEmpty(rule.markdownDescription())) {
-      ruleDto.setDescriptionFormat(Format.MARKDOWN);
-      return true;
-    }
-    return false;
-  }
-
-  private static boolean containsHtmlDescription(RulesDefinition.Rule rule) {
-    return isNotEmpty(rule.htmlDescription()) || !rule.ruleDescriptionSections().isEmpty();
-  }
-
-  private static boolean ruleDescriptionSectionsUnchanged(RuleDto ruleDto, Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos) {
-    if (ruleDto.getRuleDescriptionSectionDtos().size() != newRuleDescriptionSectionDtos.size()) {
-      return false;
-    }
-    return ruleDto.getRuleDescriptionSectionDtos().stream()
-      .allMatch(sectionDto -> contains(newRuleDescriptionSectionDtos, sectionDto));
-  }
-
-  private static boolean contains(Set<RuleDescriptionSectionDto> sectionDtos, RuleDescriptionSectionDto sectionDto) {
-    return sectionDtos.stream()
-      .filter(s -> s.getKey().equals(sectionDto.getKey()) && s.getContent().equals(sectionDto.getContent()))
-      .anyMatch(s -> Objects.equals(s.getContext(), sectionDto.getContext()));
-  }
-
-  private static boolean mergeDebtDefinitions(RulesDefinition.Rule def, RuleDto dto) {
-    // Debt definitions are set to null if the sub-characteristic and the remediation function are null
-    DebtRemediationFunction debtRemediationFunction = def.debtRemediationFunction();
-    boolean hasDebt = debtRemediationFunction != null;
-    if (hasDebt) {
-      return mergeDebtDefinitions(dto,
-        debtRemediationFunction.type().name(),
-        debtRemediationFunction.gapMultiplier(),
-        debtRemediationFunction.baseEffort(),
-        def.gapDescription());
-    }
-    return mergeDebtDefinitions(dto, null, null, null, null);
-  }
-
-  private static boolean mergeDebtDefinitions(RuleDto dto, @Nullable String remediationFunction,
-    @Nullable String remediationCoefficient, @Nullable String remediationOffset, @Nullable String gapDescription) {
-    boolean changed = false;
-
-    if (!Objects.equals(dto.getDefRemediationFunction(), remediationFunction)) {
-      dto.setDefRemediationFunction(remediationFunction);
-      changed = true;
-    }
-    if (!Objects.equals(dto.getDefRemediationGapMultiplier(), remediationCoefficient)) {
-      dto.setDefRemediationGapMultiplier(remediationCoefficient);
-      changed = true;
-    }
-    if (!Objects.equals(dto.getDefRemediationBaseEffort(), remediationOffset)) {
-      dto.setDefRemediationBaseEffort(remediationOffset);
-      changed = true;
-    }
-    if (!Objects.equals(dto.getGapDescription(), gapDescription)) {
-      dto.setGapDescription(gapDescription);
-      changed = true;
-    }
-    return changed;
-  }
-
-  private void mergeParams(RegisterRulesContext context, RulesDefinition.Rule ruleDef, RuleDto rule, DbSession session) {
-    List<RuleParamDto> paramDtos = context.getRuleParametersFor(rule.getUuid());
-    Map<String, RuleParamDto> existingParamsByName = new HashMap<>();
-
-    Profiler profiler = Profiler.create(Loggers.get(getClass()));
-    for (RuleParamDto paramDto : paramDtos) {
-      RulesDefinition.Param paramDef = ruleDef.param(paramDto.getName());
-      if (paramDef == null) {
-        profiler.start();
-        dbClient.activeRuleDao().deleteParamsByRuleParam(session, paramDto);
-        profiler.stopDebug(format("Propagate deleted param with name %s to active rules of rule %s", paramDto.getName(), rule.getKey()));
-        dbClient.ruleDao().deleteRuleParam(session, paramDto.getUuid());
-      } else {
-        if (mergeParam(paramDto, paramDef)) {
-          dbClient.ruleDao().updateRuleParam(session, rule, paramDto);
-        }
-        existingParamsByName.put(paramDto.getName(), paramDto);
-      }
-    }
-
-    // Create newly parameters
-    for (RulesDefinition.Param param : ruleDef.params()) {
-      RuleParamDto paramDto = existingParamsByName.get(param.key());
-      if (paramDto != null) {
-        continue;
-      }
-      paramDto = RuleParamDto.createFor(rule)
-        .setName(param.key())
-        .setDescription(param.description())
-        .setDefaultValue(param.defaultValue())
-        .setType(param.type().toString());
-      dbClient.ruleDao().insertRuleParam(session, rule, paramDto);
-      if (StringUtils.isEmpty(param.defaultValue())) {
-        continue;
-      }
-      // Propagate the default value to existing active rule parameters
-      profiler.start();
-      for (ActiveRuleDto activeRule : dbClient.activeRuleDao().selectByRuleUuid(session, rule.getUuid())) {
-        ActiveRuleParamDto activeParam = ActiveRuleParamDto.createFor(paramDto).setValue(param.defaultValue());
-        dbClient.activeRuleDao().insertParam(session, activeRule, activeParam);
-      }
-      profiler.stopDebug(format("Propagate new param with name %s to active rules of rule %s", paramDto.getName(), rule.getKey()));
-    }
-  }
-
-  private static boolean mergeParam(RuleParamDto paramDto, RulesDefinition.Param paramDef) {
-    boolean changed = false;
-    if (!Objects.equals(paramDto.getType(), paramDef.type().toString())) {
-      paramDto.setType(paramDef.type().toString());
-      changed = true;
-    }
-    if (!Objects.equals(paramDto.getDefaultValue(), paramDef.defaultValue())) {
-      paramDto.setDefaultValue(paramDef.defaultValue());
-      changed = true;
-    }
-    if (!Objects.equals(paramDto.getDescription(), paramDef.description())) {
-      paramDto.setDescription(paramDef.description());
-      changed = true;
-    }
-    return changed;
-  }
-
-  private void updateDeprecatedKeys(RegisterRulesContext context, RulesDefinition.Rule ruleDef, RuleDto rule, DbSession dbSession) {
-
-    Set<SingleDeprecatedRuleKey> deprecatedRuleKeysFromDefinition = SingleDeprecatedRuleKey.from(ruleDef);
-    Set<SingleDeprecatedRuleKey> deprecatedRuleKeysFromDB = context.getDBDeprecatedKeysFor(rule);
-
-    // DeprecatedKeys that must be deleted
-    List<String> uuidsToBeDeleted = difference(deprecatedRuleKeysFromDB, deprecatedRuleKeysFromDefinition).stream()
-      .map(SingleDeprecatedRuleKey::getUuid)
-      .toList();
-
-    dbClient.ruleDao().deleteDeprecatedRuleKeys(dbSession, uuidsToBeDeleted);
-
-    // DeprecatedKeys that must be created
-    Sets.SetView<SingleDeprecatedRuleKey> deprecatedRuleKeysToBeCreated = difference(deprecatedRuleKeysFromDefinition, deprecatedRuleKeysFromDB);
-
-    deprecatedRuleKeysToBeCreated
-      .forEach(r -> dbClient.ruleDao().insert(dbSession, new DeprecatedRuleKeyDto()
-        .setUuid(uuidFactory.create())
-        .setRuleUuid(rule.getUuid())
-        .setOldRepositoryKey(r.getOldRepositoryKey())
-        .setOldRuleKey(r.getOldRuleKey())
-        .setCreatedAt(system2.now())));
-  }
-
-  private static boolean mergeTags(RulesDefinition.Rule ruleDef, RuleDto dto) {
-    boolean changed = false;
-
-    if (RuleStatus.REMOVED == ruleDef.status()) {
-      dto.setSystemTags(emptySet());
-      changed = true;
-    } else if (dto.getSystemTags().size() != ruleDef.tags().size() ||
-      !dto.getSystemTags().containsAll(ruleDef.tags())) {
-      dto.setSystemTags(ruleDef.tags());
-      changed = true;
-    }
-    return changed;
-  }
-
-  private static boolean mergeSecurityStandards(RulesDefinition.Rule ruleDef, RuleDto dto) {
-    boolean changed = false;
-
-    if (RuleStatus.REMOVED == ruleDef.status()) {
-      dto.setSecurityStandards(emptySet());
-      changed = true;
-    } else if (dto.getSecurityStandards().size() != ruleDef.securityStandards().size() ||
-      !dto.getSecurityStandards().containsAll(ruleDef.securityStandards())) {
-      dto.setSecurityStandards(ruleDef.securityStandards());
-      changed = true;
-    }
-    return changed;
-  }
-
-  private static boolean mergeEducationPrinciples(RulesDefinition.Rule ruleDef, RuleDto dto) {
-    boolean changed = false;
-    if (dto.getEducationPrinciples().size() != ruleDef.educationPrincipleKeys().size() ||
-      !dto.getEducationPrinciples().containsAll(ruleDef.educationPrincipleKeys())) {
-      dto.setEducationPrinciples(ruleDef.educationPrincipleKeys());
-      changed = true;
-    }
-    return changed;
-  }
-
-  private void processRemainingDbRules(RegisterRulesContext recorder, DbSession dbSession) {
-    // custom rules check status of template, so they must be processed at the end
-    List<RuleDto> customRules = new ArrayList<>();
-
-    recorder.getRemaining().forEach(rule -> {
-      if (rule.isCustomRule()) {
-        customRules.add(rule);
-      } else if (!rule.isAdHoc() && rule.getStatus() != RuleStatus.REMOVED) {
-        removeRule(dbSession, recorder, rule);
-      }
-    });
-
-    for (RuleDto customRule : customRules) {
-      String templateUuid = customRule.getTemplateUuid();
-      checkNotNull(templateUuid, "Template uuid of the custom rule '%s' is null", customRule);
-      Optional<RuleDto> template = dbClient.ruleDao().selectByUuid(templateUuid, dbSession);
-      if (template.isPresent() && template.get().getStatus() != RuleStatus.REMOVED) {
-        if (updateCustomRuleFromTemplateRule(customRule, template.get())) {
-          recorder.updated(customRule);
-          update(dbSession, customRule);
-        }
-      } else {
-        removeRule(dbSession, recorder, customRule);
-      }
-    }
-
-    dbSession.commit();
-  }
-
-  private void removeRule(DbSession session, RegisterRulesContext recorder, RuleDto rule) {
-    LOG.info(format("Disable rule %s", rule.getKey()));
-    rule.setStatus(RuleStatus.REMOVED);
-    rule.setSystemTags(emptySet());
-    update(session, rule);
-    // FIXME resetting the tags for all organizations must be handled a different way
-    // rule.setTags(Collections.emptySet());
-    // update(session, rule.getMetadata());
-    recorder.removed(rule);
-    if (recorder.getRemoved().count() % 100 == 0) {
-      session.commit();
-    }
-  }
-
-  private static boolean updateCustomRuleFromTemplateRule(RuleDto customRule, RuleDto templateRule) {
-    boolean changed = false;
-    if (!Objects.equals(customRule.getLanguage(), templateRule.getLanguage())) {
-      customRule.setLanguage(templateRule.getLanguage());
-      changed = true;
-    }
-    if (!Objects.equals(customRule.getConfigKey(), templateRule.getConfigKey())) {
-      customRule.setConfigKey(templateRule.getConfigKey());
-      changed = true;
-    }
-    if (!Objects.equals(customRule.getPluginKey(), templateRule.getPluginKey())) {
-      customRule.setPluginKey(templateRule.getPluginKey());
-      changed = true;
-    }
-    if (!Objects.equals(customRule.getDefRemediationFunction(), templateRule.getDefRemediationFunction())) {
-      customRule.setDefRemediationFunction(templateRule.getDefRemediationFunction());
-      changed = true;
-    }
-    if (!Objects.equals(customRule.getDefRemediationGapMultiplier(), templateRule.getDefRemediationGapMultiplier())) {
-      customRule.setDefRemediationGapMultiplier(templateRule.getDefRemediationGapMultiplier());
-      changed = true;
-    }
-    if (!Objects.equals(customRule.getDefRemediationBaseEffort(), templateRule.getDefRemediationBaseEffort())) {
-      customRule.setDefRemediationBaseEffort(templateRule.getDefRemediationBaseEffort());
-      changed = true;
-    }
-    if (!Objects.equals(customRule.getGapDescription(), templateRule.getGapDescription())) {
-      customRule.setGapDescription(templateRule.getGapDescription());
-      changed = true;
-    }
-    if (customRule.getStatus() != templateRule.getStatus()) {
-      customRule.setStatus(templateRule.getStatus());
-      changed = true;
-    }
-    if (!Objects.equals(customRule.getSeverityString(), templateRule.getSeverityString())) {
-      customRule.setSeverity(templateRule.getSeverityString());
-      changed = true;
-    }
-    if (!Objects.equals(customRule.getRepositoryKey(), templateRule.getRepositoryKey())) {
-      customRule.setRepositoryKey(templateRule.getRepositoryKey());
-      changed = true;
-    }
-    return changed;
-  }
-
-  /**
-   * SONAR-4642
-   * <p/>
-   * Remove active rules on repositories that still exists.
-   * <p/>
-   * For instance, if the javascript repository do not provide anymore some rules, active rules related to this rules will be removed.
-   * But if the javascript repository do not exists anymore, then related active rules will not be removed.
-   * <p/>
-   * The side effect of this approach is that extended repositories will not be managed the same way.
-   * If an extended repository do not exists anymore, then related active rules will be removed.
-   */
-  private List<ActiveRuleChange> removeActiveRulesOnStillExistingRepositories(DbSession dbSession, RegisterRulesContext recorder, List<RulesDefinition.Repository> context) {
-    Set<String> existingAndRenamedRepositories = getExistingAndRenamedRepositories(recorder, context);
-    List<ActiveRuleChange> changes = new ArrayList<>();
-    Profiler profiler = Profiler.create(Loggers.get(getClass()));
-
-    recorder.getRemoved()
-      .filter(rule -> existingAndRenamedRepositories.contains(rule.getRepositoryKey()))
-      .forEach(rule -> {
-        // SONAR-4642 Remove active rules only when repository still exists
-        profiler.start();
-        changes.addAll(qProfileRules.deleteRule(dbSession, rule));
-        profiler.stopDebug(format("Remove active rule for rule %s", rule.getKey()));
-      });
-
-    return changes;
-  }
-
-  private static Set<String> getExistingAndRenamedRepositories(RegisterRulesContext recorder, Collection<RulesDefinition.Repository> context) {
-    return Stream.concat(
-        context.stream().map(RulesDefinition.ExtendedRepository::key),
-        recorder.getRenamed().map(Map.Entry::getValue).map(RuleKey::repository))
-      .collect(Collectors.toSet());
-  }
-
-  private void update(DbSession session, RuleDto rule) {
-    rule.setUpdatedAt(system2.now());
-    dbClient.ruleDao().update(session, rule);
-  }
-
-  private static void verifyRuleKeyConsistency(List<RulesDefinition.Repository> repositories, RegisterRulesContext registerRulesContext) {
-    List<RulesDefinition.Rule> definedRules = repositories.stream()
-      .flatMap(r -> r.rules().stream())
-      .toList();
-
-    Set<RuleKey> definedRuleKeys = definedRules.stream()
-      .map(r -> RuleKey.of(r.repository().key(), r.key()))
-      .collect(Collectors.toSet());
-
-    List<RuleKey> definedDeprecatedRuleKeys = definedRules.stream()
-      .flatMap(r -> r.deprecatedRuleKeys().stream())
-      .toList();
-
-    // Find duplicates in declared deprecated rule keys
-    Set<RuleKey> duplicates = findDuplicates(definedDeprecatedRuleKeys);
-    checkState(duplicates.isEmpty(), "The following deprecated rule keys are declared at least twice [%s]",
-      lazyToString(() -> duplicates.stream().map(RuleKey::toString).collect(Collectors.joining(","))));
-
-    // Find rule keys that are both deprecated and used
-    Set<RuleKey> intersection = intersection(new HashSet<>(definedRuleKeys), new HashSet<>(definedDeprecatedRuleKeys)).immutableCopy();
-    checkState(intersection.isEmpty(), "The following rule keys are declared both as deprecated and used key [%s]",
-      lazyToString(() -> intersection.stream().map(RuleKey::toString).collect(Collectors.joining(","))));
-
-    // Find incorrect usage of deprecated keys
-    Map<RuleKey, SingleDeprecatedRuleKey> dbDeprecatedRuleKeysByOldRuleKey = registerRulesContext.getDbDeprecatedKeysByOldRuleKey();
-
-    Set<String> incorrectRuleKeyMessage = definedRules.stream()
-      .flatMap(r -> filterInvalidDeprecatedRuleKeys(dbDeprecatedRuleKeysByOldRuleKey, r))
-      .filter(Objects::nonNull)
-      .collect(Collectors.toSet());
-
-    checkState(incorrectRuleKeyMessage.isEmpty(), "An incorrect state of deprecated rule keys has been detected.\n %s",
-      lazyToString(() -> String.join("\n", incorrectRuleKeyMessage)));
-  }
-
-  private static Stream<String> filterInvalidDeprecatedRuleKeys(Map<RuleKey, SingleDeprecatedRuleKey> dbDeprecatedRuleKeysByOldRuleKey, RulesDefinition.Rule rule) {
-    return rule.deprecatedRuleKeys().stream()
-      .map(rk -> {
-        SingleDeprecatedRuleKey singleDeprecatedRuleKey = dbDeprecatedRuleKeysByOldRuleKey.get(rk);
-        if (singleDeprecatedRuleKey == null) {
-          // new deprecated rule key : OK
-          return null;
-        }
-        RuleKey parentRuleKey = RuleKey.of(rule.repository().key(), rule.key());
-        if (parentRuleKey.equals(singleDeprecatedRuleKey.getNewRuleKeyAsRuleKey())) {
-          // same parent : OK
-          return null;
-        }
-        if (rule.deprecatedRuleKeys().contains(singleDeprecatedRuleKey.getNewRuleKeyAsRuleKey())) {
-          // the new rule is deprecating the old parentRuleKey : OK
-          return null;
-        }
-        return format("The deprecated rule key [%s] was previously deprecated by [%s]. [%s] should be a deprecated key of [%s],",
-          rk.toString(),
-          singleDeprecatedRuleKey.getNewRuleKeyAsRuleKey().toString(),
-          singleDeprecatedRuleKey.getNewRuleKeyAsRuleKey().toString(),
-          RuleKey.of(rule.repository().key(), rule.key()).toString());
-      });
-  }
-
-  private static Object lazyToString(Supplier<String> toString) {
-    return new Object() {
-      @Override
-      public String toString() {
-        return toString.get();
-      }
-    };
-  }
-
-  private static <T> Set<T> findDuplicates(Collection<T> list) {
-    Set<T> duplicates = new HashSet<>();
-    Set<T> uniques = new HashSet<>();
-
-    list.forEach(t -> {
-      if (!uniques.add(t)) {
-        duplicates.add(t);
-      }
-    });
-
-    return duplicates;
-  }
-}
index d3fd65836b55453135d8bee8e06e29ed56c7f545..417dd32b2fc5e5fda64760efd6536c5401ac8df7 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.rule;
 
 import java.util.Set;
 import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
 
 import static java.util.stream.Collectors.toSet;
 import static org.sonar.api.utils.Preconditions.checkState;
@@ -32,7 +33,7 @@ public class RuleDescriptionSectionsGeneratorResolver {
     this.ruleDescriptionSectionsGenerators = ruleDescriptionSectionsGenerators;
   }
 
-  RuleDescriptionSectionsGenerator getRuleDescriptionSectionsGenerator(RulesDefinition.Rule ruleDef) {
+  public RuleDescriptionSectionsGenerator getRuleDescriptionSectionsGenerator(RulesDefinition.Rule ruleDef) {
     Set<RuleDescriptionSectionsGenerator> generatorsFound = ruleDescriptionSectionsGenerators.stream()
       .filter(generator -> generator.isGeneratorForRule(ruleDef))
       .collect(toSet());
@@ -41,4 +42,8 @@ public class RuleDescriptionSectionsGeneratorResolver {
     return generatorsFound.iterator().next();
   }
 
+  public Set<RuleDescriptionSectionDto> generateFor(RulesDefinition.Rule ruleDef) {
+    return getRuleDescriptionSectionsGenerator(ruleDef).generateSections(ruleDef);
+  }
+
 }
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java
deleted file mode 100644 (file)
index ff72d36..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.rule;
-
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.Immutable;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.server.rule.RulesDefinition;
-import org.sonar.db.rule.DeprecatedRuleKeyDto;
-
-@Immutable
-class SingleDeprecatedRuleKey {
-  private String oldRuleKey;
-  private String oldRepositoryKey;
-  private String newRuleKey;
-  private String newRepositoryKey;
-  private String uuid;
-  private String ruleUuid;
-
-  /**
-   * static methods {@link #from(RulesDefinition.Rule)} and {@link #from(DeprecatedRuleKeyDto)} must be used
-   */
-  private SingleDeprecatedRuleKey() {
-    // empty
-  }
-
-  public static Set<SingleDeprecatedRuleKey> from(RulesDefinition.Rule rule) {
-    rule.deprecatedRuleKeys();
-    return rule.deprecatedRuleKeys().stream()
-      .map(r -> new SingleDeprecatedRuleKey()
-        .setNewRepositoryKey(rule.repository().key())
-        .setNewRuleKey(rule.key())
-        .setOldRepositoryKey(r.repository())
-        .setOldRuleKey(r.rule()))
-      .collect(Collectors.toSet());
-  }
-
-  public static SingleDeprecatedRuleKey from(DeprecatedRuleKeyDto rule) {
-    return new SingleDeprecatedRuleKey()
-      .setUuid(rule.getUuid())
-      .setRuleUuid(rule.getRuleUuid())
-      .setNewRepositoryKey(rule.getNewRepositoryKey())
-      .setNewRuleKey(rule.getNewRuleKey())
-      .setOldRepositoryKey(rule.getOldRepositoryKey())
-      .setOldRuleKey(rule.getOldRuleKey());
-  }
-
-  public String getOldRuleKey() {
-    return oldRuleKey;
-  }
-
-  public String getOldRepositoryKey() {
-    return oldRepositoryKey;
-  }
-
-  public RuleKey getOldRuleKeyAsRuleKey() {
-    return RuleKey.of(oldRepositoryKey, oldRuleKey);
-  }
-
-  public RuleKey getNewRuleKeyAsRuleKey() {
-    return RuleKey.of(newRepositoryKey, newRuleKey);
-  }
-
-  @CheckForNull
-  public String getNewRuleKey() {
-    return newRuleKey;
-  }
-
-  @CheckForNull
-  public String getNewRepositoryKey() {
-    return newRepositoryKey;
-  }
-
-  @CheckForNull
-  public String getUuid() {
-    return uuid;
-  }
-
-  @CheckForNull
-  public String getRuleUuid() {
-    return ruleUuid;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-    SingleDeprecatedRuleKey that = (SingleDeprecatedRuleKey) o;
-    return Objects.equals(oldRuleKey, that.oldRuleKey) &&
-      Objects.equals(oldRepositoryKey, that.oldRepositoryKey) &&
-      Objects.equals(newRuleKey, that.newRuleKey) &&
-      Objects.equals(newRepositoryKey, that.newRepositoryKey);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(oldRuleKey, oldRepositoryKey, newRuleKey, newRepositoryKey);
-  }
-
-  private SingleDeprecatedRuleKey setRuleUuid(String ruleUuid) {
-    this.ruleUuid = ruleUuid;
-    return this;
-  }
-
-  private SingleDeprecatedRuleKey setUuid(String uuid) {
-    this.uuid = uuid;
-    return this;
-  }
-
-  private SingleDeprecatedRuleKey setOldRuleKey(String oldRuleKey) {
-    this.oldRuleKey = oldRuleKey;
-    return this;
-  }
-
-  private SingleDeprecatedRuleKey setOldRepositoryKey(String oldRepositoryKey) {
-    this.oldRepositoryKey = oldRepositoryKey;
-    return this;
-  }
-
-  private SingleDeprecatedRuleKey setNewRuleKey(@Nullable String newRuleKey) {
-    this.newRuleKey = newRuleKey;
-    return this;
-  }
-
-  private SingleDeprecatedRuleKey setNewRepositoryKey(@Nullable String newRepositoryKey) {
-    this.newRepositoryKey = newRepositoryKey;
-    return this;
-  }
-}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesKeyVerifier.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesKeyVerifier.java
new file mode 100644 (file)
index 0000000..888bdb0
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.rule.registration;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Sets.intersection;
+import static java.lang.String.format;
+
+public class RulesKeyVerifier {
+
+  void verifyRuleKeyConsistency(List<RulesDefinition.Repository> repositories, RulesRegistrationContext rulesRegistrationContext) {
+    List<RulesDefinition.Rule> definedRules = repositories.stream()
+      .flatMap(r -> r.rules().stream())
+      .toList();
+
+    Set<RuleKey> definedRuleKeys = definedRules.stream()
+      .map(r -> RuleKey.of(r.repository().key(), r.key()))
+      .collect(Collectors.toSet());
+
+    List<RuleKey> definedDeprecatedRuleKeys = definedRules.stream()
+      .flatMap(r -> r.deprecatedRuleKeys().stream())
+      .toList();
+
+    // Find duplicates in declared deprecated rule keys
+    Set<RuleKey> duplicates = findDuplicates(definedDeprecatedRuleKeys);
+    checkState(duplicates.isEmpty(), "The following deprecated rule keys are declared at least twice [%s]",
+      lazyToString(() -> duplicates.stream().map(RuleKey::toString).collect(Collectors.joining(","))));
+
+    // Find rule keys that are both deprecated and used
+    Set<RuleKey> intersection = intersection(new HashSet<>(definedRuleKeys), new HashSet<>(definedDeprecatedRuleKeys)).immutableCopy();
+    checkState(intersection.isEmpty(), "The following rule keys are declared both as deprecated and used key [%s]",
+      lazyToString(() -> intersection.stream().map(RuleKey::toString).collect(Collectors.joining(","))));
+
+    // Find incorrect usage of deprecated keys
+    Map<RuleKey, SingleDeprecatedRuleKey> dbDeprecatedRuleKeysByOldRuleKey = rulesRegistrationContext.getDbDeprecatedKeysByOldRuleKey();
+
+    Set<String> incorrectRuleKeyMessage = definedRules.stream()
+      .flatMap(r -> filterInvalidDeprecatedRuleKeys(dbDeprecatedRuleKeysByOldRuleKey, r))
+      .filter(Objects::nonNull)
+      .collect(Collectors.toSet());
+
+    checkState(incorrectRuleKeyMessage.isEmpty(), "An incorrect state of deprecated rule keys has been detected.\n %s",
+      lazyToString(() -> String.join("\n", incorrectRuleKeyMessage)));
+  }
+
+  private static Stream<String> filterInvalidDeprecatedRuleKeys(Map<RuleKey, SingleDeprecatedRuleKey> dbDeprecatedRuleKeysByOldRuleKey, RulesDefinition.Rule rule) {
+    return rule.deprecatedRuleKeys().stream()
+      .map(rk -> {
+        SingleDeprecatedRuleKey singleDeprecatedRuleKey = dbDeprecatedRuleKeysByOldRuleKey.get(rk);
+        if (singleDeprecatedRuleKey == null) {
+          // new deprecated rule key : OK
+          return null;
+        }
+        RuleKey parentRuleKey = RuleKey.of(rule.repository().key(), rule.key());
+        if (parentRuleKey.equals(singleDeprecatedRuleKey.getNewRuleKeyAsRuleKey())) {
+          // same parent : OK
+          return null;
+        }
+        if (rule.deprecatedRuleKeys().contains(singleDeprecatedRuleKey.getNewRuleKeyAsRuleKey())) {
+          // the new rule is deprecating the old parentRuleKey : OK
+          return null;
+        }
+        return format("The deprecated rule key [%s] was previously deprecated by [%s]. [%s] should be a deprecated key of [%s],",
+          rk.toString(),
+          singleDeprecatedRuleKey.getNewRuleKeyAsRuleKey().toString(),
+          singleDeprecatedRuleKey.getNewRuleKeyAsRuleKey().toString(),
+          RuleKey.of(rule.repository().key(), rule.key()).toString());
+      });
+  }
+
+  private static Object lazyToString(Supplier<String> toString) {
+    return new Object() {
+      @Override
+      public String toString() {
+        return toString.get();
+      }
+    };
+  }
+
+  private static <T> Set<T> findDuplicates(Collection<T> list) {
+    Set<T> duplicates = new HashSet<>();
+    Set<T> uniques = new HashSet<>();
+
+    list.forEach(t -> {
+      if (!uniques.add(t)) {
+        duplicates.add(t);
+      }
+    });
+
+    return duplicates;
+  }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrant.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrant.java
new file mode 100644 (file)
index 0000000..4f03212
--- /dev/null
@@ -0,0 +1,327 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.rule.registration;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+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 java.util.stream.Stream;
+import javax.annotation.Nonnull;
+import org.sonar.api.Startable;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.System2;
+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.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleRepositoryDto;
+import org.sonar.server.es.metadata.MetadataIndex;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.QProfileRules;
+import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
+import org.sonar.server.rule.RuleDefinitionsLoader;
+import org.sonar.server.rule.RuleDescriptionSectionsGeneratorResolver;
+import org.sonar.server.rule.WebServerRuleFinder;
+import org.sonar.server.rule.index.RuleIndexer;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
+
+/**
+ * Registers rules at server startup
+ */
+public class RulesRegistrant implements Startable {
+
+  private static final Logger LOG = Loggers.get(RulesRegistrant.class);
+
+  private final RuleDefinitionsLoader defLoader;
+  private final QProfileRules qProfileRules;
+  private final DbClient dbClient;
+  private final RuleIndexer ruleIndexer;
+  private final ActiveRuleIndexer activeRuleIndexer;
+  private final Languages languages;
+  private final System2 system2;
+  private final WebServerRuleFinder webServerRuleFinder;
+  private final UuidFactory uuidFactory;
+  private final MetadataIndex metadataIndex;
+  private final RuleDescriptionSectionsGeneratorResolver ruleDescriptionSectionsGeneratorResolver;
+  private final RulesKeyVerifier rulesKeyVerifier;
+  private final StartupRuleUpdater startupRuleUpdater;
+
+  public RulesRegistrant(RuleDefinitionsLoader defLoader, QProfileRules qProfileRules, DbClient dbClient, RuleIndexer ruleIndexer,
+    ActiveRuleIndexer activeRuleIndexer, Languages languages, System2 system2,
+    WebServerRuleFinder webServerRuleFinder, UuidFactory uuidFactory, MetadataIndex metadataIndex,
+    RuleDescriptionSectionsGeneratorResolver ruleDescriptionSectionsGeneratorResolver,
+    RulesKeyVerifier rulesKeyVerifier, StartupRuleUpdater startupRuleUpdater) {
+    this.defLoader = defLoader;
+    this.qProfileRules = qProfileRules;
+    this.dbClient = dbClient;
+    this.ruleIndexer = ruleIndexer;
+    this.activeRuleIndexer = activeRuleIndexer;
+    this.languages = languages;
+    this.system2 = system2;
+    this.webServerRuleFinder = webServerRuleFinder;
+    this.uuidFactory = uuidFactory;
+    this.metadataIndex = metadataIndex;
+    this.ruleDescriptionSectionsGeneratorResolver = ruleDescriptionSectionsGeneratorResolver;
+    this.rulesKeyVerifier = rulesKeyVerifier;
+    this.startupRuleUpdater = startupRuleUpdater;
+  }
+
+  @Override
+  public void start() {
+    Profiler profiler = Profiler.create(LOG).startInfo("Register rules");
+    try (DbSession dbSession = dbClient.openSession(true)) {
+      List<RulesDefinition.Repository> repositories = defLoader.load().repositories();
+      RulesRegistrationContext rulesRegistrationContext = RulesRegistrationContext.create(dbClient, dbSession);
+      rulesKeyVerifier.verifyRuleKeyConsistency(repositories, rulesRegistrationContext);
+
+      for (RulesDefinition.ExtendedRepository repoDef : repositories) {
+        if (languages.get(repoDef.language()) != null) {
+          registerRules(rulesRegistrationContext, repoDef.rules(), dbSession);
+          dbSession.commit();
+        }
+      }
+      processRemainingDbRules(rulesRegistrationContext, dbSession);
+      List<ActiveRuleChange> changes = removeActiveRulesOnStillExistingRepositories(dbSession, rulesRegistrationContext, repositories);
+      dbSession.commit();
+
+      persistRepositories(dbSession, repositories);
+      // FIXME lack of resiliency, active rules index is corrupted if rule index fails
+      // to be updated. Only a single DB commit should be executed.
+      ruleIndexer.commitAndIndex(dbSession, rulesRegistrationContext.getAllModified().map(RuleDto::getUuid).collect(Collectors.toSet()));
+      changes.forEach(arChange -> dbClient.qProfileChangeDao().insert(dbSession, arChange.toDto(null)));
+      activeRuleIndexer.commitAndIndex(dbSession, changes);
+      rulesRegistrationContext.getRenamed().forEach(e -> LOG.info("Rule {} re-keyed to {}", e.getValue(), e.getKey().getKey()));
+      profiler.stopDebug();
+
+      if (!rulesRegistrationContext.hasDbRules()) {
+        Stream.concat(ruleIndexer.getIndexTypes().stream(), activeRuleIndexer.getIndexTypes().stream())
+          .forEach(t -> metadataIndex.setInitialized(t, true));
+      }
+
+      webServerRuleFinder.startCaching();
+    }
+  }
+
+  private void persistRepositories(DbSession dbSession, List<RulesDefinition.Repository> repositories) {
+    List<String> keys = repositories.stream().map(RulesDefinition.Repository::key).toList();
+    Set<String> existingKeys = dbClient.ruleRepositoryDao().selectAllKeys(dbSession);
+
+    Map<Boolean, List<RuleRepositoryDto>> dtos = repositories.stream()
+      .map(r -> new RuleRepositoryDto(r.key(), r.language(), r.name()))
+      .collect(Collectors.groupingBy(i -> existingKeys.contains(i.getKey())));
+
+    dbClient.ruleRepositoryDao().update(dbSession, dtos.getOrDefault(true, emptyList()));
+    dbClient.ruleRepositoryDao().insert(dbSession, dtos.getOrDefault(false, emptyList()));
+    dbClient.ruleRepositoryDao().deleteIfKeyNotIn(dbSession, keys);
+    dbSession.commit();
+  }
+
+  @Override
+  public void stop() {
+    // nothing
+  }
+
+  private void registerRules(RulesRegistrationContext context, List<RulesDefinition.Rule> ruleDefs, DbSession session) {
+    Map<RulesDefinition.Rule, RuleDto> dtos = new LinkedHashMap<>(ruleDefs.size());
+
+    for (RulesDefinition.Rule ruleDef : ruleDefs) {
+      RuleKey ruleKey = RuleKey.of(ruleDef.repository().key(), ruleDef.key());
+      RuleDto ruleDto = findOrCreateRuleDto(context, session, ruleDef);
+      dtos.put(ruleDef, ruleDto);
+
+      // we must detect renaming __before__ we modify the DTO
+      if (!ruleDto.getKey().equals(ruleKey)) {
+        context.renamed(ruleDto);
+        ruleDto.setRuleKey(ruleKey);
+      }
+
+      if (startupRuleUpdater.findChangesAndUpdateRule(ruleDef, ruleDto)) {
+        context.updated(ruleDto);
+      }
+
+      if (context.isUpdated(ruleDto) || context.isRenamed(ruleDto)) {
+        update(session, ruleDto);
+      } else if (!context.isCreated(ruleDto)) {
+        context.unchanged(ruleDto);
+      }
+    }
+
+    for (Map.Entry<RulesDefinition.Rule, RuleDto> e : dtos.entrySet()) {
+      startupRuleUpdater.mergeParams(context, e.getKey(), e.getValue(), session);
+      startupRuleUpdater.updateDeprecatedKeys(context, e.getKey(), e.getValue(), session);
+    }
+  }
+
+  @Nonnull
+  private RuleDto findOrCreateRuleDto(RulesRegistrationContext context, DbSession session, RulesDefinition.Rule ruleDef) {
+    return context.getDbRuleFor(ruleDef)
+      .orElseGet(() -> {
+        RuleDto newRule = RuleDto.from(ruleDef, ruleDescriptionSectionsGeneratorResolver.generateFor(ruleDef), uuidFactory, system2.now());
+        dbClient.ruleDao().insert(session, newRule);
+        context.created(newRule);
+        return newRule;
+      });
+  }
+
+  private void processRemainingDbRules(RulesRegistrationContext recorder, DbSession dbSession) {
+    // custom rules check status of template, so they must be processed at the end
+    List<RuleDto> customRules = new ArrayList<>();
+
+    recorder.getRemaining().forEach(rule -> {
+      if (rule.isCustomRule()) {
+        customRules.add(rule);
+      } else if (!rule.isAdHoc() && rule.getStatus() != RuleStatus.REMOVED) {
+        removeRule(dbSession, recorder, rule);
+      }
+    });
+
+    for (RuleDto customRule : customRules) {
+      String templateUuid = customRule.getTemplateUuid();
+      checkNotNull(templateUuid, "Template uuid of the custom rule '%s' is null", customRule);
+      Optional<RuleDto> template = dbClient.ruleDao().selectByUuid(templateUuid, dbSession);
+      if (template.isPresent() && template.get().getStatus() != RuleStatus.REMOVED) {
+        if (updateCustomRuleFromTemplateRule(customRule, template.get())) {
+          recorder.updated(customRule);
+          update(dbSession, customRule);
+        }
+      } else {
+        removeRule(dbSession, recorder, customRule);
+      }
+    }
+
+    dbSession.commit();
+  }
+
+  private void removeRule(DbSession session, RulesRegistrationContext recorder, RuleDto rule) {
+    LOG.info(format("Disable rule %s", rule.getKey()));
+    rule.setStatus(RuleStatus.REMOVED);
+    rule.setSystemTags(emptySet());
+    update(session, rule);
+    // FIXME resetting the tags for all organizations must be handled a different way
+    // rule.setTags(Collections.emptySet());
+    // update(session, rule.getMetadata());
+    recorder.removed(rule);
+    if (recorder.getRemoved().count() % 100 == 0) {
+      session.commit();
+    }
+  }
+
+  private static boolean updateCustomRuleFromTemplateRule(RuleDto customRule, RuleDto templateRule) {
+    boolean changed = false;
+    if (!Objects.equals(customRule.getLanguage(), templateRule.getLanguage())) {
+      customRule.setLanguage(templateRule.getLanguage());
+      changed = true;
+    }
+    if (!Objects.equals(customRule.getConfigKey(), templateRule.getConfigKey())) {
+      customRule.setConfigKey(templateRule.getConfigKey());
+      changed = true;
+    }
+    if (!Objects.equals(customRule.getPluginKey(), templateRule.getPluginKey())) {
+      customRule.setPluginKey(templateRule.getPluginKey());
+      changed = true;
+    }
+    if (!Objects.equals(customRule.getDefRemediationFunction(), templateRule.getDefRemediationFunction())) {
+      customRule.setDefRemediationFunction(templateRule.getDefRemediationFunction());
+      changed = true;
+    }
+    if (!Objects.equals(customRule.getDefRemediationGapMultiplier(), templateRule.getDefRemediationGapMultiplier())) {
+      customRule.setDefRemediationGapMultiplier(templateRule.getDefRemediationGapMultiplier());
+      changed = true;
+    }
+    if (!Objects.equals(customRule.getDefRemediationBaseEffort(), templateRule.getDefRemediationBaseEffort())) {
+      customRule.setDefRemediationBaseEffort(templateRule.getDefRemediationBaseEffort());
+      changed = true;
+    }
+    if (!Objects.equals(customRule.getGapDescription(), templateRule.getGapDescription())) {
+      customRule.setGapDescription(templateRule.getGapDescription());
+      changed = true;
+    }
+    if (customRule.getStatus() != templateRule.getStatus()) {
+      customRule.setStatus(templateRule.getStatus());
+      changed = true;
+    }
+    if (!Objects.equals(customRule.getSeverityString(), templateRule.getSeverityString())) {
+      customRule.setSeverity(templateRule.getSeverityString());
+      changed = true;
+    }
+    if (!Objects.equals(customRule.getRepositoryKey(), templateRule.getRepositoryKey())) {
+      customRule.setRepositoryKey(templateRule.getRepositoryKey());
+      changed = true;
+    }
+    return changed;
+  }
+
+  /**
+   * SONAR-4642
+   * <p/>
+   * Remove active rules on repositories that still exists.
+   * <p/>
+   * For instance, if the javascript repository do not provide anymore some rules, active rules related to this rules will be removed.
+   * But if the javascript repository do not exists anymore, then related active rules will not be removed.
+   * <p/>
+   * The side effect of this approach is that extended repositories will not be managed the same way.
+   * If an extended repository do not exists anymore, then related active rules will be removed.
+   */
+  private List<ActiveRuleChange> removeActiveRulesOnStillExistingRepositories(DbSession dbSession, RulesRegistrationContext recorder, List<RulesDefinition.Repository> context) {
+    Set<String> existingAndRenamedRepositories = getExistingAndRenamedRepositories(recorder, context);
+    List<ActiveRuleChange> changes = new ArrayList<>();
+    Profiler profiler = Profiler.create(LOG);
+
+    recorder.getRemoved()
+      .filter(rule -> existingAndRenamedRepositories.contains(rule.getRepositoryKey()))
+      .forEach(rule -> {
+        // SONAR-4642 Remove active rules only when repository still exists
+        profiler.start();
+        changes.addAll(qProfileRules.deleteRule(dbSession, rule));
+        profiler.stopDebug(format("Remove active rule for rule %s", rule.getKey()));
+      });
+
+    return changes;
+  }
+
+  private static Set<String> getExistingAndRenamedRepositories(RulesRegistrationContext recorder, Collection<RulesDefinition.Repository> context) {
+    return Stream.concat(
+        context.stream().map(RulesDefinition.ExtendedRepository::key),
+        recorder.getRenamed().map(Map.Entry::getValue).map(RuleKey::repository))
+      .collect(Collectors.toSet());
+  }
+
+  private void update(DbSession session, RuleDto rule) {
+    rule.setUpdatedAt(system2.now());
+    dbClient.ruleDao().update(session, rule);
+  }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java
new file mode 100644 (file)
index 0000000..b3fd9eb
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.rule.registration;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleParamDto;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.unmodifiableMap;
+
+class RulesRegistrationContext {
+
+  private static final Logger LOG = Loggers.get(RulesRegistrationContext.class);
+
+  // initial immutable data
+  private final Map<RuleKey, RuleDto> dbRules;
+  private final Set<RuleDto> known;
+  private final Map<String, Set<SingleDeprecatedRuleKey>> dbDeprecatedKeysByUuid;
+  private final Map<String, List<RuleParamDto>> ruleParamsByRuleUuid;
+  private final Map<RuleKey, RuleDto> dbRulesByDbDeprecatedKey;
+  // mutable data
+  private final Set<RuleDto> created = new HashSet<>();
+  private final Map<RuleDto, RuleKey> renamed = new HashMap<>();
+  private final Set<RuleDto> updated = new HashSet<>();
+  private final Set<RuleDto> unchanged = new HashSet<>();
+  private final Set<RuleDto> removed = new HashSet<>();
+
+  private RulesRegistrationContext(Map<RuleKey, RuleDto> dbRules, Map<String, Set<SingleDeprecatedRuleKey>> dbDeprecatedKeysByUuid,
+    Map<String, List<RuleParamDto>> ruleParamsByRuleUuid) {
+    this.dbRules = ImmutableMap.copyOf(dbRules);
+    this.known = ImmutableSet.copyOf(dbRules.values());
+    this.dbDeprecatedKeysByUuid = dbDeprecatedKeysByUuid;
+    this.ruleParamsByRuleUuid = ruleParamsByRuleUuid;
+    this.dbRulesByDbDeprecatedKey = buildDbRulesByDbDeprecatedKey(dbDeprecatedKeysByUuid, dbRules);
+  }
+
+  private static Map<RuleKey, RuleDto> buildDbRulesByDbDeprecatedKey(Map<String, Set<SingleDeprecatedRuleKey>> dbDeprecatedKeysByUuid,
+    Map<RuleKey, RuleDto> dbRules) {
+    Map<String, RuleDto> dbRulesByRuleUuid = dbRules.values().stream()
+      .collect(Collectors.toMap(RuleDto::getUuid, Function.identity()));
+
+    Map<RuleKey, RuleDto> rulesByKey = new LinkedHashMap<>();
+    for (Map.Entry<String, Set<SingleDeprecatedRuleKey>> entry : dbDeprecatedKeysByUuid.entrySet()) {
+      String ruleUuid = entry.getKey();
+      RuleDto rule = dbRulesByRuleUuid.get(ruleUuid);
+      if (rule == null) {
+        LOG.warn("Could not retrieve rule with uuid %s referenced by a deprecated rule key. " +
+            "The following deprecated rule keys seem to be referencing a non-existing rule",
+          ruleUuid, entry.getValue());
+      } else {
+        entry.getValue().forEach(d -> rulesByKey.put(d.getOldRuleKeyAsRuleKey(), rule));
+      }
+    }
+    return unmodifiableMap(rulesByKey);
+  }
+
+  boolean hasDbRules() {
+    return !dbRules.isEmpty();
+  }
+
+  Optional<RuleDto> getDbRuleFor(RulesDefinition.Rule ruleDef) {
+    RuleKey ruleKey = RuleKey.of(ruleDef.repository().key(), ruleDef.key());
+    Optional<RuleDto> res = Stream.concat(Stream.of(ruleKey), ruleDef.deprecatedRuleKeys().stream())
+      .map(dbRules::get)
+      .filter(Objects::nonNull)
+      .findFirst();
+    // may occur in case of plugin downgrade
+    if (res.isEmpty()) {
+      return Optional.ofNullable(dbRulesByDbDeprecatedKey.get(ruleKey));
+    }
+    return res;
+  }
+
+  Map<RuleKey, SingleDeprecatedRuleKey> getDbDeprecatedKeysByOldRuleKey() {
+    return dbDeprecatedKeysByUuid.values().stream()
+      .flatMap(Collection::stream)
+      .collect(Collectors.toMap(SingleDeprecatedRuleKey::getOldRuleKeyAsRuleKey, Function.identity()));
+  }
+
+  Set<SingleDeprecatedRuleKey> getDBDeprecatedKeysFor(RuleDto rule) {
+    return dbDeprecatedKeysByUuid.getOrDefault(rule.getUuid(), emptySet());
+  }
+
+  List<RuleParamDto> getRuleParametersFor(String ruleUuid) {
+    return ruleParamsByRuleUuid.getOrDefault(ruleUuid, emptyList());
+  }
+
+  Stream<RuleDto> getRemaining() {
+    Set<RuleDto> res = new HashSet<>(dbRules.values());
+    res.removeAll(unchanged);
+    res.removeAll(renamed.keySet());
+    res.removeAll(updated);
+    res.removeAll(removed);
+    return res.stream();
+  }
+
+  Stream<RuleDto> getRemoved() {
+    return removed.stream();
+  }
+
+  public Stream<Map.Entry<RuleDto, RuleKey>> getRenamed() {
+    return renamed.entrySet().stream();
+  }
+
+  Stream<RuleDto> getAllModified() {
+    return Stream.of(
+        created.stream(),
+        updated.stream(),
+        removed.stream(),
+        renamed.keySet().stream())
+      .flatMap(s -> s);
+  }
+
+  boolean isCreated(RuleDto ruleDto) {
+    return created.contains(ruleDto);
+  }
+
+  boolean isRenamed(RuleDto ruleDto) {
+    return renamed.containsKey(ruleDto);
+  }
+
+  boolean isUpdated(RuleDto ruleDto) {
+    return updated.contains(ruleDto);
+  }
+
+  void created(RuleDto ruleDto) {
+    checkState(!known.contains(ruleDto), "known RuleDto can't be created");
+    created.add(ruleDto);
+  }
+
+  void renamed(RuleDto ruleDto) {
+    ensureKnown(ruleDto);
+    renamed.put(ruleDto, ruleDto.getKey());
+  }
+
+  void updated(RuleDto ruleDto) {
+    ensureKnown(ruleDto);
+    updated.add(ruleDto);
+  }
+
+  void removed(RuleDto ruleDto) {
+    ensureKnown(ruleDto);
+    removed.add(ruleDto);
+  }
+
+  void unchanged(RuleDto ruleDto) {
+    ensureKnown(ruleDto);
+    unchanged.add(ruleDto);
+  }
+
+  private void ensureKnown(RuleDto ruleDto) {
+    checkState(known.contains(ruleDto), "unknown RuleDto");
+  }
+
+  static RulesRegistrationContext create(DbClient dbClient, DbSession dbSession) {
+    Map<RuleKey, RuleDto> allRules = dbClient.ruleDao().selectAll(dbSession).stream()
+      .collect(Collectors.toMap(RuleDto::getKey, Function.identity()));
+    Map<String, Set<SingleDeprecatedRuleKey>> existingDeprecatedKeysById = loadDeprecatedRuleKeys(dbClient, dbSession);
+    Map<String, List<RuleParamDto>> ruleParamsByRuleUuid = loadAllRuleParameters(dbClient, dbSession);
+    return new RulesRegistrationContext(allRules, existingDeprecatedKeysById, ruleParamsByRuleUuid);
+  }
+
+  private static Map<String, List<RuleParamDto>> loadAllRuleParameters(DbClient dbClient, DbSession dbSession) {
+    return dbClient.ruleDao().selectAllRuleParams(dbSession).stream()
+      .collect(Collectors.groupingBy(RuleParamDto::getRuleUuid));
+  }
+
+  private static Map<String, Set<SingleDeprecatedRuleKey>> loadDeprecatedRuleKeys(DbClient dbClient, DbSession dbSession) {
+    return dbClient.ruleDao().selectAllDeprecatedRuleKeys(dbSession).stream()
+      .map(SingleDeprecatedRuleKey::from)
+      .collect(Collectors.groupingBy(SingleDeprecatedRuleKey::getRuleUuid, Collectors.toSet()));
+  }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/SingleDeprecatedRuleKey.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/SingleDeprecatedRuleKey.java
new file mode 100644 (file)
index 0000000..68cd19f
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.rule.registration;
+
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+
+@Immutable
+class SingleDeprecatedRuleKey {
+  private String oldRuleKey;
+  private String oldRepositoryKey;
+  private String newRuleKey;
+  private String newRepositoryKey;
+  private String uuid;
+  private String ruleUuid;
+
+  /**
+   * static methods {@link #from(RulesDefinition.Rule)} and {@link #from(DeprecatedRuleKeyDto)} must be used
+   */
+  private SingleDeprecatedRuleKey() {
+    // empty
+  }
+
+  public static Set<SingleDeprecatedRuleKey> from(RulesDefinition.Rule rule) {
+    rule.deprecatedRuleKeys();
+    return rule.deprecatedRuleKeys().stream()
+      .map(r -> new SingleDeprecatedRuleKey()
+        .setNewRepositoryKey(rule.repository().key())
+        .setNewRuleKey(rule.key())
+        .setOldRepositoryKey(r.repository())
+        .setOldRuleKey(r.rule()))
+      .collect(Collectors.toSet());
+  }
+
+  public static SingleDeprecatedRuleKey from(DeprecatedRuleKeyDto rule) {
+    return new SingleDeprecatedRuleKey()
+      .setUuid(rule.getUuid())
+      .setRuleUuid(rule.getRuleUuid())
+      .setNewRepositoryKey(rule.getNewRepositoryKey())
+      .setNewRuleKey(rule.getNewRuleKey())
+      .setOldRepositoryKey(rule.getOldRepositoryKey())
+      .setOldRuleKey(rule.getOldRuleKey());
+  }
+
+  public String getOldRuleKey() {
+    return oldRuleKey;
+  }
+
+  public String getOldRepositoryKey() {
+    return oldRepositoryKey;
+  }
+
+  public RuleKey getOldRuleKeyAsRuleKey() {
+    return RuleKey.of(oldRepositoryKey, oldRuleKey);
+  }
+
+  public RuleKey getNewRuleKeyAsRuleKey() {
+    return RuleKey.of(newRepositoryKey, newRuleKey);
+  }
+
+  @CheckForNull
+  public String getNewRuleKey() {
+    return newRuleKey;
+  }
+
+  @CheckForNull
+  public String getNewRepositoryKey() {
+    return newRepositoryKey;
+  }
+
+  @CheckForNull
+  public String getUuid() {
+    return uuid;
+  }
+
+  @CheckForNull
+  public String getRuleUuid() {
+    return ruleUuid;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    SingleDeprecatedRuleKey that = (SingleDeprecatedRuleKey) o;
+    return Objects.equals(oldRuleKey, that.oldRuleKey) &&
+      Objects.equals(oldRepositoryKey, that.oldRepositoryKey) &&
+      Objects.equals(newRuleKey, that.newRuleKey) &&
+      Objects.equals(newRepositoryKey, that.newRepositoryKey);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(oldRuleKey, oldRepositoryKey, newRuleKey, newRepositoryKey);
+  }
+
+  private SingleDeprecatedRuleKey setRuleUuid(String ruleUuid) {
+    this.ruleUuid = ruleUuid;
+    return this;
+  }
+
+  private SingleDeprecatedRuleKey setUuid(String uuid) {
+    this.uuid = uuid;
+    return this;
+  }
+
+  private SingleDeprecatedRuleKey setOldRuleKey(String oldRuleKey) {
+    this.oldRuleKey = oldRuleKey;
+    return this;
+  }
+
+  private SingleDeprecatedRuleKey setOldRepositoryKey(String oldRepositoryKey) {
+    this.oldRepositoryKey = oldRepositoryKey;
+    return this;
+  }
+
+  private SingleDeprecatedRuleKey setNewRuleKey(@Nullable String newRuleKey) {
+    this.newRuleKey = newRuleKey;
+    return this;
+  }
+
+  private SingleDeprecatedRuleKey setNewRepositoryKey(@Nullable String newRepositoryKey) {
+    this.newRepositoryKey = newRepositoryKey;
+    return this;
+  }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/StartupRuleUpdater.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/StartupRuleUpdater.java
new file mode 100644 (file)
index 0000000..be40beb
--- /dev/null
@@ -0,0 +1,335 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.rule.registration;
+
+import com.google.common.collect.Sets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.System2;
+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.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.rule.RuleDescriptionSectionsGeneratorResolver;
+
+import static com.google.common.collect.Sets.difference;
+import static java.lang.String.format;
+import static java.util.Collections.emptySet;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
+
+/**
+ * The class detects changes between the rule definition coming from plugins during startup and rule from database.
+ * In case any changes are detected the rule is updated with the new information from plugin.
+ */
+public class StartupRuleUpdater {
+
+  private static final Logger LOG = Loggers.get(StartupRuleUpdater.class);
+
+  private final DbClient dbClient;
+  private final System2 system2;
+  private final UuidFactory uuidFactory;
+  private final RuleDescriptionSectionsGeneratorResolver sectionsGeneratorResolver;
+
+  public StartupRuleUpdater(DbClient dbClient, System2 system2, UuidFactory uuidFactory,
+    RuleDescriptionSectionsGeneratorResolver sectionsGeneratorResolver) {
+    this.dbClient = dbClient;
+    this.system2 = system2;
+    this.uuidFactory = uuidFactory;
+    this.sectionsGeneratorResolver = sectionsGeneratorResolver;
+  }
+
+  /**
+   * Returns true in case there was any change detected between rule in the database and rule from the plugin.
+   */
+  boolean findChangesAndUpdateRule(RulesDefinition.Rule ruleDef, RuleDto ruleDto) {
+    boolean ruleMerged = mergeRule(ruleDef, ruleDto);
+    boolean debtDefinitionsMerged = mergeDebtDefinitions(ruleDef, ruleDto);
+    boolean tagsMerged = mergeTags(ruleDef, ruleDto);
+    boolean securityStandardsMerged = mergeSecurityStandards(ruleDef, ruleDto);
+    boolean educationPrinciplesMerged = mergeEducationPrinciples(ruleDef, ruleDto);
+    return ruleMerged || debtDefinitionsMerged || tagsMerged || securityStandardsMerged || educationPrinciplesMerged;
+  }
+
+  void updateDeprecatedKeys(RulesRegistrationContext context, RulesDefinition.Rule ruleDef, RuleDto rule, DbSession dbSession) {
+    Set<SingleDeprecatedRuleKey> deprecatedRuleKeysFromDefinition = SingleDeprecatedRuleKey.from(ruleDef);
+    Set<SingleDeprecatedRuleKey> deprecatedRuleKeysFromDB = context.getDBDeprecatedKeysFor(rule);
+
+    // DeprecatedKeys that must be deleted
+    List<String> uuidsToBeDeleted = difference(deprecatedRuleKeysFromDB, deprecatedRuleKeysFromDefinition).stream()
+      .map(SingleDeprecatedRuleKey::getUuid)
+      .toList();
+
+    dbClient.ruleDao().deleteDeprecatedRuleKeys(dbSession, uuidsToBeDeleted);
+
+    // DeprecatedKeys that must be created
+    Sets.SetView<SingleDeprecatedRuleKey> deprecatedRuleKeysToBeCreated = difference(deprecatedRuleKeysFromDefinition, deprecatedRuleKeysFromDB);
+
+    deprecatedRuleKeysToBeCreated
+      .forEach(r -> dbClient.ruleDao().insert(dbSession, new DeprecatedRuleKeyDto()
+        .setUuid(uuidFactory.create())
+        .setRuleUuid(rule.getUuid())
+        .setOldRepositoryKey(r.getOldRepositoryKey())
+        .setOldRuleKey(r.getOldRuleKey())
+        .setCreatedAt(system2.now())));
+  }
+
+  private boolean mergeRule(RulesDefinition.Rule def, RuleDto dto) {
+    boolean changed = false;
+    if (!Objects.equals(dto.getName(), def.name())) {
+      dto.setName(def.name());
+      changed = true;
+    }
+    if (mergeDescription(def, dto)) {
+      changed = true;
+    }
+    if (!Objects.equals(dto.getPluginKey(), def.pluginKey())) {
+      dto.setPluginKey(def.pluginKey());
+      changed = true;
+    }
+    if (!Objects.equals(dto.getConfigKey(), def.internalKey())) {
+      dto.setConfigKey(def.internalKey());
+      changed = true;
+    }
+    String severity = def.severity();
+    if (!Objects.equals(dto.getSeverityString(), severity)) {
+      dto.setSeverity(severity);
+      changed = true;
+    }
+    boolean isTemplate = def.template();
+    if (isTemplate != dto.isTemplate()) {
+      dto.setIsTemplate(isTemplate);
+      changed = true;
+    }
+    if (def.status() != dto.getStatus()) {
+      dto.setStatus(def.status());
+      changed = true;
+    }
+    if (!Objects.equals(dto.getScope().name(), def.scope().name())) {
+      dto.setScope(RuleDto.Scope.valueOf(def.scope().name()));
+      changed = true;
+    }
+    if (!Objects.equals(dto.getLanguage(), def.repository().language())) {
+      dto.setLanguage(def.repository().language());
+      changed = true;
+    }
+    RuleType type = RuleType.valueOf(def.type().name());
+    if (!Objects.equals(dto.getType(), type.getDbConstant())) {
+      dto.setType(type);
+      changed = true;
+    }
+    if (dto.isAdHoc()) {
+      dto.setIsAdHoc(false);
+      changed = true;
+    }
+    return changed;
+  }
+
+  private static boolean mergeEducationPrinciples(RulesDefinition.Rule ruleDef, RuleDto dto) {
+    boolean changed = false;
+    if (dto.getEducationPrinciples().size() != ruleDef.educationPrincipleKeys().size() ||
+      !dto.getEducationPrinciples().containsAll(ruleDef.educationPrincipleKeys())) {
+      dto.setEducationPrinciples(ruleDef.educationPrincipleKeys());
+      changed = true;
+    }
+    return changed;
+  }
+
+  private static boolean mergeTags(RulesDefinition.Rule ruleDef, RuleDto dto) {
+    boolean changed = false;
+
+    if (RuleStatus.REMOVED == ruleDef.status()) {
+      dto.setSystemTags(emptySet());
+      changed = true;
+    } else if (dto.getSystemTags().size() != ruleDef.tags().size() ||
+      !dto.getSystemTags().containsAll(ruleDef.tags())) {
+      dto.setSystemTags(ruleDef.tags());
+      changed = true;
+    }
+    return changed;
+  }
+
+  private static boolean mergeSecurityStandards(RulesDefinition.Rule ruleDef, RuleDto dto) {
+    boolean changed = false;
+
+    if (RuleStatus.REMOVED == ruleDef.status()) {
+      dto.setSecurityStandards(emptySet());
+      changed = true;
+    } else if (dto.getSecurityStandards().size() != ruleDef.securityStandards().size() ||
+      !dto.getSecurityStandards().containsAll(ruleDef.securityStandards())) {
+      dto.setSecurityStandards(ruleDef.securityStandards());
+      changed = true;
+    }
+    return changed;
+  }
+
+  private static boolean containsHtmlDescription(RulesDefinition.Rule rule) {
+    return isNotEmpty(rule.htmlDescription()) || !rule.ruleDescriptionSections().isEmpty();
+  }
+
+  private static boolean ruleDescriptionSectionsUnchanged(RuleDto ruleDto, Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos) {
+    if (ruleDto.getRuleDescriptionSectionDtos().size() != newRuleDescriptionSectionDtos.size()) {
+      return false;
+    }
+    return ruleDto.getRuleDescriptionSectionDtos().stream()
+      .allMatch(sectionDto -> contains(newRuleDescriptionSectionDtos, sectionDto));
+  }
+
+  private static boolean contains(Set<RuleDescriptionSectionDto> sectionDtos, RuleDescriptionSectionDto sectionDto) {
+    return sectionDtos.stream()
+      .filter(s -> s.getKey().equals(sectionDto.getKey()) && s.getContent().equals(sectionDto.getContent()))
+      .anyMatch(s -> Objects.equals(s.getContext(), sectionDto.getContext()));
+  }
+
+  private static boolean mergeDebtDefinitions(RuleDto dto, @Nullable String remediationFunction,
+    @Nullable String remediationCoefficient, @Nullable String remediationOffset, @Nullable String gapDescription) {
+    boolean changed = false;
+
+    if (!Objects.equals(dto.getDefRemediationFunction(), remediationFunction)) {
+      dto.setDefRemediationFunction(remediationFunction);
+      changed = true;
+    }
+    if (!Objects.equals(dto.getDefRemediationGapMultiplier(), remediationCoefficient)) {
+      dto.setDefRemediationGapMultiplier(remediationCoefficient);
+      changed = true;
+    }
+    if (!Objects.equals(dto.getDefRemediationBaseEffort(), remediationOffset)) {
+      dto.setDefRemediationBaseEffort(remediationOffset);
+      changed = true;
+    }
+    if (!Objects.equals(dto.getGapDescription(), gapDescription)) {
+      dto.setGapDescription(gapDescription);
+      changed = true;
+    }
+    return changed;
+  }
+
+
+  private static boolean mergeDebtDefinitions(RulesDefinition.Rule def, RuleDto dto) {
+    // Debt definitions are set to null if the sub-characteristic and the remediation function are null
+    DebtRemediationFunction debtRemediationFunction = def.debtRemediationFunction();
+    boolean hasDebt = debtRemediationFunction != null;
+    if (hasDebt) {
+      return mergeDebtDefinitions(dto,
+        debtRemediationFunction.type().name(),
+        debtRemediationFunction.gapMultiplier(),
+        debtRemediationFunction.baseEffort(),
+        def.gapDescription());
+    }
+    return mergeDebtDefinitions(dto, null, null, null, null);
+  }
+
+
+  private boolean mergeDescription(RulesDefinition.Rule rule, RuleDto ruleDto) {
+    Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos = sectionsGeneratorResolver.generateFor(rule);
+    if (ruleDescriptionSectionsUnchanged(ruleDto, newRuleDescriptionSectionDtos)) {
+      return false;
+    }
+    ruleDto.replaceRuleDescriptionSectionDtos(newRuleDescriptionSectionDtos);
+    if (containsHtmlDescription(rule)) {
+      ruleDto.setDescriptionFormat(RuleDto.Format.HTML);
+      return true;
+    } else if (isNotEmpty(rule.markdownDescription())) {
+      ruleDto.setDescriptionFormat(RuleDto.Format.MARKDOWN);
+      return true;
+    }
+    return false;
+  }
+
+
+  void mergeParams(RulesRegistrationContext context, RulesDefinition.Rule ruleDef, RuleDto rule, DbSession session) {
+    List<RuleParamDto> paramDtos = context.getRuleParametersFor(rule.getUuid());
+    Map<String, RuleParamDto> existingParamsByName = new HashMap<>();
+
+    Profiler profiler = Profiler.create(LOG);
+    for (RuleParamDto paramDto : paramDtos) {
+      RulesDefinition.Param paramDef = ruleDef.param(paramDto.getName());
+      if (paramDef == null) {
+        profiler.start();
+        dbClient.activeRuleDao().deleteParamsByRuleParam(session, paramDto);
+        profiler.stopDebug(format("Propagate deleted param with name %s to active rules of rule %s", paramDto.getName(), rule.getKey()));
+        dbClient.ruleDao().deleteRuleParam(session, paramDto.getUuid());
+      } else {
+        if (mergeParam(paramDto, paramDef)) {
+          dbClient.ruleDao().updateRuleParam(session, rule, paramDto);
+        }
+        existingParamsByName.put(paramDto.getName(), paramDto);
+      }
+    }
+
+    // Create newly parameters
+    for (RulesDefinition.Param param : ruleDef.params()) {
+      RuleParamDto paramDto = existingParamsByName.get(param.key());
+      if (paramDto != null) {
+        continue;
+      }
+      paramDto = RuleParamDto.createFor(rule)
+        .setName(param.key())
+        .setDescription(param.description())
+        .setDefaultValue(param.defaultValue())
+        .setType(param.type().toString());
+      dbClient.ruleDao().insertRuleParam(session, rule, paramDto);
+      if (StringUtils.isEmpty(param.defaultValue())) {
+        continue;
+      }
+      // Propagate the default value to existing active rule parameters
+      profiler.start();
+      for (ActiveRuleDto activeRule : dbClient.activeRuleDao().selectByRuleUuid(session, rule.getUuid())) {
+        ActiveRuleParamDto activeParam = ActiveRuleParamDto.createFor(paramDto).setValue(param.defaultValue());
+        dbClient.activeRuleDao().insertParam(session, activeRule, activeParam);
+      }
+      profiler.stopDebug(format("Propagate new param with name %s to active rules of rule %s", paramDto.getName(), rule.getKey()));
+    }
+  }
+
+  private static boolean mergeParam(RuleParamDto paramDto, RulesDefinition.Param paramDef) {
+    boolean changed = false;
+    if (!Objects.equals(paramDto.getType(), paramDef.type().toString())) {
+      paramDto.setType(paramDef.type().toString());
+      changed = true;
+    }
+    if (!Objects.equals(paramDto.getDefaultValue(), paramDef.defaultValue())) {
+      paramDto.setDefaultValue(paramDef.defaultValue());
+      changed = true;
+    }
+    if (!Objects.equals(paramDto.getDescription(), paramDef.description())) {
+      paramDto.setDescription(paramDef.description());
+      changed = true;
+    }
+    return changed;
+  }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java
deleted file mode 100644 (file)
index 89a8a76..0000000
+++ /dev/null
@@ -1,1344 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.rule;
-
-import com.tngtech.java.junit.dataprovider.DataProvider;
-import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import org.elasticsearch.common.util.set.Sets;
-import org.jetbrains.annotations.Nullable;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.sonar.api.impl.utils.TestSystem2;
-import org.sonar.api.resources.Language;
-import org.sonar.api.resources.Languages;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rule.RuleScope;
-import org.sonar.api.rule.RuleStatus;
-import org.sonar.api.rules.RuleType;
-import org.sonar.api.server.debt.DebtRemediationFunction;
-import org.sonar.api.server.rule.Context;
-import org.sonar.api.server.rule.RuleDescriptionSection;
-import org.sonar.api.server.rule.RulesDefinition;
-import org.sonar.api.utils.DateUtils;
-import org.sonar.api.testfixtures.log.LogTester;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.core.util.UuidFactoryFast;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.DbTester;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.QProfileChangeDto;
-import org.sonar.db.qualityprofile.QProfileChangeQuery;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.rule.DeprecatedRuleKeyDto;
-import org.sonar.db.rule.RuleDescriptionSectionContextDto;
-import org.sonar.db.rule.RuleDescriptionSectionDto;
-import org.sonar.db.rule.RuleDto;
-import org.sonar.db.rule.RuleDto.Scope;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.db.rule.RuleRepositoryDto;
-import org.sonar.server.es.EsTester;
-import org.sonar.server.es.SearchIdResult;
-import org.sonar.server.es.SearchOptions;
-import org.sonar.server.es.metadata.MetadataIndex;
-import org.sonar.server.plugins.ServerPluginRepository;
-import org.sonar.server.qualityprofile.ActiveRuleChange;
-import org.sonar.server.qualityprofile.QProfileRules;
-import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
-import org.sonar.server.rule.index.RuleIndex;
-import org.sonar.server.rule.index.RuleIndexDefinition;
-import org.sonar.server.rule.index.RuleIndexer;
-import org.sonar.server.rule.index.RuleQuery;
-
-import static com.google.common.collect.Sets.newHashSet;
-import static java.lang.String.format;
-import static java.lang.String.valueOf;
-import static java.util.Collections.emptySet;
-import static java.util.Collections.singletonList;
-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;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.rule.RuleStatus.READY;
-import static org.sonar.api.rule.RuleStatus.REMOVED;
-import static org.sonar.api.rule.Severity.BLOCKER;
-import static org.sonar.api.rule.Severity.INFO;
-import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY;
-import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY;
-import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.RESOURCES_SECTION_KEY;
-import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
-import static org.sonar.api.server.rule.RulesDefinition.NewRepository;
-import static org.sonar.api.server.rule.RulesDefinition.NewRule;
-import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10;
-import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.builder;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
-
-@RunWith(DataProviderRunner.class)
-public class RegisterRulesTest {
-
-  private static final String FAKE_PLUGIN_KEY = "unittest";
-  private static final Date DATE1 = DateUtils.parseDateTime("2014-01-01T19:10:03+0100");
-  private static final Date DATE2 = DateUtils.parseDateTime("2014-02-01T12:10:03+0100");
-  private static final Date DATE3 = DateUtils.parseDateTime("2014-03-01T12:10:03+0100");
-
-  private static final RuleKey EXTERNAL_RULE_KEY1 = RuleKey.of("external_eslint", "rule1");
-  private static final RuleKey EXTERNAL_HOTSPOT_RULE_KEY = RuleKey.of("external_eslint", "hotspot");
-
-  private static final RuleKey RULE_KEY1 = RuleKey.of("fake", "rule1");
-  private static final RuleKey RULE_KEY2 = RuleKey.of("fake", "rule2");
-  private static final RuleKey RULE_KEY3 = RuleKey.of("fake", "rule3");
-  private static final RuleKey HOTSPOT_RULE_KEY = RuleKey.of("fake", "hotspot");
-
-  private final TestSystem2 system = new TestSystem2().setNow(DATE1.getTime());
-
-  @org.junit.Rule
-  public DbTester db = DbTester.create(system);
-  @org.junit.Rule
-  public EsTester es = EsTester.create();
-  @org.junit.Rule
-  public LogTester logTester = new LogTester();
-
-  private final QProfileRules qProfileRules = mock(QProfileRules.class);
-  private final WebServerRuleFinder webServerRuleFinder = mock(WebServerRuleFinder.class);
-  private final DbClient dbClient = db.getDbClient();
-  private final MetadataIndex metadataIndex = mock(MetadataIndex.class);
-  private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();
-
-  private RuleIndexer ruleIndexer;
-  private ActiveRuleIndexer activeRuleIndexer;
-  private RuleIndex ruleIndex;
-  private final RuleDescriptionSectionsGenerator ruleDescriptionSectionsGenerator = mock(RuleDescriptionSectionsGenerator.class);
-  private final RuleDescriptionSectionsGeneratorResolver resolver = mock(RuleDescriptionSectionsGeneratorResolver.class);
-
-  @Before
-  public void before() {
-    ruleIndexer = new RuleIndexer(es.client(), dbClient);
-    ruleIndex = new RuleIndex(es.client(), system);
-    activeRuleIndexer = new ActiveRuleIndexer(dbClient, es.client());
-    when(resolver.getRuleDescriptionSectionsGenerator(any())).thenReturn(ruleDescriptionSectionsGenerator);
-    when(ruleDescriptionSectionsGenerator.generateSections(any())).thenAnswer(answer -> {
-      RulesDefinition.Rule rule = answer.getArgument(0, RulesDefinition.Rule.class);
-      String description = rule.htmlDescription() == null ? rule.markdownDescription() : rule.htmlDescription();
-
-      Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = rule.ruleDescriptionSections().stream() //
-        .map(s -> builder()
-          .uuid(UuidFactoryFast.getInstance().create())
-          .key(s.getKey())
-          .content(s.getHtmlContent())
-          .context(s.getContext().map(c -> RuleDescriptionSectionContextDto.of(c.getKey(), c.getDisplayName())).orElse(null))
-          .build()
-        )
-        .collect(Collectors.toSet());
-      return Sets.union(ruleDescriptionSectionDtos, Set.of(builder().uuid(UuidFactoryFast.getInstance().create()).key("default").content(description).build()));
-    });
-
-    when(ruleDescriptionSectionsGenerator.isGeneratorForRule(any())).thenReturn(true);
-  }
-
-  @Test
-  public void insert_new_rules() {
-    execute(new FakeRepositoryV1());
-
-    // verify db
-    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(3);
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    verifyRule(rule1);
-    assertThat(rule1.isExternal()).isFalse();
-    assertThat(rule1.getDefRemediationFunction()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET.name());
-    assertThat(rule1.getDefRemediationGapMultiplier()).isEqualTo("5d");
-    assertThat(rule1.getDefRemediationBaseEffort()).isEqualTo("10h");
-
-    RuleDto hotspotRule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), HOTSPOT_RULE_KEY);
-    verifyHotspot(hotspotRule);
-
-    List<RuleParamDto> params = dbClient.ruleDao().selectRuleParamsByRuleKey(db.getSession(), RULE_KEY1);
-    assertThat(params).hasSize(2);
-    RuleParamDto param = getParam(params, "param1");
-    assertThat(param.getDescription()).isEqualTo("parameter one");
-    assertThat(param.getDefaultValue()).isEqualTo("default1");
-
-    // verify index
-    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
-    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule2.getUuid(), hotspotRule.getUuid());
-    verifyIndicesMarkedAsInitialized();
-
-    // verify repositories
-    assertThat(dbClient.ruleRepositoryDao().selectAll(db.getSession())).extracting(RuleRepositoryDto::getKey).containsOnly("fake");
-  }
-
-  private void verifyHotspot(RuleDto hotspotRule) {
-    assertThat(hotspotRule.getName()).isEqualTo("Hotspot");
-    assertThat(hotspotRule.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Minimal hotspot");
-    assertThat(hotspotRule.getCreatedAt()).isEqualTo(RegisterRulesTest.DATE1.getTime());
-    assertThat(hotspotRule.getUpdatedAt()).isEqualTo(RegisterRulesTest.DATE1.getTime());
-    assertThat(hotspotRule.getType()).isEqualTo(RuleType.SECURITY_HOTSPOT.getDbConstant());
-    assertThat(hotspotRule.getSecurityStandards()).containsExactly("cwe:1", "cwe:123", "cwe:863", "owaspTop10-2021:a1", "owaspTop10-2021:a3");
-  }
-
-  @Test
-  public void insert_new_external_rule() {
-    execute(new ExternalRuleRepository());
-
-    // verify db
-    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(2);
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), EXTERNAL_RULE_KEY1);
-    verifyRule(rule1);
-    assertThat(rule1.isExternal()).isTrue();
-    assertThat(rule1.getDefRemediationFunction()).isNull();
-    assertThat(rule1.getDefRemediationGapMultiplier()).isNull();
-    assertThat(rule1.getDefRemediationBaseEffort()).isNull();
-
-    RuleDto hotspotRule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), EXTERNAL_HOTSPOT_RULE_KEY);
-    verifyHotspot(hotspotRule);
-  }
-
-  private void verifyRule(RuleDto rule) {
-    assertThat(rule.getName()).isEqualTo("One");
-    assertThat(rule.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Description of One");
-    assertThat(rule.getSeverityString()).isEqualTo(BLOCKER);
-    assertThat(rule.getTags()).isEmpty();
-    assertThat(rule.getSystemTags()).containsOnly("tag1", "tag2", "tag3");
-    assertThat(rule.getConfigKey()).isEqualTo("config1");
-    assertThat(rule.getStatus()).isEqualTo(RuleStatus.BETA);
-    assertThat(rule.getCreatedAt()).isEqualTo(DATE1.getTime());
-    assertThat(rule.getScope()).isEqualTo(Scope.ALL);
-    assertThat(rule.getUpdatedAt()).isEqualTo(DATE1.getTime());
-    assertThat(rule.getType()).isEqualTo(RuleType.CODE_SMELL.getDbConstant());
-    assertThat(rule.getPluginKey()).isEqualTo(FAKE_PLUGIN_KEY);
-    assertThat(rule.isAdHoc()).isFalse();
-    assertThat(rule.getEducationPrinciples()).containsOnly("concept1", "concept2", "concept3");
-  }
-
-  @Test
-  public void insert_then_remove_rule() {
-    String ruleKey = randomAlphanumeric(5);
-
-    // register one rule
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule(ruleKey)
-        .setName(randomAlphanumeric(5))
-        .setHtmlDescription(randomAlphanumeric(20));
-      repo.done();
-    });
-
-    // verify db
-    List<RuleDto> rules = dbClient.ruleDao().selectAll(db.getSession());
-    assertThat(rules)
-      .extracting(RuleDto::getKey)
-      .extracting(RuleKey::rule)
-      .containsExactly(ruleKey);
-    RuleDto rule = rules.iterator().next();
-
-    // verify index
-    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids())
-      .containsExactly(rule.getUuid());
-    verifyIndicesMarkedAsInitialized();
-
-    // register no rule
-    execute(context -> context.createRepository("fake", "java").done());
-
-    // verify db
-    assertThat(dbClient.ruleDao().selectAll(db.getSession()))
-      .extracting(RuleDto::getKey)
-      .extracting(RuleKey::rule)
-      .containsExactly(ruleKey);
-    assertThat(dbClient.ruleDao().selectAll(db.getSession()))
-      .extracting(RuleDto::getStatus)
-      .containsExactly(REMOVED);
-
-    // verify index
-    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids())
-      .isEmpty();
-    verifyIndicesNotMarkedAsInitialized();
-  }
-
-  @Test
-  public void mass_insert_then_remove_rule() {
-    int numberOfRules = 5000;
-
-    // register many rules
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      IntStream.range(0, numberOfRules)
-        .mapToObj(i -> "rule-" + i)
-        .forEach(ruleKey -> repo.createRule(ruleKey)
-          .setName(randomAlphanumeric(20))
-          .setHtmlDescription(randomAlphanumeric(20)));
-      repo.done();
-    });
-
-    // verify db
-    assertThat(dbClient.ruleDao().selectAll(db.getSession()))
-      .hasSize(numberOfRules)
-      .extracting(RuleDto::getStatus)
-      .containsOnly(READY);
-
-    // verify index
-    assertThat(es.countDocuments(RuleIndexDefinition.TYPE_RULE)).isEqualTo(numberOfRules);
-    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids())
-      .isNotEmpty();
-
-    // register no rule
-    execute(context -> context.createRepository("fake", "java").done());
-
-    // verify db
-    assertThat(dbClient.ruleDao().selectAll(db.getSession()))
-      .hasSize(numberOfRules)
-      .extracting(RuleDto::getStatus)
-      .containsOnly(REMOVED);
-
-    // verify index (documents are still in the index, but all are removed)
-    assertThat(es.countDocuments(RuleIndexDefinition.TYPE_RULE)).isEqualTo(numberOfRules);
-    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids())
-      .isEmpty();
-  }
-
-  @Test
-  public void delete_repositories_that_have_been_uninstalled() {
-    RuleRepositoryDto repository = new RuleRepositoryDto("findbugs", "java", "Findbugs");
-    DbSession dbSession = db.getSession();
-    db.getDbClient().ruleRepositoryDao().insert(dbSession, singletonList(repository));
-    dbSession.commit();
-
-    execute(new FakeRepositoryV1());
-
-    assertThat(db.getDbClient().ruleRepositoryDao().selectAll(dbSession)).extracting(RuleRepositoryDto::getKey).containsOnly("fake");
-  }
-
-  @Test
-  public void update_and_remove_rules_on_changes() {
-    execute(new FakeRepositoryV1());
-    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(3);
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
-    RuleDto hotspotRule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), HOTSPOT_RULE_KEY);
-    assertThat(es.getIds(RuleIndexDefinition.TYPE_RULE)).containsOnly(valueOf(rule1.getUuid()), valueOf(rule2.getUuid()), valueOf(hotspotRule.getUuid()));
-    verifyIndicesMarkedAsInitialized();
-
-    // user adds tags and sets markdown note
-    rule1.setTags(newHashSet("usertag1", "usertag2"));
-    rule1.setNoteData("user *note*");
-    rule1.setNoteUserUuid("marius");
-    dbClient.ruleDao().update(db.getSession(), rule1);
-    db.getSession().commit();
-
-    system.setNow(DATE2.getTime());
-    execute(new FakeRepositoryV2());
-
-    verifyIndicesNotMarkedAsInitialized();
-    // rule1 has been updated
-    rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    assertThatRule1IsV2(rule1);
-
-    List<RuleParamDto> params = dbClient.ruleDao().selectRuleParamsByRuleKey(db.getSession(), RULE_KEY1);
-    assertThat(params).hasSize(2);
-    RuleParamDto param = getParam(params, "param1");
-    assertThat(param.getDescription()).isEqualTo("parameter one v2");
-    assertThat(param.getDefaultValue()).isEqualTo("default1 v2");
-
-    // rule2 has been removed -> status set to REMOVED but db row is not deleted
-    rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
-    assertThat(rule2.getStatus()).isEqualTo(REMOVED);
-    assertThat(rule2.getUpdatedAt()).isEqualTo(DATE2.getTime());
-
-    // rule3 has been created
-    RuleDto rule3 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY3);
-    assertThat(rule3).isNotNull();
-    assertThat(rule3.getStatus()).isEqualTo(READY);
-
-    // verify index
-    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule3.getUuid());
-
-    // verify repositories
-    assertThat(dbClient.ruleRepositoryDao().selectAll(db.getSession())).extracting(RuleRepositoryDto::getKey).containsOnly("fake");
-
-    system.setNow(DATE3.getTime());
-    execute(new FakeRepositoryV3());
-    rule3 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY3);
-    assertThat(rule3.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Rule Three V2");
-    assertThat(rule3.getDescriptionFormat()).isEqualTo(RuleDto.Format.MARKDOWN);
-  }
-
-  private void assertThatRule1IsV2(RuleDto rule1) {
-    assertThat(rule1.getName()).isEqualTo("One v2");
-    RuleDescriptionSectionDto defaultRuleDescriptionSection = rule1.getDefaultRuleDescriptionSection();
-    assertThat(defaultRuleDescriptionSection.getContent()).isEqualTo("Description of One v2");
-    assertThat(defaultRuleDescriptionSection.getKey()).isEqualTo(DEFAULT_KEY);
-    assertThat(rule1.getDescriptionFormat()).isEqualTo(RuleDto.Format.HTML);
-    assertThat(rule1.getSeverityString()).isEqualTo(INFO);
-    assertThat(rule1.getTags()).containsOnly("usertag1", "usertag2");
-    assertThat(rule1.getSystemTags()).containsOnly("tag1", "tag4");
-    assertThat(rule1.getConfigKey()).isEqualTo("config1 v2");
-    assertThat(rule1.getNoteData()).isEqualTo("user *note*");
-    assertThat(rule1.getNoteUserUuid()).isEqualTo("marius");
-    assertThat(rule1.getStatus()).isEqualTo(READY);
-    assertThat(rule1.getType()).isEqualTo(RuleType.BUG.getDbConstant());
-    assertThat(rule1.getCreatedAt()).isEqualTo(DATE1.getTime());
-    assertThat(rule1.getUpdatedAt()).isEqualTo(DATE2.getTime());
-    assertThat(rule1.getEducationPrinciples()).containsOnly("concept1","concept4");
-  }
-
-  @Test
-  public void add_new_tag() {
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule1")
-        .setName("Rule One")
-        .setHtmlDescription("Description of Rule One")
-        .setTags("tag1");
-      repo.done();
-    });
-
-    RuleDto rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    assertThat(rule.getSystemTags()).containsOnly("tag1");
-
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule1")
-        .setName("Rule One")
-        .setHtmlDescription("Description of Rule One")
-        .setTags("tag1", "tag2");
-      repo.done();
-    });
-
-    rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    assertThat(rule.getSystemTags()).containsOnly("tag1", "tag2");
-  }
-
-  @Test
-  public void add_new_security_standards() {
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule1")
-        .setName("Rule One")
-        .setHtmlDescription("Description of Rule One")
-        .addOwaspTop10(Y2021, OwaspTop10.A1)
-        .addCwe(123);
-      repo.done();
-    });
-
-    RuleDto rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    assertThat(rule.getSecurityStandards()).containsOnly("cwe:123", "owaspTop10-2021:a1");
-
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule1")
-        .setName("Rule One")
-        .setHtmlDescription("Description of Rule One")
-        .addOwaspTop10(Y2021, OwaspTop10.A1, OwaspTop10.A3)
-        .addCwe(1, 123, 863);
-      repo.done();
-    });
-
-    rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    assertThat(rule.getSecurityStandards()).containsOnly("cwe:1", "cwe:123", "cwe:863", "owaspTop10-2021:a1", "owaspTop10-2021:a3");
-  }
-
-  @Test
-  public void update_only_rule_name() {
-    system.setNow(DATE1.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule")
-        .setName("Name1")
-        .setHtmlDescription("Description");
-      repo.done();
-    });
-
-    system.setNow(DATE2.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule")
-        .setName("Name2")
-        .setHtmlDescription("Description");
-      repo.done();
-    });
-
-    // rule1 has been updated
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("fake", "rule"));
-    assertThat(rule1.getName()).isEqualTo("Name2");
-    assertThat(rule1.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Description");
-
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name2"), new SearchOptions()).getTotal()).isOne();
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getTotal()).isZero();
-  }
-
-  @Test
-  public void update_template_rule_key_should_also_update_custom_rules() {
-    system.setNow(DATE1.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository("squid", "java");
-      repo.createRule("rule")
-        .setName("Name1")
-        .setHtmlDescription("Description")
-        .setTemplate(true);
-      repo.done();
-    });
-
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("squid", "rule"));
-
-    // insert custom rule
-    db.rules().insert(new RuleDto()
-      .setRuleKey(RuleKey.of("squid", "custom"))
-      .setLanguage("java")
-      .setScope(Scope.ALL)
-      .setTemplateUuid(rule1.getUuid())
-      .setName("custom1"));
-    db.commit();
-
-    // re-key rule
-    execute(context -> {
-      NewRepository repo = context.createRepository("java", "java");
-      repo.createRule("rule")
-        .setName("Name1")
-        .setHtmlDescription("Description")
-        .addDeprecatedRuleKey("squid", "rule")
-        .setTemplate(true);
-      repo.done();
-    });
-
-    // template rule and custom rule have been updated
-    rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("java", "rule"));
-    RuleDto custom = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("java", "custom"));
-  }
-
-  @Test
-  public void update_if_rule_key_renamed_and_deprecated_key_declared() {
-    String ruleKey1 = "rule1";
-    String ruleKey2 = "rule2";
-    String repository = "fake";
-
-    system.setNow(DATE1.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository(repository, "java");
-      repo.createRule(ruleKey1)
-        .setName("Name1")
-        .setHtmlDescription("Description");
-      repo.done();
-    });
-
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository, ruleKey1));
-    SearchIdResult<String> searchRule1 = ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions());
-    assertThat(searchRule1.getUuids()).containsOnly(rule1.getUuid());
-    assertThat(searchRule1.getTotal()).isOne();
-
-    system.setNow(DATE2.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository(repository, "java");
-      repo.createRule(ruleKey2)
-        .setName("Name2")
-        .setHtmlDescription("Description")
-        .addDeprecatedRuleKey(repository, ruleKey1);
-      repo.done();
-    });
-
-    // rule2 is actually rule1
-    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository, ruleKey2));
-    assertThat(rule2.getUuid()).isEqualTo(rule1.getUuid());
-    assertThat(rule2.getName()).isEqualTo("Name2");
-    assertThat(rule2.getDefaultRuleDescriptionSection().getContent()).isEqualTo(rule1.getDefaultRuleDescriptionSection().getContent());
-
-    SearchIdResult<String> searchRule2 = ruleIndex.search(new RuleQuery().setQueryText("Name2"), new SearchOptions());
-    assertThat(searchRule2.getUuids()).containsOnly(rule2.getUuid());
-    assertThat(searchRule2.getTotal()).isOne();
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getTotal()).isZero();
-  }
-
-  @Test
-  public void update_if_repository_changed_and_deprecated_key_declared() {
-    String ruleKey = "rule";
-    String repository1 = "fake1";
-    String repository2 = "fake2";
-
-    system.setNow(DATE1.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository(repository1, "java");
-      repo.createRule(ruleKey)
-        .setName("Name1")
-        .setHtmlDescription("Description");
-      repo.done();
-    });
-
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository1, ruleKey));
-    SearchIdResult<String> searchRule1 = ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions());
-    assertThat(searchRule1.getUuids()).containsOnly(rule1.getUuid());
-    assertThat(searchRule1.getTotal()).isOne();
-
-    system.setNow(DATE2.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository(repository2, "java");
-      repo.createRule(ruleKey)
-        .setName("Name2")
-        .setHtmlDescription("Description")
-        .addDeprecatedRuleKey(repository1, ruleKey);
-      repo.done();
-    });
-
-    // rule2 is actually rule1
-    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository2, ruleKey));
-    assertThat(rule2.getUuid()).isEqualTo(rule1.getUuid());
-    assertThat(rule2.getName()).isEqualTo("Name2");
-    assertThat(rule2.getDefaultRuleDescriptionSection().getContent()).isEqualTo(rule1.getDefaultRuleDescriptionSection().getContent());
-
-    SearchIdResult<String> searchRule2 = ruleIndex.search(new RuleQuery().setQueryText("Name2"), new SearchOptions());
-    assertThat(searchRule2.getUuids()).containsOnly(rule2.getUuid());
-    assertThat(searchRule2.getTotal()).isOne();
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getTotal()).isZero();
-  }
-
-  @Test
-  @UseDataProvider("allRenamingCases")
-  public void update_if_only_renamed_and_deprecated_key_declared(String ruleKey1, String repo1, String ruleKey2, String repo2) {
-    String name = "Name1";
-    String description = "Description";
-    system.setNow(DATE1.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository(repo1, "java");
-      repo.createRule(ruleKey1)
-        .setName(name)
-        .setHtmlDescription(description);
-      repo.done();
-    });
-
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repo1, ruleKey1));
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText(name), new SearchOptions()).getUuids())
-      .containsOnly(rule1.getUuid());
-
-    system.setNow(DATE2.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository(repo2, "java");
-      repo.createRule(ruleKey2)
-        .setName(name)
-        .setHtmlDescription(description)
-        .addDeprecatedRuleKey(repo1, ruleKey1);
-      repo.done();
-    });
-
-    // rule2 is actually rule1
-    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repo2, ruleKey2));
-    assertThat(rule2.getUuid()).isEqualTo(rule1.getUuid());
-    assertThat(rule2.getName()).isEqualTo(rule1.getName());
-    assertThat(rule2.getDefaultRuleDescriptionSection().getContent()).isEqualTo(rule1.getDefaultRuleDescriptionSection().getContent());
-
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText(name), new SearchOptions()).getUuids())
-      .containsOnly(rule2.getUuid());
-  }
-
-  @DataProvider
-  public static Object[][] allRenamingCases() {
-    return new Object[][]{
-      {"repo1", "rule1", "repo1", "rule2"},
-      {"repo1", "rule1", "repo2", "rule1"},
-      {"repo1", "rule1", "repo2", "rule2"},
-    };
-  }
-
-  @Test
-  public void update_if_repository_and_key_changed_and_deprecated_key_declared_among_others() {
-    String ruleKey1 = "rule1";
-    String ruleKey2 = "rule2";
-    String repository1 = "fake1";
-    String repository2 = "fake2";
-
-    system.setNow(DATE1.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository(repository1, "java");
-      repo.createRule(ruleKey1)
-        .setName("Name1")
-        .setHtmlDescription("Description");
-      repo.done();
-    });
-
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository1, ruleKey1));
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getUuids())
-      .containsOnly(rule1.getUuid());
-
-    system.setNow(DATE2.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository(repository2, "java");
-      repo.createRule(ruleKey2)
-        .setName("Name2")
-        .setHtmlDescription("Description")
-        .addDeprecatedRuleKey("foo", "bar")
-        .addDeprecatedRuleKey(repository1, ruleKey1)
-        .addDeprecatedRuleKey("some", "noise");
-      repo.done();
-    });
-
-    // rule2 is actually rule1
-    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository2, ruleKey2));
-    assertThat(rule2.getUuid()).isEqualTo(rule1.getUuid());
-
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name2"), new SearchOptions()).getUuids())
-      .containsOnly(rule1.getUuid());
-  }
-
-  @Test
-  public void update_only_rule_description() {
-    system.setNow(DATE1.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule")
-        .setName("Name")
-        .setHtmlDescription("Desc1");
-      repo.done();
-    });
-
-    system.setNow(DATE2.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule")
-        .setName("Name")
-        .setHtmlDescription("Desc2");
-      repo.done();
-    });
-
-    // rule1 has been updated
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("fake", "rule"));
-    assertThat(rule1.getName()).isEqualTo("Name");
-    assertThat(rule1.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Desc2");
-
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Desc2"), new SearchOptions()).getTotal()).isOne();
-    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Desc1"), new SearchOptions()).getTotal()).isZero();
-  }
-
-  @Test
-  public void update_several_rule_descriptions() {
-    system.setNow(DATE1.getTime());
-
-    RuleDescriptionSection section1context1 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "section1 ctx1 content", "ctx_1");
-    RuleDescriptionSection section1context2 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "section1 ctx2 content", "ctx_2");
-    RuleDescriptionSection section2context1 = createRuleDescriptionSection(RESOURCES_SECTION_KEY, "section2 content", "ctx_1");
-    RuleDescriptionSection section2context2 = createRuleDescriptionSection(RESOURCES_SECTION_KEY,"section2 ctx2 content", "ctx_2");
-    RuleDescriptionSection section3noContext = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "section3 content", null);
-    RuleDescriptionSection section4noContext = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, "section4 content", null);
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule")
-        .setName("Name")
-        .addDescriptionSection(section1context1)
-        .addDescriptionSection(section1context2)
-        .addDescriptionSection(section2context1)
-        .addDescriptionSection(section2context2)
-        .addDescriptionSection(section3noContext)
-        .addDescriptionSection(section4noContext)
-        .setHtmlDescription("Desc1");
-      repo.done();
-    });
-
-    RuleDescriptionSection section1context2updated = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "section1 ctx2 updated content", "ctx_2");
-    RuleDescriptionSection section2updatedWithoutContext = createRuleDescriptionSection(RESOURCES_SECTION_KEY, section2context1.getHtmlContent(), null);
-    RuleDescriptionSection section4updatedWithContext1 = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, section4noContext.getHtmlContent(), "ctx_1");
-    RuleDescriptionSection section4updatedWithContext2 = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, section4noContext.getHtmlContent(), "ctx_2");
-    system.setNow(DATE2.getTime());
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      repo.createRule("rule")
-        .setName("Name")
-        .addDescriptionSection(section1context1)
-        .addDescriptionSection(section1context2updated)
-        .addDescriptionSection(section2updatedWithoutContext)
-        .addDescriptionSection(section3noContext)
-        .addDescriptionSection(section4updatedWithContext1)
-        .addDescriptionSection(section4updatedWithContext2)
-        .setHtmlDescription("Desc2");
-      repo.done();
-
-    });
-
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("fake", "rule"));
-    assertThat(rule1.getName()).isEqualTo("Name");
-    assertThat(rule1.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Desc2");
-
-    Set<RuleDescriptionSection> expectedSections = Set.of(section1context1, section1context2updated,
-      section2updatedWithoutContext, section3noContext, section4updatedWithContext1, section4updatedWithContext2);
-    assertThat(rule1.getRuleDescriptionSectionDtos()).hasSize(expectedSections.size() + 1);
-    expectedSections.forEach(apiSection -> assertSectionExists(apiSection, rule1.getRuleDescriptionSectionDtos()));
-  }
-
-  private static RuleDescriptionSection createRuleDescriptionSection(String sectionKey, String description, @Nullable String contextKey) {
-    Context context = Optional.ofNullable(contextKey).map(key -> new Context(contextKey, contextKey + randomAlphanumeric(10))).orElse(null);
-    return RuleDescriptionSection.builder().sectionKey(sectionKey)
-      .htmlContent(description)
-      .context(context)
-      .build();
-  }
-
-  private static void assertSectionExists(RuleDescriptionSection apiSection, Set<RuleDescriptionSectionDto> sectionDtos) {
-    sectionDtos.stream()
-      .filter(sectionDto -> sectionDto.getKey().equals(apiSection.getKey()) && sectionDto.getContent().equals(apiSection.getHtmlContent()))
-      .filter(sectionDto -> isSameContext(apiSection.getContext(), sectionDto.getContext()))
-      .findAny()
-      .orElseThrow(() -> new AssertionError(format("Impossible to find a section dto matching the API section %s", apiSection.getKey())));
-  }
-
-  private static boolean isSameContext(Optional<Context> apiContext, @Nullable RuleDescriptionSectionContextDto contextDto) {
-    if (apiContext.isEmpty() && contextDto == null) {
-      return true;
-    }
-    return apiContext.filter(context -> isSameContext(context, contextDto)).isPresent();
-  }
-
-  private static boolean isSameContext(Context apiContext, @Nullable RuleDescriptionSectionContextDto contextDto) {
-    if (contextDto == null) {
-      return false;
-    }
-    return Objects.equals(apiContext.getKey(), contextDto.getKey()) && Objects.equals(apiContext.getDisplayName(), contextDto.getDisplayName());
-  }
-
-  @Test
-  public void rule_previously_created_as_adhoc_becomes_none_adhoc() {
-    RuleDto rule = db.rules().insert(r -> r.setRepositoryKey("external_fake").setIsExternal(true).setIsAdHoc(true));
-    system.setNow(DATE2.getTime());
-    execute(context -> {
-      NewRepository repo = context.createExternalRepository("fake", rule.getLanguage());
-      repo.createRule(rule.getRuleKey())
-        .setName(rule.getName())
-        .setHtmlDescription(rule.getDefaultRuleDescriptionSection().getContent());
-      repo.done();
-    });
-
-    RuleDto reloaded = dbClient.ruleDao().selectByKey(db.getSession(), rule.getKey()).get();
-    assertThat(reloaded.isAdHoc()).isFalse();
-  }
-
-  @Test
-  public void remove_no_more_defined_external_rule() {
-    RuleDto rule = db.rules().insert(r -> r.setRepositoryKey("external_fake")
-      .setStatus(READY)
-      .setIsExternal(true)
-      .setIsAdHoc(false));
-
-    execute();
-
-    RuleDto reloaded = dbClient.ruleDao().selectByKey(db.getSession(), rule.getKey()).get();
-    assertThat(reloaded.getStatus()).isEqualTo(REMOVED);
-  }
-
-  @Test
-  public void do_not_remove_no_more_defined_ad_hoc_rule() {
-    RuleDto rule = db.rules().insert(r -> r.setRepositoryKey("external_fake")
-      .setStatus(READY)
-      .setIsExternal(true)
-      .setIsAdHoc(true));
-
-    execute();
-
-    RuleDto reloaded = dbClient.ruleDao().selectByKey(db.getSession(), rule.getKey()).get();
-    assertThat(reloaded.getStatus()).isEqualTo(READY);
-  }
-
-  @Test
-  public void disable_then_enable_rule() {
-    // Install rule
-    system.setNow(DATE1.getTime());
-    execute(new FakeRepositoryV1());
-
-    // Uninstall rule
-    system.setNow(DATE2.getTime());
-    execute();
-
-    RuleDto rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    assertThat(rule.getStatus()).isEqualTo(REMOVED);
-    assertThat(ruleIndex.search(new RuleQuery().setKey(RULE_KEY1.toString()), new SearchOptions()).getTotal()).isZero();
-
-    // Re-install rule
-    system.setNow(DATE3.getTime());
-    execute(new FakeRepositoryV1());
-
-    rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    assertThat(rule.getStatus()).isEqualTo(RuleStatus.BETA);
-    assertThat(ruleIndex.search(new RuleQuery().setKey(RULE_KEY1.toString()), new SearchOptions()).getTotal()).isOne();
-  }
-
-  @Test
-  public void do_not_update_rules_when_no_changes() {
-    execute(new FakeRepositoryV1());
-    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(3);
-
-    system.setNow(DATE2.getTime());
-    execute(new FakeRepositoryV1());
-
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    assertThat(rule1.getCreatedAt()).isEqualTo(DATE1.getTime());
-    assertThat(rule1.getUpdatedAt()).isEqualTo(DATE1.getTime());
-  }
-
-  @Test
-  public void do_not_update_already_removed_rules() {
-    execute(new FakeRepositoryV1());
-    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(3);
-
-    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
-    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
-    RuleDto hotspotRule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), HOTSPOT_RULE_KEY);
-    assertThat(es.getIds(RuleIndexDefinition.TYPE_RULE)).containsOnly(valueOf(rule1.getUuid()), valueOf(rule2.getUuid()), valueOf(hotspotRule.getUuid()));
-
-    assertThat(rule2.getStatus()).isEqualTo(READY);
-
-    system.setNow(DATE2.getTime());
-    execute(new FakeRepositoryV2());
-
-    // On MySQL, need to update a rule otherwise rule2 will be seen as READY, but why ???
-    dbClient.ruleDao().update(db.getSession(), rule1);
-    db.getSession().commit();
-
-    // rule2 is removed
-    rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
-    RuleDto rule3 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY3);
-    assertThat(rule2.getStatus()).isEqualTo(REMOVED);
-
-    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule3.getUuid());
-
-    system.setNow(DATE3.getTime());
-    execute(new FakeRepositoryV2());
-    db.getSession().commit();
-
-    // -> rule2 is still removed, but not update at DATE3
-    rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
-    assertThat(rule2.getStatus()).isEqualTo(REMOVED);
-    assertThat(rule2.getUpdatedAt()).isEqualTo(DATE2.getTime());
-
-    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule3.getUuid());
-  }
-
-  @Test
-  public void mass_insert() {
-    execute(new BigRepository());
-    assertThat(db.countRowsOfTable("rules")).isEqualTo(BigRepository.SIZE);
-    assertThat(db.countRowsOfTable("rules_parameters")).isEqualTo(BigRepository.SIZE * 20);
-    assertThat(es.getIds(RuleIndexDefinition.TYPE_RULE)).hasSize(BigRepository.SIZE);
-  }
-
-  @Test
-  public void manage_repository_extensions() {
-    execute(new FindbugsRepository(), new FbContribRepository());
-    List<RuleDto> rules = dbClient.ruleDao().selectAll(db.getSession());
-    assertThat(rules).hasSize(2);
-    for (RuleDto rule : rules) {
-      assertThat(rule.getRepositoryKey()).isEqualTo("findbugs");
-    }
-  }
-
-  @Test
-  public void remove_system_tags_when_plugin_does_not_provide_any() {
-    // Rule already exists in DB, with some system tags
-    db.rules().insert(new RuleDto()
-      .setRuleKey("rule1")
-      .setRepositoryKey("findbugs")
-      .setName("Rule One")
-      .setScope(Scope.ALL)
-      .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule one description"))
-      .setDescriptionFormat(RuleDto.Format.HTML)
-      .setSystemTags(newHashSet("tag1", "tag2")));
-    db.getSession().commit();
-
-    // Synchronize rule without tag
-    execute(new FindbugsRepository());
-
-    List<RuleDto> rules = dbClient.ruleDao().selectAll(db.getSession());
-    assertThat(rules).hasSize(1).extracting(RuleDto::getKey, RuleDto::getSystemTags)
-      .containsOnly(tuple(RuleKey.of("findbugs", "rule1"), emptySet()));
-  }
-
-  @Test
-  public void rules_that_deprecate_previous_rule_must_be_recorded() {
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      createRule(repo, "rule1");
-      repo.done();
-    });
-
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      createRule(repo, "newKey")
-        .addDeprecatedRuleKey("fake", "rule1")
-        .addDeprecatedRuleKey("fake", "rule2");
-      repo.done();
-    });
-
-    List<RuleDto> rules = dbClient.ruleDao().selectAll(db.getSession());
-    Set<DeprecatedRuleKeyDto> deprecatedRuleKeys = dbClient.ruleDao().selectAllDeprecatedRuleKeys(db.getSession());
-    assertThat(rules).hasSize(1);
-    assertThat(deprecatedRuleKeys).hasSize(2);
-  }
-
-  @Test
-  public void rules_that_remove_deprecated_key_must_remove_records() {
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      createRule(repo, "rule1");
-      repo.done();
-    });
-
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      createRule(repo, "newKey")
-        .addDeprecatedRuleKey("fake", "rule1")
-        .addDeprecatedRuleKey("fake", "rule2");
-      repo.done();
-    });
-
-    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(1);
-    Set<DeprecatedRuleKeyDto> deprecatedRuleKeys = dbClient.ruleDao().selectAllDeprecatedRuleKeys(db.getSession());
-    assertThat(deprecatedRuleKeys).hasSize(2);
-
-    execute(context -> {
-      NewRepository repo = context.createRepository("fake", "java");
-      createRule(repo, "newKey");
-      repo.done();
-    });
-
-    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(1);
-    deprecatedRuleKeys = dbClient.ruleDao().selectAllDeprecatedRuleKeys(db.getSession());
-    assertThat(deprecatedRuleKeys).isEmpty();
-  }
-
-  @Test
-  public void declaring_two_rules_with_same_deprecated_RuleKey_should_throw_ISE() {
-    assertThatThrownBy(() -> {
-      execute(context -> {
-        NewRepository repo = context.createRepository("fake", "java");
-        createRule(repo, "newKey1")
-          .addDeprecatedRuleKey("fake", "old");
-        createRule(repo, "newKey2")
-          .addDeprecatedRuleKey("fake", "old");
-        repo.done();
-      });
-    })
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("The following deprecated rule keys are declared at least twice [fake:old]");
-  }
-
-  @Test
-  public void declaring_a_rule_with_a_deprecated_RuleKey_still_used_should_throw_ISE() {
-    assertThatThrownBy(() -> {
-      execute(context -> {
-        NewRepository repo = context.createRepository("fake", "java");
-        createRule(repo, "newKey1");
-        createRule(repo, "newKey2")
-          .addDeprecatedRuleKey("fake", "newKey1");
-        repo.done();
-      });
-    })
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("The following rule keys are declared both as deprecated and used key [fake:newKey1]");
-  }
-
-  @Test
-  public void updating_the_deprecated_to_a_new_ruleKey_should_throw_an_ISE() {
-    // On this new rule add a deprecated key
-    execute(context -> createRule(context, "javascript", "javascript", "s103",
-      r -> r.addDeprecatedRuleKey("javascript", "linelength")));
-
-    assertThatThrownBy(() -> {
-      // This rule should have been moved to another repository
-      execute(context -> createRule(context, "javascript", "sonarjs", "s103",
-        r -> r.addDeprecatedRuleKey("javascript", "linelength")));
-    })
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("An incorrect state of deprecated rule keys has been detected.\n " +
-        "The deprecated rule key [javascript:linelength] was previously deprecated by [javascript:s103]. [javascript:s103] should be a deprecated key of [sonarjs:s103],");
-  }
-
-  @Test
-  public void deprecate_rule_that_deprecated_another_rule() {
-    execute(context -> createRule(context, "javascript", "javascript", "s103"));
-    execute(context -> createRule(context, "javascript", "javascript", "s104",
-      r -> r.addDeprecatedRuleKey("javascript", "s103")));
-
-    // This rule should have been moved to another repository
-    execute(context -> createRule(context, "javascript", "sonarjs", "s105",
-      r -> r.addDeprecatedRuleKey("javascript", "s103")
-        .addDeprecatedRuleKey("javascript", "s104")));
-  }
-
-  @Test
-  public void declaring_a_rule_with_an_existing_RuleKey_still_used_should_throw_IAE() {
-    assertThatThrownBy(() -> {
-      execute(context -> {
-        NewRepository repo = context.createRepository("fake", "java");
-        createRule(repo, "newKey1");
-        createRule(repo, "newKey1");
-        repo.done();
-      });
-    })
-      .isInstanceOf(IllegalArgumentException.class)
-      .hasMessage("The rule 'newKey1' of repository 'fake' is declared several times");
-  }
-
-  @Test
-  public void removed_rule_should_appear_in_changelog() {
-    //GIVEN
-    QProfileDto qProfileDto = db.qualityProfiles().insert();
-    RuleDto ruleDto = db.rules().insert(RULE_KEY1);
-    db.qualityProfiles().activateRule(qProfileDto, ruleDto);
-    ActiveRuleChange arChange = new ActiveRuleChange(DEACTIVATED, ActiveRuleDto.createFor(qProfileDto, ruleDto), ruleDto);
-    when(qProfileRules.deleteRule(any(DbSession.class), eq(ruleDto))).thenReturn(List.of(arChange));
-    //WHEN
-    execute(context -> context.createRepository("fake", "java").done());
-    //THEN
-    List<QProfileChangeDto> qProfileChangeDtos = dbClient.qProfileChangeDao().selectByQuery(db.getSession(), new QProfileChangeQuery(qProfileDto.getKee()));
-    assertThat(qProfileChangeDtos).extracting(QProfileChangeDto::getRulesProfileUuid, QProfileChangeDto::getChangeType)
-      .contains(tuple(qProfileDto.getRulesProfileUuid(), "DEACTIVATED"));
-  }
-
-  @Test
-  public void removed_rule_should_be_deleted_when_renamed_repository() {
-    //GIVEN
-    RuleDto removedRuleDto = db.rules().insert(RuleKey.of("old_repo", "removed_rule"));
-    RuleDto renamedRuleDto = db.rules().insert(RuleKey.of("old_repo", "renamed_rule"));
-    //WHEN
-    execute(context -> createRule(context, "java", "new_repo", renamedRuleDto.getRuleKey(),
-      rule -> rule.addDeprecatedRuleKey(renamedRuleDto.getRepositoryKey(), renamedRuleDto.getRuleKey())));
-    //THEN
-    verify(qProfileRules).deleteRule(any(DbSession.class), eq(removedRuleDto));
-  }
-
-  private void execute(RulesDefinition... defs) {
-    ServerPluginRepository pluginRepository = mock(ServerPluginRepository.class);
-    when(pluginRepository.getPluginKey(any(RulesDefinition.class))).thenReturn(FAKE_PLUGIN_KEY);
-    RuleDefinitionsLoader loader = new RuleDefinitionsLoader(pluginRepository, defs);
-    Languages languages = mock(Languages.class);
-    when(languages.get(any())).thenReturn(mock(Language.class));
-    reset(webServerRuleFinder);
-
-    RegisterRules task = new RegisterRules(loader, qProfileRules, dbClient, ruleIndexer, activeRuleIndexer, languages, system, webServerRuleFinder, uuidFactory, metadataIndex,
-      resolver);
-    task.start();
-    // Execute a commit to refresh session state as the task is using its own session
-    db.getSession().commit();
-
-    verify(webServerRuleFinder).startCaching();
-  }
-
-  private NewRule createRule(NewRepository repo, String key) {
-    return repo.createRule(key)
-      .setName(key + " name")
-      .setHtmlDescription("Description of " + key)
-      .setSeverity(BLOCKER)
-      .setInternalKey("config1")
-      .setTags("tag1", "tag2", "tag3")
-      .setType(RuleType.CODE_SMELL)
-      .setStatus(RuleStatus.BETA);
-  }
-
-  @SafeVarargs
-  private void createRule(RulesDefinition.Context context, String language, String repositoryKey, String ruleKey, Consumer<NewRule>... consumers) {
-    NewRepository repo = context.createRepository(repositoryKey, language);
-    NewRule newRule = repo.createRule(ruleKey)
-      .setName(ruleKey)
-      .setHtmlDescription("Description of One")
-      .setSeverity(BLOCKER)
-      .setType(RuleType.CODE_SMELL)
-      .setStatus(RuleStatus.BETA);
-
-    Arrays.stream(consumers).forEach(c -> c.accept(newRule));
-    repo.done();
-  }
-
-  private void verifyIndicesMarkedAsInitialized() {
-    verify(metadataIndex).setInitialized(RuleIndexDefinition.TYPE_RULE, true);
-    verify(metadataIndex).setInitialized(RuleIndexDefinition.TYPE_ACTIVE_RULE, true);
-    reset(metadataIndex);
-  }
-
-  private void verifyIndicesNotMarkedAsInitialized() {
-    verifyNoInteractions(metadataIndex);
-  }
-
-  private RuleParamDto getParam(List<RuleParamDto> params, String key) {
-    for (RuleParamDto param : params) {
-      if (param.getName().equals(key)) {
-        return param;
-      }
-    }
-    return null;
-  }
-
-  static class FakeRepositoryV1 implements RulesDefinition {
-    @Override
-    public void define(Context context) {
-      NewRepository repo = context.createRepository("fake", "java");
-      NewRule rule1 = repo.createRule(RULE_KEY1.rule())
-        .setName("One")
-        .setHtmlDescription("Description of One")
-        .setSeverity(BLOCKER)
-        .setInternalKey("config1")
-        .setTags("tag1", "tag2", "tag3")
-        .setScope(RuleScope.ALL)
-        .setType(RuleType.CODE_SMELL)
-        .setStatus(RuleStatus.BETA)
-        .setGapDescription("java.S115.effortToFix")
-        .addEducationPrincipleKeys("concept1", "concept2", "concept3");
-      rule1.setDebtRemediationFunction(rule1.debtRemediationFunctions().linearWithOffset("5d", "10h"));
-
-      rule1.createParam("param1").setDescription("parameter one").setDefaultValue("default1");
-      rule1.createParam("param2").setDescription("parameter two").setDefaultValue("default2");
-
-      repo.createRule(HOTSPOT_RULE_KEY.rule())
-        .setName("Hotspot")
-        .setHtmlDescription("Minimal hotspot")
-        .setType(RuleType.SECURITY_HOTSPOT)
-        .addOwaspTop10(Y2021, OwaspTop10.A1, OwaspTop10.A3)
-        .addCwe(1, 123, 863);
-
-      repo.createRule(RULE_KEY2.rule())
-        .setName("Two")
-        .setHtmlDescription("Minimal rule");
-      repo.done();
-    }
-  }
-
-  /**
-   * FakeRepositoryV1 with some changes
-   */
-  static class FakeRepositoryV2 implements RulesDefinition {
-    @Override
-    public void define(Context context) {
-      NewRepository repo = context.createRepository("fake", "java");
-
-      // almost all the attributes of rule1 are changed
-      NewRule rule1 = repo.createRule(RULE_KEY1.rule())
-        .setName("One v2")
-        .setHtmlDescription("Description of One v2")
-        .setSeverity(INFO)
-        .setInternalKey("config1 v2")
-        // tag2 and tag3 removed, tag4 added
-        .setTags("tag1", "tag4")
-        .setType(RuleType.BUG)
-        .setStatus(READY)
-        .setGapDescription("java.S115.effortToFix.v2")
-        .addEducationPrincipleKeys("concept1", "concept4");
-      rule1.setDebtRemediationFunction(rule1.debtRemediationFunctions().linearWithOffset("6d", "2h"));
-      rule1.createParam("param1").setDescription("parameter one v2").setDefaultValue("default1 v2");
-      rule1.createParam("param2").setDescription("parameter two v2").setDefaultValue("default2 v2");
-
-      // rule2 is dropped, rule3 is new
-      repo.createRule(RULE_KEY3.rule())
-        .setName("Three")
-        .setHtmlDescription("Rule Three");
-
-      repo.done();
-    }
-  }
-
-  static class FakeRepositoryV3 implements RulesDefinition {
-    @Override
-    public void define(Context context) {
-      NewRepository repo = context.createRepository("fake", "java");
-      // rule 3 is dropped
-      repo.createRule(RULE_KEY3.rule())
-        .setName("Three")
-        .setMarkdownDescription("Rule Three V2");
-
-      repo.done();
-    }
-  }
-
-  static class ExternalRuleRepository implements RulesDefinition {
-    @Override
-    public void define(Context context) {
-      NewRepository repo = context.createExternalRepository("eslint", "js");
-      repo.createRule(RULE_KEY1.rule())
-        .setName("One")
-        .setHtmlDescription("Description of One")
-        .setSeverity(BLOCKER)
-        .setInternalKey("config1")
-        .setTags("tag1", "tag2", "tag3")
-        .setScope(RuleScope.ALL)
-        .setType(RuleType.CODE_SMELL)
-        .setStatus(RuleStatus.BETA)
-        .addEducationPrincipleKeys("concept1", "concept2", "concept3");
-
-      repo.createRule(EXTERNAL_HOTSPOT_RULE_KEY.rule())
-        .setName("Hotspot")
-        .setHtmlDescription("Minimal hotspot")
-        .setType(RuleType.SECURITY_HOTSPOT)
-        .addOwaspTop10(Y2021, OwaspTop10.A1, OwaspTop10.A3)
-        .addCwe(1, 123, 863);
-
-      repo.done();
-    }
-  }
-
-  static class BigRepository implements RulesDefinition {
-    static final int SIZE = 500;
-
-    @Override
-    public void define(Context context) {
-      NewRepository repo = context.createRepository("big", "java");
-      for (int i = 0; i < SIZE; i++) {
-        NewRule rule = repo.createRule("rule" + i)
-          .setName("name of " + i)
-          .setHtmlDescription("description of " + i);
-        for (int j = 0; j < 20; j++) {
-          rule.createParam("param" + j);
-        }
-
-      }
-      repo.done();
-    }
-  }
-
-  static class FindbugsRepository implements RulesDefinition {
-    @Override
-    public void define(Context context) {
-      NewRepository repo = context.createRepository("findbugs", "java");
-      repo.createRule("rule1")
-        .setName("Rule One")
-        .setHtmlDescription("Description of Rule One");
-      repo.done();
-    }
-  }
-
-  static class FbContribRepository implements RulesDefinition {
-    @Override
-    public void define(Context context) {
-      NewExtendedRepository repo = context.createRepository("findbugs", "java");
-      repo.createRule("rule2")
-        .setName("Rule Two")
-        .setHtmlDescription("Description of Rule Two");
-      repo.done();
-    }
-  }
-}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java
deleted file mode 100644 (file)
index 1ab9b1e..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.rule;
-
-import com.google.common.collect.ImmutableSet;
-import java.util.Set;
-import org.assertj.core.groups.Tuple;
-import org.junit.Test;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.server.rule.RulesDefinition;
-import org.sonar.db.rule.DeprecatedRuleKeyDto;
-
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.groups.Tuple.tuple;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class SingleDeprecatedRuleKeyTest {
-
-  @Test
-  public void test_creation_from_DeprecatedRuleKeyDto() {
-    // Creation from DeprecatedRuleKeyDto
-    DeprecatedRuleKeyDto deprecatedRuleKeyDto = new DeprecatedRuleKeyDto()
-      .setOldRuleKey(randomAlphanumeric(50))
-      .setOldRepositoryKey(randomAlphanumeric(50))
-      .setRuleUuid(randomAlphanumeric(50))
-      .setUuid(randomAlphanumeric(40));
-
-    SingleDeprecatedRuleKey singleDeprecatedRuleKey = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto);
-
-    assertThat(singleDeprecatedRuleKey.getOldRepositoryKey()).isEqualTo(deprecatedRuleKeyDto.getOldRepositoryKey());
-    assertThat(singleDeprecatedRuleKey.getOldRuleKey()).isEqualTo(deprecatedRuleKeyDto.getOldRuleKey());
-    assertThat(singleDeprecatedRuleKey.getNewRepositoryKey()).isEqualTo(deprecatedRuleKeyDto.getNewRepositoryKey());
-    assertThat(singleDeprecatedRuleKey.getNewRuleKey()).isEqualTo(deprecatedRuleKeyDto.getNewRuleKey());
-    assertThat(singleDeprecatedRuleKey.getUuid()).isEqualTo(deprecatedRuleKeyDto.getUuid());
-    assertThat(singleDeprecatedRuleKey.getRuleUuid()).isEqualTo(deprecatedRuleKeyDto.getRuleUuid());
-    assertThat(singleDeprecatedRuleKey.getOldRuleKeyAsRuleKey())
-      .isEqualTo(RuleKey.of(deprecatedRuleKeyDto.getOldRepositoryKey(), deprecatedRuleKeyDto.getOldRuleKey()));
-  }
-
-  @Test
-  public void test_creation_from_RulesDefinitionRule() {
-    // Creation from RulesDefinition.Rule
-    ImmutableSet<RuleKey> deprecatedRuleKeys = ImmutableSet.of(
-      RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)),
-      RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)),
-      RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)));
-
-    RulesDefinition.Repository repository = mock(RulesDefinition.Repository.class);
-    when(repository.key()).thenReturn(randomAlphanumeric(50));
-
-    RulesDefinition.Rule rule = mock(RulesDefinition.Rule.class);
-    when(rule.key()).thenReturn(randomAlphanumeric(50));
-    when(rule.deprecatedRuleKeys()).thenReturn(deprecatedRuleKeys);
-    when(rule.repository()).thenReturn(repository);
-
-    Set<SingleDeprecatedRuleKey> singleDeprecatedRuleKeys = SingleDeprecatedRuleKey.from(rule);
-    assertThat(singleDeprecatedRuleKeys).hasSize(deprecatedRuleKeys.size());
-    assertThat(singleDeprecatedRuleKeys)
-      .extracting(SingleDeprecatedRuleKey::getUuid, SingleDeprecatedRuleKey::getOldRepositoryKey, SingleDeprecatedRuleKey::getOldRuleKey,
-        SingleDeprecatedRuleKey::getNewRepositoryKey, SingleDeprecatedRuleKey::getNewRuleKey, SingleDeprecatedRuleKey::getOldRuleKeyAsRuleKey)
-      .containsExactlyInAnyOrder(
-        deprecatedRuleKeys.stream().map(
-          r -> tuple(null, r.repository(), r.rule(), rule.repository().key(), rule.key(), RuleKey.of(r.repository(), r.rule())))
-          .toList().toArray(new Tuple[deprecatedRuleKeys.size()]));
-  }
-
-  @Test
-  public void test_equality() {
-    DeprecatedRuleKeyDto deprecatedRuleKeyDto1 = new DeprecatedRuleKeyDto()
-      .setOldRuleKey(randomAlphanumeric(50))
-      .setOldRepositoryKey(randomAlphanumeric(50))
-      .setUuid(randomAlphanumeric(40))
-      .setRuleUuid("some-uuid");
-
-    DeprecatedRuleKeyDto deprecatedRuleKeyDto1WithoutUuid = new DeprecatedRuleKeyDto()
-      .setOldRuleKey(deprecatedRuleKeyDto1.getOldRuleKey())
-      .setOldRepositoryKey(deprecatedRuleKeyDto1.getOldRepositoryKey());
-
-    DeprecatedRuleKeyDto deprecatedRuleKeyDto2 = new DeprecatedRuleKeyDto()
-      .setOldRuleKey(randomAlphanumeric(50))
-      .setOldRepositoryKey(randomAlphanumeric(50))
-      .setUuid(randomAlphanumeric(40));
-
-    SingleDeprecatedRuleKey singleDeprecatedRuleKey1 = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1);
-    SingleDeprecatedRuleKey singleDeprecatedRuleKey2 = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2);
-
-    assertThat(singleDeprecatedRuleKey1)
-      .isEqualTo(singleDeprecatedRuleKey1)
-      .isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1))
-      .isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1WithoutUuid));
-    assertThat(singleDeprecatedRuleKey2).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2));
-
-    assertThat(singleDeprecatedRuleKey1)
-      .hasSameHashCodeAs(singleDeprecatedRuleKey1)
-      .hasSameHashCodeAs(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1))
-      .hasSameHashCodeAs(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1WithoutUuid));
-    assertThat(singleDeprecatedRuleKey2).hasSameHashCodeAs(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2));
-
-    assertThat(singleDeprecatedRuleKey1)
-      .isNotNull()
-      .isNotEqualTo("")
-      .isNotEqualTo(null)
-      .isNotEqualTo(singleDeprecatedRuleKey2);
-    assertThat(singleDeprecatedRuleKey2).isNotEqualTo(singleDeprecatedRuleKey1);
-
-    assertThat(singleDeprecatedRuleKey1.hashCode()).isNotEqualTo(singleDeprecatedRuleKey2.hashCode());
-    assertThat(singleDeprecatedRuleKey2.hashCode()).isNotEqualTo(singleDeprecatedRuleKey1.hashCode());
-  }
-}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrantIT.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrantIT.java
new file mode 100644 (file)
index 0000000..050c2a0
--- /dev/null
@@ -0,0 +1,1350 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.rule.registration;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.elasticsearch.common.util.set.Sets;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleScope;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.rule.Context;
+import org.sonar.api.server.rule.RuleDescriptionSection;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.testfixtures.log.LogTester;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.QProfileChangeDto;
+import org.sonar.db.qualityprofile.QProfileChangeQuery;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+import org.sonar.db.rule.RuleDescriptionSectionContextDto;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleDto.Scope;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.db.rule.RuleRepositoryDto;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.SearchIdResult;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.metadata.MetadataIndex;
+import org.sonar.server.plugins.ServerPluginRepository;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+import org.sonar.server.qualityprofile.QProfileRules;
+import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
+import org.sonar.server.rule.RuleDefinitionsLoader;
+import org.sonar.server.rule.RuleDescriptionSectionsGenerator;
+import org.sonar.server.rule.RuleDescriptionSectionsGeneratorResolver;
+import org.sonar.server.rule.WebServerRuleFinder;
+import org.sonar.server.rule.index.RuleIndex;
+import org.sonar.server.rule.index.RuleIndexDefinition;
+import org.sonar.server.rule.index.RuleIndexer;
+import org.sonar.server.rule.index.RuleQuery;
+
+import static com.google.common.collect.Sets.newHashSet;
+import static java.lang.String.format;
+import static java.lang.String.valueOf;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+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;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.rule.RuleStatus.READY;
+import static org.sonar.api.rule.RuleStatus.REMOVED;
+import static org.sonar.api.rule.Severity.BLOCKER;
+import static org.sonar.api.rule.Severity.INFO;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.RESOURCES_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
+import static org.sonar.api.server.rule.RulesDefinition.NewRepository;
+import static org.sonar.api.server.rule.RulesDefinition.NewRule;
+import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10;
+import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.builder;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
+
+@RunWith(DataProviderRunner.class)
+public class RulesRegistrantIT {
+
+  private static final String FAKE_PLUGIN_KEY = "unittest";
+  private static final Date DATE1 = DateUtils.parseDateTime("2014-01-01T19:10:03+0100");
+  private static final Date DATE2 = DateUtils.parseDateTime("2014-02-01T12:10:03+0100");
+  private static final Date DATE3 = DateUtils.parseDateTime("2014-03-01T12:10:03+0100");
+
+  private static final RuleKey EXTERNAL_RULE_KEY1 = RuleKey.of("external_eslint", "rule1");
+  private static final RuleKey EXTERNAL_HOTSPOT_RULE_KEY = RuleKey.of("external_eslint", "hotspot");
+
+  private static final RuleKey RULE_KEY1 = RuleKey.of("fake", "rule1");
+  private static final RuleKey RULE_KEY2 = RuleKey.of("fake", "rule2");
+  private static final RuleKey RULE_KEY3 = RuleKey.of("fake", "rule3");
+  private static final RuleKey HOTSPOT_RULE_KEY = RuleKey.of("fake", "hotspot");
+
+  private final TestSystem2 system = new TestSystem2().setNow(DATE1.getTime());
+
+  @org.junit.Rule
+  public DbTester db = DbTester.create(system);
+  @org.junit.Rule
+  public EsTester es = EsTester.create();
+  @org.junit.Rule
+  public LogTester logTester = new LogTester();
+
+  private final QProfileRules qProfileRules = mock(QProfileRules.class);
+  private final WebServerRuleFinder webServerRuleFinder = mock(WebServerRuleFinder.class);
+  private final DbClient dbClient = db.getDbClient();
+  private final MetadataIndex metadataIndex = mock(MetadataIndex.class);
+  private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();
+
+  private RuleIndexer ruleIndexer;
+  private ActiveRuleIndexer activeRuleIndexer;
+  private RuleIndex ruleIndex;
+  private final RuleDescriptionSectionsGenerator ruleDescriptionSectionsGenerator = mock(RuleDescriptionSectionsGenerator.class);
+  private final RuleDescriptionSectionsGeneratorResolver resolver = mock(RuleDescriptionSectionsGeneratorResolver.class);
+
+  private final RulesKeyVerifier rulesKeyVerifier = new RulesKeyVerifier();
+  private final StartupRuleUpdater startupRuleUpdater = new StartupRuleUpdater(dbClient, system, uuidFactory, resolver);
+
+  @Before
+  public void before() {
+    ruleIndexer = new RuleIndexer(es.client(), dbClient);
+    ruleIndex = new RuleIndex(es.client(), system);
+    activeRuleIndexer = new ActiveRuleIndexer(dbClient, es.client());
+    when(resolver.generateFor(any())).thenAnswer(answer -> {
+      RulesDefinition.Rule rule = answer.getArgument(0, RulesDefinition.Rule.class);
+      String description = rule.htmlDescription() == null ? rule.markdownDescription() : rule.htmlDescription();
+
+      Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = rule.ruleDescriptionSections().stream()
+        .map(s -> builder()
+          .uuid(UuidFactoryFast.getInstance().create())
+          .key(s.getKey())
+          .content(s.getHtmlContent())
+          .context(s.getContext().map(c -> RuleDescriptionSectionContextDto.of(c.getKey(), c.getDisplayName())).orElse(null))
+          .build()
+        )
+        .collect(Collectors.toSet());
+      return Sets.union(ruleDescriptionSectionDtos, Set.of(builder().uuid(UuidFactoryFast.getInstance().create()).key("default").content(description).build()));
+    });
+
+    when(ruleDescriptionSectionsGenerator.isGeneratorForRule(any())).thenReturn(true);
+  }
+
+  @Test
+  public void insert_new_rules() {
+    execute(new FakeRepositoryV1());
+
+    // verify db
+    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(3);
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    verifyRule(rule1);
+    assertThat(rule1.isExternal()).isFalse();
+    assertThat(rule1.getDefRemediationFunction()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET.name());
+    assertThat(rule1.getDefRemediationGapMultiplier()).isEqualTo("5d");
+    assertThat(rule1.getDefRemediationBaseEffort()).isEqualTo("10h");
+
+    RuleDto hotspotRule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), HOTSPOT_RULE_KEY);
+    verifyHotspot(hotspotRule);
+
+    List<RuleParamDto> params = dbClient.ruleDao().selectRuleParamsByRuleKey(db.getSession(), RULE_KEY1);
+    assertThat(params).hasSize(2);
+    RuleParamDto param = getParam(params, "param1");
+    assertThat(param.getDescription()).isEqualTo("parameter one");
+    assertThat(param.getDefaultValue()).isEqualTo("default1");
+
+    // verify index
+    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
+    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule2.getUuid(), hotspotRule.getUuid());
+    verifyIndicesMarkedAsInitialized();
+
+    // verify repositories
+    assertThat(dbClient.ruleRepositoryDao().selectAll(db.getSession())).extracting(RuleRepositoryDto::getKey).containsOnly("fake");
+  }
+
+  private void verifyHotspot(RuleDto hotspotRule) {
+    assertThat(hotspotRule.getName()).isEqualTo("Hotspot");
+    assertThat(hotspotRule.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Minimal hotspot");
+    assertThat(hotspotRule.getCreatedAt()).isEqualTo(RulesRegistrantIT.DATE1.getTime());
+    assertThat(hotspotRule.getUpdatedAt()).isEqualTo(RulesRegistrantIT.DATE1.getTime());
+    assertThat(hotspotRule.getType()).isEqualTo(RuleType.SECURITY_HOTSPOT.getDbConstant());
+    assertThat(hotspotRule.getSecurityStandards()).containsExactly("cwe:1", "cwe:123", "cwe:863", "owaspTop10-2021:a1", "owaspTop10-2021:a3");
+  }
+
+  @Test
+  public void insert_new_external_rule() {
+    execute(new ExternalRuleRepository());
+
+    // verify db
+    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(2);
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), EXTERNAL_RULE_KEY1);
+    verifyRule(rule1);
+    assertThat(rule1.isExternal()).isTrue();
+    assertThat(rule1.getDefRemediationFunction()).isNull();
+    assertThat(rule1.getDefRemediationGapMultiplier()).isNull();
+    assertThat(rule1.getDefRemediationBaseEffort()).isNull();
+
+    RuleDto hotspotRule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), EXTERNAL_HOTSPOT_RULE_KEY);
+    verifyHotspot(hotspotRule);
+  }
+
+  private void verifyRule(RuleDto rule) {
+    assertThat(rule.getName()).isEqualTo("One");
+    assertThat(rule.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Description of One");
+    assertThat(rule.getSeverityString()).isEqualTo(BLOCKER);
+    assertThat(rule.getTags()).isEmpty();
+    assertThat(rule.getSystemTags()).containsOnly("tag1", "tag2", "tag3");
+    assertThat(rule.getConfigKey()).isEqualTo("config1");
+    assertThat(rule.getStatus()).isEqualTo(RuleStatus.BETA);
+    assertThat(rule.getCreatedAt()).isEqualTo(DATE1.getTime());
+    assertThat(rule.getScope()).isEqualTo(Scope.ALL);
+    assertThat(rule.getUpdatedAt()).isEqualTo(DATE1.getTime());
+    assertThat(rule.getType()).isEqualTo(RuleType.CODE_SMELL.getDbConstant());
+    assertThat(rule.getPluginKey()).isEqualTo(FAKE_PLUGIN_KEY);
+    assertThat(rule.isAdHoc()).isFalse();
+    assertThat(rule.getEducationPrinciples()).containsOnly("concept1", "concept2", "concept3");
+  }
+
+  @Test
+  public void insert_then_remove_rule() {
+    String ruleKey = randomAlphanumeric(5);
+
+    // register one rule
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule(ruleKey)
+        .setName(randomAlphanumeric(5))
+        .setHtmlDescription(randomAlphanumeric(20));
+      repo.done();
+    });
+
+    // verify db
+    List<RuleDto> rules = dbClient.ruleDao().selectAll(db.getSession());
+    assertThat(rules)
+      .extracting(RuleDto::getKey)
+      .extracting(RuleKey::rule)
+      .containsExactly(ruleKey);
+    RuleDto rule = rules.iterator().next();
+
+    // verify index
+    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids())
+      .containsExactly(rule.getUuid());
+    verifyIndicesMarkedAsInitialized();
+
+    // register no rule
+    execute(context -> context.createRepository("fake", "java").done());
+
+    // verify db
+    assertThat(dbClient.ruleDao().selectAll(db.getSession()))
+      .extracting(RuleDto::getKey)
+      .extracting(RuleKey::rule)
+      .containsExactly(ruleKey);
+    assertThat(dbClient.ruleDao().selectAll(db.getSession()))
+      .extracting(RuleDto::getStatus)
+      .containsExactly(REMOVED);
+
+    // verify index
+    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids())
+      .isEmpty();
+    verifyIndicesNotMarkedAsInitialized();
+  }
+
+  @Test
+  public void mass_insert_then_remove_rule() {
+    int numberOfRules = 5000;
+
+    // register many rules
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      IntStream.range(0, numberOfRules)
+        .mapToObj(i -> "rule-" + i)
+        .forEach(ruleKey -> repo.createRule(ruleKey)
+          .setName(randomAlphanumeric(20))
+          .setHtmlDescription(randomAlphanumeric(20)));
+      repo.done();
+    });
+
+    // verify db
+    assertThat(dbClient.ruleDao().selectAll(db.getSession()))
+      .hasSize(numberOfRules)
+      .extracting(RuleDto::getStatus)
+      .containsOnly(READY);
+
+    // verify index
+    assertThat(es.countDocuments(RuleIndexDefinition.TYPE_RULE)).isEqualTo(numberOfRules);
+    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids())
+      .isNotEmpty();
+
+    // register no rule
+    execute(context -> context.createRepository("fake", "java").done());
+
+    // verify db
+    assertThat(dbClient.ruleDao().selectAll(db.getSession()))
+      .hasSize(numberOfRules)
+      .extracting(RuleDto::getStatus)
+      .containsOnly(REMOVED);
+
+    // verify index (documents are still in the index, but all are removed)
+    assertThat(es.countDocuments(RuleIndexDefinition.TYPE_RULE)).isEqualTo(numberOfRules);
+    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids())
+      .isEmpty();
+  }
+
+  @Test
+  public void delete_repositories_that_have_been_uninstalled() {
+    RuleRepositoryDto repository = new RuleRepositoryDto("findbugs", "java", "Findbugs");
+    DbSession dbSession = db.getSession();
+    db.getDbClient().ruleRepositoryDao().insert(dbSession, singletonList(repository));
+    dbSession.commit();
+
+    execute(new FakeRepositoryV1());
+
+    assertThat(db.getDbClient().ruleRepositoryDao().selectAll(dbSession)).extracting(RuleRepositoryDto::getKey).containsOnly("fake");
+  }
+
+  @Test
+  public void update_and_remove_rules_on_changes() {
+    execute(new FakeRepositoryV1());
+    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(3);
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
+    RuleDto hotspotRule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), HOTSPOT_RULE_KEY);
+    assertThat(es.getIds(RuleIndexDefinition.TYPE_RULE)).containsOnly(valueOf(rule1.getUuid()), valueOf(rule2.getUuid()), valueOf(hotspotRule.getUuid()));
+    verifyIndicesMarkedAsInitialized();
+
+    // user adds tags and sets markdown note
+    rule1.setTags(newHashSet("usertag1", "usertag2"));
+    rule1.setNoteData("user *note*");
+    rule1.setNoteUserUuid("marius");
+    dbClient.ruleDao().update(db.getSession(), rule1);
+    db.getSession().commit();
+
+    system.setNow(DATE2.getTime());
+    execute(new FakeRepositoryV2());
+
+    verifyIndicesNotMarkedAsInitialized();
+    // rule1 has been updated
+    rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    assertThatRule1IsV2(rule1);
+
+    List<RuleParamDto> params = dbClient.ruleDao().selectRuleParamsByRuleKey(db.getSession(), RULE_KEY1);
+    assertThat(params).hasSize(2);
+    RuleParamDto param = getParam(params, "param1");
+    assertThat(param.getDescription()).isEqualTo("parameter one v2");
+    assertThat(param.getDefaultValue()).isEqualTo("default1 v2");
+
+    // rule2 has been removed -> status set to REMOVED but db row is not deleted
+    rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
+    assertThat(rule2.getStatus()).isEqualTo(REMOVED);
+    assertThat(rule2.getUpdatedAt()).isEqualTo(DATE2.getTime());
+
+    // rule3 has been created
+    RuleDto rule3 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY3);
+    assertThat(rule3).isNotNull();
+    assertThat(rule3.getStatus()).isEqualTo(READY);
+
+    // verify index
+    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule3.getUuid());
+
+    // verify repositories
+    assertThat(dbClient.ruleRepositoryDao().selectAll(db.getSession())).extracting(RuleRepositoryDto::getKey).containsOnly("fake");
+
+    system.setNow(DATE3.getTime());
+    execute(new FakeRepositoryV3());
+    rule3 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY3);
+    assertThat(rule3.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Rule Three V2");
+    assertThat(rule3.getDescriptionFormat()).isEqualTo(RuleDto.Format.MARKDOWN);
+  }
+
+  private void assertThatRule1IsV2(RuleDto rule1) {
+    assertThat(rule1.getName()).isEqualTo("One v2");
+    RuleDescriptionSectionDto defaultRuleDescriptionSection = rule1.getDefaultRuleDescriptionSection();
+    assertThat(defaultRuleDescriptionSection.getContent()).isEqualTo("Description of One v2");
+    assertThat(defaultRuleDescriptionSection.getKey()).isEqualTo(DEFAULT_KEY);
+    assertThat(rule1.getDescriptionFormat()).isEqualTo(RuleDto.Format.HTML);
+    assertThat(rule1.getSeverityString()).isEqualTo(INFO);
+    assertThat(rule1.getTags()).containsOnly("usertag1", "usertag2");
+    assertThat(rule1.getSystemTags()).containsOnly("tag1", "tag4");
+    assertThat(rule1.getConfigKey()).isEqualTo("config1 v2");
+    assertThat(rule1.getNoteData()).isEqualTo("user *note*");
+    assertThat(rule1.getNoteUserUuid()).isEqualTo("marius");
+    assertThat(rule1.getStatus()).isEqualTo(READY);
+    assertThat(rule1.getType()).isEqualTo(RuleType.BUG.getDbConstant());
+    assertThat(rule1.getCreatedAt()).isEqualTo(DATE1.getTime());
+    assertThat(rule1.getUpdatedAt()).isEqualTo(DATE2.getTime());
+    assertThat(rule1.getEducationPrinciples()).containsOnly("concept1","concept4");
+  }
+
+  @Test
+  public void add_new_tag() {
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule1")
+        .setName("Rule One")
+        .setHtmlDescription("Description of Rule One")
+        .setTags("tag1");
+      repo.done();
+    });
+
+    RuleDto rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    assertThat(rule.getSystemTags()).containsOnly("tag1");
+
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule1")
+        .setName("Rule One")
+        .setHtmlDescription("Description of Rule One")
+        .setTags("tag1", "tag2");
+      repo.done();
+    });
+
+    rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    assertThat(rule.getSystemTags()).containsOnly("tag1", "tag2");
+  }
+
+  @Test
+  public void add_new_security_standards() {
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule1")
+        .setName("Rule One")
+        .setHtmlDescription("Description of Rule One")
+        .addOwaspTop10(Y2021, OwaspTop10.A1)
+        .addCwe(123);
+      repo.done();
+    });
+
+    RuleDto rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    assertThat(rule.getSecurityStandards()).containsOnly("cwe:123", "owaspTop10-2021:a1");
+
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule1")
+        .setName("Rule One")
+        .setHtmlDescription("Description of Rule One")
+        .addOwaspTop10(Y2021, OwaspTop10.A1, OwaspTop10.A3)
+        .addCwe(1, 123, 863);
+      repo.done();
+    });
+
+    rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    assertThat(rule.getSecurityStandards()).containsOnly("cwe:1", "cwe:123", "cwe:863", "owaspTop10-2021:a1", "owaspTop10-2021:a3");
+  }
+
+  @Test
+  public void update_only_rule_name() {
+    system.setNow(DATE1.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule")
+        .setName("Name1")
+        .setHtmlDescription("Description");
+      repo.done();
+    });
+
+    system.setNow(DATE2.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule")
+        .setName("Name2")
+        .setHtmlDescription("Description");
+      repo.done();
+    });
+
+    // rule1 has been updated
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("fake", "rule"));
+    assertThat(rule1.getName()).isEqualTo("Name2");
+    assertThat(rule1.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Description");
+
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name2"), new SearchOptions()).getTotal()).isOne();
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getTotal()).isZero();
+  }
+
+  @Test
+  public void update_template_rule_key_should_also_update_custom_rules() {
+    system.setNow(DATE1.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository("squid", "java");
+      repo.createRule("rule")
+        .setName("Name1")
+        .setHtmlDescription("Description")
+        .setTemplate(true);
+      repo.done();
+    });
+
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("squid", "rule"));
+
+    // insert custom rule
+    db.rules().insert(new RuleDto()
+      .setRuleKey(RuleKey.of("squid", "custom"))
+      .setLanguage("java")
+      .setScope(Scope.ALL)
+      .setTemplateUuid(rule1.getUuid())
+      .setName("custom1"));
+    db.commit();
+
+    // re-key rule
+    execute(context -> {
+      NewRepository repo = context.createRepository("java", "java");
+      repo.createRule("rule")
+        .setName("Name1")
+        .setHtmlDescription("Description")
+        .addDeprecatedRuleKey("squid", "rule")
+        .setTemplate(true);
+      repo.done();
+    });
+
+    // template rule and custom rule have been updated
+    rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("java", "rule"));
+    RuleDto custom = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("java", "custom"));
+  }
+
+  @Test
+  public void update_if_rule_key_renamed_and_deprecated_key_declared() {
+    String ruleKey1 = "rule1";
+    String ruleKey2 = "rule2";
+    String repository = "fake";
+
+    system.setNow(DATE1.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository(repository, "java");
+      repo.createRule(ruleKey1)
+        .setName("Name1")
+        .setHtmlDescription("Description");
+      repo.done();
+    });
+
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository, ruleKey1));
+    SearchIdResult<String> searchRule1 = ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions());
+    assertThat(searchRule1.getUuids()).containsOnly(rule1.getUuid());
+    assertThat(searchRule1.getTotal()).isOne();
+
+    system.setNow(DATE2.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository(repository, "java");
+      repo.createRule(ruleKey2)
+        .setName("Name2")
+        .setHtmlDescription("Description")
+        .addDeprecatedRuleKey(repository, ruleKey1);
+      repo.done();
+    });
+
+    // rule2 is actually rule1
+    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository, ruleKey2));
+    assertThat(rule2.getUuid()).isEqualTo(rule1.getUuid());
+    assertThat(rule2.getName()).isEqualTo("Name2");
+    assertThat(rule2.getDefaultRuleDescriptionSection().getContent()).isEqualTo(rule1.getDefaultRuleDescriptionSection().getContent());
+
+    SearchIdResult<String> searchRule2 = ruleIndex.search(new RuleQuery().setQueryText("Name2"), new SearchOptions());
+    assertThat(searchRule2.getUuids()).containsOnly(rule2.getUuid());
+    assertThat(searchRule2.getTotal()).isOne();
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getTotal()).isZero();
+  }
+
+  @Test
+  public void update_if_repository_changed_and_deprecated_key_declared() {
+    String ruleKey = "rule";
+    String repository1 = "fake1";
+    String repository2 = "fake2";
+
+    system.setNow(DATE1.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository(repository1, "java");
+      repo.createRule(ruleKey)
+        .setName("Name1")
+        .setHtmlDescription("Description");
+      repo.done();
+    });
+
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository1, ruleKey));
+    SearchIdResult<String> searchRule1 = ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions());
+    assertThat(searchRule1.getUuids()).containsOnly(rule1.getUuid());
+    assertThat(searchRule1.getTotal()).isOne();
+
+    system.setNow(DATE2.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository(repository2, "java");
+      repo.createRule(ruleKey)
+        .setName("Name2")
+        .setHtmlDescription("Description")
+        .addDeprecatedRuleKey(repository1, ruleKey);
+      repo.done();
+    });
+
+    // rule2 is actually rule1
+    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository2, ruleKey));
+    assertThat(rule2.getUuid()).isEqualTo(rule1.getUuid());
+    assertThat(rule2.getName()).isEqualTo("Name2");
+    assertThat(rule2.getDefaultRuleDescriptionSection().getContent()).isEqualTo(rule1.getDefaultRuleDescriptionSection().getContent());
+
+    SearchIdResult<String> searchRule2 = ruleIndex.search(new RuleQuery().setQueryText("Name2"), new SearchOptions());
+    assertThat(searchRule2.getUuids()).containsOnly(rule2.getUuid());
+    assertThat(searchRule2.getTotal()).isOne();
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getTotal()).isZero();
+  }
+
+  @Test
+  @UseDataProvider("allRenamingCases")
+  public void update_if_only_renamed_and_deprecated_key_declared(String ruleKey1, String repo1, String ruleKey2, String repo2) {
+    String name = "Name1";
+    String description = "Description";
+    system.setNow(DATE1.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository(repo1, "java");
+      repo.createRule(ruleKey1)
+        .setName(name)
+        .setHtmlDescription(description);
+      repo.done();
+    });
+
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repo1, ruleKey1));
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText(name), new SearchOptions()).getUuids())
+      .containsOnly(rule1.getUuid());
+
+    system.setNow(DATE2.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository(repo2, "java");
+      repo.createRule(ruleKey2)
+        .setName(name)
+        .setHtmlDescription(description)
+        .addDeprecatedRuleKey(repo1, ruleKey1);
+      repo.done();
+    });
+
+    // rule2 is actually rule1
+    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repo2, ruleKey2));
+    assertThat(rule2.getUuid()).isEqualTo(rule1.getUuid());
+    assertThat(rule2.getName()).isEqualTo(rule1.getName());
+    assertThat(rule2.getDefaultRuleDescriptionSection().getContent()).isEqualTo(rule1.getDefaultRuleDescriptionSection().getContent());
+
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText(name), new SearchOptions()).getUuids())
+      .containsOnly(rule2.getUuid());
+  }
+
+  @DataProvider
+  public static Object[][] allRenamingCases() {
+    return new Object[][]{
+      {"repo1", "rule1", "repo1", "rule2"},
+      {"repo1", "rule1", "repo2", "rule1"},
+      {"repo1", "rule1", "repo2", "rule2"},
+    };
+  }
+
+  @Test
+  public void update_if_repository_and_key_changed_and_deprecated_key_declared_among_others() {
+    String ruleKey1 = "rule1";
+    String ruleKey2 = "rule2";
+    String repository1 = "fake1";
+    String repository2 = "fake2";
+
+    system.setNow(DATE1.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository(repository1, "java");
+      repo.createRule(ruleKey1)
+        .setName("Name1")
+        .setHtmlDescription("Description");
+      repo.done();
+    });
+
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository1, ruleKey1));
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getUuids())
+      .containsOnly(rule1.getUuid());
+
+    system.setNow(DATE2.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository(repository2, "java");
+      repo.createRule(ruleKey2)
+        .setName("Name2")
+        .setHtmlDescription("Description")
+        .addDeprecatedRuleKey("foo", "bar")
+        .addDeprecatedRuleKey(repository1, ruleKey1)
+        .addDeprecatedRuleKey("some", "noise");
+      repo.done();
+    });
+
+    // rule2 is actually rule1
+    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of(repository2, ruleKey2));
+    assertThat(rule2.getUuid()).isEqualTo(rule1.getUuid());
+
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name2"), new SearchOptions()).getUuids())
+      .containsOnly(rule1.getUuid());
+  }
+
+  @Test
+  public void update_only_rule_description() {
+    system.setNow(DATE1.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule")
+        .setName("Name")
+        .setHtmlDescription("Desc1");
+      repo.done();
+    });
+
+    system.setNow(DATE2.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule")
+        .setName("Name")
+        .setHtmlDescription("Desc2");
+      repo.done();
+    });
+
+    // rule1 has been updated
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("fake", "rule"));
+    assertThat(rule1.getName()).isEqualTo("Name");
+    assertThat(rule1.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Desc2");
+
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Desc2"), new SearchOptions()).getTotal()).isOne();
+    assertThat(ruleIndex.search(new RuleQuery().setQueryText("Desc1"), new SearchOptions()).getTotal()).isZero();
+  }
+
+  @Test
+  public void update_several_rule_descriptions() {
+    system.setNow(DATE1.getTime());
+
+    RuleDescriptionSection section1context1 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "section1 ctx1 content", "ctx_1");
+    RuleDescriptionSection section1context2 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "section1 ctx2 content", "ctx_2");
+    RuleDescriptionSection section2context1 = createRuleDescriptionSection(RESOURCES_SECTION_KEY, "section2 content", "ctx_1");
+    RuleDescriptionSection section2context2 = createRuleDescriptionSection(RESOURCES_SECTION_KEY,"section2 ctx2 content", "ctx_2");
+    RuleDescriptionSection section3noContext = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "section3 content", null);
+    RuleDescriptionSection section4noContext = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, "section4 content", null);
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule")
+        .setName("Name")
+        .addDescriptionSection(section1context1)
+        .addDescriptionSection(section1context2)
+        .addDescriptionSection(section2context1)
+        .addDescriptionSection(section2context2)
+        .addDescriptionSection(section3noContext)
+        .addDescriptionSection(section4noContext)
+        .setHtmlDescription("Desc1");
+      repo.done();
+    });
+
+    RuleDescriptionSection section1context2updated = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "section1 ctx2 updated content", "ctx_2");
+    RuleDescriptionSection section2updatedWithoutContext = createRuleDescriptionSection(RESOURCES_SECTION_KEY, section2context1.getHtmlContent(), null);
+    RuleDescriptionSection section4updatedWithContext1 = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, section4noContext.getHtmlContent(), "ctx_1");
+    RuleDescriptionSection section4updatedWithContext2 = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, section4noContext.getHtmlContent(), "ctx_2");
+    system.setNow(DATE2.getTime());
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      repo.createRule("rule")
+        .setName("Name")
+        .addDescriptionSection(section1context1)
+        .addDescriptionSection(section1context2updated)
+        .addDescriptionSection(section2updatedWithoutContext)
+        .addDescriptionSection(section3noContext)
+        .addDescriptionSection(section4updatedWithContext1)
+        .addDescriptionSection(section4updatedWithContext2)
+        .setHtmlDescription("Desc2");
+      repo.done();
+
+    });
+
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RuleKey.of("fake", "rule"));
+    assertThat(rule1.getName()).isEqualTo("Name");
+    assertThat(rule1.getDefaultRuleDescriptionSection().getContent()).isEqualTo("Desc2");
+
+    Set<RuleDescriptionSection> expectedSections = Set.of(section1context1, section1context2updated,
+      section2updatedWithoutContext, section3noContext, section4updatedWithContext1, section4updatedWithContext2);
+    assertThat(rule1.getRuleDescriptionSectionDtos()).hasSize(expectedSections.size() + 1);
+    expectedSections.forEach(apiSection -> assertSectionExists(apiSection, rule1.getRuleDescriptionSectionDtos()));
+  }
+
+  private static RuleDescriptionSection createRuleDescriptionSection(String sectionKey, String description, @Nullable String contextKey) {
+    Context context = Optional.ofNullable(contextKey).map(key -> new Context(contextKey, contextKey + randomAlphanumeric(10))).orElse(null);
+    return RuleDescriptionSection.builder().sectionKey(sectionKey)
+      .htmlContent(description)
+      .context(context)
+      .build();
+  }
+
+  private static void assertSectionExists(RuleDescriptionSection apiSection, Set<RuleDescriptionSectionDto> sectionDtos) {
+    sectionDtos.stream()
+      .filter(sectionDto -> sectionDto.getKey().equals(apiSection.getKey()) && sectionDto.getContent().equals(apiSection.getHtmlContent()))
+      .filter(sectionDto -> isSameContext(apiSection.getContext(), sectionDto.getContext()))
+      .findAny()
+      .orElseThrow(() -> new AssertionError(format("Impossible to find a section dto matching the API section %s", apiSection.getKey())));
+  }
+
+  private static boolean isSameContext(Optional<Context> apiContext, @Nullable RuleDescriptionSectionContextDto contextDto) {
+    if (apiContext.isEmpty() && contextDto == null) {
+      return true;
+    }
+    return apiContext.filter(context -> isSameContext(context, contextDto)).isPresent();
+  }
+
+  private static boolean isSameContext(Context apiContext, @Nullable RuleDescriptionSectionContextDto contextDto) {
+    if (contextDto == null) {
+      return false;
+    }
+    return Objects.equals(apiContext.getKey(), contextDto.getKey()) && Objects.equals(apiContext.getDisplayName(), contextDto.getDisplayName());
+  }
+
+  @Test
+  public void rule_previously_created_as_adhoc_becomes_none_adhoc() {
+    RuleDto rule = db.rules().insert(r -> r.setRepositoryKey("external_fake").setIsExternal(true).setIsAdHoc(true));
+    system.setNow(DATE2.getTime());
+    execute(context -> {
+      NewRepository repo = context.createExternalRepository("fake", rule.getLanguage());
+      repo.createRule(rule.getRuleKey())
+        .setName(rule.getName())
+        .setHtmlDescription(rule.getDefaultRuleDescriptionSection().getContent());
+      repo.done();
+    });
+
+    RuleDto reloaded = dbClient.ruleDao().selectByKey(db.getSession(), rule.getKey()).get();
+    assertThat(reloaded.isAdHoc()).isFalse();
+  }
+
+  @Test
+  public void remove_no_more_defined_external_rule() {
+    RuleDto rule = db.rules().insert(r -> r.setRepositoryKey("external_fake")
+      .setStatus(READY)
+      .setIsExternal(true)
+      .setIsAdHoc(false));
+
+    execute();
+
+    RuleDto reloaded = dbClient.ruleDao().selectByKey(db.getSession(), rule.getKey()).get();
+    assertThat(reloaded.getStatus()).isEqualTo(REMOVED);
+  }
+
+  @Test
+  public void do_not_remove_no_more_defined_ad_hoc_rule() {
+    RuleDto rule = db.rules().insert(r -> r.setRepositoryKey("external_fake")
+      .setStatus(READY)
+      .setIsExternal(true)
+      .setIsAdHoc(true));
+
+    execute();
+
+    RuleDto reloaded = dbClient.ruleDao().selectByKey(db.getSession(), rule.getKey()).get();
+    assertThat(reloaded.getStatus()).isEqualTo(READY);
+  }
+
+  @Test
+  public void disable_then_enable_rule() {
+    // Install rule
+    system.setNow(DATE1.getTime());
+    execute(new FakeRepositoryV1());
+
+    // Uninstall rule
+    system.setNow(DATE2.getTime());
+    execute();
+
+    RuleDto rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    assertThat(rule.getStatus()).isEqualTo(REMOVED);
+    assertThat(ruleIndex.search(new RuleQuery().setKey(RULE_KEY1.toString()), new SearchOptions()).getTotal()).isZero();
+
+    // Re-install rule
+    system.setNow(DATE3.getTime());
+    execute(new FakeRepositoryV1());
+
+    rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    assertThat(rule.getStatus()).isEqualTo(RuleStatus.BETA);
+    assertThat(ruleIndex.search(new RuleQuery().setKey(RULE_KEY1.toString()), new SearchOptions()).getTotal()).isOne();
+  }
+
+  @Test
+  public void do_not_update_rules_when_no_changes() {
+    execute(new FakeRepositoryV1());
+    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(3);
+
+    system.setNow(DATE2.getTime());
+    execute(new FakeRepositoryV1());
+
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    assertThat(rule1.getCreatedAt()).isEqualTo(DATE1.getTime());
+    assertThat(rule1.getUpdatedAt()).isEqualTo(DATE1.getTime());
+  }
+
+  @Test
+  public void do_not_update_already_removed_rules() {
+    execute(new FakeRepositoryV1());
+    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(3);
+
+    RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
+    RuleDto rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
+    RuleDto hotspotRule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), HOTSPOT_RULE_KEY);
+    assertThat(es.getIds(RuleIndexDefinition.TYPE_RULE)).containsOnly(valueOf(rule1.getUuid()), valueOf(rule2.getUuid()), valueOf(hotspotRule.getUuid()));
+
+    assertThat(rule2.getStatus()).isEqualTo(READY);
+
+    system.setNow(DATE2.getTime());
+    execute(new FakeRepositoryV2());
+
+    // On MySQL, need to update a rule otherwise rule2 will be seen as READY, but why ???
+    dbClient.ruleDao().update(db.getSession(), rule1);
+    db.getSession().commit();
+
+    // rule2 is removed
+    rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
+    RuleDto rule3 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY3);
+    assertThat(rule2.getStatus()).isEqualTo(REMOVED);
+
+    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule3.getUuid());
+
+    system.setNow(DATE3.getTime());
+    execute(new FakeRepositoryV2());
+    db.getSession().commit();
+
+    // -> rule2 is still removed, but not update at DATE3
+    rule2 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY2);
+    assertThat(rule2.getStatus()).isEqualTo(REMOVED);
+    assertThat(rule2.getUpdatedAt()).isEqualTo(DATE2.getTime());
+
+    assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule3.getUuid());
+  }
+
+  @Test
+  public void mass_insert() {
+    execute(new BigRepository());
+    assertThat(db.countRowsOfTable("rules")).isEqualTo(BigRepository.SIZE);
+    assertThat(db.countRowsOfTable("rules_parameters")).isEqualTo(BigRepository.SIZE * 20);
+    assertThat(es.getIds(RuleIndexDefinition.TYPE_RULE)).hasSize(BigRepository.SIZE);
+  }
+
+  @Test
+  public void manage_repository_extensions() {
+    execute(new FindbugsRepository(), new FbContribRepository());
+    List<RuleDto> rules = dbClient.ruleDao().selectAll(db.getSession());
+    assertThat(rules).hasSize(2);
+    for (RuleDto rule : rules) {
+      assertThat(rule.getRepositoryKey()).isEqualTo("findbugs");
+    }
+  }
+
+  @Test
+  public void remove_system_tags_when_plugin_does_not_provide_any() {
+    // Rule already exists in DB, with some system tags
+    db.rules().insert(new RuleDto()
+      .setRuleKey("rule1")
+      .setRepositoryKey("findbugs")
+      .setName("Rule One")
+      .setScope(Scope.ALL)
+      .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule one description"))
+      .setDescriptionFormat(RuleDto.Format.HTML)
+      .setSystemTags(newHashSet("tag1", "tag2")));
+    db.getSession().commit();
+
+    // Synchronize rule without tag
+    execute(new FindbugsRepository());
+
+    List<RuleDto> rules = dbClient.ruleDao().selectAll(db.getSession());
+    assertThat(rules).hasSize(1).extracting(RuleDto::getKey, RuleDto::getSystemTags)
+      .containsOnly(tuple(RuleKey.of("findbugs", "rule1"), emptySet()));
+  }
+
+  @Test
+  public void rules_that_deprecate_previous_rule_must_be_recorded() {
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      createRule(repo, "rule1");
+      repo.done();
+    });
+
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      createRule(repo, "newKey")
+        .addDeprecatedRuleKey("fake", "rule1")
+        .addDeprecatedRuleKey("fake", "rule2");
+      repo.done();
+    });
+
+    List<RuleDto> rules = dbClient.ruleDao().selectAll(db.getSession());
+    Set<DeprecatedRuleKeyDto> deprecatedRuleKeys = dbClient.ruleDao().selectAllDeprecatedRuleKeys(db.getSession());
+    assertThat(rules).hasSize(1);
+    assertThat(deprecatedRuleKeys).hasSize(2);
+  }
+
+  @Test
+  public void rules_that_remove_deprecated_key_must_remove_records() {
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      createRule(repo, "rule1");
+      repo.done();
+    });
+
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      createRule(repo, "newKey")
+        .addDeprecatedRuleKey("fake", "rule1")
+        .addDeprecatedRuleKey("fake", "rule2");
+      repo.done();
+    });
+
+    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(1);
+    Set<DeprecatedRuleKeyDto> deprecatedRuleKeys = dbClient.ruleDao().selectAllDeprecatedRuleKeys(db.getSession());
+    assertThat(deprecatedRuleKeys).hasSize(2);
+
+    execute(context -> {
+      NewRepository repo = context.createRepository("fake", "java");
+      createRule(repo, "newKey");
+      repo.done();
+    });
+
+    assertThat(dbClient.ruleDao().selectAll(db.getSession())).hasSize(1);
+    deprecatedRuleKeys = dbClient.ruleDao().selectAllDeprecatedRuleKeys(db.getSession());
+    assertThat(deprecatedRuleKeys).isEmpty();
+  }
+
+  @Test
+  public void declaring_two_rules_with_same_deprecated_RuleKey_should_throw_ISE() {
+    assertThatThrownBy(() -> {
+      execute(context -> {
+        NewRepository repo = context.createRepository("fake", "java");
+        createRule(repo, "newKey1")
+          .addDeprecatedRuleKey("fake", "old");
+        createRule(repo, "newKey2")
+          .addDeprecatedRuleKey("fake", "old");
+        repo.done();
+      });
+    })
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("The following deprecated rule keys are declared at least twice [fake:old]");
+  }
+
+  @Test
+  public void declaring_a_rule_with_a_deprecated_RuleKey_still_used_should_throw_ISE() {
+    assertThatThrownBy(() -> {
+      execute(context -> {
+        NewRepository repo = context.createRepository("fake", "java");
+        createRule(repo, "newKey1");
+        createRule(repo, "newKey2")
+          .addDeprecatedRuleKey("fake", "newKey1");
+        repo.done();
+      });
+    })
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("The following rule keys are declared both as deprecated and used key [fake:newKey1]");
+  }
+
+  @Test
+  public void updating_the_deprecated_to_a_new_ruleKey_should_throw_an_ISE() {
+    // On this new rule add a deprecated key
+    execute(context -> createRule(context, "javascript", "javascript", "s103",
+      r -> r.addDeprecatedRuleKey("javascript", "linelength")));
+
+    assertThatThrownBy(() -> {
+      // This rule should have been moved to another repository
+      execute(context -> createRule(context, "javascript", "sonarjs", "s103",
+        r -> r.addDeprecatedRuleKey("javascript", "linelength")));
+    })
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("An incorrect state of deprecated rule keys has been detected.\n " +
+        "The deprecated rule key [javascript:linelength] was previously deprecated by [javascript:s103]. [javascript:s103] should be a deprecated key of [sonarjs:s103],");
+  }
+
+  @Test
+  public void deprecate_rule_that_deprecated_another_rule() {
+    execute(context -> createRule(context, "javascript", "javascript", "s103"));
+    execute(context -> createRule(context, "javascript", "javascript", "s104",
+      r -> r.addDeprecatedRuleKey("javascript", "s103")));
+
+    // This rule should have been moved to another repository
+    execute(context -> createRule(context, "javascript", "sonarjs", "s105",
+      r -> r.addDeprecatedRuleKey("javascript", "s103")
+        .addDeprecatedRuleKey("javascript", "s104")));
+  }
+
+  @Test
+  public void declaring_a_rule_with_an_existing_RuleKey_still_used_should_throw_IAE() {
+    assertThatThrownBy(() -> {
+      execute(context -> {
+        NewRepository repo = context.createRepository("fake", "java");
+        createRule(repo, "newKey1");
+        createRule(repo, "newKey1");
+        repo.done();
+      });
+    })
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("The rule 'newKey1' of repository 'fake' is declared several times");
+  }
+
+  @Test
+  public void removed_rule_should_appear_in_changelog() {
+    //GIVEN
+    QProfileDto qProfileDto = db.qualityProfiles().insert();
+    RuleDto ruleDto = db.rules().insert(RULE_KEY1);
+    db.qualityProfiles().activateRule(qProfileDto, ruleDto);
+    ActiveRuleChange arChange = new ActiveRuleChange(DEACTIVATED, ActiveRuleDto.createFor(qProfileDto, ruleDto), ruleDto);
+    when(qProfileRules.deleteRule(any(DbSession.class), eq(ruleDto))).thenReturn(List.of(arChange));
+    //WHEN
+    execute(context -> context.createRepository("fake", "java").done());
+    //THEN
+    List<QProfileChangeDto> qProfileChangeDtos = dbClient.qProfileChangeDao().selectByQuery(db.getSession(), new QProfileChangeQuery(qProfileDto.getKee()));
+    assertThat(qProfileChangeDtos).extracting(QProfileChangeDto::getRulesProfileUuid, QProfileChangeDto::getChangeType)
+      .contains(tuple(qProfileDto.getRulesProfileUuid(), "DEACTIVATED"));
+  }
+
+  @Test
+  public void removed_rule_should_be_deleted_when_renamed_repository() {
+    //GIVEN
+    RuleDto removedRuleDto = db.rules().insert(RuleKey.of("old_repo", "removed_rule"));
+    RuleDto renamedRuleDto = db.rules().insert(RuleKey.of("old_repo", "renamed_rule"));
+    //WHEN
+    execute(context -> createRule(context, "java", "new_repo", renamedRuleDto.getRuleKey(),
+      rule -> rule.addDeprecatedRuleKey(renamedRuleDto.getRepositoryKey(), renamedRuleDto.getRuleKey())));
+    //THEN
+    verify(qProfileRules).deleteRule(any(DbSession.class), eq(removedRuleDto));
+  }
+
+  private void execute(RulesDefinition... defs) {
+    ServerPluginRepository pluginRepository = mock(ServerPluginRepository.class);
+    when(pluginRepository.getPluginKey(any(RulesDefinition.class))).thenReturn(FAKE_PLUGIN_KEY);
+    RuleDefinitionsLoader loader = new RuleDefinitionsLoader(pluginRepository, defs);
+    Languages languages = mock(Languages.class);
+    when(languages.get(any())).thenReturn(mock(Language.class));
+    reset(webServerRuleFinder);
+
+    RulesRegistrant task = new RulesRegistrant(loader, qProfileRules, dbClient, ruleIndexer, activeRuleIndexer, languages, system, webServerRuleFinder, uuidFactory, metadataIndex,
+      resolver, rulesKeyVerifier, startupRuleUpdater);
+    task.start();
+    // Execute a commit to refresh session state as the task is using its own session
+    db.getSession().commit();
+
+    verify(webServerRuleFinder).startCaching();
+  }
+
+  private NewRule createRule(NewRepository repo, String key) {
+    return repo.createRule(key)
+      .setName(key + " name")
+      .setHtmlDescription("Description of " + key)
+      .setSeverity(BLOCKER)
+      .setInternalKey("config1")
+      .setTags("tag1", "tag2", "tag3")
+      .setType(RuleType.CODE_SMELL)
+      .setStatus(RuleStatus.BETA);
+  }
+
+  @SafeVarargs
+  private void createRule(RulesDefinition.Context context, String language, String repositoryKey, String ruleKey, Consumer<NewRule>... consumers) {
+    NewRepository repo = context.createRepository(repositoryKey, language);
+    NewRule newRule = repo.createRule(ruleKey)
+      .setName(ruleKey)
+      .setHtmlDescription("Description of One")
+      .setSeverity(BLOCKER)
+      .setType(RuleType.CODE_SMELL)
+      .setStatus(RuleStatus.BETA);
+
+    Arrays.stream(consumers).forEach(c -> c.accept(newRule));
+    repo.done();
+  }
+
+  private void verifyIndicesMarkedAsInitialized() {
+    verify(metadataIndex).setInitialized(RuleIndexDefinition.TYPE_RULE, true);
+    verify(metadataIndex).setInitialized(RuleIndexDefinition.TYPE_ACTIVE_RULE, true);
+    reset(metadataIndex);
+  }
+
+  private void verifyIndicesNotMarkedAsInitialized() {
+    verifyNoInteractions(metadataIndex);
+  }
+
+  private RuleParamDto getParam(List<RuleParamDto> params, String key) {
+    for (RuleParamDto param : params) {
+      if (param.getName().equals(key)) {
+        return param;
+      }
+    }
+    return null;
+  }
+
+  static class FakeRepositoryV1 implements RulesDefinition {
+    @Override
+    public void define(Context context) {
+      NewRepository repo = context.createRepository("fake", "java");
+      NewRule rule1 = repo.createRule(RULE_KEY1.rule())
+        .setName("One")
+        .setHtmlDescription("Description of One")
+        .setSeverity(BLOCKER)
+        .setInternalKey("config1")
+        .setTags("tag1", "tag2", "tag3")
+        .setScope(RuleScope.ALL)
+        .setType(RuleType.CODE_SMELL)
+        .setStatus(RuleStatus.BETA)
+        .setGapDescription("java.S115.effortToFix")
+        .addEducationPrincipleKeys("concept1", "concept2", "concept3");
+      rule1.setDebtRemediationFunction(rule1.debtRemediationFunctions().linearWithOffset("5d", "10h"));
+
+      rule1.createParam("param1").setDescription("parameter one").setDefaultValue("default1");
+      rule1.createParam("param2").setDescription("parameter two").setDefaultValue("default2");
+
+      repo.createRule(HOTSPOT_RULE_KEY.rule())
+        .setName("Hotspot")
+        .setHtmlDescription("Minimal hotspot")
+        .setType(RuleType.SECURITY_HOTSPOT)
+        .addOwaspTop10(Y2021, OwaspTop10.A1, OwaspTop10.A3)
+        .addCwe(1, 123, 863);
+
+      repo.createRule(RULE_KEY2.rule())
+        .setName("Two")
+        .setHtmlDescription("Minimal rule");
+      repo.done();
+    }
+  }
+
+  /**
+   * FakeRepositoryV1 with some changes
+   */
+  static class FakeRepositoryV2 implements RulesDefinition {
+    @Override
+    public void define(Context context) {
+      NewRepository repo = context.createRepository("fake", "java");
+
+      // almost all the attributes of rule1 are changed
+      NewRule rule1 = repo.createRule(RULE_KEY1.rule())
+        .setName("One v2")
+        .setHtmlDescription("Description of One v2")
+        .setSeverity(INFO)
+        .setInternalKey("config1 v2")
+        // tag2 and tag3 removed, tag4 added
+        .setTags("tag1", "tag4")
+        .setType(RuleType.BUG)
+        .setStatus(READY)
+        .setGapDescription("java.S115.effortToFix.v2")
+        .addEducationPrincipleKeys("concept1", "concept4");
+      rule1.setDebtRemediationFunction(rule1.debtRemediationFunctions().linearWithOffset("6d", "2h"));
+      rule1.createParam("param1").setDescription("parameter one v2").setDefaultValue("default1 v2");
+      rule1.createParam("param2").setDescription("parameter two v2").setDefaultValue("default2 v2");
+
+      // rule2 is dropped, rule3 is new
+      repo.createRule(RULE_KEY3.rule())
+        .setName("Three")
+        .setHtmlDescription("Rule Three");
+
+      repo.done();
+    }
+  }
+
+  static class FakeRepositoryV3 implements RulesDefinition {
+    @Override
+    public void define(Context context) {
+      NewRepository repo = context.createRepository("fake", "java");
+      // rule 3 is dropped
+      repo.createRule(RULE_KEY3.rule())
+        .setName("Three")
+        .setMarkdownDescription("Rule Three V2");
+
+      repo.done();
+    }
+  }
+
+  static class ExternalRuleRepository implements RulesDefinition {
+    @Override
+    public void define(Context context) {
+      NewRepository repo = context.createExternalRepository("eslint", "js");
+      repo.createRule(RULE_KEY1.rule())
+        .setName("One")
+        .setHtmlDescription("Description of One")
+        .setSeverity(BLOCKER)
+        .setInternalKey("config1")
+        .setTags("tag1", "tag2", "tag3")
+        .setScope(RuleScope.ALL)
+        .setType(RuleType.CODE_SMELL)
+        .setStatus(RuleStatus.BETA)
+        .addEducationPrincipleKeys("concept1", "concept2", "concept3");
+
+      repo.createRule(EXTERNAL_HOTSPOT_RULE_KEY.rule())
+        .setName("Hotspot")
+        .setHtmlDescription("Minimal hotspot")
+        .setType(RuleType.SECURITY_HOTSPOT)
+        .addOwaspTop10(Y2021, OwaspTop10.A1, OwaspTop10.A3)
+        .addCwe(1, 123, 863);
+
+      repo.done();
+    }
+  }
+
+  static class BigRepository implements RulesDefinition {
+    static final int SIZE = 500;
+
+    @Override
+    public void define(Context context) {
+      NewRepository repo = context.createRepository("big", "java");
+      for (int i = 0; i < SIZE; i++) {
+        NewRule rule = repo.createRule("rule" + i)
+          .setName("name of " + i)
+          .setHtmlDescription("description of " + i);
+        for (int j = 0; j < 20; j++) {
+          rule.createParam("param" + j);
+        }
+
+      }
+      repo.done();
+    }
+  }
+
+  static class FindbugsRepository implements RulesDefinition {
+    @Override
+    public void define(Context context) {
+      NewRepository repo = context.createRepository("findbugs", "java");
+      repo.createRule("rule1")
+        .setName("Rule One")
+        .setHtmlDescription("Description of Rule One");
+      repo.done();
+    }
+  }
+
+  static class FbContribRepository implements RulesDefinition {
+    @Override
+    public void define(Context context) {
+      NewExtendedRepository repo = context.createRepository("findbugs", "java");
+      repo.createRule("rule2")
+        .setName("Rule Two")
+        .setHtmlDescription("Description of Rule Two");
+      repo.done();
+    }
+  }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/SingleDeprecatedRuleKeyTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/SingleDeprecatedRuleKeyTest.java
new file mode 100644 (file)
index 0000000..77c8e1e
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.rule.registration;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.assertj.core.groups.Tuple;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+import org.sonar.server.rule.registration.SingleDeprecatedRuleKey;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SingleDeprecatedRuleKeyTest {
+
+  @Test
+  public void test_creation_from_DeprecatedRuleKeyDto() {
+    // Creation from DeprecatedRuleKeyDto
+    DeprecatedRuleKeyDto deprecatedRuleKeyDto = new DeprecatedRuleKeyDto()
+      .setOldRuleKey(randomAlphanumeric(50))
+      .setOldRepositoryKey(randomAlphanumeric(50))
+      .setRuleUuid(randomAlphanumeric(50))
+      .setUuid(randomAlphanumeric(40));
+
+    SingleDeprecatedRuleKey singleDeprecatedRuleKey = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto);
+
+    assertThat(singleDeprecatedRuleKey.getOldRepositoryKey()).isEqualTo(deprecatedRuleKeyDto.getOldRepositoryKey());
+    assertThat(singleDeprecatedRuleKey.getOldRuleKey()).isEqualTo(deprecatedRuleKeyDto.getOldRuleKey());
+    assertThat(singleDeprecatedRuleKey.getNewRepositoryKey()).isEqualTo(deprecatedRuleKeyDto.getNewRepositoryKey());
+    assertThat(singleDeprecatedRuleKey.getNewRuleKey()).isEqualTo(deprecatedRuleKeyDto.getNewRuleKey());
+    assertThat(singleDeprecatedRuleKey.getUuid()).isEqualTo(deprecatedRuleKeyDto.getUuid());
+    assertThat(singleDeprecatedRuleKey.getRuleUuid()).isEqualTo(deprecatedRuleKeyDto.getRuleUuid());
+    assertThat(singleDeprecatedRuleKey.getOldRuleKeyAsRuleKey())
+      .isEqualTo(RuleKey.of(deprecatedRuleKeyDto.getOldRepositoryKey(), deprecatedRuleKeyDto.getOldRuleKey()));
+  }
+
+  @Test
+  public void test_creation_from_RulesDefinitionRule() {
+    // Creation from RulesDefinition.Rule
+    ImmutableSet<RuleKey> deprecatedRuleKeys = ImmutableSet.of(
+      RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)),
+      RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)),
+      RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)));
+
+    RulesDefinition.Repository repository = mock(RulesDefinition.Repository.class);
+    when(repository.key()).thenReturn(randomAlphanumeric(50));
+
+    RulesDefinition.Rule rule = mock(RulesDefinition.Rule.class);
+    when(rule.key()).thenReturn(randomAlphanumeric(50));
+    when(rule.deprecatedRuleKeys()).thenReturn(deprecatedRuleKeys);
+    when(rule.repository()).thenReturn(repository);
+
+    Set<SingleDeprecatedRuleKey> singleDeprecatedRuleKeys = SingleDeprecatedRuleKey.from(rule);
+    assertThat(singleDeprecatedRuleKeys).hasSize(deprecatedRuleKeys.size());
+    assertThat(singleDeprecatedRuleKeys)
+      .extracting(SingleDeprecatedRuleKey::getUuid, SingleDeprecatedRuleKey::getOldRepositoryKey, SingleDeprecatedRuleKey::getOldRuleKey,
+        SingleDeprecatedRuleKey::getNewRepositoryKey, SingleDeprecatedRuleKey::getNewRuleKey, SingleDeprecatedRuleKey::getOldRuleKeyAsRuleKey)
+      .containsExactlyInAnyOrder(
+        deprecatedRuleKeys.stream().map(
+          r -> tuple(null, r.repository(), r.rule(), rule.repository().key(), rule.key(), RuleKey.of(r.repository(), r.rule())))
+          .toList().toArray(new Tuple[deprecatedRuleKeys.size()]));
+  }
+
+  @Test
+  public void test_equality() {
+    DeprecatedRuleKeyDto deprecatedRuleKeyDto1 = new DeprecatedRuleKeyDto()
+      .setOldRuleKey(randomAlphanumeric(50))
+      .setOldRepositoryKey(randomAlphanumeric(50))
+      .setUuid(randomAlphanumeric(40))
+      .setRuleUuid("some-uuid");
+
+    DeprecatedRuleKeyDto deprecatedRuleKeyDto1WithoutUuid = new DeprecatedRuleKeyDto()
+      .setOldRuleKey(deprecatedRuleKeyDto1.getOldRuleKey())
+      .setOldRepositoryKey(deprecatedRuleKeyDto1.getOldRepositoryKey());
+
+    DeprecatedRuleKeyDto deprecatedRuleKeyDto2 = new DeprecatedRuleKeyDto()
+      .setOldRuleKey(randomAlphanumeric(50))
+      .setOldRepositoryKey(randomAlphanumeric(50))
+      .setUuid(randomAlphanumeric(40));
+
+    SingleDeprecatedRuleKey singleDeprecatedRuleKey1 = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1);
+    SingleDeprecatedRuleKey singleDeprecatedRuleKey2 = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2);
+
+    assertThat(singleDeprecatedRuleKey1)
+      .isEqualTo(singleDeprecatedRuleKey1)
+      .isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1))
+      .isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1WithoutUuid));
+    assertThat(singleDeprecatedRuleKey2).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2));
+
+    assertThat(singleDeprecatedRuleKey1)
+      .hasSameHashCodeAs(singleDeprecatedRuleKey1)
+      .hasSameHashCodeAs(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1))
+      .hasSameHashCodeAs(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1WithoutUuid));
+    assertThat(singleDeprecatedRuleKey2).hasSameHashCodeAs(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2));
+
+    assertThat(singleDeprecatedRuleKey1)
+      .isNotNull()
+      .isNotEqualTo("")
+      .isNotEqualTo(null)
+      .isNotEqualTo(singleDeprecatedRuleKey2);
+    assertThat(singleDeprecatedRuleKey2).isNotEqualTo(singleDeprecatedRuleKey1);
+
+    assertThat(singleDeprecatedRuleKey1.hashCode()).isNotEqualTo(singleDeprecatedRuleKey2.hashCode());
+    assertThat(singleDeprecatedRuleKey2.hashCode()).isNotEqualTo(singleDeprecatedRuleKey1.hashCode());
+  }
+}
index f25523ff456c997f50c43cd938665227cb885226..9e6c5f13f8c68842a9a3e89c5ab5cdc32b1f2a58 100644 (file)
@@ -39,9 +39,11 @@ import org.sonar.server.qualityprofile.builtin.BuiltInQualityProfilesUpdateListe
 import org.sonar.server.rule.AdvancedRuleDescriptionSectionsGenerator;
 import org.sonar.server.rule.LegacyHotspotRuleDescriptionSectionsGenerator;
 import org.sonar.server.rule.LegacyIssueRuleDescriptionSectionsGenerator;
-import org.sonar.server.rule.RegisterRules;
+import org.sonar.server.rule.registration.RulesKeyVerifier;
+import org.sonar.server.rule.registration.RulesRegistrant;
 import org.sonar.server.rule.RuleDescriptionSectionsGeneratorResolver;
 import org.sonar.server.rule.WebServerRuleFinder;
+import org.sonar.server.rule.registration.StartupRuleUpdater;
 import org.sonar.server.startup.GeneratePluginIndex;
 import org.sonar.server.startup.RegisterMetrics;
 import org.sonar.server.startup.RegisterPermissionTemplates;
@@ -72,7 +74,9 @@ public class PlatformLevelStartup extends PlatformLevel {
       AdvancedRuleDescriptionSectionsGenerator.class,
       LegacyHotspotRuleDescriptionSectionsGenerator.class,
       LegacyIssueRuleDescriptionSectionsGenerator.class,
-      RegisterRules.class,
+      RulesRegistrant.class,
+      RulesKeyVerifier.class,
+      StartupRuleUpdater.class,
       BuiltInQProfileLoader.class);
     addIfStartupLeader(
       BuiltInQualityProfilesUpdateListener.class,