]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15919 add ruleset changes
authorPierre <pierre.guillot@sonarsource.com>
Thu, 3 Feb 2022 11:10:18 +0000 (12:10 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Feb 2022 15:48:04 +0000 (15:48 +0000)
42 files changed:
server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdateImpl.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DistributedRuleActivatorEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventService.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventServiceImpl.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivator.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivatorEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/StandaloneRuleActivatorEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/BuiltInQProfileUpdateImplTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/qualityprofile/RuleActivatorTest.java
server/sonar-webserver-pushapi/build.gradle
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileResetImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileRulesImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileTreeImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/AddProjectAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/RemoveProjectAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileComparisonTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileResetImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileRuleImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileRulesImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileTreeImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/RegisterQualityProfilesNotificationTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/AddProjectActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ChangeParentActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/CreateActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/InheritanceActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/QProfilesWsMediumTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/RemoveProjectActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/SearchActionTest.java
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
sonar-core/src/main/java/org/sonar/core/util/ParamChange.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/util/RuleActivationListener.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/util/RuleChange.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/util/RuleSetChangeEvent.java [new file with mode: 0644]

index 40875de72c1b6341a5dd949615435a06978ba086..cf0c8a1cd9e82f29cc2b1d0dfb07f0ddc3021de4 100644 (file)
@@ -40,6 +40,8 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.sonar.application.config.TestAppSettings;
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
 import org.sonar.process.cluster.hz.DistributedAnswer;
 import org.sonar.process.cluster.hz.DistributedCall;
 import org.sonar.process.cluster.hz.DistributedCallback;
@@ -194,6 +196,16 @@ public class AppNodesClusterHostsConsistencyTest {
       callback.onComplete((Map<Member, T>) hostsPerMember);
     }
 
+    @Override
+    public void subscribeRuleActivationTopic(RuleActivationListener listener) {
+
+    }
+
+    @Override
+    public void publishEvent(RuleSetChangeEvent event) {
+
+    }
+
     @Override
     public void close() {
 
index 65f43bae7076e3fcef91683c6cc849009089e8b3..320a867eb294654d2bd957121c9be387bcf6b80d 100644 (file)
@@ -26,6 +26,8 @@ import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.locks.Lock;
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
 import org.sonar.process.ProcessId;
 
 public interface HazelcastMember extends AutoCloseable {
@@ -106,6 +108,10 @@ public interface HazelcastMember extends AutoCloseable {
    */
   <T> void callAsync(DistributedCall<T> callable, MemberSelector memberSelector, DistributedCallback<T> callback);
 
+  void subscribeRuleActivationTopic(RuleActivationListener listener);
+
+  void publishEvent(RuleSetChangeEvent event);
+
   @Override
   void close();
 }
index 685910361af93ec59896cd4f54c7019282ade41e..9b223cb0c6119a1388f9c2524a750bcba092e260 100644 (file)
@@ -27,6 +27,8 @@ import com.hazelcast.core.HazelcastInstanceNotActiveException;
 import com.hazelcast.core.IExecutorService;
 import com.hazelcast.core.MultiExecutionCallback;
 import com.hazelcast.cp.IAtomicReference;
+import com.hazelcast.topic.ITopic;
+import com.hazelcast.topic.MessageListener;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
@@ -37,6 +39,8 @@ import java.util.concurrent.TimeoutException;
 import java.util.concurrent.locks.Lock;
 import java.util.stream.Collectors;
 import org.slf4j.LoggerFactory;
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
 
 class HazelcastMemberImpl implements HazelcastMember {
 
@@ -125,6 +129,18 @@ class HazelcastMemberImpl implements HazelcastMember {
     });
   }
 
+  @Override
+  public void subscribeRuleActivationTopic(RuleActivationListener listener) {
+    ITopic<RuleSetChangeEvent> topic = hzInstance.getTopic("ruleActivated");
+    MessageListener<RuleSetChangeEvent> hzListener = message -> listener.listen(message.getMessageObject());
+    topic.addMessageListener(hzListener);
+  }
+
+  @Override
+  public void publishEvent(RuleSetChangeEvent event) {
+    hzInstance.getTopic("ruleActivated").publish(event);
+  }
+
   @Override
   public void close() {
     try {
index d6e019d79c49eaddfcb8494e77189036ef8ab507..9f0f059f6b52749c4b30a1c09250b96be71ae929 100644 (file)
@@ -40,11 +40,14 @@ public class BuiltInQProfileUpdateImpl implements BuiltInQProfileUpdate {
   private final DbClient dbClient;
   private final RuleActivator ruleActivator;
   private final ActiveRuleIndexer activeRuleIndexer;
+  private final QualityProfileChangeEventService qualityProfileChangeEventService;
 
-  public BuiltInQProfileUpdateImpl(DbClient dbClient, RuleActivator ruleActivator, ActiveRuleIndexer activeRuleIndexer) {
+  public BuiltInQProfileUpdateImpl(DbClient dbClient, RuleActivator ruleActivator, ActiveRuleIndexer activeRuleIndexer,
+    QualityProfileChangeEventService qualityProfileChangeEventService) {
     this.dbClient = dbClient;
     this.ruleActivator = ruleActivator;
     this.activeRuleIndexer = activeRuleIndexer;
+    this.qualityProfileChangeEventService = qualityProfileChangeEventService;
   }
 
   public List<ActiveRuleChange> update(DbSession dbSession, BuiltInQProfile builtInDefinition, RulesProfileDto initialRuleProfile) {
@@ -56,8 +59,8 @@ public class BuiltInQProfileUpdateImpl implements BuiltInQProfileUpdate {
 
     // all rules, including those which are removed from built-in profile
     Set<String> ruleUuids = Stream.concat(
-      deactivatedRuleUuids.stream(),
-      builtInDefinition.getActiveRules().stream().map(BuiltInQProfile.ActiveRule::getRuleUuid))
+        deactivatedRuleUuids.stream(),
+        builtInDefinition.getActiveRules().stream().map(BuiltInQProfile.ActiveRule::getRuleUuid))
       .collect(toSet());
 
     Collection<RuleActivation> activations = new ArrayList<>();
@@ -69,13 +72,16 @@ public class BuiltInQProfileUpdateImpl implements BuiltInQProfileUpdate {
 
     RuleActivationContext context = ruleActivator.createContextForBuiltInProfile(dbSession, initialRuleProfile, ruleUuids);
     List<ActiveRuleChange> changes = new ArrayList<>();
-    for (RuleActivation activation : activations) {
-      changes.addAll(ruleActivator.activate(dbSession, activation, context));
-    }
+
+    changes.addAll(ruleActivator.activate(dbSession, activations, context));
 
     // these rules are no longer part of the built-in profile
     deactivatedRuleUuids.forEach(ruleUuid -> changes.addAll(ruleActivator.deactivate(dbSession, context, ruleUuid, false)));
 
+    if (!changes.isEmpty()) {
+      qualityProfileChangeEventService.distributeRuleChangeEvent(context.getProfiles(), changes, initialRuleProfile.getLanguage());
+    }
+
     activeRuleIndexer.commitAndIndex(dbSession, changes);
     return changes;
   }
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DistributedRuleActivatorEventsDistributor.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/DistributedRuleActivatorEventsDistributor.java
new file mode 100644 (file)
index 0000000..a6a3b84
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualityprofile;
+
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
+import org.sonar.process.cluster.hz.HazelcastMember;
+
+@ServerSide
+public class DistributedRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {
+
+  private HazelcastMember hazelcastMember;
+
+  public DistributedRuleActivatorEventsDistributor(HazelcastMember hazelcastMember) {
+    this.hazelcastMember = hazelcastMember;
+  }
+
+  @Override
+  public void subscribe(RuleActivationListener listener) {
+    hazelcastMember.subscribeRuleActivationTopic(listener);
+  }
+
+  @Override
+  public void pushEvent(RuleSetChangeEvent event) {
+    hazelcastMember.publishEvent(event);
+  }
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventService.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventService.java
new file mode 100644 (file)
index 0000000..008afff
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualityprofile;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+
+public interface QualityProfileChangeEventService {
+
+  void publishRuleActivationToSonarLintClients(ProjectDto project, Optional<QProfileDto> activatedProfile, Optional<QProfileDto> deactivatedProfile);
+
+  void distributeRuleChangeEvent(Collection<QProfileDto> profiles, List<ActiveRuleChange> activeRuleChanges, String language);
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventServiceImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/QualityProfileChangeEventServiceImpl.java
new file mode 100644 (file)
index 0000000..22c96e6
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualityprofile;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.NotNull;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.ParamChange;
+import org.sonar.core.util.RuleChange;
+import org.sonar.core.util.RuleSetChangeEvent;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.ProjectQprofileAssociationDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.server.rule.index.RuleIndex;
+import org.sonar.server.rule.index.RuleQuery;
+
+@ServerSide
+public class QualityProfileChangeEventServiceImpl implements QualityProfileChangeEventService {
+
+  private final DbClient dbClient;
+  private final RuleIndex ruleIndex;
+  private final RuleActivatorEventsDistributor eventsDistributor;
+
+  public QualityProfileChangeEventServiceImpl(DbClient dbClient, RuleIndex ruleIndex, RuleActivatorEventsDistributor eventsDistributor) {
+    this.dbClient = dbClient;
+    this.ruleIndex = ruleIndex;
+    this.eventsDistributor = eventsDistributor;
+  }
+
+  @Override
+  public void publishRuleActivationToSonarLintClients(ProjectDto project, Optional<QProfileDto> activatedProfile, Optional<QProfileDto> deactivatedProfile) {
+    List<RuleChange> activatedRules = new ArrayList<>();
+    List<RuleChange> deactivatedRules = new ArrayList<>();
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+
+      if (activatedProfile.isPresent()) {
+        RuleQuery query = new RuleQuery().setQProfile(activatedProfile.get()).setActivation(true).setIncludeExternal(true);
+        // .setLanguages() ?
+        Iterator<String> searchIdResult = ruleIndex.searchAll(query);
+        List<String> uuids = new ArrayList<>();
+        while (searchIdResult.hasNext()) {
+          uuids.add(searchIdResult.next());
+        }
+
+        List<RuleDto> ruleDtos = dbClient.ruleDao().selectByUuids(dbSession, uuids);
+        Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid = dbClient.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, uuids)
+          .stream().collect(Collectors.groupingBy(ActiveRuleParamDto::getActiveRuleUuid));
+
+        for (RuleDto ruleDto : ruleDtos) {
+          RuleChange ruleChange = toRuleChange(ruleDto, paramsByRuleUuid);
+          activatedRules.add(ruleChange);
+        }
+      }
+
+      if (deactivatedProfile.isPresent()) {
+        RuleQuery query = new RuleQuery().setQProfile(deactivatedProfile.get()).setActivation(true).setIncludeExternal(true);
+        // .setLanguages() ?
+        Iterator<String> searchIdResult = ruleIndex.searchAll(query);
+        List<String> uuids = new ArrayList<>();
+        while (searchIdResult.hasNext()) {
+          uuids.add(searchIdResult.next());
+        }
+
+        List<RuleDto> ruleDtos = dbClient.ruleDao().selectByUuids(dbSession, uuids);
+        Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid = dbClient.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, uuids)
+          .stream().collect(Collectors.groupingBy(ActiveRuleParamDto::getActiveRuleUuid));
+
+        for (RuleDto ruleDto : ruleDtos) {
+          RuleChange ruleChange = toRuleChange(ruleDto, paramsByRuleUuid);
+          deactivatedRules.add(ruleChange);
+        }
+      }
+
+    }
+
+    RuleSetChangeEvent event = new RuleSetChangeEvent(new String[]{project.getKey()}, activatedRules.toArray(new RuleChange[0]), deactivatedRules.toArray(new RuleChange[0]));
+    eventsDistributor.pushEvent(event);
+  }
+
+  @NotNull
+  private RuleChange toRuleChange(RuleDto ruleDto, Map<String, List<ActiveRuleParamDto>> paramsByRuleUuid) {
+    RuleChange ruleChange = new RuleChange();
+    ruleChange.setKey(ruleDto.getRuleKey());
+    ruleChange.setLanguage(ruleDto.getLanguage());
+    ruleChange.setSeverity(ruleDto.getSeverityString());
+
+    List<ParamChange> paramChanges = new ArrayList<>();
+    List<ActiveRuleParamDto> activeRuleParamDtos = paramsByRuleUuid.getOrDefault(ruleDto.getUuid(), new ArrayList<>());
+    for (ActiveRuleParamDto activeRuleParam : activeRuleParamDtos) {
+      paramChanges.add(new ParamChange(activeRuleParam.getKey(), activeRuleParam.getValue()));
+    }
+    ruleChange.setParams(paramChanges.toArray(new ParamChange[0]));
+
+    String templateUuid = ruleDto.getTemplateUuid();
+    if (templateUuid != null && !"".equals(templateUuid)) {
+      try (DbSession dbSession = dbClient.openSession(false)) {
+        RuleDto templateRule = dbClient.ruleDao().selectByUuid(templateUuid, dbSession)
+          .orElseThrow(() -> new IllegalStateException(String.format("unknow Template Rule '%s'", templateUuid)));
+        ruleChange.setTemplateKey(templateRule.getRuleKey());
+      }
+    }
+
+    return ruleChange;
+  }
+
+  public void distributeRuleChangeEvent(Collection<QProfileDto> profiles, List<ActiveRuleChange> activeRuleChanges, String language) {
+    if (activeRuleChanges.isEmpty()) {
+      return;
+    }
+
+    Set<RuleChange> activatedRules = new HashSet<>();
+    Set<RuleChange> deactivatedRules = new HashSet<>();
+
+    for (ActiveRuleChange arc : activeRuleChanges) {
+
+      RuleChange ruleChange = new RuleChange();
+      ruleChange.setKey(arc.getActiveRule().getRuleKey().rule());
+      ruleChange.setSeverity(arc.getSeverity());
+      ruleChange.setLanguage(language);
+
+      Optional<String> templateKey = templateKey(arc);
+      templateKey.ifPresent(ruleChange::setTemplateKey);
+
+      // params
+      List<ParamChange> paramChanges = new ArrayList<>();
+      for (Map.Entry<String, String> entry : arc.getParameters().entrySet()) {
+        paramChanges.add(new ParamChange(entry.getKey(), entry.getValue()));
+      }
+      ruleChange.setParams(paramChanges.toArray(new ParamChange[0]));
+
+      switch (arc.getType()) {
+        case ACTIVATED:
+        case UPDATED:
+          activatedRules.add(ruleChange);
+          break;
+        case DEACTIVATED:
+          deactivatedRules.add(ruleChange);
+          break;
+      }
+    }
+
+    Set<String> projectKeys = getProjectKeys(profiles);
+
+    RuleSetChangeEvent event = new RuleSetChangeEvent(projectKeys.toArray(new String[0]), activatedRules.toArray(new RuleChange[0]), deactivatedRules.toArray(new RuleChange[0]));
+    eventsDistributor.pushEvent(event);
+
+  }
+
+  private Optional<String> templateKey(ActiveRuleChange arc) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      String ruleUuid = arc.getRuleUuid();
+      RuleDto rule = dbClient.ruleDao().selectByUuid(ruleUuid, dbSession).orElseThrow(() -> new IllegalStateException("unknow rule"));
+      String templateUuid = rule.getTemplateUuid();
+      if (templateUuid != null && !"".equals(templateUuid)) {
+        RuleDto templateRule = dbClient.ruleDao().selectByUuid(templateUuid, dbSession)
+          .orElseThrow(() -> new IllegalStateException(String.format("unknow Template Rule '%s'", templateUuid)));
+        return Optional.of(templateRule.getRuleKey());
+      }
+    }
+    return Optional.empty();
+  }
+
+  private Set<String> getProjectKeys(Collection<QProfileDto> profiles) {
+    Set<String> projectKeys = new HashSet<>();
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      for (QProfileDto profileDto : profiles) {
+        List<ProjectQprofileAssociationDto> associationDtos = dbClient.qualityProfileDao().selectSelectedProjects(dbSession, profileDto, null);
+        for (ProjectQprofileAssociationDto associationDto : associationDtos) {
+          projectKeys.add(associationDto.getProjectKey());
+        }
+      }
+      return projectKeys;
+    }
+  }
+
+}
index 63a98c72e4971534ce52c63e28c7e414af2b160b..713ae6ce9abb90bd03ace03c5471cf4434f0ecd4 100644 (file)
@@ -52,6 +52,7 @@ import org.sonar.server.user.UserSession;
 import org.sonar.server.util.TypeValidations;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.stream.Collectors.toList;
 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
 
 /**
@@ -72,9 +73,17 @@ public class RuleActivator {
     this.userSession = userSession;
   }
 
+
+  public List<ActiveRuleChange> activate(DbSession dbSession, Collection<RuleActivation> activations, RuleActivationContext context) {
+    return activations.stream().map(a -> activate(dbSession, a, context))
+      .flatMap(List::stream)
+      .collect(toList());
+  }
+
   public List<ActiveRuleChange> activate(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
     context.reset(activation.getRuleUuid());
-    return doActivate(dbSession, activation, context);
+    List<ActiveRuleChange> activeRuleChanges = doActivate(dbSession, activation, context);
+    return activeRuleChanges;
   }
 
   private List<ActiveRuleChange> doActivate(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
@@ -357,7 +366,8 @@ public class RuleActivator {
 
   public List<ActiveRuleChange> deactivate(DbSession dbSession, RuleActivationContext context, String ruleUuid, boolean force) {
     context.reset(ruleUuid);
-    return doDeactivate(dbSession, context, force);
+    List<ActiveRuleChange> activeRuleChanges = doDeactivate(dbSession, context, force);
+    return activeRuleChanges;
   }
 
   private List<ActiveRuleChange> doDeactivate(DbSession dbSession, RuleActivationContext context, boolean force) {
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivatorEventsDistributor.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/RuleActivatorEventsDistributor.java
new file mode 100644 (file)
index 0000000..d41f1a5
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualityprofile;
+
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
+
+public interface RuleActivatorEventsDistributor {
+
+  void subscribe(RuleActivationListener listener);
+
+  void pushEvent(RuleSetChangeEvent event);
+}
\ No newline at end of file
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/StandaloneRuleActivatorEventsDistributor.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/qualityprofile/StandaloneRuleActivatorEventsDistributor.java
new file mode 100644 (file)
index 0000000..43fc97d
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualityprofile;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleSetChangeEvent;
+
+@ServerSide
+public class StandaloneRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {
+
+  private List<RuleActivationListener> listeners = new ArrayList<>();
+
+  @Override
+  public void subscribe(RuleActivationListener listener) {
+    listeners.add(listener);
+  }
+
+  @Override
+  public void pushEvent(RuleSetChangeEvent event) {
+    listeners.forEach(l -> l.listen(event));
+  }
+}
index cfbf49f137fb62bca91b6c15bdb0ddafbd6ca866..024de98b39f917dcc549002f0c502f95f5d5ddd8 100644 (file)
@@ -56,7 +56,12 @@ import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.sonar.api.rules.RulePriority.BLOCKER;
 import static org.sonar.api.rules.RulePriority.CRITICAL;
 import static org.sonar.api.rules.RulePriority.MAJOR;
@@ -78,9 +83,11 @@ public class BuiltInQProfileUpdateImplTest {
   private System2 system2 = new TestSystem2().setNow(NOW);
   private ActiveRuleIndexer activeRuleIndexer = mock(ActiveRuleIndexer.class);
   private TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private RuleActivator ruleActivator = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
 
-  private BuiltInQProfileUpdateImpl underTest = new BuiltInQProfileUpdateImpl(db.getDbClient(), ruleActivator, activeRuleIndexer);
+  private BuiltInQProfileUpdateImpl underTest = new BuiltInQProfileUpdateImpl(db.getDbClient(), ruleActivator, activeRuleIndexer,
+    qualityProfileChangeEventService);
 
   private RulesProfileDto persistedProfile;
 
@@ -105,13 +112,14 @@ public class BuiltInQProfileUpdateImplTest {
     newQp.done();
     BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile("xoo", "Sonar way"), rule1, rule2);
 
-    underTest.update(db.getSession(), builtIn, persistedProfile);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
 
     List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
     assertThat(activeRules).hasSize(2);
     assertThatRuleIsNewlyActivated(activeRules, rule1, CRITICAL);
     assertThatRuleIsNewlyActivated(activeRules, rule2, MAJOR);
     assertThatProfileIsMarkedAsUpdated(persistedProfile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   @Test
@@ -125,12 +133,13 @@ public class BuiltInQProfileUpdateImplTest {
 
     activateRuleInDb(persistedProfile, rule, BLOCKER);
 
-    underTest.update(db.getSession(), builtIn, persistedProfile);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
 
     List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
     assertThat(activeRules).hasSize(1);
     assertThatRuleIsUpdated(activeRules, rule, CRITICAL);
     assertThatProfileIsMarkedAsUpdated(persistedProfile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   @Test
@@ -150,6 +159,7 @@ public class BuiltInQProfileUpdateImplTest {
     assertThat(activeRules).hasSize(1);
     assertThatRuleIsUntouched(activeRules, rule, CRITICAL);
     assertThatProfileIsNotMarkedAsUpdated(persistedProfile);
+    verifyNoInteractions(qualityProfileChangeEventService);
   }
 
   @Test
@@ -166,12 +176,13 @@ public class BuiltInQProfileUpdateImplTest {
     // so rule1 must be deactivated
     activateRuleInDb(persistedProfile, rule1, CRITICAL);
 
-    underTest.update(db.getSession(), builtIn, persistedProfile);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
 
     List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
     assertThat(activeRules).hasSize(1);
     assertThatRuleIsDeactivated(activeRules, rule1);
     assertThatProfileIsMarkedAsUpdated(persistedProfile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   @Test
@@ -193,7 +204,7 @@ public class BuiltInQProfileUpdateImplTest {
     activateRuleInDb(persistedProfile, rule1, BLOCKER);
     activateRuleInDb(persistedProfile, rule3, BLOCKER);
 
-    underTest.update(db.getSession(), builtIn, persistedProfile);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
 
     List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
     assertThat(activeRules).hasSize(2);
@@ -201,6 +212,7 @@ public class BuiltInQProfileUpdateImplTest {
     assertThatRuleIsNewlyActivated(activeRules, rule2, MAJOR);
     assertThatRuleIsDeactivated(activeRules, rule3);
     assertThatProfileIsMarkedAsUpdated(persistedProfile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   // SONAR-10473
@@ -222,9 +234,10 @@ public class BuiltInQProfileUpdateImplTest {
     rule.setSeverity(Severity.MINOR);
     db.rules().update(rule);
 
-    underTest.update(db.getSession(), builtIn, persistedProfile);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
     activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
     assertThatRuleIsNewlyActivated(activeRules, rule, MINOR);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   @Test
@@ -237,20 +250,22 @@ public class BuiltInQProfileUpdateImplTest {
     newQp.activateRule(rule.getRepositoryKey(), rule.getRuleKey());
     newQp.done();
     BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(newQp.language(), newQp.name()), rule);
-    underTest.update(db.getSession(), builtIn, persistedProfile);
+    List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, persistedProfile);
 
     List<ActiveRuleDto> activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
     assertThat(activeRules).hasSize(1);
     assertThatRuleHasParams(db, activeRules.get(0), tuple("min", "10"));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
 
     // emulate an upgrade of analyzer that changes the default value of parameter min
     ruleParam.setDefaultValue("20");
     db.getDbClient().ruleDao().updateRuleParam(db.getSession(), rule, ruleParam);
 
-    underTest.update(db.getSession(), builtIn, persistedProfile);
+    changes = underTest.update(db.getSession(), builtIn, persistedProfile);
     activeRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), persistedProfile);
     assertThat(activeRules).hasSize(1);
     assertThatRuleHasParams(db, activeRules.get(0), tuple("min", "20"));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   @Test
@@ -272,6 +287,7 @@ public class BuiltInQProfileUpdateImplTest {
     assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, emptyMap());
     assertThatRuleIsActivated(childProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
     assertThatRuleIsActivated(grandchildProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   // SONAR-14559
@@ -299,6 +315,7 @@ public class BuiltInQProfileUpdateImplTest {
 
     List<ActiveRuleDto> childActiveRules = db.getDbClient().activeRuleDao().selectByRuleProfile(db.getSession(), RulesProfileDto.from(childProfile));
     assertThatRuleIsUpdated(childActiveRules, rule, RulePriority.BLOCKER, INHERITED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   @Test
@@ -327,6 +344,7 @@ public class BuiltInQProfileUpdateImplTest {
 
     assertThatRuleHasParams(db, parentActiveRuleDto, tuple("min", "10"));
     assertThatRuleHasParams(db, childActiveRuleDto, tuple("min", "10"));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
   }
 
   @Test
@@ -345,6 +363,7 @@ public class BuiltInQProfileUpdateImplTest {
     BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
     List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
     assertThat(changes).hasSize(2).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.ACTIVATED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
 
     // second run, without any input changes
     RuleActivator ruleActivatorWithoutDescendants = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession) {
@@ -355,8 +374,10 @@ public class BuiltInQProfileUpdateImplTest {
         };
       }
     };
-    changes = new BuiltInQProfileUpdateImpl(db.getDbClient(), ruleActivatorWithoutDescendants, activeRuleIndexer).update(db.getSession(), builtIn, RulesProfileDto.from(profile));
+    changes = new BuiltInQProfileUpdateImpl(db.getDbClient(), ruleActivatorWithoutDescendants, activeRuleIndexer, qualityProfileChangeEventService)
+      .update(db.getSession(), builtIn, RulesProfileDto.from(profile));
     assertThat(changes).isEmpty();
+    verifyNoMoreInteractions(qualityProfileChangeEventService);
   }
 
   @Test
@@ -376,6 +397,7 @@ public class BuiltInQProfileUpdateImplTest {
     BuiltInQProfile builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
     List<ActiveRuleChange> changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
     assertThat(changes).hasSize(3).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.ACTIVATED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
 
     // second run to deactivate the rule
     context = new BuiltInQualityProfilesDefinition.Context();
@@ -384,6 +406,7 @@ public class BuiltInQProfileUpdateImplTest {
     builtIn = builtInProfileRepository.create(context.profile(profile.getLanguage(), profile.getName()), rule);
     changes = underTest.update(db.getSession(), builtIn, RulesProfileDto.from(profile));
     assertThat(changes).hasSize(3).extracting(ActiveRuleChange::getType).containsOnly(ActiveRuleChange.Type.DEACTIVATED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(persistedProfile.getLanguage()));
 
     assertThatRuleIsDeactivated(profile, rule);
     assertThatRuleIsDeactivated(childProfile, rule);
index 45310b7d9b34680048eb29afe00a088c1e767245..cdcd4fa41e2135e5952fdec31681a442e983d0e6 100644 (file)
@@ -50,6 +50,7 @@ import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
 import static org.sonar.server.qualityprofile.ActiveRuleInheritance.INHERITED;
 import static org.sonar.server.qualityprofile.ActiveRuleInheritance.OVERRIDES;
 
@@ -69,6 +70,7 @@ public class RuleActivatorTest {
   private final System2 system2 = new TestSystem2().setNow(NOW);
   private final TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
 
+  private final QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private final RuleActivator underTest = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
 
   @Test
index a2c94714bd02bc09a3e327f24f060d0bc3211b4f..f3f66fea49436cc83383bc6ebf3e4b814ecd9a23 100644 (file)
@@ -8,6 +8,7 @@ dependencies {
     compile 'javax.servlet:javax.servlet-api'
     compile project(':server:sonar-webserver-auth')
     compile project(':server:sonar-webserver-ws')
+    compile project(':server:sonar-webserver-webapi')
 
     testCompile 'junit:junit'
     testCompile 'org.assertj:assertj-core'
index 4904b9e7cd1d6c4e4c38037bd777edfc5cd6a70a..cc0d3d7f0c71c4002e107f278a5f25222d70dd20 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.pushapi;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -34,6 +35,17 @@ public abstract class ServerPushClient {
   private static final Logger LOG = Loggers.get(ServerPushClient.class);
   private static final int DEFAULT_HEARTBEAT_PERIOD = 60;
 
+
+
+
+  private static final byte[] CRLF = new byte[] {'\r', '\n'};
+  private static final byte[] DATA_END = new byte[] {'\n', '\n'};
+  private static final byte[] DATA_FIELD = "data: ".getBytes(StandardCharsets.UTF_8);
+
+
+
+
+
   protected final AsyncContext asyncContext;
 
   private final ScheduledExecutorService executorService;
@@ -50,6 +62,11 @@ public abstract class ServerPushClient {
     startedHeartbeat = executorService.schedule(heartbeatTask, DEFAULT_HEARTBEAT_PERIOD, TimeUnit.SECONDS);
   }
 
+  public void writeAndFlush(String payload) throws IOException {
+    output().write(payload.getBytes(StandardCharsets.UTF_8));
+    flush();
+  }
+
   public void writeAndFlush(char character) {
     write(character);
     flush();
index b022cbcb25d9207235e0731ec38638e0a843fb65..93512a974ecd36fc6472d10bda0b3963754b759d 100644 (file)
@@ -39,6 +39,14 @@ public class SonarLintClient extends ServerPushClient {
     this.languages = languages;
   }
 
+  public Set<String> getLanguages() {
+    return languages;
+  }
+
+  public Set<String> getClientProjectKeys() {
+    return projectKeys;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
index e93bf5f90732bc42a2483ac30585f565c7de9aec..4b72f6de66d3e9846bf42677d7f8db7cdb09f922 100644 (file)
  */
 package org.sonar.server.pushapi.sonarlint;
 
+import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Predicate;
 import javax.servlet.AsyncEvent;
 import javax.servlet.AsyncListener;
+import org.json.JSONArray;
+import org.json.JSONObject;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.util.ParamChange;
+import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.RuleChange;
+import org.sonar.core.util.RuleSetChangeEvent;
+import org.sonar.server.qualityprofile.RuleActivatorEventsDistributor;
+
+import static java.util.Arrays.asList;
 
 @ServerSide
-public class SonarLintClientsRegistry {
+public class SonarLintClientsRegistry implements RuleActivationListener {
 
   private static final Logger LOG = Loggers.get(SonarLintClientsRegistry.class);
 
+  private final RuleActivatorEventsDistributor ruleActivatorEventsDistributor;
+
+  public SonarLintClientsRegistry(RuleActivatorEventsDistributor ruleActivatorEventsDistributor) {
+    this.ruleActivatorEventsDistributor = ruleActivatorEventsDistributor;
+  }
+
+
   private final List<SonarLintClient> clients = new CopyOnWriteArrayList<>();
 
   public void registerClient(SonarLintClient sonarLintClient) {
     clients.add(sonarLintClient);
     sonarLintClient.scheduleHeartbeat();
     sonarLintClient.addListener(new SonarLintClientEventsListener(sonarLintClient));
+    ruleActivatorEventsDistributor.subscribe(this);
+
     LOG.debug("Registering new SonarLint client");
   }
 
@@ -50,6 +71,71 @@ public class SonarLintClientsRegistry {
     return clients.size();
   }
 
+  @Override
+  public void listen(RuleSetChangeEvent ruleChangeEvent) {
+    LOG.info("Generating a RuleSetChangeEvent");
+    // TODO filter on languages here as well
+    broadcastMessage(ruleChangeEvent, f -> f.getClientProjectKeys().isEmpty() || !Collections.disjoint(f.getClientProjectKeys(), asList(ruleChangeEvent.getProjects())));
+  }
+
+
+  public void broadcastMessage(RuleSetChangeEvent message, Predicate<SonarLintClient> filter) {
+    String jsonString = getJSONString(message);
+    clients.stream().filter(filter).forEach(c -> {
+      try {
+        c.writeAndFlush(jsonString);
+      } catch (IOException e) {
+        LOG.error("Unable to send message to a client: " + e.getMessage());
+      }
+    });
+  }
+
+
+  public String getJSONString(RuleSetChangeEvent ruleSetChangeEvent) {
+    JSONObject result = new JSONObject();
+    result.put("event", ruleSetChangeEvent.getEvent());
+
+    JSONObject data = new JSONObject();
+    data.put("projects", ruleSetChangeEvent.getProjects());
+
+    JSONArray activatedRulesJson = new JSONArray();
+    for (RuleChange rule : ruleSetChangeEvent.getActivatedRules()) {
+      activatedRulesJson.put(toJson(rule));
+    }
+    data.put("activatedRules", activatedRulesJson);
+
+    JSONArray deactivatedRulesJson = new JSONArray();
+    for (RuleChange rule : ruleSetChangeEvent.getDeactivatedRules()) {
+      deactivatedRulesJson.put(toJson(rule));
+    }
+    data.put("deactivatedRules", deactivatedRulesJson);
+
+    result.put("data", data);
+    return result.toString();
+  }
+
+  private JSONObject toJson(RuleChange rule) {
+    JSONObject ruleJson = new JSONObject();
+    ruleJson.put("key", rule.getKey());
+    ruleJson.put("language", rule.getLanguage());
+    ruleJson.put("severity", rule.getSeverity());
+    ruleJson.put("templateKey", rule.getTemplateKey());
+
+    JSONArray params = new JSONArray();
+    for (ParamChange paramChange : rule.getParams()) {
+      params.put(toJson(paramChange));
+    }
+    ruleJson.put("params", params);
+    return ruleJson;
+  }
+
+  private JSONObject toJson(ParamChange paramChange) {
+    JSONObject param = new JSONObject();
+    param.put("key", paramChange.getKey());
+    param.put("value", paramChange.getValue());
+    return param;
+  }
+
   class SonarLintClientEventsListener implements AsyncListener {
     private final SonarLintClient client;
 
index 5bcce895562bb9c9ed801aa3755c50f81e9a41b8..2c27c48cda12d8cc2cdbed8c6d75eafdd44617f4 100644 (file)
@@ -84,7 +84,7 @@ public class SonarLintPushAction extends ServerPushAction {
 
     if (!isServerSideEventsRequest(servletRequest)) {
       servletResponse.stream().setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
-      return;
+      return; // TODO fixme this is not closing the connexion properly
     }
 
     setHeadersForResponse(servletResponse);
@@ -92,7 +92,7 @@ public class SonarLintPushAction extends ServerPushAction {
     AsyncContext asyncContext = servletRequest.startAsync();
     asyncContext.setTimeout(0);
 
-    var sonarLintClient = new SonarLintClient(asyncContext, params.getProjectKeys(), params.getLanguages());
+    SonarLintClient sonarLintClient = new SonarLintClient(asyncContext, params.getProjectKeys(), params.getLanguages());
 
     clientsRegistry.registerClient(sonarLintClient);
   }
index 450245c13c0c2c9e376bb251a82f2cdee3470107..49076199685c6587e11cac6acc82a10a4085d457 100644 (file)
@@ -23,6 +23,7 @@ import java.util.Set;
 import javax.servlet.AsyncContext;
 import org.junit.Before;
 import org.junit.Test;
+import org.sonar.server.qualityprofile.StandaloneRuleActivatorEventsDistributor;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
@@ -39,7 +40,7 @@ public class SonarLintClientsRegistryTest {
 
   @Before
   public void before() {
-    underTest = new SonarLintClientsRegistry();
+    underTest = new SonarLintClientsRegistry(mock(StandaloneRuleActivatorEventsDistributor.class));
   }
 
   @Test
index e4e0cab8bf1fac5152e8bb4ed7bf892e6b03c62b..7deadcbbfeed370cd7fe51fbb5dae3277d8f8cce 100644 (file)
@@ -41,11 +41,13 @@ public class QProfileResetImpl implements QProfileReset {
   private final DbClient db;
   private final RuleActivator activator;
   private final ActiveRuleIndexer activeRuleIndexer;
+  private final QualityProfileChangeEventService qualityProfileChangeEventService;
 
-  public QProfileResetImpl(DbClient db, RuleActivator activator, ActiveRuleIndexer activeRuleIndexer) {
+  public QProfileResetImpl(DbClient db, RuleActivator activator, ActiveRuleIndexer activeRuleIndexer, QualityProfileChangeEventService qualityProfileChangeEventService) {
     this.db = db;
     this.activator = activator;
     this.activeRuleIndexer = activeRuleIndexer;
+    this.qualityProfileChangeEventService = qualityProfileChangeEventService;
   }
 
   @Override
@@ -87,6 +89,7 @@ public class QProfileResetImpl implements QProfileReset {
         // ignore, probably a rule inherited from parent that can't be deactivated
       }
     }
+    qualityProfileChangeEventService.distributeRuleChangeEvent(List.of(profile), changes, profile.getLanguage());
     activeRuleIndexer.commitAndIndex(dbSession, changes);
     return result;
   }
index dca861c597eff237c89a3597bbe038073475230f..db891a34eccf37be72bfe809d619e8cf480ee3a7 100644 (file)
@@ -44,12 +44,15 @@ public class QProfileRulesImpl implements QProfileRules {
   private final RuleActivator ruleActivator;
   private final RuleIndex ruleIndex;
   private final ActiveRuleIndexer activeRuleIndexer;
+  private final QualityProfileChangeEventService qualityProfileChangeEventService;
 
-  public QProfileRulesImpl(DbClient db, RuleActivator ruleActivator, RuleIndex ruleIndex, ActiveRuleIndexer activeRuleIndexer) {
+  public QProfileRulesImpl(DbClient db, RuleActivator ruleActivator, RuleIndex ruleIndex, ActiveRuleIndexer activeRuleIndexer,
+    QualityProfileChangeEventService qualityProfileChangeEventService) {
     this.db = db;
     this.ruleActivator = ruleActivator;
     this.ruleIndex = ruleIndex;
     this.activeRuleIndexer = activeRuleIndexer;
+    this.qualityProfileChangeEventService = qualityProfileChangeEventService;
   }
 
   @Override
@@ -63,6 +66,7 @@ public class QProfileRulesImpl implements QProfileRules {
     for (RuleActivation activation : activations) {
       changes.addAll(ruleActivator.activate(dbSession, activation, context));
     }
+    qualityProfileChangeEventService.distributeRuleChangeEvent(List.of(profile), changes, profile.getLanguage());
     activeRuleIndexer.commitAndIndex(dbSession, changes);
     return changes;
   }
@@ -70,10 +74,12 @@ public class QProfileRulesImpl implements QProfileRules {
   @Override
   public BulkChangeResult bulkActivateAndCommit(DbSession dbSession, QProfileDto profile, RuleQuery ruleQuery, @Nullable String severity) {
     verifyNotBuiltIn(profile);
-    return doBulk(dbSession, profile, ruleQuery, (context, ruleDefinition) -> {
+    BulkChangeResult bulkChangeResult = doBulk(dbSession, profile, ruleQuery, (context, ruleDefinition) -> {
       RuleActivation activation = RuleActivation.create(ruleDefinition.getUuid(), severity, null);
       return ruleActivator.activate(dbSession, activation, context);
     });
+    qualityProfileChangeEventService.distributeRuleChangeEvent(List.of(profile), bulkChangeResult.getChanges(), profile.getLanguage());
+    return bulkChangeResult;
   }
 
   @Override
@@ -85,6 +91,9 @@ public class QProfileRulesImpl implements QProfileRules {
     for (String ruleUuid : ruleUuids) {
       changes.addAll(ruleActivator.deactivate(dbSession, context, ruleUuid, false));
     }
+
+    qualityProfileChangeEventService.distributeRuleChangeEvent(List.of(profile), changes, profile.getLanguage());
+
     activeRuleIndexer.commitAndIndex(dbSession, changes);
     return changes;
   }
@@ -92,7 +101,12 @@ public class QProfileRulesImpl implements QProfileRules {
   @Override
   public BulkChangeResult bulkDeactivateAndCommit(DbSession dbSession, QProfileDto profile, RuleQuery ruleQuery) {
     verifyNotBuiltIn(profile);
-    return doBulk(dbSession, profile, ruleQuery, (context, ruleDefinition) -> ruleActivator.deactivate(dbSession, context, ruleDefinition.getUuid(), false));
+    BulkChangeResult bulkChangeResult = doBulk(dbSession, profile, ruleQuery, (context, ruleDefinition) ->
+      ruleActivator.deactivate(dbSession, context, ruleDefinition.getUuid(), false));
+
+    qualityProfileChangeEventService.distributeRuleChangeEvent(List.of(profile), bulkChangeResult.getChanges(), profile.getLanguage());
+
+    return bulkChangeResult;
   }
 
   @Override
index b38f695d8f8f6bbde95681506aa43b13068ec414..d315e15b2cd53491e2058ccf15af217ce6666c29 100644 (file)
@@ -41,12 +41,14 @@ public class QProfileTreeImpl implements QProfileTree {
   private final RuleActivator ruleActivator;
   private final System2 system2;
   private final ActiveRuleIndexer activeRuleIndexer;
+  private final QualityProfileChangeEventService qualityProfileChangeEventService;
 
-  public QProfileTreeImpl(DbClient db, RuleActivator ruleActivator, System2 system2, ActiveRuleIndexer activeRuleIndexer) {
+  public QProfileTreeImpl(DbClient db, RuleActivator ruleActivator, System2 system2, ActiveRuleIndexer activeRuleIndexer, QualityProfileChangeEventService qualityProfileChangeEventService) {
     this.db = db;
     this.ruleActivator = ruleActivator;
     this.system2 = system2;
     this.activeRuleIndexer = activeRuleIndexer;
+    this.qualityProfileChangeEventService = qualityProfileChangeEventService;
   }
 
   @Override
@@ -92,6 +94,7 @@ public class QProfileTreeImpl implements QProfileTree {
         // TODO return errors
       }
     }
+    qualityProfileChangeEventService.distributeRuleChangeEvent(List.of(profile), changes, profile.getLanguage());
     return changes;
   }
 
@@ -120,6 +123,8 @@ public class QProfileTreeImpl implements QProfileTree {
         changes.add(new ActiveRuleChange(ActiveRuleChange.Type.UPDATED, activeRule, context.getRule().get()).setInheritance(null));
       }
     }
+
+    qualityProfileChangeEventService.distributeRuleChangeEvent(List.of(profile), changes, profile.getLanguage());
     return changes;
   }
 
index 60646999ff515297640f12eb772e8efd0894587e..202758dc4463a1f5a715b3da7e1a310deeb37610 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.qualityprofile.ws;
 
+import java.util.Optional;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -30,8 +31,11 @@ import org.sonar.db.DbSession;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.user.UserSession;
 
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_ADD_PROJECT;
@@ -44,13 +48,16 @@ public class AddProjectAction implements QProfileWsAction {
   private final Languages languages;
   private final ComponentFinder componentFinder;
   private final QProfileWsSupport wsSupport;
+  private final QualityProfileChangeEventService qualityProfileChangeEventService;
 
-  public AddProjectAction(DbClient dbClient, UserSession userSession, Languages languages, ComponentFinder componentFinder, QProfileWsSupport wsSupport) {
+  public AddProjectAction(DbClient dbClient, UserSession userSession, Languages languages, ComponentFinder componentFinder,
+    QProfileWsSupport wsSupport, QualityProfileChangeEventService qualityProfileChangeEventService) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.languages = languages;
     this.componentFinder = componentFinder;
     this.wsSupport = wsSupport;
+    this.qualityProfileChangeEventService = qualityProfileChangeEventService;
   }
 
   @Override
@@ -80,24 +87,36 @@ public class AddProjectAction implements QProfileWsAction {
     userSession.checkLoggedIn();
 
     try (DbSession dbSession = dbClient.openSession(false)) {
+
       ProjectDto project = loadProject(dbSession, request);
       QProfileDto profile = wsSupport.getProfile(dbSession, QProfileReference.fromName(request));
       checkPermissions(dbSession, profile, project);
-
       QProfileDto currentProfile = dbClient.qualityProfileDao().selectAssociatedToProjectAndLanguage(dbSession, project, profile.getLanguage());
+
+      Optional<QProfileDto> deactivatedProfile = empty();
+
       if (currentProfile == null) {
+        QProfileDto defaultProfile = dbClient.qualityProfileDao().selectDefaultProfile(dbSession, profile.getLanguage());
+        if (defaultProfile != null) {
+          deactivatedProfile = of(defaultProfile);
+        }
+
         // project uses the default profile
         dbClient.qualityProfileDao().insertProjectProfileAssociation(dbSession, project, profile);
         dbSession.commit();
       } else if (!profile.getKee().equals(currentProfile.getKee())) {
+        deactivatedProfile = of(currentProfile);
         dbClient.qualityProfileDao().updateProjectProfileAssociation(dbSession, project, profile.getKee(), currentProfile.getKee());
         dbSession.commit();
       }
+      Optional<QProfileDto> activatedProfile = of(profile);
+      qualityProfileChangeEventService.publishRuleActivationToSonarLintClients(project, activatedProfile, deactivatedProfile);
     }
 
     response.noContent();
   }
 
+
   private ProjectDto loadProject(DbSession dbSession, Request request) {
     String projectKey = request.mandatoryParam(PARAM_PROJECT);
     return componentFinder.getProjectByKey(dbSession, projectKey);
index 4ddf8ced7a4f0b517151e649dd89c5231fadc8e6..81c7987dc5b9f4d252d50162ecb3b26c55370a33 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.qualityprofile.ws;
 
+import java.util.Optional;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -30,8 +31,11 @@ import org.sonar.db.DbSession;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.user.UserSession;
 
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_REMOVE_PROJECT;
@@ -44,13 +48,16 @@ public class RemoveProjectAction implements QProfileWsAction {
   private final Languages languages;
   private final ComponentFinder componentFinder;
   private final QProfileWsSupport wsSupport;
+  private final QualityProfileChangeEventService qualityProfileChangeEventService;
 
-  public RemoveProjectAction(DbClient dbClient, UserSession userSession, Languages languages, ComponentFinder componentFinder, QProfileWsSupport wsSupport) {
+  public RemoveProjectAction(DbClient dbClient, UserSession userSession, Languages languages, ComponentFinder componentFinder,
+    QProfileWsSupport wsSupport, QualityProfileChangeEventService qualityProfileChangeEventService) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.languages = languages;
     this.componentFinder = componentFinder;
     this.wsSupport = wsSupport;
+    this.qualityProfileChangeEventService = qualityProfileChangeEventService;
   }
 
   @Override
@@ -86,6 +93,17 @@ public class RemoveProjectAction implements QProfileWsAction {
       dbClient.qualityProfileDao().deleteProjectProfileAssociation(dbSession, project, profile);
       dbSession.commit();
 
+      Optional<QProfileDto> deactivatedProfile = of(profile);
+      Optional<QProfileDto> activatedProfile = empty();
+
+      // publish change for rules in the default quality profile
+      QProfileDto defaultProfile = dbClient.qualityProfileDao().selectDefaultProfile(dbSession, profile.getLanguage());
+      if (defaultProfile != null) {
+        activatedProfile = of(defaultProfile);
+      }
+
+      qualityProfileChangeEventService.publishRuleActivationToSonarLintClients(project, activatedProfile, deactivatedProfile);
+
       response.noContent();
     }
   }
index 2f410c9e96b879cb4fab7e86e1f8471ea12f16fe..3254844f7be3c5e4ae0201172b40254c926d8871 100644 (file)
@@ -49,6 +49,7 @@ import org.sonar.server.util.TypeValidations;
 import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
 
 public class QProfileComparisonTest {
 
@@ -74,8 +75,9 @@ public class QProfileComparisonTest {
     dbSession = db.openSession(false);
     RuleIndex ruleIndex = new RuleIndex(es.client(), System2.INSTANCE);
     ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(db, es.client());
+    QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
     RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, db, new TypeValidations(singletonList(new IntegerTypeValidation())), userSession);
-    qProfileRules = new QProfileRulesImpl(db, ruleActivator, ruleIndex, activeRuleIndexer);
+    qProfileRules = new QProfileRulesImpl(db, ruleActivator, ruleIndex, activeRuleIndexer, qualityProfileChangeEventService);
     comparison = new QProfileComparison(db);
 
     xooRule1 = RuleTesting.newXooX1().setSeverity("MINOR").getDefinition();
index ef1df8f5be6b5e70a68b7bae7c29d2c8f5c7c0f3..08f2b22255a8fbfa3e887d6ce9b4755fc2f91fc2 100644 (file)
@@ -41,7 +41,12 @@ import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.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.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.internal.verification.VerificationModeFactory.times;
 import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
 
 public class QProfileResetImplTest {
@@ -55,11 +60,12 @@ public class QProfileResetImplTest {
 
   private System2 system2 = new AlwaysIncreasingSystem2();
   private ActiveRuleIndexer activeRuleIndexer = mock(ActiveRuleIndexer.class);
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
   private RuleActivator ruleActivator = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
-  private QProfileTree qProfileTree = new QProfileTreeImpl(db.getDbClient(), ruleActivator, system2, activeRuleIndexer);
-  private QProfileRules qProfileRules = new QProfileRulesImpl(db.getDbClient(), ruleActivator, null, activeRuleIndexer);
-  private QProfileResetImpl underTest = new QProfileResetImpl(db.getDbClient(), ruleActivator, activeRuleIndexer);
+  private QProfileTree qProfileTree = new QProfileTreeImpl(db.getDbClient(), ruleActivator, system2, activeRuleIndexer, mock(QualityProfileChangeEventService.class));
+  private QProfileRules qProfileRules = new QProfileRulesImpl(db.getDbClient(), ruleActivator, null, activeRuleIndexer, qualityProfileChangeEventService);
+  private QProfileResetImpl underTest = new QProfileResetImpl(db.getDbClient(), ruleActivator, activeRuleIndexer, mock(QualityProfileChangeEventService.class));
 
   @Test
   public void reset() {
@@ -77,6 +83,7 @@ public class QProfileResetImplTest {
     assertThat(result.getChanges())
       .extracting(ActiveRuleChange::getKey, ActiveRuleChange::getType)
       .containsExactlyInAnyOrder(tuple(ActiveRuleKey.of(profile, newRule.getKey()), ACTIVATED));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   @Test
@@ -94,6 +101,7 @@ public class QProfileResetImplTest {
     assertThat(db.getDbClient().activeRuleDao().selectByProfile(db.getSession(), childProfile))
       .extracting(OrgActiveRuleDto::getRuleKey)
       .containsExactlyInAnyOrder(newRule.getKey(), existingRule.getKey());
+    verify(qualityProfileChangeEventService, times(2)).distributeRuleChangeEvent(any(), any(), eq(childProfile.getLanguage()));
   }
 
   @Test
@@ -106,6 +114,7 @@ public class QProfileResetImplTest {
     })
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage(String.format("Operation forbidden for built-in Quality Profile '%s'", profile.getKee()));
+    verifyNoInteractions(qualityProfileChangeEventService);
   }
 
   @Test
@@ -118,5 +127,7 @@ public class QProfileResetImplTest {
     })
       .isInstanceOf(NullPointerException.class)
       .hasMessage("Quality profile must be persisted");
+
+    verifyNoInteractions(qualityProfileChangeEventService);
   }
 }
index 21e8a820c8c44925178e15bbbcd8cb999b3acdea..e1479fcb1e4a5a51153f9ac4257d55ca4a2fc521 100644 (file)
@@ -61,6 +61,11 @@ 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.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.sonar.api.rule.Severity.BLOCKER;
 import static org.sonar.api.rule.Severity.CRITICAL;
 import static org.sonar.api.rule.Severity.MAJOR;
@@ -81,9 +86,10 @@ public class QProfileRuleImplTest {
   private ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(db.getDbClient(), es.client());
   private RuleIndexer ruleIndexer = new RuleIndexer(es.client(), db.getDbClient());
   private TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
 
   private RuleActivator ruleActivator = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
-  private QProfileRules underTest = new QProfileRulesImpl(db.getDbClient(), ruleActivator, ruleIndex, activeRuleIndexer);
+  private QProfileRules underTest = new QProfileRulesImpl(db.getDbClient(), ruleActivator, ruleIndex, activeRuleIndexer, qualityProfileChangeEventService);
 
   @Test
   public void system_activates_rule_without_parameters() {
@@ -94,6 +100,7 @@ public class QProfileRuleImplTest {
 
     assertThatRuleIsActivated(profile, rule, changes, BLOCKER, null, emptyMap());
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   @Test
@@ -106,6 +113,7 @@ public class QProfileRuleImplTest {
 
     assertThatRuleIsActivated(profile, rule, changes, BLOCKER, null, emptyMap());
     assertThatProfileIsUpdatedByUser(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   @Test
@@ -119,6 +127,7 @@ public class QProfileRuleImplTest {
 
     assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, of("min", "10"));
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   @Test
@@ -132,6 +141,7 @@ public class QProfileRuleImplTest {
 
     assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, of("min", "15"));
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   @Test
@@ -144,6 +154,7 @@ public class QProfileRuleImplTest {
 
     assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, emptyMap());
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   /**
@@ -160,6 +171,7 @@ public class QProfileRuleImplTest {
 
     assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, of("min", "10"));
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   /**
@@ -179,6 +191,7 @@ public class QProfileRuleImplTest {
     assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null,
       of(paramWithoutDefault.getName(), "-10", paramWithDefault.getName(), paramWithDefault.getDefaultValue()));
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   @Test
@@ -192,6 +205,7 @@ public class QProfileRuleImplTest {
 
     assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, of(param.getName(), param.getDefaultValue()));
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   @Test
@@ -202,14 +216,16 @@ public class QProfileRuleImplTest {
 
     // initial activation
     RuleActivation activation = RuleActivation.create(rule.getUuid(), MAJOR, null);
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
     // update
     RuleActivation updateActivation = RuleActivation.create(rule.getUuid(), CRITICAL, of(param.getName(), "20"));
-    List<ActiveRuleChange> changes = activate(profile, updateActivation);
+    changes = activate(profile, updateActivation);
 
     assertThatRuleIsUpdated(profile, rule, CRITICAL, null, of(param.getName(), "20"));
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -221,14 +237,16 @@ public class QProfileRuleImplTest {
 
     // initial activation -> param "max" has a default value
     RuleActivation activation = RuleActivation.create(rule.getUuid());
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
 
     // update param "min", which has no default value
     RuleActivation updateActivation = RuleActivation.create(rule.getUuid(), MAJOR, of(paramWithoutDefault.getName(), "3"));
-    List<ActiveRuleChange> changes = activate(profile, updateActivation);
+    changes = activate(profile, updateActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
     assertThatRuleIsUpdated(profile, rule, MAJOR, null, of(paramWithDefault.getName(), "10", paramWithoutDefault.getName(), "3"));
     assertThatProfileIsUpdatedBySystem(profile);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -239,14 +257,16 @@ public class QProfileRuleImplTest {
 
     // initial activation -> param "max" has a default value
     RuleActivation activation = RuleActivation.create(rule.getUuid(), null, of(paramWithDefault.getName(), "20"));
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
     // reset to default_value
     RuleActivation updateActivation = RuleActivation.create(rule.getUuid(), null, of(paramWithDefault.getName(), ""));
-    List<ActiveRuleChange> changes = activate(profile, updateActivation);
+    changes = activate(profile, updateActivation);
 
     assertThatRuleIsUpdated(profile, rule, rule.getSeverityString(), null, of(paramWithDefault.getName(), "10"));
     assertThat(changes).hasSize(1);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -258,14 +278,16 @@ public class QProfileRuleImplTest {
 
     // initial activation -> param "max" has a default value
     RuleActivation activation = RuleActivation.create(rule.getUuid(), null, of(paramWithoutDefault.getName(), "20"));
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
     // remove parameter
     RuleActivation updateActivation = RuleActivation.create(rule.getUuid(), null, of(paramWithoutDefault.getName(), ""));
-    List<ActiveRuleChange> changes = activate(profile, updateActivation);
+    changes = activate(profile, updateActivation);
 
     assertThatRuleIsUpdated(profile, rule, rule.getSeverityString(), null, of(paramWithDefault.getName(), paramWithDefault.getDefaultValue()));
     assertThat(changes).hasSize(1);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -279,6 +301,7 @@ public class QProfileRuleImplTest {
     List<ActiveRuleChange> changes = activate(profile, activation);
     db.getDbClient().activeRuleDao().deleteParametersByRuleProfileUuids(db.getSession(), asList(profile.getRulesProfileUuid()));
     assertThatRuleIsActivated(profile, rule, changes, rule.getSeverityString(), null, emptyMap());
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
     // contrary to activerule, the param is supposed to be inserted but not updated
     RuleActivation updateActivation = RuleActivation.create(rule.getUuid(), null, of(param.getName(), ""));
@@ -286,6 +309,7 @@ public class QProfileRuleImplTest {
 
     assertThatRuleIsUpdated(profile, rule, rule.getSeverityString(), null, of(param.getName(), param.getDefaultValue()));
     assertThat(changes).hasSize(1);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -295,13 +319,15 @@ public class QProfileRuleImplTest {
 
     // initial activation
     RuleActivation activation = RuleActivation.create(rule.getUuid());
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
     // update with exactly the same severity and params
     activation = RuleActivation.create(rule.getUuid());
-    List<ActiveRuleChange> changes = activate(profile, activation);
+    changes = activate(profile, activation);
 
     assertThat(changes).isEmpty();
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -312,13 +338,16 @@ public class QProfileRuleImplTest {
 
     // initial activation -> param "max" has a default value
     RuleActivation activation = RuleActivation.create(rule.getUuid(), BLOCKER, of(param.getName(), "20"));
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
+
 
     // update without any severity or params => keep
     RuleActivation update = RuleActivation.create(rule.getUuid());
-    List<ActiveRuleChange> changes = activate(profile, update);
+    changes = activate(profile, update);
 
     assertThat(changes).isEmpty();
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -328,6 +357,7 @@ public class QProfileRuleImplTest {
     RuleActivation activation = RuleActivation.create(rule.getUuid());
 
     expectFailure("java rule " + rule.getKey() + " cannot be activated on js profile " + profile.getKee(), () -> activate(profile, activation));
+    verifyNoInteractions(qualityProfileChangeEventService);
   }
 
   @Test
@@ -337,6 +367,7 @@ public class QProfileRuleImplTest {
     RuleActivation activation = RuleActivation.create(rule.getUuid());
 
     expectFailure("Rule was removed: " + rule.getKey(), () -> activate(profile, activation));
+    verifyNoInteractions(qualityProfileChangeEventService);
   }
 
   @Test
@@ -346,6 +377,7 @@ public class QProfileRuleImplTest {
     RuleActivation activation = RuleActivation.create(rule.getUuid());
 
     expectFailure("Rule template can't be activated on a Quality profile: " + rule.getKey(), () -> activate(profile, activation));
+    verifyNoInteractions(qualityProfileChangeEventService);
   }
 
   @Test
@@ -356,6 +388,7 @@ public class QProfileRuleImplTest {
 
     RuleActivation activation = RuleActivation.create(rule.getUuid(), null, of(param.getName(), "foo"));
     expectFailure("Value 'foo' must be an integer.", () -> activate(profile, activation));
+    verifyNoInteractions(qualityProfileChangeEventService);
   }
 
   @Test
@@ -368,13 +401,15 @@ public class QProfileRuleImplTest {
 
     // initial activation
     RuleActivation activation = RuleActivation.create(customRule.getUuid(), MAJOR, emptyMap());
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
     assertThatRuleIsActivated(profile, customRule, null, MAJOR, null, of("format", "txt"));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
     // update -> parameter is not changed
     RuleActivation updateActivation = RuleActivation.create(customRule.getUuid(), BLOCKER, of("format", "xml"));
-    activate(profile, updateActivation);
+    changes = activate(profile, updateActivation);
     assertThatRuleIsActivated(profile, customRule, null, BLOCKER, null, of("format", "txt"));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -383,13 +418,15 @@ public class QProfileRuleImplTest {
     RuleDefinitionDto rule = createRule();
     QProfileDto profile = createProfile(rule);
     RuleActivation activation = RuleActivation.create(rule.getUuid());
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
-    List<ActiveRuleChange> changes = deactivate(profile, rule);
+    changes = deactivate(profile, rule);
     verifyNoActiveRules();
     assertThatProfileIsUpdatedByUser(profile);
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).getType()).isEqualTo(ActiveRuleChange.Type.DEACTIVATED);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -397,12 +434,14 @@ public class QProfileRuleImplTest {
     RuleDefinitionDto rule = createRule();
     QProfileDto profile = createProfile(rule);
     RuleActivation activation = RuleActivation.create(rule.getUuid());
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
-    List<ActiveRuleChange> changes = deactivate(profile, rule);
+    changes = deactivate(profile, rule);
     verifyNoActiveRules();
     assertThatProfileIsUpdatedBySystem(profile);
     assertThatChangeIsDeactivation(changes, rule);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   private void assertThatChangeIsDeactivation(List<ActiveRuleChange> changes, RuleDefinitionDto rule) {
@@ -420,6 +459,7 @@ public class QProfileRuleImplTest {
     List<ActiveRuleChange> changes = deactivate(profile, rule);
     verifyNoActiveRules();
     assertThat(changes).isEmpty();
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(profile.getLanguage()));
   }
 
   @Test
@@ -427,14 +467,16 @@ public class QProfileRuleImplTest {
     RuleDefinitionDto rule = createRule();
     QProfileDto profile = createProfile(rule);
     RuleActivation activation = RuleActivation.create(rule.getUuid());
-    activate(profile, activation);
+    List<ActiveRuleChange> changes = activate(profile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
 
     rule.setStatus(RuleStatus.REMOVED);
     db.getDbClient().ruleDao().update(db.getSession(), rule);
 
-    List<ActiveRuleChange> changes = deactivate(profile, rule);
+    changes = deactivate(profile, rule);
     verifyNoActiveRules();
     assertThatChangeIsDeactivation(changes, rule);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(profile.getLanguage()));
   }
 
   @Test
@@ -448,6 +490,7 @@ public class QProfileRuleImplTest {
     assertThatProfileHasNoActiveRules(parentProfile);
     assertThatRuleIsActivated(childProfile, rule, changes, rule.getSeverityString(), null, emptyMap());
     assertThatRuleIsActivated(grandChildProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(childProfile.getLanguage()));
   }
 
   @Test
@@ -460,17 +503,20 @@ public class QProfileRuleImplTest {
 
     System.out.println("ACTIVATE ON " + childProfile.getName());
     RuleActivation initialActivation = RuleActivation.create(rule.getUuid(), MAJOR, of(param.getName(), "foo"));
-    activate(childProfile, initialActivation);
+    List<ActiveRuleChange> changes = activate(childProfile, initialActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
 
     System.out.println("---------------");
     System.out.println("ACTIVATE ON " + childProfile.getName());
     RuleActivation updateActivation = RuleActivation.create(rule.getUuid(), CRITICAL, of(param.getName(), "bar"));
-    List<ActiveRuleChange> changes = activate(childProfile, updateActivation);
+    changes = activate(childProfile, updateActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
 
     assertThatProfileHasNoActiveRules(parentProfile);
     assertThatRuleIsUpdated(childProfile, rule, CRITICAL, null, of(param.getName(), "bar"));
     assertThatRuleIsUpdated(grandChildProfile, rule, CRITICAL, INHERITED, of(param.getName(), "bar"));
     assertThat(changes).hasSize(2);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
   }
 
   @Test
@@ -482,15 +528,17 @@ public class QProfileRuleImplTest {
     QProfileDto grandChildProfile = createChildProfile(childProfile);
 
     RuleActivation initialActivation = RuleActivation.create(rule.getUuid(), MAJOR, of(param.getName(), "foo"));
-    activate(childProfile, initialActivation);
+    List<ActiveRuleChange> changes = activate(childProfile, initialActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
 
     RuleActivation overrideActivation = RuleActivation.create(rule.getUuid(), CRITICAL, of(param.getName(), "bar"));
-    List<ActiveRuleChange> changes = activate(grandChildProfile, overrideActivation);
+    changes = activate(grandChildProfile, overrideActivation);
 
     assertThatProfileHasNoActiveRules(parentProfile);
     assertThatRuleIsUpdated(childProfile, rule, MAJOR, null, of(param.getName(), "foo"));
     assertThatRuleIsUpdated(grandChildProfile, rule, CRITICAL, ActiveRuleInheritance.OVERRIDES, of(param.getName(), "bar"));
     assertThat(changes).hasSize(1);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
   }
 
   @Test
@@ -502,19 +550,22 @@ public class QProfileRuleImplTest {
     QProfileDto grandChildProfile = createChildProfile(childProfile);
 
     RuleActivation initialActivation = RuleActivation.create(rule.getUuid(), MAJOR, of(param.getName(), "foo"));
-    activate(childProfile, initialActivation);
+    List<ActiveRuleChange> changes = activate(childProfile, initialActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
 
     RuleActivation overrideActivation = RuleActivation.create(rule.getUuid(), CRITICAL, of(param.getName(), "bar"));
-    activate(grandChildProfile, overrideActivation);
+    changes = activate(grandChildProfile, overrideActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(grandChildProfile.getLanguage()));
 
     // update child --> do not touch grandChild
     RuleActivation updateActivation = RuleActivation.create(rule.getUuid(), BLOCKER, of(param.getName(), "baz"));
-    List<ActiveRuleChange> changes = activate(childProfile, updateActivation);
+    changes = activate(childProfile, updateActivation);
 
     assertThatProfileHasNoActiveRules(parentProfile);
     assertThatRuleIsUpdated(childProfile, rule, BLOCKER, null, of(param.getName(), "baz"));
     assertThatRuleIsUpdated(grandChildProfile, rule, CRITICAL, ActiveRuleInheritance.OVERRIDES, of(param.getName(), "bar"));
     assertThat(changes).hasSize(1);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
   }
 
   @Test
@@ -526,19 +577,22 @@ public class QProfileRuleImplTest {
     QProfileDto grandChildProfile = createChildProfile(childProfile);
 
     RuleActivation initialActivation = RuleActivation.create(rule.getUuid(), MAJOR, of(param.getName(), "foo"));
-    activate(parentProfile, initialActivation);
+    List<ActiveRuleChange> changes = activate(parentProfile, initialActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(parentProfile.getLanguage()));
 
     RuleActivation overrideActivation = RuleActivation.create(rule.getUuid(), CRITICAL, of(param.getName(), "bar"));
-    activate(grandChildProfile, overrideActivation);
+    changes = activate(grandChildProfile, overrideActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(grandChildProfile.getLanguage()));
 
     // reset parent --> touch child but not grandChild
     RuleActivation updateActivation = RuleActivation.createReset(rule.getUuid());
-    List<ActiveRuleChange> changes = activate(parentProfile, updateActivation);
+    changes = activate(parentProfile, updateActivation);
 
     assertThatRuleIsUpdated(parentProfile, rule, rule.getSeverityString(), null, of(param.getName(), param.getDefaultValue()));
     assertThatRuleIsUpdated(childProfile, rule, rule.getSeverityString(), INHERITED, of(param.getName(), param.getDefaultValue()));
     assertThatRuleIsUpdated(grandChildProfile, rule, CRITICAL, ActiveRuleInheritance.OVERRIDES, of(param.getName(), "bar"));
     assertThat(changes).hasSize(2);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(parentProfile.getLanguage()));
   }
 
   @Test
@@ -549,14 +603,16 @@ public class QProfileRuleImplTest {
     QProfileDto childProfile = createChildProfile(parentProfile);
 
     RuleActivation childActivation = RuleActivation.create(rule.getUuid(), MAJOR, of(param.getName(), "foo"));
-    activate(childProfile, childActivation);
+    List<ActiveRuleChange> changes = activate(childProfile, childActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
 
     RuleActivation parentActivation = RuleActivation.create(rule.getUuid(), CRITICAL, of(param.getName(), "bar"));
-    List<ActiveRuleChange> changes = activate(parentProfile, parentActivation);
+    changes = activate(parentProfile, parentActivation);
 
     assertThatRuleIsUpdated(parentProfile, rule, CRITICAL, null, of(param.getName(), "bar"));
     assertThatRuleIsUpdated(childProfile, rule, MAJOR, ActiveRuleInheritance.OVERRIDES, of(param.getName(), "foo"));
     assertThat(changes).hasSize(2);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(parentProfile.getLanguage()));
   }
 
   @Test
@@ -567,13 +623,15 @@ public class QProfileRuleImplTest {
     QProfileDto childProfile = createChildProfile(parentProfile);
 
     RuleActivation parentActivation = RuleActivation.create(rule.getUuid(), MAJOR, of(param.getName(), "foo"));
-    activate(parentProfile, parentActivation);
+    List<ActiveRuleChange> changes = activate(parentProfile, parentActivation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(parentProfile.getLanguage()));
 
     RuleActivation overrideActivation = RuleActivation.create(rule.getUuid(), MAJOR, of(param.getName(), "foo"));
-    List<ActiveRuleChange> changes = activate(childProfile, overrideActivation);
+    changes = activate(childProfile, overrideActivation);
 
     assertThatRuleIsUpdated(childProfile, rule, MAJOR, INHERITED, of(param.getName(), "foo"));
     assertThat(changes).isEmpty();
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
   }
 
   @Test
@@ -586,11 +644,13 @@ public class QProfileRuleImplTest {
     List<ActiveRuleChange> changes = activate(parentProfile, activation);
     assertThatRuleIsActivated(parentProfile, rule, changes, rule.getSeverityString(), null, emptyMap());
     assertThatRuleIsActivated(childProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(parentProfile.getLanguage()));
 
     changes = deactivate(parentProfile, rule);
     assertThatProfileHasNoActiveRules(parentProfile);
     assertThatProfileHasNoActiveRules(childProfile);
     assertThat(changes).hasSize(2);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(parentProfile.getLanguage()));
   }
 
   @Test
@@ -603,14 +663,17 @@ public class QProfileRuleImplTest {
     List<ActiveRuleChange> changes = activate(parentProfile, activation);
     assertThatRuleIsActivated(parentProfile, rule, changes, rule.getSeverityString(), null, emptyMap());
     assertThatRuleIsActivated(childProfile, rule, changes, rule.getSeverityString(), INHERITED, emptyMap());
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(parentProfile.getLanguage()));
 
     activation = RuleActivation.create(rule.getUuid(), CRITICAL, null);
-    activate(childProfile, activation);
+    changes = activate(childProfile, activation);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(childProfile.getLanguage()));
 
     changes = deactivate(parentProfile, rule);
     assertThatProfileHasNoActiveRules(parentProfile);
     assertThatProfileHasNoActiveRules(childProfile);
     assertThat(changes).hasSize(2);
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), eq(changes), eq(parentProfile.getLanguage()));
   }
 
   @Test
@@ -627,6 +690,7 @@ public class QProfileRuleImplTest {
     assertThatThrownBy(() -> deactivate(childProfile, rule))
       .isInstanceOf(BadRequestException.class)
       .hasMessageContaining("Cannot deactivate inherited rule");
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(parentProfile.getLanguage()));
   }
 
   @Test
@@ -651,6 +715,7 @@ public class QProfileRuleImplTest {
     assertThatRuleIsUpdated(childProfile, rule, CRITICAL, INHERITED, emptyMap());
     assertThatRuleIsUpdated(parentProfile, rule, CRITICAL, null, emptyMap());
     assertThat(changes).hasSize(1);
+
   }
 
   @Test
index 7d08990ec719f490fe456dab2a126397c73bbc03..1384f1d6705bf1ff5aec9575ebdd9d0afbafb02c 100644 (file)
@@ -43,6 +43,10 @@ import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 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.verify;
 
 public class QProfileRulesImplTest {
 
@@ -55,9 +59,11 @@ public class QProfileRulesImplTest {
 
   private RuleIndex ruleIndex = new RuleIndex(es.client(), System2.INSTANCE);
   private ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(db.getDbClient(), es.client());
-  private RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, db.getDbClient(), new TypeValidations(singletonList(new IntegerTypeValidation())), userSession);
+  private RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, db.getDbClient(), new TypeValidations(singletonList(new IntegerTypeValidation())),
+    userSession);
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
 
-  private QProfileRules qProfileRules = new QProfileRulesImpl(db.getDbClient(), ruleActivator, ruleIndex, activeRuleIndexer);
+  private QProfileRules qProfileRules = new QProfileRulesImpl(db.getDbClient(), ruleActivator, ruleIndex, activeRuleIndexer, qualityProfileChangeEventService);
 
   @Test
   public void activate_one_rule() {
@@ -70,6 +76,7 @@ public class QProfileRulesImplTest {
     assertThat(db.getDbClient().activeRuleDao().selectByProfile(db.getSession(), qProfile))
       .extracting(ActiveRuleDto::getRuleKey, ActiveRuleDto::getSeverityString)
       .containsExactlyInAnyOrder(tuple(rule.getKey(), Severity.CRITICAL));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(qProfile.getLanguage()));
   }
 
   @Test
@@ -85,5 +92,6 @@ public class QProfileRulesImplTest {
     assertThat(db.getDbClient().qProfileChangeDao().selectByQuery(db.getSession(), new QProfileChangeQuery(qProfile.getKee())))
       .extracting(QProfileChangeDto::getUserUuid, QProfileChangeDto::getDataAsMap)
       .containsExactlyInAnyOrder(tuple(user.getUuid(), ImmutableMap.of("ruleUuid", rule.getUuid(), "severity", Severity.CRITICAL)));
+    verify(qualityProfileChangeEventService).distributeRuleChangeEvent(any(), any(), eq(qProfile.getLanguage()));
   }
 }
index 513c1def2e48baf97f9b431b27886b32da97fa7e..ad5d09abcef487735aa2bff62bd945ef210e7538 100644 (file)
@@ -47,6 +47,12 @@ import static java.util.Collections.emptyMap;
 import static java.util.Collections.singleton;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.sonar.api.rule.Severity.BLOCKER;
 import static org.sonar.server.qualityprofile.ActiveRuleInheritance.INHERITED;
 
@@ -61,9 +67,10 @@ public class QProfileTreeImplTest {
   public UserSessionRule userSession = UserSessionRule.standalone();
   private ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(db.getDbClient(), es.client());
   private TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private RuleActivator ruleActivator = new RuleActivator(system2, db.getDbClient(), typeValidations, userSession);
-  private QProfileRules qProfileRules = new QProfileRulesImpl(db.getDbClient(), ruleActivator, null, activeRuleIndexer);
-  private QProfileTree underTest = new QProfileTreeImpl(db.getDbClient(), ruleActivator, System2.INSTANCE, activeRuleIndexer);
+  private QProfileRules qProfileRules = new QProfileRulesImpl(db.getDbClient(), ruleActivator, null, activeRuleIndexer, qualityProfileChangeEventService);
+  private QProfileTree underTest = new QProfileTreeImpl(db.getDbClient(), ruleActivator, System2.INSTANCE, activeRuleIndexer, mock(QualityProfileChangeEventService.class));
 
   @Test
   public void set_itself_as_parent_fails() {
@@ -133,11 +140,13 @@ public class QProfileTreeImplTest {
     assertThat(changes).hasSize(1);
     assertThatRuleIsActivated(profile2, rule1, changes, rule1.getSeverityString(), INHERITED, emptyMap());
     assertThatRuleIsActivated(profile2, rule2, null, rule2.getSeverityString(), null, emptyMap());
+    verify(qualityProfileChangeEventService, times(2)).distributeRuleChangeEvent(any(), any(), eq(profile2.getLanguage()));
 
     changes = underTest.removeParentAndCommit(db.getSession(), profile2);
     assertThat(changes).hasSize(1);
     assertThatRuleIsActivated(profile2, rule2, null, rule2.getSeverityString(), null, emptyMap());
     assertThatRuleIsNotPresent(profile2, rule1);
+    verify(qualityProfileChangeEventService, times(2)).distributeRuleChangeEvent(any(), any(), eq(profile2.getLanguage()));
   }
 
   @Test
@@ -156,6 +165,7 @@ public class QProfileTreeImplTest {
     assertThat(changes).hasSize(1);
     assertThatRuleIsActivated(profile2, rule1, changes, rule1.getSeverityString(), INHERITED, emptyMap());
     assertThatRuleIsActivated(profile2, rule2, null, rule2.getSeverityString(), null, emptyMap());
+    verify(qualityProfileChangeEventService, times(2)).distributeRuleChangeEvent(any(), any(), eq(profile2.getLanguage()));
 
     RuleActivation activation = RuleActivation.create(rule1.getUuid(), BLOCKER, null);
     changes = activate(profile2, activation);
@@ -168,6 +178,7 @@ public class QProfileTreeImplTest {
     // Not testing changes here since severity is not set in changelog
     assertThatRuleIsActivated(profile2, rule1, null, BLOCKER, null, emptyMap());
     assertThatRuleIsActivated(profile2, rule2, null, rule2.getSeverityString(), null, emptyMap());
+    verify(qualityProfileChangeEventService, times(3)).distributeRuleChangeEvent(anyList(), any(), eq(profile2.getLanguage()));
   }
 
   @Test
@@ -183,6 +194,7 @@ public class QProfileTreeImplTest {
 
     QProfileDto childProfile = createProfile(rule1);
     List<ActiveRuleChange> changes = underTest.setParentAndCommit(db.getSession(), childProfile, parentProfile);
+    verify(qualityProfileChangeEventService, times(2)).distributeRuleChangeEvent(any(), any(), eq(childProfile.getLanguage()));
 
     assertThatRuleIsNotPresent(childProfile, rule1);
     assertThatRuleIsActivated(childProfile, rule2, changes, rule2.getSeverityString(), INHERITED, emptyMap());
index cf3d32c7810664650d0b6db42aae862573d41157..5a49e3e5b79764f8171abb43e01663b84f8c2b0d 100644 (file)
@@ -90,12 +90,13 @@ public class RegisterQualityProfilesNotificationTest {
   private DbClient dbClient = db.getDbClient();
   private TypeValidations typeValidations = mock(TypeValidations.class);
   private ActiveRuleIndexer activeRuleIndexer = mock(ActiveRuleIndexer.class);
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private ServerRuleFinder ruleFinder = new DefaultRuleFinder(dbClient);
   private BuiltInQProfileInsert builtInQProfileInsert = new BuiltInQProfileInsertImpl(dbClient, ruleFinder, system2, UuidFactoryFast.getInstance(),
     typeValidations, activeRuleIndexer);
   private RuleActivator ruleActivator = new RuleActivator(system2, dbClient, typeValidations, userSessionRule);
-  private QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, mock(RuleIndex.class), activeRuleIndexer);
-  private BuiltInQProfileUpdate builtInQProfileUpdate = new BuiltInQProfileUpdateImpl(dbClient, ruleActivator, activeRuleIndexer);
+  private QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, mock(RuleIndex.class), activeRuleIndexer, qualityProfileChangeEventService);
+  private BuiltInQProfileUpdate builtInQProfileUpdate = new BuiltInQProfileUpdateImpl(dbClient, ruleActivator, activeRuleIndexer, qualityProfileChangeEventService);
   private BuiltInQualityProfilesUpdateListener builtInQualityProfilesNotification = mock(BuiltInQualityProfilesUpdateListener.class);
   private RegisterQualityProfiles underTest = new RegisterQualityProfiles(builtInQProfileRepositoryRule, dbClient,
     builtInQProfileInsert, builtInQProfileUpdate, builtInQualityProfilesNotification, system2);
index 010ee2997d0708428166ff45efd486e37f2c1224..ffed401c241ddb3fdf934c58e5ace522c77517e8 100644 (file)
@@ -36,14 +36,19 @@ import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.language.LanguageTesting;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
 
 import static java.lang.String.format;
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
 
 public class AddProjectActionTest {
@@ -59,7 +64,8 @@ public class AddProjectActionTest {
   private final DbClient dbClient = db.getDbClient();
   private final Languages languages = LanguageTesting.newLanguages(LANGUAGE_1, LANGUAGE_2);
   private final QProfileWsSupport wsSupport = new QProfileWsSupport(dbClient, userSession);
-  private final AddProjectAction underTest = new AddProjectAction(dbClient, userSession, languages, TestComponentFinder.from(db), wsSupport);
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
+  private final AddProjectAction underTest = new AddProjectAction(dbClient, userSession, languages, TestComponentFinder.from(db), wsSupport, qualityProfileChangeEventService);
   private final WsActionTester tester = new WsActionTester(underTest);
 
   @Test
@@ -88,6 +94,7 @@ public class AddProjectActionTest {
     assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT);
 
     assertProjectIsAssociatedToProfile(project, profile);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(profile), empty());
   }
 
   @Test
@@ -101,6 +108,7 @@ public class AddProjectActionTest {
     call(project, qualityProfile);
 
     assertProjectIsAssociatedToProfile(project, qualityProfile);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(qualityProfile), empty());
   }
 
   @Test
@@ -117,6 +125,7 @@ public class AddProjectActionTest {
 
     assertProjectIsNotAssociatedToProfile(project, profile1);
     assertProjectIsAssociatedToProfile(project, profile2);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(profile2), of(profile1));
   }
 
   @Test
@@ -132,6 +141,7 @@ public class AddProjectActionTest {
 
     assertProjectIsAssociatedToProfile(project, profile3Language1);
     assertProjectIsAssociatedToProfile(project, profile2Language2);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(profile3Language1), of(profile1Language1));
   }
 
   @Test
@@ -143,6 +153,7 @@ public class AddProjectActionTest {
     call(project, profile);
 
     assertProjectIsAssociatedToProfile(project, profile);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, of(profile), empty());
   }
 
   @Test
index 7caf79e84ea07691e7dda84c5bfeea468637a585..f2c2046719dcb254a851c41f6bd3f5bc6eeaaab4 100644 (file)
@@ -49,6 +49,7 @@ import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.language.LanguageTesting;
 import org.sonar.server.qualityprofile.QProfileTreeImpl;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
@@ -64,6 +65,7 @@ import static java.util.Arrays.asList;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
 import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_LANGUAGE;
 import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_PARENT_QUALITY_PROFILE;
@@ -98,7 +100,7 @@ public class ChangeParentActionTest {
     activeRuleIndexer = new ActiveRuleIndexer(dbClient, esClient);
     TypeValidations typeValidations = new TypeValidations(Collections.emptyList());
     RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, dbClient, typeValidations, userSession);
-    qProfileTree = new QProfileTreeImpl(dbClient, ruleActivator, System2.INSTANCE, activeRuleIndexer);
+    qProfileTree = new QProfileTreeImpl(dbClient, ruleActivator, System2.INSTANCE, activeRuleIndexer, mock(QualityProfileChangeEventService.class));
     ChangeParentAction underTest = new ChangeParentAction(
       dbClient,
       qProfileTree,
index d71fe8bc7064a23b372c4a4ac37126947b1872a8..51800d9ed99e58c4d7d1721468e66cb8e17cbdd2 100644 (file)
@@ -46,6 +46,7 @@ import org.sonar.server.qualityprofile.QProfileExporters;
 import org.sonar.server.qualityprofile.QProfileFactoryImpl;
 import org.sonar.server.qualityprofile.QProfileRules;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
@@ -62,6 +63,7 @@ import org.sonarqube.ws.Qualityprofiles.CreateWsResponse.QualityProfile;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
 import static org.sonar.db.permission.GlobalPermission.SCAN;
 import static org.sonar.server.language.LanguageTesting.newLanguages;
@@ -87,8 +89,9 @@ public class CreateActionTest {
   private RuleIndexer ruleIndexer = new RuleIndexer(es.client(), dbClient);
   private ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(dbClient, es.client());
   private ProfileImporter[] profileImporters = createImporters();
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, dbClient, null, userSession);
-  private QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, ruleIndex, activeRuleIndexer);
+  private QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, ruleIndex, activeRuleIndexer, qualityProfileChangeEventService);
   private QProfileExporters qProfileExporters = new QProfileExporters(dbClient, null, qProfileRules, profileImporters);
 
   private CreateAction underTest = new CreateAction(dbClient, new QProfileFactoryImpl(dbClient, UuidFactoryFast.getInstance(), System2.INSTANCE, activeRuleIndexer),
index ff77547505b183599962107d22de465de7312fdf..8204ffa75637ec9497f9ec034c4dd78cb1868e70 100644 (file)
@@ -45,6 +45,7 @@ import org.sonar.server.qualityprofile.QProfileRules;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
 import org.sonar.server.qualityprofile.QProfileTree;
 import org.sonar.server.qualityprofile.QProfileTreeImpl;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.RuleActivation;
 import org.sonar.server.qualityprofile.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
@@ -59,6 +60,7 @@ import static java.util.Arrays.asList;
 import static java.util.Collections.singleton;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
 import static org.sonar.test.JsonAssert.assertJson;
 import static org.sonarqube.ws.MediaTypes.PROTOBUF;
 import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_LANGUAGE;
@@ -80,9 +82,10 @@ public class InheritanceActionTest {
   private ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(dbClient, esClient);
 
   private RuleIndex ruleIndex = new RuleIndex(esClient, System2.INSTANCE);
+  private QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, dbClient, new TypeValidations(new ArrayList<>()), userSession);
-  private QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, ruleIndex, activeRuleIndexer);
-  private QProfileTree qProfileTree = new QProfileTreeImpl(dbClient, ruleActivator, System2.INSTANCE, activeRuleIndexer);
+  private QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, ruleIndex, activeRuleIndexer, qualityProfileChangeEventService);
+  private QProfileTree qProfileTree = new QProfileTreeImpl(dbClient, ruleActivator, System2.INSTANCE, activeRuleIndexer, mock(QualityProfileChangeEventService.class));
 
   private WsActionTester ws = new WsActionTester(new InheritanceAction(
     dbClient,
index 779ec6cbc8abe07a2ee1a7659de5f45c40479a18..fc1a2256dc7edeb4af1245f31eeb202b692da11e 100644 (file)
@@ -43,6 +43,7 @@ import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.qualityprofile.QProfileRules;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.rule.index.RuleIndex;
@@ -56,6 +57,7 @@ import org.sonar.server.ws.WsActionTester;
 import static java.util.Collections.emptyList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
 import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_LANGUAGES;
 import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_QPROFILE;
 import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_KEY;
@@ -81,8 +83,9 @@ public class QProfilesWsMediumTest {
   private final RuleIndexer ruleIndexer = new RuleIndexer(es.client(), dbClient);
   private final ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(dbClient, es.client());
   private final TypeValidations typeValidations = new TypeValidations(emptyList());
+  private final QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private final RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, dbClient, typeValidations, userSessionRule);
-  private final QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, ruleIndex, activeRuleIndexer);
+  private final QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, ruleIndex, activeRuleIndexer, qualityProfileChangeEventService);
   private final QProfileWsSupport qProfileWsSupport = new QProfileWsSupport(dbClient, userSessionRule);
   private final RuleQueryFactory ruleQueryFactory = new RuleQueryFactory(dbClient);
 
index 2ace97fb69e38ee257ab17c105d764f6cedde427..48d36e29b858e7cdc370894180e5edefa67f6c06 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.qualityprofile.ws;
 import java.net.HttpURLConnection;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.Mockito;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.resources.Qualifiers;
 import org.sonar.api.server.ws.WebService;
@@ -38,14 +39,18 @@ import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.language.LanguageTesting;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
 
 import static java.lang.String.format;
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.verify;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
 
 public class RemoveProjectActionTest {
@@ -61,8 +66,9 @@ public class RemoveProjectActionTest {
   private final Languages languages = LanguageTesting.newLanguages(LANGUAGE_1, LANGUAGE_2);
   private final QProfileWsSupport wsSupport = new QProfileWsSupport(dbClient, userSession);
 
+  private final QualityProfileChangeEventService qualityProfileChangeEventService = Mockito.mock(QualityProfileChangeEventService.class);
   private final RemoveProjectAction underTest = new RemoveProjectAction(dbClient, userSession, languages,
-    new ComponentFinder(dbClient, new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT)), wsSupport);
+    new ComponentFinder(dbClient, new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT)), wsSupport, qualityProfileChangeEventService);
   private final WsActionTester ws = new WsActionTester(underTest);
 
   @Test
@@ -97,6 +103,7 @@ public class RemoveProjectActionTest {
 
     assertProjectIsNotAssociatedToProfile(project, profileLang1);
     assertProjectIsAssociatedToProfile(project, profileLang2);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, empty(), of(profileLang1));
   }
 
   @Test
@@ -110,6 +117,7 @@ public class RemoveProjectActionTest {
     assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT);
 
     assertProjectIsNotAssociatedToProfile(project, profile);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, empty(), of(profile));
   }
 
   @Test
@@ -122,6 +130,7 @@ public class RemoveProjectActionTest {
     call(project, profile);
 
     assertProjectIsNotAssociatedToProfile(project, profile);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, empty(), of(profile));
   }
 
   @Test
@@ -136,6 +145,7 @@ public class RemoveProjectActionTest {
     call(project, profile);
 
     assertProjectIsNotAssociatedToProfile(project, profile);
+    verify(qualityProfileChangeEventService).publishRuleActivationToSonarLintClients(project, empty(), of(profile));
   }
 
   @Test
index 61c46293d1762557c27cefd28fc2c25016eea91e..d0026ee115f46adcdef8714b33ab685d4ac7419d 100644 (file)
@@ -48,6 +48,7 @@ import org.sonar.server.language.LanguageTesting;
 import org.sonar.server.qualityprofile.ActiveRuleChange;
 import org.sonar.server.qualityprofile.QProfileRules;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventService;
 import org.sonar.server.qualityprofile.RuleActivation;
 import org.sonar.server.qualityprofile.RuleActivator;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
@@ -105,12 +106,14 @@ public class SearchActionTest {
   private final ActiveRuleCompleter activeRuleCompleter = new ActiveRuleCompleter(db.getDbClient(), languages);
   private final RuleQueryFactory ruleQueryFactory = new RuleQueryFactory(db.getDbClient());
   private final MacroInterpreter macroInterpreter = mock(MacroInterpreter.class);
+  private final QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class);
   private final RuleMapper ruleMapper = new RuleMapper(languages, macroInterpreter);
   private final SearchAction underTest = new SearchAction(ruleIndex, activeRuleCompleter, ruleQueryFactory, db.getDbClient(), ruleMapper,
     new RuleWsSupport(db.getDbClient(), userSession));
   private final TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation()));
   private final RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, db.getDbClient(), typeValidations, userSession);
-  private final QProfileRules qProfileRules = new QProfileRulesImpl(db.getDbClient(), ruleActivator, ruleIndex, activeRuleIndexer);
+  private final QProfileRules qProfileRules = new QProfileRulesImpl(db.getDbClient(), ruleActivator, ruleIndex, activeRuleIndexer,
+    qualityProfileChangeEventService);
   private final WsActionTester ws = new WsActionTester(underTest);
 
   @Before
index cfc18bb29698d283d015559d8164f30085573d60..432f32b605c4c291c63302fc3f4cc3db198ef441 100644 (file)
@@ -69,6 +69,10 @@ public class ServletRequest extends ValidatingRequest {
         MediaTypes.DEFAULT));
   }
 
+  public HttpServletRequest getHttpRequest() {
+    return source;
+  }
+
   @Override
   public BufferedReader getReader() {
     try {
index f5c1bde9d9bdfc549476424974bb514fefbba64a..c4504d656c9296304f5a60fec73cc62cfced3e92 100644 (file)
@@ -189,6 +189,7 @@ import org.sonar.server.qualitygate.ws.QualityGateWsModule;
 import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationHandler;
 import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationTemplate;
 import org.sonar.server.qualityprofile.BuiltInQProfileRepositoryImpl;
+import org.sonar.server.qualityprofile.DistributedRuleActivatorEventsDistributor;
 import org.sonar.server.qualityprofile.QProfileBackuperImpl;
 import org.sonar.server.qualityprofile.QProfileComparison;
 import org.sonar.server.qualityprofile.QProfileCopier;
@@ -198,7 +199,9 @@ import org.sonar.server.qualityprofile.QProfileParser;
 import org.sonar.server.qualityprofile.QProfileResetImpl;
 import org.sonar.server.qualityprofile.QProfileRulesImpl;
 import org.sonar.server.qualityprofile.QProfileTreeImpl;
+import org.sonar.server.qualityprofile.QualityProfileChangeEventServiceImpl;
 import org.sonar.server.qualityprofile.RuleActivator;
+import org.sonar.server.qualityprofile.StandaloneRuleActivatorEventsDistributor;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
 import org.sonar.server.qualityprofile.ws.QProfilesWsModule;
 import org.sonar.server.root.ws.RootWsModule;
@@ -274,6 +277,9 @@ public class PlatformLevel4 extends PlatformLevel {
 
     addIfCluster(NodeHealthModule.class);
 
+    addIfCluster(DistributedRuleActivatorEventsDistributor.class);
+    addIfStandalone(StandaloneRuleActivatorEventsDistributor.class);
+
     add(
       ClusterVerification.class,
       LogServerId.class,
@@ -304,6 +310,7 @@ public class PlatformLevel4 extends PlatformLevel {
       QProfileTreeImpl.class,
       QProfileRulesImpl.class,
       RuleActivator.class,
+      QualityProfileChangeEventServiceImpl.class,
       QProfileExporters.class,
       QProfileFactoryImpl.class,
       QProfileCopier.class,
diff --git a/sonar-core/src/main/java/org/sonar/core/util/ParamChange.java b/sonar-core/src/main/java/org/sonar/core/util/ParamChange.java
new file mode 100644 (file)
index 0000000..cff8685
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.util;
+
+public class ParamChange {
+  String key;
+  String value;
+
+  public ParamChange(String key, String value) {
+    this.key = key;
+    this.value = value;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public String getValue() {
+    return value;
+  }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/RuleActivationListener.java b/sonar-core/src/main/java/org/sonar/core/util/RuleActivationListener.java
new file mode 100644 (file)
index 0000000..d234d20
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.util;
+
+public interface RuleActivationListener {
+
+  void listen(RuleSetChangeEvent event);
+}
\ No newline at end of file
diff --git a/sonar-core/src/main/java/org/sonar/core/util/RuleChange.java b/sonar-core/src/main/java/org/sonar/core/util/RuleChange.java
new file mode 100644 (file)
index 0000000..016a08e
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.util;
+
+public class RuleChange {
+  String key;
+  String language;
+  String templateKey;
+  String severity;
+  ParamChange[] params = new ParamChange[0];
+
+  public String getKey() {
+    return key;
+  }
+
+  public void setKey(String key) {
+    this.key = key;
+  }
+
+  public String getLanguage() {
+    return language;
+  }
+
+  public void setLanguage(String language) {
+    this.language = language;
+  }
+
+  public String getTemplateKey() {
+    return templateKey;
+  }
+
+  public void setTemplateKey(String templateKey) {
+    this.templateKey = templateKey;
+  }
+
+  public String getSeverity() {
+    return severity;
+  }
+
+  public void setSeverity(String severity) {
+    this.severity = severity;
+  }
+
+  public ParamChange[] getParams() {
+    return params;
+  }
+
+  public void setParams(ParamChange[] params) {
+    this.params = params;
+  }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/RuleSetChangeEvent.java b/sonar-core/src/main/java/org/sonar/core/util/RuleSetChangeEvent.java
new file mode 100644 (file)
index 0000000..2a7d487
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.util;
+
+import java.io.Serializable;
+
+public class RuleSetChangeEvent implements Serializable {
+
+  private final String event = "RuleSetChange";
+
+  private String[] projects;
+  private RuleChange[] activatedRules;
+  private RuleChange[] deactivatedRules;
+
+  public RuleSetChangeEvent(String[] projects, RuleChange[] activatedRules, RuleChange[] deactivatedRules) {
+    this.projects = projects;
+    this.activatedRules = activatedRules;
+    this.deactivatedRules = deactivatedRules;
+  }
+
+  public void setProjects(String[] projects) {
+    this.projects = projects;
+  }
+
+  public void setActivatedRules(RuleChange[] activatedRules) {
+    this.activatedRules = activatedRules;
+  }
+
+  public void setDeactivatedRules(RuleChange[] deactivatedRules) {
+    this.deactivatedRules = deactivatedRules;
+  }
+
+  public String getEvent() {
+    return event;
+  }
+
+  public String[] getProjects() {
+    return projects;
+  }
+
+  public RuleChange[] getActivatedRules() {
+    return activatedRules;
+  }
+
+  public RuleChange[] getDeactivatedRules() {
+    return deactivatedRules;
+  }
+}