]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16372 - Push IssueChangedEvents
authorBelen Pruvost <belen.pruvost@sonarsource.com>
Wed, 11 May 2022 20:17:29 +0000 (22:17 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 16 May 2022 20:03:56 +0000 (20:03 +0000)
50 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-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventService.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/package-info.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/qualityprofile/builtin/QualityProfileChangeEventServiceImplTest.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
sonar-core/src/main/java/org/sonar/core/util/RuleActivationListener.java [deleted file]
sonar-core/src/main/java/org/sonar/core/util/RuleChange.java [deleted file]
sonar-core/src/main/java/org/sonar/core/util/RuleSetChangedEvent.java [deleted file]
sonar-core/src/main/java/org/sonar/core/util/issue/Issue.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangeListener.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangedEvent.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/util/rule/RuleActivationListener.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/util/rule/RuleChange.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/util/rule/RuleSetChangedEvent.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/util/RuleSetChangedEventTest.java [deleted file]
sonar-core/src/test/java/org/sonar/core/util/issue/IssueChangedEventTest.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/util/issue/IssueTest.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/util/rule/RuleSetChangedEventTest.java [new file with mode: 0644]

index 3c4b5235f5ccb5ec3b5b6b7209818e5f2a919ba5..13581a4d364ba97940a5c3463ff6ae3ebad3f154 100644 (file)
@@ -40,8 +40,10 @@ 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.RuleSetChangedEvent;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.core.util.rule.RuleActivationListener;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 import org.sonar.process.cluster.hz.DistributedAnswer;
 import org.sonar.process.cluster.hz.DistributedCall;
 import org.sonar.process.cluster.hz.DistributedCallback;
@@ -206,6 +208,16 @@ public class AppNodesClusterHostsConsistencyTest {
 
     }
 
+    @Override
+    public void subscribeIssueChangeTopic(IssueChangeListener listener) {
+
+    }
+
+    @Override
+    public void publishEvent(IssueChangedEvent event) {
+
+    }
+
     @Override
     public void close() {
 
index 2f0ec8cb8e05c70aad88c2b0e947304aa8d4bce2..833864b8e9646d4e6800743f12a4514e259f30a6 100644 (file)
@@ -26,8 +26,10 @@ 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.RuleSetChangedEvent;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.core.util.rule.RuleActivationListener;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 import org.sonar.process.ProcessId;
 
 public interface HazelcastMember extends AutoCloseable {
@@ -112,6 +114,10 @@ public interface HazelcastMember extends AutoCloseable {
 
   void publishEvent(RuleSetChangedEvent event);
 
+  void subscribeIssueChangeTopic(IssueChangeListener listener);
+
+  void publishEvent(IssueChangedEvent event);
+
   @Override
   void close();
 }
index 89a9b75f99448a25037e6098bfb8e63974064eda..bb6a541087d4241c0146f046e6b0f86e34f8ab20 100644 (file)
@@ -39,8 +39,10 @@ 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.RuleSetChangedEvent;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.core.util.rule.RuleActivationListener;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 
 class HazelcastMemberImpl implements HazelcastMember {
 
@@ -141,6 +143,18 @@ class HazelcastMemberImpl implements HazelcastMember {
     hzInstance.getTopic("ruleActivated").publish(event);
   }
 
+  @Override
+  public void subscribeIssueChangeTopic(IssueChangeListener listener) {
+    ITopic<IssueChangedEvent> topic = hzInstance.getTopic("issueChanged");
+    MessageListener<IssueChangedEvent> hzListener = message -> listener.listen(message.getMessageObject());
+    topic.addMessageListener(hzListener);
+  }
+
+  @Override
+  public void publishEvent(IssueChangedEvent event) {
+    hzInstance.getTopic("issueChanged").publish(event);
+  }
+
   @Override
   public void close() {
     try {
index 86418d5f637ed015001801323761f50b840c951f..032143d68d35ed3f7a5f621afe76ed3019e13db0 100644 (file)
@@ -36,8 +36,8 @@ import org.junit.Test;
 import org.junit.rules.DisableOnDebug;
 import org.junit.rules.TestRule;
 import org.junit.rules.Timeout;
-import org.mockito.verification.VerificationMode;
-import org.sonar.core.util.RuleActivationListener;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.rule.RuleActivationListener;
 import org.sonar.process.NetworkUtilsImpl;
 import org.sonar.process.ProcessId;
 
@@ -128,6 +128,19 @@ public class HazelcastMemberImplTest {
     verify(topic, times(1)).addMessageListener(any());
   }
 
+  @Test
+  public void subscribeIssueChangeTopic_listenerAdded() {
+    IssueChangeListener listener = mock(IssueChangeListener.class);
+    HazelcastInstance hzInstance = mock(HazelcastInstance.class);
+    ITopic<Object> topic = mock(ITopic.class);
+    when(hzInstance.getTopic(any())).thenReturn(topic);
+    HazelcastMemberImpl underTest = new HazelcastMemberImpl(hzInstance);
+
+    underTest.subscribeIssueChangeTopic(listener);
+
+    verify(topic, times(1)).addMessageListener(any());
+  }
+
   private static HazelcastMember newHzMember(int port, int... otherPorts) {
     return new HazelcastMemberBuilder(JoinConfigurationType.TCP_IP)
       .setProcessId(ProcessId.COMPUTE_ENGINE)
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java
new file mode 100644 (file)
index 0000000..ae5bbc7
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.process.cluster.hz.HazelcastMember;
+
+@ServerSide
+public class DistributedIssueChangeEventsDistributor implements IssueChangeEventsDistributor {
+
+  private HazelcastMember hazelcastMember;
+
+  public DistributedIssueChangeEventsDistributor(HazelcastMember hazelcastMember) {
+    this.hazelcastMember = hazelcastMember;
+  }
+
+  @Override
+  public void subscribe(IssueChangeListener listener) {
+    hazelcastMember.subscribeIssueChangeTopic(listener);
+  }
+
+  @Override
+  public void pushEvent(IssueChangedEvent event) {
+    hazelcastMember.publishEvent(event);
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java
new file mode 100644 (file)
index 0000000..b784f0e
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.sonar.core.util.issue.Issue;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.server.pushapi.sonarlint.SonarLintClient;
+
+import static java.util.Arrays.asList;
+
+public class IssueChangeBroadcastUtils {
+  private IssueChangeBroadcastUtils() {
+
+  }
+
+  public static Predicate<SonarLintClient> getFilterForEvent(IssueChangedEvent issueChangedEvent) {
+    List<String> affectedProjects = asList(issueChangedEvent.getProjectKey());
+    return client -> {
+      Set<String> clientProjectKeys = client.getClientProjectKeys();
+      return !Collections.disjoint(clientProjectKeys, affectedProjects);
+    };
+  }
+
+  public static String getMessage(IssueChangedEvent issueChangedEvent) {
+    return "event: " + issueChangedEvent.getEvent() + "\n"
+      + "data: " + toJson(issueChangedEvent);
+  }
+
+  private static String toJson(IssueChangedEvent issueChangedEvent) {
+    JSONObject data = new JSONObject();
+    data.put("projectKey", issueChangedEvent.getProjectKey());
+
+    JSONArray issuesJson = new JSONArray();
+    for (Issue issue : issueChangedEvent.getIssues()) {
+      issuesJson.put(toJson(issue));
+    }
+    data.put("issues", issuesJson);
+    data.put("userSeverity", issueChangedEvent.getUserSeverity());
+    data.put("userType", issueChangedEvent.getUserType());
+    data.put("resolved", issueChangedEvent.getResolved());
+
+    return data.toString();
+  }
+
+  private static JSONObject toJson(Issue issue) {
+    JSONObject ruleJson = new JSONObject();
+    ruleJson.put("issueKey", issue.getIssueKey());
+    ruleJson.put("branchName", issue.getBranchName());
+    return ruleJson;
+  }
+
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventService.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventService.java
new file mode 100644 (file)
index 0000000..7decf72
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import java.util.Collection;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+
+public interface IssueChangeEventService {
+  void distributeIssueChangeEvent(DefaultIssue issue, @Nullable String severity, @Nullable String type,
+    @Nullable String transitionKey, BranchDto branch, String projectKey);
+
+  void distributeIssueChangeEvent(Collection<DefaultIssue> issues, Map<String, ComponentDto> projectsByUuid,
+    Map<String, BranchDto> branchesByProjectUuid);
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java
new file mode 100644 (file)
index 0000000..54dc1c7
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.FieldDiffs.Diff;
+import org.sonar.core.util.issue.Issue;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+
+import static org.elasticsearch.common.Strings.isNullOrEmpty;
+import static org.sonar.api.issue.DefaultTransitions.CONFIRM;
+import static org.sonar.api.issue.DefaultTransitions.FALSE_POSITIVE;
+import static org.sonar.api.issue.DefaultTransitions.UNCONFIRM;
+import static org.sonar.api.issue.DefaultTransitions.WONT_FIX;
+import static org.sonar.db.component.BranchType.BRANCH;
+
+@ServerSide
+public class IssueChangeEventServiceImpl implements IssueChangeEventService {
+  private static final String FALSE_POSITIVE_KEY = "FALSE-POSITIVE";
+  private static final String WONT_FIX_KEY = "WONTFIX";
+
+  private static final String RESOLUTION_KEY = "resolution";
+  private static final String SEVERITY_KEY = "severity";
+  private static final String TYPE_KEY = "type";
+
+  private final IssueChangeEventsDistributor eventsDistributor;
+
+  public IssueChangeEventServiceImpl(IssueChangeEventsDistributor eventsDistributor) {
+    this.eventsDistributor = eventsDistributor;
+  }
+
+  @Override
+  public void distributeIssueChangeEvent(DefaultIssue issue, @Nullable String severity, @Nullable String type, @Nullable String transition,
+    BranchDto branch, String projectKey) {
+    Issue changedIssue = new Issue(issue.key(), branch.getKey());
+
+    Boolean resolved = isResolved(transition);
+
+    if (severity == null && type == null && resolved == null) {
+      return;
+    }
+
+    IssueChangedEvent event = new IssueChangedEvent(projectKey, new Issue[]{changedIssue},
+      resolved, severity, type);
+    eventsDistributor.pushEvent(event);
+  }
+
+  @Override
+  public void distributeIssueChangeEvent(Collection<DefaultIssue> issues, Map<String, ComponentDto> projectsByUuid,
+    Map<String, BranchDto> branchesByProjectUuid) {
+
+    for (Entry<String, ComponentDto> entry : projectsByUuid.entrySet()) {
+      String projectKey = entry.getValue().getKey();
+
+      Set<DefaultIssue> issuesInProject = issues
+        .stream()
+        .filter(i -> i.projectUuid().equals(entry.getKey()))
+        .collect(Collectors.toSet());
+
+      Issue[] issueChanges = issuesInProject.stream()
+        .filter(i -> branchesByProjectUuid.get(i.projectUuid()).getBranchType().equals(BRANCH))
+        .map(i -> new Issue(i.key(), branchesByProjectUuid.get(i.projectUuid()).getKey()))
+        .toArray(Issue[]::new);
+
+      if (issueChanges.length == 0) {
+        continue;
+      }
+
+      IssueChangedEvent event = getIssueChangedEvent(projectKey, issuesInProject, issueChanges);
+
+      if (event != null) {
+        eventsDistributor.pushEvent(event);
+      }
+    }
+  }
+
+  @CheckForNull
+  private static IssueChangedEvent getIssueChangedEvent(String projectKey, Set<DefaultIssue> issuesInProject, Issue[] issueChanges) {
+    DefaultIssue firstIssue = issuesInProject.stream().iterator().next();
+
+    if (firstIssue.currentChange() == null) {
+      return null;
+    }
+
+    Boolean resolved = null;
+    String severity = null;
+    String type = null;
+
+    boolean isRelevantEvent = false;
+    Map<String, Diff> diffs = firstIssue.currentChange().diffs();
+
+    if (diffs.containsKey(RESOLUTION_KEY)) {
+      resolved = diffs.get(RESOLUTION_KEY).newValue() == null ? false : isResolved(diffs.get(RESOLUTION_KEY).newValue().toString());
+      isRelevantEvent = true;
+    }
+
+    if (diffs.containsKey(SEVERITY_KEY)) {
+      severity = diffs.get(SEVERITY_KEY).newValue() == null ? null : diffs.get(SEVERITY_KEY).newValue().toString();
+      isRelevantEvent = true;
+    }
+
+    if (diffs.containsKey(TYPE_KEY)) {
+      type = diffs.get(TYPE_KEY).newValue() == null ? null : diffs.get(TYPE_KEY).newValue().toString();
+      isRelevantEvent = true;
+    }
+
+    if (!isRelevantEvent) {
+      return null;
+    }
+
+    return new IssueChangedEvent(projectKey, issueChanges, resolved, severity, type);
+  }
+
+  @CheckForNull
+  private static Boolean isResolved(@Nullable String transitionOrStatus) {
+    if (isNullOrEmpty(transitionOrStatus)) {
+      return null;
+    }
+
+    if (transitionOrStatus.equals(CONFIRM) || transitionOrStatus.equals(UNCONFIRM)) {
+      return null;
+    }
+
+    return transitionOrStatus.equals(WONT_FIX) || transitionOrStatus.equals(FALSE_POSITIVE) ||
+      transitionOrStatus.equals(FALSE_POSITIVE_KEY) || transitionOrStatus.equals(WONT_FIX_KEY);
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java
new file mode 100644 (file)
index 0000000..486cc87
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+
+public interface IssueChangeEventsDistributor {
+
+  void subscribe(IssueChangeListener listener);
+
+  void pushEvent(IssueChangedEvent event);
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java
new file mode 100644 (file)
index 0000000..53aa3f3
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+
+@ServerSide
+public class StandaloneIssueChangeEventsDistributor implements IssueChangeEventsDistributor {
+
+  private List<IssueChangeListener> listeners = new ArrayList<>();
+
+  @Override
+  public void subscribe(IssueChangeListener listener) {
+    listeners.add(listener);
+  }
+
+  @Override
+  public void pushEvent(IssueChangedEvent event) {
+    listeners.forEach(l -> l.listen(event));
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/package-info.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/package-info.java
new file mode 100644 (file)
index 0000000..497a7e1
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.server.pushapi.issues;
index c5b4b9fae9d0413f9b93aac99c3e6ad70f1494ac..a45ff9ce87ff6cfbcd28d1ed43787845e97c9d2a 100644 (file)
@@ -20,8 +20,8 @@
 package org.sonar.server.pushapi.qualityprofile;
 
 import org.sonar.api.server.ServerSide;
-import org.sonar.core.util.RuleActivationListener;
-import org.sonar.core.util.RuleSetChangedEvent;
+import org.sonar.core.util.rule.RuleActivationListener;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 import org.sonar.process.cluster.hz.HazelcastMember;
 
 @ServerSide
index 568dc5eb9f3a42cd4a0e02ab7bf3d825083b04ca..76fc0799986c479915862a83c79eb334a3f54d08 100644 (file)
@@ -34,8 +34,8 @@ import org.jetbrains.annotations.NotNull;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.server.ServerSide;
 import org.sonar.core.util.ParamChange;
-import org.sonar.core.util.RuleChange;
-import org.sonar.core.util.RuleSetChangedEvent;
+import org.sonar.core.util.rule.RuleChange;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.project.ProjectDto;
index 0d8a6c26c8b77e805c532e9aeb92e0c0bc627465..8914610b28a32499c8d69d788cdc0e024d4ef6b5 100644 (file)
@@ -19,8 +19,8 @@
  */
 package org.sonar.server.pushapi.qualityprofile;
 
-import org.sonar.core.util.RuleActivationListener;
-import org.sonar.core.util.RuleSetChangedEvent;
+import org.sonar.core.util.rule.RuleActivationListener;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 
 public interface RuleActivatorEventsDistributor {
 
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java
new file mode 100644 (file)
index 0000000..159d67f
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.qualityprofile;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.sonar.core.util.ParamChange;
+import org.sonar.core.util.rule.RuleChange;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
+import org.sonar.server.pushapi.sonarlint.SonarLintClient;
+
+import static java.util.Arrays.asList;
+
+public class RuleSetChangeBroadcastUtils {
+  private RuleSetChangeBroadcastUtils() {
+  }
+
+  public static Predicate<SonarLintClient> getFilterForEvent(RuleSetChangedEvent ruleSetChangedEvent) {
+    List<String> affectedProjects = asList(ruleSetChangedEvent.getProjects());
+    return client -> {
+      Set<String> clientProjectKeys = client.getClientProjectKeys();
+      Set<String> languages = client.getLanguages();
+      return !Collections.disjoint(clientProjectKeys, affectedProjects) && languages.contains(ruleSetChangedEvent.getLanguage());
+    };
+  }
+
+  public static String getMessage(RuleSetChangedEvent ruleSetChangedEvent) {
+    return "event: " + ruleSetChangedEvent.getEvent() + "\n"
+      + "data: " + toJson(ruleSetChangedEvent);
+  }
+
+  private static String toJson(RuleSetChangedEvent ruleSetChangedEvent) {
+    JSONObject data = new JSONObject();
+    data.put("projects", ruleSetChangedEvent.getProjects());
+
+    JSONArray activatedRulesJson = new JSONArray();
+    for (RuleChange rule : ruleSetChangedEvent.getActivatedRules()) {
+      activatedRulesJson.put(toJson(rule));
+    }
+    data.put("activatedRules", activatedRulesJson);
+
+    JSONArray deactivatedRulesJson = new JSONArray();
+    for (String ruleKey : ruleSetChangedEvent.getDeactivatedRules()) {
+      deactivatedRulesJson.put(ruleKey);
+    }
+    data.put("deactivatedRules", deactivatedRulesJson);
+
+    return data.toString();
+  }
+
+  private static 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 static JSONObject toJson(ParamChange paramChange) {
+    JSONObject param = new JSONObject();
+    param.put("key", paramChange.getKey());
+    param.put("value", paramChange.getValue());
+    return param;
+  }
+
+}
index 3207154ac2e59b1496a1c63fdefbc8c9dbae2029..64a5d15e10a55fcc6f623509ca5682c6cf2c9291 100644 (file)
@@ -22,8 +22,8 @@ package org.sonar.server.pushapi.qualityprofile;
 import java.util.ArrayList;
 import java.util.List;
 import org.sonar.api.server.ServerSide;
-import org.sonar.core.util.RuleActivationListener;
-import org.sonar.core.util.RuleSetChangedEvent;
+import org.sonar.core.util.rule.RuleActivationListener;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 
 @ServerSide
 public class StandaloneRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {
index 88d874337732e9ce1f54c120f9bd16c9dcf2e235..d2508ada3318ce6982f127f481120753534d2d1c 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.server.pushapi.sonarlint;
 
 import java.io.IOException;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -28,34 +27,36 @@ 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.RuleSetChangedEvent;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.core.util.rule.RuleActivationListener;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.pushapi.issues.IssueChangeBroadcastUtils;
+import org.sonar.server.pushapi.issues.IssueChangeEventsDistributor;
 import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor;
-
-import static java.util.Arrays.asList;
+import org.sonar.server.pushapi.qualityprofile.RuleSetChangeBroadcastUtils;
 
 @ServerSide
-public class SonarLintClientsRegistry implements RuleActivationListener {
+public class SonarLintClientsRegistry implements RuleActivationListener, IssueChangeListener {
 
   private static final Logger LOG = Loggers.get(SonarLintClientsRegistry.class);
 
   private final SonarLintClientPermissionsValidator sonarLintClientPermissionsValidator;
   private final List<SonarLintClient> clients = new CopyOnWriteArrayList<>();
-  private final RuleActivatorEventsDistributor eventsDistributor;
+  private final RuleActivatorEventsDistributor ruleEventsDistributor;
+  private final IssueChangeEventsDistributor issueChangeEventsDistributor;
 
   private boolean registeredToEvents = false;
 
-  public SonarLintClientsRegistry(RuleActivatorEventsDistributor ruleActivatorEventsDistributor, SonarLintClientPermissionsValidator permissionsValidator) {
+  public SonarLintClientsRegistry(IssueChangeEventsDistributor issueChangeEventsDistributor,
+    RuleActivatorEventsDistributor ruleActivatorEventsDistributor, SonarLintClientPermissionsValidator permissionsValidator) {
+    this.issueChangeEventsDistributor = issueChangeEventsDistributor;
     this.sonarLintClientPermissionsValidator = permissionsValidator;
-    this.eventsDistributor = ruleActivatorEventsDistributor;
+    this.ruleEventsDistributor = ruleActivatorEventsDistributor;
   }
 
   public void registerClient(SonarLintClient sonarLintClient) {
@@ -71,10 +72,11 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
       return;
     }
     try {
-      eventsDistributor.subscribe(this);
+      ruleEventsDistributor.subscribe(this);
+      issueChangeEventsDistributor.subscribe(this);
       registeredToEvents = true;
     } catch (RuntimeException e) {
-      LOG.warn("Can not listen to rule activation events for server push. Web Server might not have started fully yet.", e);
+      LOG.warn("Can not listen to rule activation or issue events for server push. Web Server might not have started fully yet.", e);
     }
   }
 
@@ -90,16 +92,12 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
 
   @Override
   public void listen(RuleSetChangedEvent ruleSetChangedEvent) {
-    broadcastMessage(ruleSetChangedEvent, getFilterForEvent(ruleSetChangedEvent));
+    broadcastMessage(ruleSetChangedEvent, RuleSetChangeBroadcastUtils.getFilterForEvent(ruleSetChangedEvent));
   }
 
-  private static Predicate<SonarLintClient> getFilterForEvent(RuleSetChangedEvent ruleSetChangedEvent) {
-    List<String> affectedProjects = asList(ruleSetChangedEvent.getProjects());
-    return client -> {
-      Set<String> clientProjectKeys = client.getClientProjectKeys();
-      Set<String> languages = client.getLanguages();
-      return !Collections.disjoint(clientProjectKeys, affectedProjects) && languages.contains(ruleSetChangedEvent.getLanguage());
-    };
+  @Override
+  public void listen(IssueChangedEvent issueChangedEvent) {
+    broadcastMessage(issueChangedEvent, IssueChangeBroadcastUtils.getFilterForEvent(issueChangedEvent));
   }
 
   public void broadcastMessage(RuleSetChangedEvent event, Predicate<SonarLintClient> filter) {
@@ -110,7 +108,7 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
         sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient);
         RuleSetChangedEvent personalizedEvent = new RuleSetChangedEvent(projectKeysInterestingForClient.toArray(String[]::new), event.getActivatedRules(),
           event.getDeactivatedRules(), event.getLanguage());
-        String message = getMessage(personalizedEvent);
+        String message = RuleSetChangeBroadcastUtils.getMessage(personalizedEvent);
         c.writeAndFlush(message);
       } catch (ForbiddenException forbiddenException) {
         LOG.debug("Client is no longer authenticated: " + forbiddenException.getMessage());
@@ -121,50 +119,23 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
       }
     });
   }
-  private static String getMessage(RuleSetChangedEvent ruleSetChangedEvent) {
-    return "event: " + ruleSetChangedEvent.getEvent() + "\n"
-      + "data: " + toJson(ruleSetChangedEvent);
-  }
-
-  private static String toJson(RuleSetChangedEvent ruleSetChangedEvent) {
-    JSONObject data = new JSONObject();
-    data.put("projects", ruleSetChangedEvent.getProjects());
-
-    JSONArray activatedRulesJson = new JSONArray();
-    for (RuleChange rule : ruleSetChangedEvent.getActivatedRules()) {
-      activatedRulesJson.put(toJson(rule));
-    }
-    data.put("activatedRules", activatedRulesJson);
-
-    JSONArray deactivatedRulesJson = new JSONArray();
-    for (String ruleKey : ruleSetChangedEvent.getDeactivatedRules()) {
-      deactivatedRulesJson.put(ruleKey);
-    }
-    data.put("deactivatedRules", deactivatedRulesJson);
-
-    return data.toString();
-  }
-
-  private static 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 static JSONObject toJson(ParamChange paramChange) {
-    JSONObject param = new JSONObject();
-    param.put("key", paramChange.getKey());
-    param.put("value", paramChange.getValue());
-    return param;
+  public void broadcastMessage(IssueChangedEvent event, Predicate<SonarLintClient> filter) {
+    clients.stream().filter(filter).forEach(c -> {
+      Set<String> projectKeysInterestingForClient = new HashSet<>(c.getClientProjectKeys());
+      projectKeysInterestingForClient.retainAll(Set.of(event.getProjectKey()));
+      try {
+        sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient);
+        String message = IssueChangeBroadcastUtils.getMessage(event);
+        c.writeAndFlush(message);
+      } catch (ForbiddenException forbiddenException) {
+        LOG.debug("Client is no longer authenticated: " + forbiddenException.getMessage());
+        unregisterClient(c);
+      } catch (IllegalStateException | IOException e) {
+        LOG.error("Unable to send message to a client: " + e.getMessage());
+        unregisterClient(c);
+      }
+    });
   }
 
   class SonarLintClientEventsListener implements AsyncListener {
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java
new file mode 100644 (file)
index 0000000..b08a831
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import org.junit.Test;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.process.cluster.hz.HazelcastMember;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class DistributedIssueChangeEventsDistributorTest {
+  HazelcastMember hazelcastMember = mock(HazelcastMember.class);
+  IssueChangeListener issueChangeListener = mock(IssueChangeListener.class);
+  IssueChangedEvent event = mock(IssueChangedEvent.class);
+
+  public final DistributedIssueChangeEventsDistributor underTest = new DistributedIssueChangeEventsDistributor(hazelcastMember);
+
+  @Test
+  public void subscribe_subscribesHazelCastMember() {
+    underTest.subscribe(issueChangeListener);
+    verify(hazelcastMember).subscribeIssueChangeTopic(issueChangeListener);
+  }
+
+  @Test
+  public void pushEvent_publishesEvent() {
+    underTest.pushEvent(event);
+    verify(hazelcastMember).publishEvent(event);
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java
new file mode 100644 (file)
index 0000000..8e65c83
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import java.util.Set;
+import java.util.function.Predicate;
+import javax.servlet.AsyncContext;
+import org.junit.Test;
+import org.sonar.core.util.issue.Issue;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.server.pushapi.sonarlint.SonarLintClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class IssueChangeBroadcastUtilsTest {
+
+  private final String PROJECT_KEY = "projectKey";
+  private final String USER_UUID = "userUUID";
+  private final AsyncContext asyncContext = mock(AsyncContext.class);
+
+  @Test
+  public void getsFilterForEvent() {
+    Issue[] issues = new Issue[]{ new Issue("issue-1", "branch-1")};
+    IssueChangedEvent issueChangedEvent = new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG");
+    Predicate<SonarLintClient> predicate = IssueChangeBroadcastUtils.getFilterForEvent(issueChangedEvent);
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(PROJECT_KEY), Set.of(), USER_UUID))).isTrue();
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(), Set.of(), USER_UUID))).isFalse();
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of("another-project"), Set.of(), USER_UUID))).isFalse();
+  }
+
+  @Test
+  public void getsMessageForEvent() {
+    Issue[] issues = new Issue[]{ new Issue("issue-1", "branch-1")};
+    IssueChangedEvent issueChangedEvent = new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG");
+    String message = IssueChangeBroadcastUtils.getMessage(issueChangedEvent);
+
+    assertThat(message).isEqualTo("event: IssueChangedEvent\n" +
+      "data: {\"projectKey\":\""+ PROJECT_KEY+"\"," +
+      "\"userType\":\"BUG\"," +
+      "\"issues\":[{\"issueKey\":\"issue-1\",\"branchName\":\"branch-1\"}]," +
+      "\"userSeverity\":\"BLOCKER\"," +
+      "\"resolved\":true}");
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java
new file mode 100644 (file)
index 0000000..91d543e
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.rules.RuleType;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.FieldDiffs;
+import org.sonar.core.util.issue.IssueChangedEvent;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonarqube.ws.Common;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.sonar.api.issue.DefaultTransitions.CONFIRM;
+import static org.sonar.api.issue.DefaultTransitions.FALSE_POSITIVE;
+import static org.sonar.api.issue.DefaultTransitions.REOPEN;
+import static org.sonar.api.issue.DefaultTransitions.RESOLVE;
+import static org.sonar.api.issue.DefaultTransitions.UNCONFIRM;
+import static org.sonar.api.issue.DefaultTransitions.WONT_FIX;
+import static org.sonar.api.rules.RuleType.CODE_SMELL;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonarqube.ws.Common.Severity.BLOCKER;
+import static org.sonarqube.ws.Common.Severity.CRITICAL;
+import static org.sonarqube.ws.Common.Severity.MAJOR;
+
+public class IssueChangeEventServiceImplTest {
+
+  @Rule
+  public DbTester db = DbTester.create();
+
+  IssueChangeEventsDistributor eventsDistributor = mock(IssueChangeEventsDistributor.class);
+
+  public final IssueChangeEventServiceImpl underTest = new IssueChangeEventServiceImpl(eventsDistributor);
+
+  @Test
+  public void distributeIssueChangeEvent_singleIssueChange_severityChange() {
+    ComponentDto componentDto = db.components().insertPublicProject();
+    ProjectDto project = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto.uuid()).get();
+    BranchDto branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.getUuid()).get();
+    RuleDto rule = db.rules().insert();
+    IssueDto issue = db.issues().insert(rule, project, componentDto, i-> i.setSeverity(MAJOR.name()));
+
+    assertIssueDistribution(project, branch, issue, BLOCKER.name(), null, null, null, 1);
+  }
+
+  @Test
+  public void distributeIssueChangeEvent_singleIssueChange_typeChange() {
+    ComponentDto componentDto = db.components().insertPublicProject();
+    ProjectDto project = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto.uuid()).get();
+    BranchDto branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.getUuid()).get();
+    RuleDto rule = db.rules().insert();
+    IssueDto issue = db.issues().insert(rule, project, componentDto, i-> i.setSeverity(MAJOR.name()));
+
+    assertIssueDistribution(project, branch, issue, null, Common.RuleType.BUG.name(), null, null, 1);
+  }
+
+  @Test
+  public void distributeIssueChangeEvent_singleIssueChange_transitionChanges() {
+    ComponentDto componentDto = db.components().insertPublicProject();
+    ProjectDto project = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto.uuid()).get();
+    BranchDto branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.getUuid()).get();
+    RuleDto rule = db.rules().insert();
+    IssueDto issue = db.issues().insert(rule, project, componentDto, i-> i.setSeverity(MAJOR.name()));
+
+    assertIssueDistribution(project, branch, issue, null, null, WONT_FIX, true, 1);
+    assertIssueDistribution(project, branch, issue, null, null, REOPEN, false, 2);
+    assertIssueDistribution(project, branch, issue, null, null, FALSE_POSITIVE, true, 3);
+    assertIssueDistribution(project, branch, issue, null, null, REOPEN, false, 4);
+    assertIssueDistribution(project, branch, issue, null, null, RESOLVE, false, 5);
+    assertIssueDistribution(project, branch, issue, null, null, REOPEN, false, 6);
+    assertNoIssueDistribution(project, branch, issue, null, null, CONFIRM);
+    assertNoIssueDistribution(project, branch, issue, null, null, UNCONFIRM);
+  }
+
+  @Test
+  public void distributeIssueChangeEvent_singleIssueChange_severalChanges() {
+    ComponentDto componentDto = db.components().insertPublicProject();
+    ProjectDto project = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto.uuid()).get();
+    BranchDto branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.getUuid()).get();
+    RuleDto rule = db.rules().insert();
+    IssueDto issue = db.issues().insert(rule, project, componentDto, i-> i.setSeverity(MAJOR.name()));
+
+    assertIssueDistribution(project, branch, issue, BLOCKER.name(), Common.RuleType.BUG.name(), WONT_FIX, true, 1);
+  }
+
+  @Test
+  public void distributeIssueChangeEvent_bulkIssueChange() {
+    RuleDto rule = db.rules().insert();
+
+    ComponentDto componentDto1 = db.components().insertPublicProject();
+    ProjectDto project1 = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto1.uuid()).get();
+    BranchDto branch1 = db.getDbClient().branchDao().selectByUuid(db.getSession(), project1.getUuid()).get();
+    IssueDto issue1 = db.issues().insert(rule, project1, componentDto1, i-> i.setSeverity(MAJOR.name()).setType(RuleType.BUG));
+
+    ComponentDto componentDto2 = db.components().insertPublicProject();
+    ProjectDto project2 = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto2.uuid()).get();
+    BranchDto branch2 = db.getDbClient().branchDao().selectByUuid(db.getSession(), project2.getUuid()).get();
+    IssueDto issue2 = db.issues().insert(rule, project2, componentDto2, i-> i.setSeverity(MAJOR.name()).setType(RuleType.BUG));
+
+    ComponentDto componentDto3 = db.components().insertPublicProject();
+    ProjectDto project3 = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto3.uuid()).get();
+    BranchDto branch3 = db.getDbClient().branchDao().selectByUuid(db.getSession(), project3.getUuid()).get();
+    IssueDto issue3 = db.issues().insert(rule, project3, componentDto3, i-> i.setSeverity(MAJOR.name()).setType(RuleType.BUG));
+
+    DefaultIssue defaultIssue1 = issue1.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs()
+      .setDiff("resolution", null, null)
+      .setDiff("severity", MAJOR.name(), CRITICAL.name())
+      .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name()));
+    DefaultIssue defaultIssue2 = issue2.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs()
+      .setDiff("resolution", "OPEN", "FALSE-POSITIVE")
+      .setDiff("severity", MAJOR.name(), CRITICAL.name())
+      .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name()));
+
+    Set<DefaultIssue> issues = Set.of(defaultIssue1, defaultIssue2, issue3.toDefaultIssue());
+    Map<String, ComponentDto> projectsByUuid = new HashMap<>();
+    projectsByUuid.put(componentDto1.projectUuid(), componentDto1);
+    projectsByUuid.put(componentDto2.projectUuid(), componentDto2);
+    projectsByUuid.put(componentDto3.projectUuid(), componentDto3);
+    Map<String, BranchDto> branchesByProjectUuid = new HashMap<>();
+    branchesByProjectUuid.put(componentDto1.projectUuid(), branch1);
+    branchesByProjectUuid.put(componentDto2.projectUuid(), branch2);
+    branchesByProjectUuid.put(componentDto3.projectUuid(), branch3);
+
+    underTest.distributeIssueChangeEvent(issues, projectsByUuid, branchesByProjectUuid);
+
+    ArgumentCaptor<IssueChangedEvent> eventCaptor = ArgumentCaptor.forClass(IssueChangedEvent.class);
+    verify(eventsDistributor, times(2)).pushEvent(eventCaptor.capture());
+
+    List<IssueChangedEvent> issueChangedEvents = eventCaptor.getAllValues();
+    assertThat(issueChangedEvents).hasSize(2);
+
+    assertThat(issueChangedEvents)
+      .extracting(IssueChangedEvent::getEvent, IssueChangedEvent::getProjectKey,
+        IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType, IssueChangedEvent::getResolved)
+      .containsExactlyInAnyOrder(
+        tuple("IssueChangedEvent", project1.getKey(), CRITICAL.name(), CODE_SMELL.name(), false),
+        tuple("IssueChangedEvent", project2.getKey(), CRITICAL.name(), CODE_SMELL.name(), true));
+  }
+
+  @Test
+  public void doNotDistributeIssueChangeEvent_forPullRequestIssues() {
+    RuleDto rule = db.rules().insert();
+
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setKey("myBranch1")
+      .setBranchType(BranchType.PULL_REQUEST)
+      .setMergeBranchUuid(project.uuid()));
+    BranchDto branch1 = db.getDbClient().branchDao().selectByUuid(db.getSession(), pullRequest.uuid()).get();
+    ComponentDto file = db.components().insertComponent(newFileDto(pullRequest));
+    IssueDto issue1 = db.issues().insert(rule, pullRequest, file, i-> i.setSeverity(MAJOR.name()).setType(RuleType.BUG));
+
+    DefaultIssue defaultIssue1 = issue1.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs()
+      .setDiff("resolution", null, null)
+      .setDiff("severity", MAJOR.name(), CRITICAL.name())
+      .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name()));
+
+    Set<DefaultIssue> issues = Set.of(defaultIssue1);
+    Map<String, ComponentDto> projectsByUuid = new HashMap<>();
+    projectsByUuid.put(project.projectUuid(), project);
+    Map<String, BranchDto> branchesByProjectUuid = new HashMap<>();
+    branchesByProjectUuid.put(project.projectUuid(), branch1);
+
+    underTest.distributeIssueChangeEvent(issues, projectsByUuid, branchesByProjectUuid);
+
+    verifyNoInteractions(eventsDistributor);
+  }
+
+  private void assertIssueDistribution(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity,
+    @Nullable String type, @Nullable String transition, Boolean resolved, int times) {
+    underTest.distributeIssueChangeEvent(issue.toDefaultIssue(), severity, type, transition, branch, project.getKey());
+
+    ArgumentCaptor<IssueChangedEvent> eventCaptor = ArgumentCaptor.forClass(IssueChangedEvent.class);
+    verify(eventsDistributor, times(times)).pushEvent(eventCaptor.capture());
+
+    IssueChangedEvent issueChangedEvent = eventCaptor.getValue();
+    assertThat(issueChangedEvent).isNotNull();
+    assertThat(issueChangedEvent).extracting(IssueChangedEvent::getEvent, IssueChangedEvent::getProjectKey,
+        IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType, IssueChangedEvent::getResolved)
+      .containsExactly("IssueChangedEvent", project.getKey(), severity, type, resolved);
+  }
+
+  private void assertNoIssueDistribution(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity,
+    @Nullable String type, @Nullable String transition) {
+    underTest.distributeIssueChangeEvent(issue.toDefaultIssue(), severity, type, transition, branch, project.getKey());
+
+    ArgumentCaptor<IssueChangedEvent> eventCaptor = ArgumentCaptor.forClass(IssueChangedEvent.class);
+    verifyNoMoreInteractions(eventsDistributor);
+  }
+
+}
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java
new file mode 100644 (file)
index 0000000..b3b9e0b
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.issues;
+
+import org.junit.Test;
+import org.sonar.core.util.issue.IssueChangeListener;
+import org.sonar.core.util.issue.IssueChangedEvent;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class StandaloneIssueChangeEventsDistributorTest {
+  IssueChangeListener issueChangeListener = mock(IssueChangeListener.class);
+  IssueChangedEvent event = mock(IssueChangedEvent.class);
+
+  public final StandaloneIssueChangeEventsDistributor underTest = new StandaloneIssueChangeEventsDistributor();
+
+  @Test
+  public void subscribe_and_push_publishesToListener() {
+    underTest.subscribe(issueChangeListener);
+    underTest.pushEvent(event);
+    verify(issueChangeListener).listen(event);
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java
new file mode 100644 (file)
index 0000000..1b4c10e
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.qualityprofile;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.util.ParamChange;
+import org.sonar.core.util.rule.RuleChange;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
+import org.sonar.db.DbTester;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.qualityprofile.QualityProfileTesting;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+
+import static java.util.List.of;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
+import static org.sonar.db.rule.RuleTesting.newCustomRule;
+import static org.sonar.db.rule.RuleTesting.newTemplateRule;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
+
+public class QualityProfileChangeEventServiceImplTest {
+
+  @Rule
+  public DbTester db = DbTester.create();
+
+  RuleActivatorEventsDistributor eventsDistributor = mock(RuleActivatorEventsDistributor.class);
+
+  public final QualityProfileChangeEventServiceImpl underTest = new QualityProfileChangeEventServiceImpl(db.getDbClient(), eventsDistributor);
+
+  @Test
+  public void distributeRuleChangeEvent() {
+    QProfileDto qualityProfileDto = QualityProfileTesting.newQualityProfileDto();
+
+    // Template rule
+    RuleDto templateRule = newTemplateRule(RuleKey.of("xoo", "template-key"));
+    db.rules().insert(templateRule);
+    // Custom rule
+    RuleDto rule1 = newCustomRule(templateRule)
+      .setLanguage("xoo")
+      .setRepositoryKey("repo")
+      .setRuleKey("ruleKey")
+      .setDescriptionFormat(RuleDto.Format.MARKDOWN)
+      .addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", "<div>line1\nline2</div>"));
+    db.rules().insert(rule1);
+
+    ActiveRuleDto activeRuleDto = ActiveRuleDto.createFor(qualityProfileDto, rule1);
+
+    ActiveRuleChange activeRuleChange = new ActiveRuleChange(ACTIVATED, activeRuleDto, rule1);
+    activeRuleChange.setParameter("paramChangeKey", "paramChangeValue");
+
+    Collection<QProfileDto> profiles = Collections.singleton(qualityProfileDto);
+
+    ProjectDto project = db.components().insertPrivateProjectDto();
+    db.qualityProfiles().associateWithProject(project, qualityProfileDto);
+
+    underTest.distributeRuleChangeEvent(profiles, of(activeRuleChange), "xoo");
+
+    ArgumentCaptor<RuleSetChangedEvent> eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class);
+    verify(eventsDistributor).pushEvent(eventCaptor.capture());
+
+    RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue();
+    assertThat(ruleSetChangedEvent).isNotNull();
+    assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent,
+        RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects)
+      .containsExactly("RuleSetChanged", "xoo", new String[]{project.getKey()});
+
+    assertThat(ruleSetChangedEvent.getActivatedRules())
+      .extracting(RuleChange::getKey, RuleChange::getLanguage,
+        RuleChange::getSeverity, RuleChange::getTemplateKey)
+      .containsExactly(tuple("repo:ruleKey", "xoo", null, "xoo:template-key"));
+
+    assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1);
+    ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0];
+    assertThat(actualParamChange)
+      .extracting(ParamChange::getKey, ParamChange::getValue)
+      .containsExactly("paramChangeKey", "paramChangeValue");
+
+    assertThat(ruleSetChangedEvent.getDeactivatedRules()).isEmpty();
+
+  }
+
+  @Test
+  public void publishRuleActivationToSonarLintClients() {
+    ProjectDto projectDao = new ProjectDto();
+    QProfileDto activatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
+    activatedQualityProfile.setLanguage("xoo");
+    db.qualityProfiles().insert(activatedQualityProfile);
+    RuleDto rule1 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo").setRuleKey("ruleKey"));
+    RuleParamDto rule1Param = db.rules().insertRuleParam(rule1);
+
+    ActiveRuleDto activeRule1 = db.qualityProfiles().activateRule(activatedQualityProfile, rule1);
+    ActiveRuleParamDto activeRuleParam1 = ActiveRuleParamDto.createFor(rule1Param).setValue(randomAlphanumeric(20));
+    db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule1, activeRuleParam1);
+    db.getSession().commit();
+
+    QProfileDto deactivatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
+    db.qualityProfiles().insert(deactivatedQualityProfile);
+    RuleDto rule2 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo2").setRuleKey("ruleKey2"));
+    RuleParamDto rule2Param = db.rules().insertRuleParam(rule2);
+
+    ActiveRuleDto activeRule2 = db.qualityProfiles().activateRule(deactivatedQualityProfile, rule2);
+    ActiveRuleParamDto activeRuleParam2 = ActiveRuleParamDto.createFor(rule2Param).setValue(randomAlphanumeric(20));
+    db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule2, activeRuleParam2);
+    db.getSession().commit();
+
+    underTest.publishRuleActivationToSonarLintClients(projectDao, activatedQualityProfile, deactivatedQualityProfile);
+
+    ArgumentCaptor<RuleSetChangedEvent> eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class);
+    verify(eventsDistributor).pushEvent(eventCaptor.capture());
+
+    RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue();
+    assertThat(ruleSetChangedEvent).isNotNull();
+    assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent,
+        RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects)
+      .containsExactly("RuleSetChanged", "xoo", new String[]{null});
+
+    // activated rule
+    assertThat(ruleSetChangedEvent.getActivatedRules())
+      .extracting(RuleChange::getKey, RuleChange::getLanguage,
+        RuleChange::getSeverity, RuleChange::getTemplateKey)
+      .containsExactly(tuple("repo:ruleKey", "xoo", rule1.getSeverityString(), null));
+
+    assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1);
+    ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0];
+    assertThat(actualParamChange)
+      .extracting(ParamChange::getKey, ParamChange::getValue)
+      .containsExactly(activeRuleParam1.getKey(), activeRuleParam1.getValue());
+
+    // deactivated rule
+    assertThat(ruleSetChangedEvent.getDeactivatedRules())
+      .containsExactly("repo2:ruleKey2");
+  }
+
+}
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java
new file mode 100644 (file)
index 0000000..e36a5df
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi.qualityprofile;
+
+import java.util.Set;
+import java.util.function.Predicate;
+import javax.servlet.AsyncContext;
+import org.junit.Test;
+import org.sonar.api.rule.Severity;
+import org.sonar.core.util.ParamChange;
+import org.sonar.core.util.rule.RuleChange;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
+import org.sonar.server.pushapi.sonarlint.SonarLintClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class RuleSetChangeBroadcastUtilsTest {
+
+  private final static String JAVA_KEY = "java";
+  private final static String PROJECT_KEY_1 = "projectKey1";
+  private final static String PROJECT_KEY_2 = "projectKey2";
+  private final static String USER_UUID = "userUUID";
+  private final static String[] DEACTIVATED_RULES = {"repo2:rule-key2"};
+
+  private final static Set<String> EXAMPLE_KEYS = Set.of(PROJECT_KEY_1, PROJECT_KEY_2);
+
+  private final AsyncContext asyncContext = mock(AsyncContext.class);
+
+  @Test
+  public void getsFilterForEvent() {
+    RuleChange javaRule = new RuleChange();
+    javaRule.setLanguage(JAVA_KEY);
+    javaRule.setParams(new ParamChange[]{new ParamChange("param-key", "param-value")});
+    javaRule.setTemplateKey("repo:template-key");
+    javaRule.setSeverity(Severity.CRITICAL);
+    javaRule.setKey("repo:rule-key");
+
+    RuleChange[] activatedRules = {javaRule};
+    RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(EXAMPLE_KEYS.toArray(String[]::new), activatedRules,
+      DEACTIVATED_RULES, JAVA_KEY);
+    Predicate<SonarLintClient> predicate = RuleSetChangeBroadcastUtils.getFilterForEvent(ruleSetChangedEvent);
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(PROJECT_KEY_1), Set.of(JAVA_KEY), USER_UUID))).isTrue();
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(PROJECT_KEY_2), Set.of(JAVA_KEY), USER_UUID))).isTrue();
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(PROJECT_KEY_1), Set.of(), USER_UUID))).isFalse();
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(), Set.of(JAVA_KEY), USER_UUID))).isFalse();
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of("another-project"), Set.of(), USER_UUID))).isFalse();
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(""), Set.of("another-language"), USER_UUID))).isFalse();
+    assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of("another-project"), Set.of("another-language"), USER_UUID))).isFalse();
+  }
+
+  @Test
+  public void getsMessageForEvent() {
+    RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(new String[]{PROJECT_KEY_1}, new RuleChange[0],
+      DEACTIVATED_RULES, JAVA_KEY);
+
+    String message = RuleSetChangeBroadcastUtils.getMessage(ruleSetChangedEvent);
+
+    assertThat(message).isEqualTo("event: RuleSetChanged\n" +
+      "data: {\"activatedRules\":[]," +
+      "\"projects\":[\"" + PROJECT_KEY_1 + "\"]," +
+      "\"deactivatedRules\":[\"repo2:rule-key2\"]}");
+  }
+}
index 2147bf50ecf09e49327c7dafef8080085a78cafc..48cbb2184110894381ccdf12ea8f7e0625717289 100644 (file)
@@ -28,10 +28,13 @@ import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.sonar.api.rule.Severity;
+import org.sonar.core.util.issue.Issue;
+import org.sonar.core.util.issue.IssueChangedEvent;
 import org.sonar.core.util.ParamChange;
-import org.sonar.core.util.RuleChange;
-import org.sonar.core.util.RuleSetChangedEvent;
+import org.sonar.core.util.rule.RuleChange;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
 import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.pushapi.issues.StandaloneIssueChangeEventsDistributor;
 import org.sonar.server.pushapi.qualityprofile.StandaloneRuleActivatorEventsDistributor;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -58,13 +61,14 @@ public class SonarLintClientsRegistryTest {
   private final ServletOutputStream outputStream = mock(ServletOutputStream.class);
 
   private final SonarLintClientPermissionsValidator permissionsValidator = mock(SonarLintClientPermissionsValidator.class);
-  private final StandaloneRuleActivatorEventsDistributor eventsDistributor = mock(StandaloneRuleActivatorEventsDistributor.class);
+  private final StandaloneRuleActivatorEventsDistributor ruleEventsDistributor = mock(StandaloneRuleActivatorEventsDistributor.class);
+  private final StandaloneIssueChangeEventsDistributor issueChangeEventsDistributor = mock(StandaloneIssueChangeEventsDistributor.class);
 
   private SonarLintClientsRegistry underTest;
 
   @Before
   public void before() {
-    underTest = new SonarLintClientsRegistry(eventsDistributor, permissionsValidator);
+    underTest = new SonarLintClientsRegistry(issueChangeEventsDistributor, ruleEventsDistributor, permissionsValidator);
   }
 
   @Test
@@ -154,7 +158,7 @@ public class SonarLintClientsRegistryTest {
   }
 
   @Test
-  public void listen_givenUserNotPermittedToReceiveEvent_closeConnection() {
+  public void listen_givenUserNotPermittedToReceiveRuleSetChangedEvent_closeConnection() {
     RuleChange[] activatedRules = {};
     String[] deactivatedRules = {"repo:rule-key"};
     RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules, "java");
@@ -168,6 +172,20 @@ public class SonarLintClientsRegistryTest {
     verify(sonarLintClient).close();
   }
 
+  @Test
+  public void listen_givenUserNotPermittedToReceiveIssueChangeEvent_closeConnection() {
+    Issue[] issues = new Issue[]{ new Issue("issue-1", "branch-1")};
+    IssueChangedEvent issueChangedEvent = new IssueChangedEvent("project1", issues, true, "BLOCKER", "BUG");
+
+    SonarLintClient sonarLintClient = createSampleSLClient();
+    underTest.registerClient(sonarLintClient);
+    doThrow(new ForbiddenException("Access forbidden")).when(permissionsValidator).validateUserCanReceivePushEventForProjects(anyString(), anySet());
+
+    underTest.listen(issueChangedEvent);
+
+    verify(sonarLintClient).close();
+  }
+
   @Test
   public void listen_givenUnregisteredClient_closeConnection() throws IOException {
     RuleChange[] activatedRules = {};
@@ -192,26 +210,39 @@ public class SonarLintClientsRegistryTest {
   public void registerClient_whenCalledFirstTime_registerAlsoToListenToEvents() {
     underTest.registerClient(createSampleSLClient());
 
-    verify(eventsDistributor).subscribe(underTest);
+    verify(ruleEventsDistributor).subscribe(underTest);
+    verify(issueChangeEventsDistributor).subscribe(underTest);
   }
 
   @Test
   public void registerClient_whenCalledSecondTime_doNotRegisterToEvents() {
     underTest.registerClient(createSampleSLClient());
-    clearInvocations(eventsDistributor);
+    clearInvocations(ruleEventsDistributor);
+    clearInvocations(issueChangeEventsDistributor);
+
+    underTest.registerClient(createSampleSLClient());
+    verifyNoInteractions(ruleEventsDistributor);
+    verifyNoInteractions(issueChangeEventsDistributor);
+  }
+
+  @Test
+  public void registerClient_whenExceptionAndCalledSecondTime_registerToRuleEvents() {
+    doThrow(new RuntimeException()).when(ruleEventsDistributor).subscribe(any());
+    underTest.registerClient(createSampleSLClient());
+    clearInvocations(ruleEventsDistributor);
 
     underTest.registerClient(createSampleSLClient());
-    verifyNoInteractions(eventsDistributor);
+    verify(ruleEventsDistributor).subscribe(underTest);
   }
 
   @Test
-  public void registerClient_whenExceptionAndCalledSecondTime_registerToEvents() {
-    doThrow(new RuntimeException()).when(eventsDistributor).subscribe(any());
+  public void registerClient_whenExceptionAndCalledSecondTime_registerToIssueChangeEvents() {
+    doThrow(new RuntimeException()).when(issueChangeEventsDistributor).subscribe(any());
     underTest.registerClient(createSampleSLClient());
-    clearInvocations(eventsDistributor);
+    clearInvocations(issueChangeEventsDistributor);
 
     underTest.registerClient(createSampleSLClient());
-    verify(eventsDistributor).subscribe(underTest);
+    verify(issueChangeEventsDistributor).subscribe(underTest);
   }
 
   private SonarLintClient createSampleSLClient() {
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/qualityprofile/builtin/QualityProfileChangeEventServiceImplTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/qualityprofile/builtin/QualityProfileChangeEventServiceImplTest.java
deleted file mode 100644 (file)
index a67e8de..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.qualityprofile.builtin;
-
-import java.util.Collection;
-import java.util.Collections;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.core.util.ParamChange;
-import org.sonar.core.util.RuleChange;
-import org.sonar.core.util.RuleSetChangedEvent;
-import org.sonar.db.DbTester;
-import org.sonar.db.project.ProjectDto;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.qualityprofile.QualityProfileTesting;
-import org.sonar.db.rule.RuleDto;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventServiceImpl;
-import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor;
-import org.sonar.server.qualityprofile.ActiveRuleChange;
-
-import static java.util.List.of;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.tuple;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
-import static org.sonar.db.rule.RuleTesting.newCustomRule;
-import static org.sonar.db.rule.RuleTesting.newTemplateRule;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
-
-public class QualityProfileChangeEventServiceImplTest {
-
-  @Rule
-  public DbTester db = DbTester.create();
-
-  RuleActivatorEventsDistributor eventsDistributor = mock(RuleActivatorEventsDistributor.class);
-
-  public final QualityProfileChangeEventServiceImpl underTest = new QualityProfileChangeEventServiceImpl(db.getDbClient(), eventsDistributor);
-
-  @Test
-  public void distributeRuleChangeEvent() {
-    QProfileDto qualityProfileDto = QualityProfileTesting.newQualityProfileDto();
-
-    // Template rule
-    RuleDto templateRule = newTemplateRule(RuleKey.of("xoo", "template-key"));
-    db.rules().insert(templateRule);
-    // Custom rule
-    RuleDto rule1 = newCustomRule(templateRule)
-      .setLanguage("xoo")
-      .setRepositoryKey("repo")
-      .setRuleKey("ruleKey")
-      .setDescriptionFormat(RuleDto.Format.MARKDOWN)
-      .addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", "<div>line1\nline2</div>"));
-    db.rules().insert(rule1);
-
-    ActiveRuleDto activeRuleDto = ActiveRuleDto.createFor(qualityProfileDto, rule1);
-
-    ActiveRuleChange activeRuleChange = new ActiveRuleChange(ACTIVATED, activeRuleDto, rule1);
-    activeRuleChange.setParameter("paramChangeKey", "paramChangeValue");
-
-    Collection<QProfileDto> profiles = Collections.singleton(qualityProfileDto);
-
-    ProjectDto project = db.components().insertPrivateProjectDto();
-    db.qualityProfiles().associateWithProject(project, qualityProfileDto);
-
-    underTest.distributeRuleChangeEvent(profiles, of(activeRuleChange), "xoo");
-
-    ArgumentCaptor<RuleSetChangedEvent> eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class);
-    verify(eventsDistributor).pushEvent(eventCaptor.capture());
-
-    RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue();
-    assertThat(ruleSetChangedEvent).isNotNull();
-    assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent,
-        RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects)
-      .containsExactly("RuleSetChanged", "xoo", new String[]{project.getKey()});
-
-    assertThat(ruleSetChangedEvent.getActivatedRules())
-      .extracting(RuleChange::getKey, RuleChange::getLanguage,
-        RuleChange::getSeverity, RuleChange::getTemplateKey)
-      .containsExactly(tuple("repo:ruleKey", "xoo", null, "xoo:template-key"));
-
-    assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1);
-    ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0];
-    assertThat(actualParamChange)
-      .extracting(ParamChange::getKey, ParamChange::getValue)
-      .containsExactly("paramChangeKey", "paramChangeValue");
-
-    assertThat(ruleSetChangedEvent.getDeactivatedRules()).isEmpty();
-
-  }
-
-  @Test
-  public void publishRuleActivationToSonarLintClients() {
-    ProjectDto projectDao = new ProjectDto();
-    QProfileDto activatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
-    activatedQualityProfile.setLanguage("xoo");
-    db.qualityProfiles().insert(activatedQualityProfile);
-    RuleDto rule1 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo").setRuleKey("ruleKey"));
-    RuleParamDto rule1Param = db.rules().insertRuleParam(rule1);
-
-    ActiveRuleDto activeRule1 = db.qualityProfiles().activateRule(activatedQualityProfile, rule1);
-    ActiveRuleParamDto activeRuleParam1 = ActiveRuleParamDto.createFor(rule1Param).setValue(randomAlphanumeric(20));
-    db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule1, activeRuleParam1);
-    db.getSession().commit();
-
-    QProfileDto deactivatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
-    db.qualityProfiles().insert(deactivatedQualityProfile);
-    RuleDto rule2 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo2").setRuleKey("ruleKey2"));
-    RuleParamDto rule2Param = db.rules().insertRuleParam(rule2);
-
-    ActiveRuleDto activeRule2 = db.qualityProfiles().activateRule(deactivatedQualityProfile, rule2);
-    ActiveRuleParamDto activeRuleParam2 = ActiveRuleParamDto.createFor(rule2Param).setValue(randomAlphanumeric(20));
-    db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule2, activeRuleParam2);
-    db.getSession().commit();
-
-    underTest.publishRuleActivationToSonarLintClients(projectDao, activatedQualityProfile, deactivatedQualityProfile);
-
-    ArgumentCaptor<RuleSetChangedEvent> eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class);
-    verify(eventsDistributor).pushEvent(eventCaptor.capture());
-
-    RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue();
-    assertThat(ruleSetChangedEvent).isNotNull();
-    assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent,
-        RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects)
-      .containsExactly("RuleSetChanged", "xoo", new String[]{null});
-
-    // activated rule
-    assertThat(ruleSetChangedEvent.getActivatedRules())
-      .extracting(RuleChange::getKey, RuleChange::getLanguage,
-        RuleChange::getSeverity, RuleChange::getTemplateKey)
-      .containsExactly(tuple("repo:ruleKey", "xoo", rule1.getSeverityString(), null));
-
-    assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1);
-    ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0];
-    assertThat(actualParamChange)
-      .extracting(ParamChange::getKey, ParamChange::getValue)
-      .containsExactly(activeRuleParam1.getKey(), activeRuleParam1.getValue());
-
-    // deactivated rule
-    assertThat(ruleSetChangedEvent.getDeactivatedRules())
-      .containsExactly("repo2:ruleKey2");
-  }
-
-}
index 9a170755d843820c8f2996583dda59fb4e0caeb0..c45a8e54d475c8ef9402d8dd1c570e20c9f25531 100644 (file)
@@ -70,6 +70,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
 import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer;
 import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.pushapi.issues.IssueChangeEventService;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Issues;
 
@@ -117,6 +118,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SET_TYPE;
 public class BulkChangeAction implements IssuesWsAction {
 
   private static final Logger LOG = Loggers.get(BulkChangeAction.class);
+  private static final List<String> ACTIONS_TO_DISTRIBUTE = List.of(SET_SEVERITY_KEY, SET_TYPE_KEY, DO_TRANSITION_KEY);
 
   private final System2 system2;
   private final UserSession userSession;
@@ -126,10 +128,12 @@ public class BulkChangeAction implements IssuesWsAction {
   private final List<Action> actions;
   private final IssueChangePostProcessor issueChangePostProcessor;
   private final IssuesChangesNotificationSerializer notificationSerializer;
+  private final IssueChangeEventService issueChangeEventService;
 
   public BulkChangeAction(System2 system2, UserSession userSession, DbClient dbClient, WebIssueStorage issueStorage,
-                          NotificationManager notificationService, List<Action> actions,
-                          IssueChangePostProcessor issueChangePostProcessor, IssuesChangesNotificationSerializer notificationSerializer) {
+    NotificationManager notificationService, List<Action> actions,
+    IssueChangePostProcessor issueChangePostProcessor, IssuesChangesNotificationSerializer notificationSerializer,
+    IssueChangeEventService issueChangeEventService) {
     this.system2 = system2;
     this.userSession = userSession;
     this.dbClient = dbClient;
@@ -138,6 +142,7 @@ public class BulkChangeAction implements IssuesWsAction {
     this.actions = actions;
     this.issueChangePostProcessor = issueChangePostProcessor;
     this.notificationSerializer = notificationSerializer;
+    this.issueChangeEventService = issueChangeEventService;
   }
 
   @Override
@@ -217,6 +222,7 @@ public class BulkChangeAction implements IssuesWsAction {
     UserDto author = dbClient.userDao().selectByUuid(dbSession, authorUuid);
     checkState(author != null, "User with uuid '%s' does not exist");
     sendNotification(items, bulkChangeData, userDtoByUuid, author);
+    distributeEvents(items, bulkChangeData);
 
     return result;
   }
@@ -285,6 +291,28 @@ public class BulkChangeAction implements IssuesWsAction {
     notificationService.scheduleForSending(notificationSerializer.serialize(builder));
   }
 
+  private void distributeEvents(Collection<DefaultIssue> issues, BulkChangeData bulkChangeData) {
+    boolean anyActionToDistribute = bulkChangeData.availableActions
+      .stream()
+      .anyMatch(a -> ACTIONS_TO_DISTRIBUTE.contains(a.key()));
+
+    if (!anyActionToDistribute) {
+      return;
+    }
+
+    Set<DefaultIssue> changedIssues = issues.stream()
+      // should not happen but filter it out anyway to avoid NPE in oldestUpdateDate call below
+      .filter(issue -> issue.updateDate() != null)
+      .filter(Objects::nonNull)
+      .collect(toSet(issues.size()));
+
+    if (changedIssues.isEmpty()) {
+      return;
+    }
+
+    issueChangeEventService.distributeIssueChangeEvent(issues, bulkChangeData.projectsByUuid, bulkChangeData.branchesByProjectUuid);
+  }
+
   @CheckForNull
   private ChangedIssue toNotification(BulkChangeData bulkChangeData, Map<String, UserDto> userDtoByUuid, DefaultIssue issue) {
     BranchDto branchDto = bulkChangeData.branchesByProjectUuid.get(issue.projectUuid());
@@ -366,7 +394,7 @@ public class BulkChangeAction implements IssuesWsAction {
         issues.stream().map(DefaultIssue::componentUuid).collect(MoreCollectors.toSet())).stream()
         .collect(uniqueIndex(ComponentDto::uuid, identity()));
       this.rulesByKey = dbClient.ruleDao().selectByKeys(dbSession,
-        issues.stream().map(DefaultIssue::ruleKey).collect(MoreCollectors.toSet())).stream()
+          issues.stream().map(DefaultIssue::ruleKey).collect(MoreCollectors.toSet())).stream()
         .collect(uniqueIndex(RuleDto::getKey, identity()));
 
       this.availableActions = actions.stream()
index cfd1f7180172543aa19a78ee812747633b267f48..2776f28ca5e5c8331d06c45ea1db9c285e0e197f 100644 (file)
@@ -32,9 +32,11 @@ import org.sonar.core.issue.IssueChangeContext;
 import org.sonar.core.util.Uuids;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.server.issue.IssueFinder;
 import org.sonar.server.issue.TransitionService;
+import org.sonar.server.pushapi.issues.IssueChangeEventService;
 import org.sonar.server.user.UserSession;
 
 import static java.lang.String.format;
@@ -42,6 +44,7 @@ import static org.sonar.api.issue.DefaultTransitions.OPEN_AS_VULNERABILITY;
 import static org.sonar.api.issue.DefaultTransitions.RESET_AS_TO_REVIEW;
 import static org.sonar.api.issue.DefaultTransitions.RESOLVE_AS_REVIEWED;
 import static org.sonar.api.issue.DefaultTransitions.SET_AS_IN_REVIEW;
+import static org.sonar.db.component.BranchType.BRANCH;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_DO_TRANSITION;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TRANSITION;
@@ -50,16 +53,19 @@ public class DoTransitionAction implements IssuesWsAction {
 
   private final DbClient dbClient;
   private final UserSession userSession;
+  private final IssueChangeEventService issueChangeEventService;
   private final IssueFinder issueFinder;
   private final IssueUpdater issueUpdater;
   private final TransitionService transitionService;
   private final OperationResponseWriter responseWriter;
   private final System2 system2;
 
-  public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService,
+  public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueChangeEventService issueChangeEventService,
+    IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService,
     OperationResponseWriter responseWriter, System2 system2) {
     this.dbClient = dbClient;
     this.userSession = userSession;
+    this.issueChangeEventService = issueChangeEventService;
     this.issueFinder = issueFinder;
     this.issueUpdater = issueUpdater;
     this.transitionService = transitionService;
@@ -110,7 +116,14 @@ public class DoTransitionAction implements IssuesWsAction {
     IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid());
     transitionService.checkTransitionPermission(transitionKey, defaultIssue);
     if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
-      return issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, true);
+      BranchDto branch = issueUpdater.getBranch(session, defaultIssue, defaultIssue.projectUuid());
+      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, true, branch);
+
+      if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(defaultIssue.projectUuid()) != null) {
+        issueChangeEventService.distributeIssueChangeEvent(defaultIssue, null, null, transitionKey, branch,
+          response.getComponentByUuid(defaultIssue.projectUuid()).getKey());
+      }
+      return response;
     }
     return new SearchResponseData(issueDto);
   }
index dee115bf149222745bde0951c1537ad3fb899238..494c9bd4828d01365ddfc06fd6669b10d23925ea 100644 (file)
@@ -70,10 +70,15 @@ public class IssueUpdater {
 
   public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue,
     IssueChangeContext context, boolean refreshMeasures) {
+    BranchDto branch = getBranch(dbSession, issue, issue.projectUuid());
+    return saveIssueAndPreloadSearchResponseData(dbSession, issue, context, refreshMeasures, branch);
+  }
+
+  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue,
+    IssueChangeContext context, boolean refreshMeasures, BranchDto branch) {
 
     Optional<RuleDto> rule = getRuleByKey(dbSession, issue.getRuleKey());
     ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid());
-    BranchDto branch = getBranch(dbSession, issue, issue.projectUuid());
     ComponentDto component = getComponent(dbSession, issue, issue.componentUuid());
     IssueDto issueDto = doSaveIssue(dbSession, issue, context, rule.orElse(null), project, branch);
 
@@ -90,6 +95,14 @@ public class IssueUpdater {
     return result;
   }
 
+  protected BranchDto getBranch(DbSession dbSession, DefaultIssue issue, @Nullable String projectUuid) {
+    String issueKey = issue.key();
+    checkState(projectUuid != null, "Issue '%s' has no project", issueKey);
+    BranchDto component = dbClient.branchDao().selectByUuid(dbSession, projectUuid).orElse(null);
+    checkState(component != null, "Branch uuid '%s' for issue key '%s' cannot be found", projectUuid, issueKey);
+    return component;
+  }
+
   private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context,
     @Nullable RuleDto ruleDto, ComponentDto project, BranchDto branchDto) {
     IssueDto issueDto = issueStorage.save(session, singletonList(issue)).iterator().next();
@@ -137,14 +150,6 @@ public class IssueUpdater {
     return component;
   }
 
-  private BranchDto getBranch(DbSession dbSession, DefaultIssue issue, @Nullable String projectUuid) {
-    String issueKey = issue.key();
-    checkState(projectUuid != null, "Issue '%s' has no project", issueKey);
-    BranchDto component = dbClient.branchDao().selectByUuid(dbSession, projectUuid).orElse(null);
-    checkState(component != null, "Branch uuid '%s' for issue key '%s' cannot be found", projectUuid, issueKey);
-    return component;
-  }
-
   private Optional<RuleDto> getRuleByKey(DbSession session, RuleKey ruleKey) {
     Optional<RuleDto> rule = dbClient.ruleDao().selectByKey(session, ruleKey);
     return (rule.isPresent() && rule.get().getStatus() != RuleStatus.REMOVED) ? rule : Optional.empty();
index 1b998ed234c3d8507c1c8c959654679c98c81aca..0150ad3196257120da8dddaa3f569f4a37241320 100644 (file)
@@ -31,12 +31,15 @@ import org.sonar.core.issue.IssueChangeContext;
 import org.sonar.core.util.Uuids;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.pushapi.issues.IssueChangeEventService;
 import org.sonar.server.user.UserSession;
 
 import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
+import static org.sonar.db.component.BranchType.BRANCH;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_SEVERITY;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SEVERITY;
@@ -45,15 +48,18 @@ public class SetSeverityAction implements IssuesWsAction {
 
   private final UserSession userSession;
   private final DbClient dbClient;
+  private final IssueChangeEventService issueChangeEventService;
   private final IssueFinder issueFinder;
   private final IssueFieldsSetter issueFieldsSetter;
   private final IssueUpdater issueUpdater;
   private final OperationResponseWriter responseWriter;
 
-  public SetSeverityAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater,
+  public SetSeverityAction(UserSession userSession, DbClient dbClient, IssueChangeEventService issueChangeEventService,
+    IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater,
     OperationResponseWriter responseWriter) {
     this.userSession = userSession;
     this.dbClient = dbClient;
+    this.issueChangeEventService = issueChangeEventService;
     this.issueFinder = issueFinder;
     this.issueFieldsSetter = issueFieldsSetter;
     this.issueUpdater = issueUpdater;
@@ -106,7 +112,14 @@ public class SetSeverityAction implements IssuesWsAction {
 
     IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getUuid());
     if (issueFieldsSetter.setManualSeverity(issue, severity, context)) {
-      return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true);
+      BranchDto branch = issueUpdater.getBranch(session, issue, issue.projectUuid());
+      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true, branch);
+
+      if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) {
+        issueChangeEventService.distributeIssueChangeEvent(issue, severity, null, null,
+          branch, response.getComponentByUuid(issue.projectUuid()).getKey());
+      }
+      return response;
     }
     return new SearchResponseData(issueDto);
   }
index 3e08765ee6ad4599eb7b9f59ee9d2b578bec3d29..aee2c8a7fb446996b24954cae37b654843f50760 100644 (file)
@@ -33,12 +33,15 @@ import org.sonar.core.issue.IssueChangeContext;
 import org.sonar.core.util.Uuids;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.pushapi.issues.IssueChangeEventService;
 import org.sonar.server.user.UserSession;
 
 import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
+import static org.sonar.db.component.BranchType.BRANCH;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_TYPE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TYPE;
@@ -48,16 +51,18 @@ public class SetTypeAction implements IssuesWsAction {
 
   private final UserSession userSession;
   private final DbClient dbClient;
+  private final IssueChangeEventService issueChangeEventService;
   private final IssueFinder issueFinder;
   private final IssueFieldsSetter issueFieldsSetter;
   private final IssueUpdater issueUpdater;
   private final OperationResponseWriter responseWriter;
   private final System2 system2;
 
-  public SetTypeAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater,
-    OperationResponseWriter responseWriter, System2 system2) {
+  public SetTypeAction(UserSession userSession, DbClient dbClient, IssueChangeEventService issueChangeEventService, IssueFinder issueFinder,
+    IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, OperationResponseWriter responseWriter, System2 system2) {
     this.userSession = userSession;
     this.dbClient = dbClient;
+    this.issueChangeEventService = issueChangeEventService;
     this.issueFinder = issueFinder;
     this.issueFieldsSetter = issueFieldsSetter;
     this.issueUpdater = issueUpdater;
@@ -112,7 +117,13 @@ public class SetTypeAction implements IssuesWsAction {
 
     IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid());
     if (issueFieldsSetter.setType(issue, ruleType, context)) {
-      return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true);
+      BranchDto branch = issueUpdater.getBranch(session, issue, issue.projectUuid());
+      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true, branch);
+      if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) {
+        issueChangeEventService.distributeIssueChangeEvent(issue, null, ruleType.name(), null, branch,
+          response.getComponentByUuid(issue.projectUuid()).getKey());
+      }
+      return response;
     }
     return new SearchResponseData(issueDto);
   }
index 2d671139ae426c05b28dff68cc59e15f97397f28..d9115d8fd0c6c0d4334b332a7225e38844a2f1e0 100644 (file)
@@ -51,7 +51,6 @@ import static org.sonar.db.rule.RuleTesting.newRule;
 
 public class TransitionActionTest {
 
-
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
index 9797f60a958392c15e96102506926b3dc2933519..f4ff321ce5b404c09d2f74f7c715d0e1ac9ff7ff 100644 (file)
@@ -59,6 +59,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer;
 import org.sonar.server.issue.workflow.FunctionExecutor;
 import org.sonar.server.issue.workflow.IssueWorkflow;
 import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.pushapi.issues.IssueChangeEventService;
 import org.sonar.server.rule.DefaultRuleFinder;
 import org.sonar.server.rule.RuleDescriptionFormatter;
 import org.sonar.server.tester.UserSessionRule;
@@ -75,6 +76,7 @@ import static java.util.Optional.ofNullable;
 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.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
@@ -113,6 +115,7 @@ public class BulkChangeActionTest {
 
   private DbClient dbClient = db.getDbClient();
 
+  private IssueChangeEventService issueChangeEventService = mock(IssueChangeEventService.class);
   private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter();
   private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
   private WebIssueStorage issueStorage = new WebIssueStorage(system2, dbClient,
@@ -125,7 +128,7 @@ public class BulkChangeActionTest {
   private List<Action> actions = new ArrayList<>();
 
   private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions,
-    issueChangePostProcessor, issuesChangesSerializer));
+    issueChangePostProcessor, issuesChangesSerializer, issueChangeEventService));
 
   @Before
   public void setUp() {
@@ -155,6 +158,7 @@ public class BulkChangeActionTest {
     assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
 
     verifyPostProcessorCalled(file);
+    verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any());
   }
 
   @Test
@@ -179,6 +183,7 @@ public class BulkChangeActionTest {
     assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
 
     verifyPostProcessorCalled(file);
+    verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any());
   }
 
   @Test
@@ -204,6 +209,7 @@ public class BulkChangeActionTest {
 
     // no need to refresh measures
     verifyPostProcessorNotCalled();
+    verifyNoInteractions(issueChangeEventService);
   }
 
   @Test
@@ -230,6 +236,7 @@ public class BulkChangeActionTest {
 
     // no need to refresh measures
     verifyPostProcessorNotCalled();
+    verifyNoInteractions(issueChangeEventService);
   }
 
   @Test
@@ -255,6 +262,7 @@ public class BulkChangeActionTest {
     assertThat(issueComment.getChangeData()).isEqualTo("type was badly defined");
 
     verifyPostProcessorCalled(file);
+    verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any());
   }
 
   @Test
@@ -290,6 +298,7 @@ public class BulkChangeActionTest {
         tuple(issue3.getKey(), userToAssign.getUuid(), VULNERABILITY.getDbConstant(), MINOR, NOW));
 
     verifyPostProcessorCalled(file);
+    verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any());
   }
 
   @Test
index 8fefd3eaa062bf7124fbb00b7d692785ee4bf15c..babaff80e6065460f715232be97c9a9b21938484 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.api.utils.System2;
 import org.sonar.core.util.SequenceUuidFactory;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.rule.RuleDto;
@@ -50,6 +51,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer;
 import org.sonar.server.issue.workflow.FunctionExecutor;
 import org.sonar.server.issue.workflow.IssueWorkflow;
 import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.pushapi.issues.IssueChangeEventService;
 import org.sonar.server.rule.DefaultRuleFinder;
 import org.sonar.server.rule.RuleDescriptionFormatter;
 import org.sonar.server.tester.UserSessionRule;
@@ -64,12 +66,15 @@ 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.issue.Issue.STATUS_CONFIRMED;
 import static org.sonar.api.issue.Issue.STATUS_OPEN;
+import static org.sonar.api.rule.Severity.MAJOR;
 import static org.sonar.api.rules.RuleType.CODE_SMELL;
 import static org.sonar.api.web.UserRole.CODEVIEWER;
 import static org.sonar.api.web.UserRole.USER;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.issue.IssueTesting.newIssue;
 
 public class DoTransitionActionTest {
 
@@ -77,7 +82,6 @@ public class DoTransitionActionTest {
 
   private System2 system2 = new TestSystem2().setNow(NOW);
 
-
   @Rule
   public DbTester db = DbTester.create(system2);
 
@@ -89,6 +93,7 @@ public class DoTransitionActionTest {
 
   private DbClient dbClient = db.getDbClient();
 
+  private IssueChangeEventService issueChangeEventService = mock(IssueChangeEventService.class);
   private IssueFieldsSetter updater = new IssueFieldsSetter();
   private IssueWorkflow workflow = new IssueWorkflow(new FunctionExecutor(updater), updater);
   private TransitionService transitionService = new TransitionService(userSession, workflow);
@@ -101,7 +106,8 @@ public class DoTransitionActionTest {
     mock(NotificationManager.class), issueChangePostProcessor, issuesChangesSerializer);
   private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
 
-  private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2);
+  private WsAction underTest = new DoTransitionAction(dbClient, userSession, issueChangeEventService,
+    new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2);
   private WsActionTester tester = new WsActionTester(underTest);
 
   @Before
@@ -121,11 +127,31 @@ public class DoTransitionActionTest {
 
     verify(responseWriter).write(eq(issue.getKey()), preloadedSearchResponseDataCaptor.capture(), any(Request.class), any(Response.class));
     verifyContentOfPreloadedSearchResponseData(issue);
+    verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any(), any(), any(), any());
     IssueDto issueReloaded = db.getDbClient().issueDao().selectByKey(db.getSession(), issue.getKey()).get();
     assertThat(issueReloaded.getStatus()).isEqualTo(STATUS_CONFIRMED);
     assertThat(issueChangePostProcessor.calledComponents()).containsExactlyInAnyOrder(file);
   }
 
+  @Test
+  public void do_transition_is_not_distributed_for_pull_request() {
+    RuleDto rule = db.rules().insertIssueRule();
+    ComponentDto project = db.components().insertPrivateProject();
+
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setKey("myBranch1")
+      .setBranchType(BranchType.PULL_REQUEST)
+      .setMergeBranchUuid(project.uuid()));
+
+    ComponentDto file = db.components().insertComponent(newFileDto(pullRequest));
+    IssueDto issue = newIssue(rule, pullRequest, file).setType(CODE_SMELL).setSeverity(MAJOR);
+    db.issues().insertIssue(issue);
+    userSession.logIn(db.users().insertUser()).addProjectPermission(USER, pullRequest, file);
+
+    call(issue.getKey(), "confirm");
+
+    verifyNoInteractions(issueChangeEventService);
+  }
+
   @Test
   public void fail_if_external_issue() {
     ComponentDto project = db.components().insertPrivateProject();
index e334d80a7a373a0f0a282b806932c96b7cc2f3ed..565fb7fd3e8202371d3e0ba5122caa582ea5b0d3 100644 (file)
@@ -68,7 +68,6 @@ public class IssueUpdaterTest {
 
   private System2 system2 = mock(System2.class);
 
-
   @Rule
   public DbTester db = DbTester.create(system2);
 
index 3936efae996a40819cc1269dc1757f8e3046f643..fc7c97915774d2638e61ea571530adcf0dd2f063 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.core.issue.FieldDiffs;
 import org.sonar.core.util.SequenceUuidFactory;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDbTester;
 import org.sonar.db.issue.IssueDto;
@@ -49,6 +50,7 @@ import org.sonar.server.issue.index.IssueIndexer;
 import org.sonar.server.issue.index.IssueIteratorFactory;
 import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer;
 import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.pushapi.issues.IssueChangeEventService;
 import org.sonar.server.rule.DefaultRuleFinder;
 import org.sonar.server.rule.RuleDescriptionFormatter;
 import org.sonar.server.tester.UserSessionRule;
@@ -63,10 +65,14 @@ 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.MAJOR;
 import static org.sonar.api.rule.Severity.MINOR;
+import static org.sonar.api.rules.RuleType.CODE_SMELL;
 import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
 import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.issue.IssueTesting.newIssue;
 
 public class SetSeverityActionTest {
 
@@ -84,10 +90,12 @@ public class SetSeverityActionTest {
   private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class);
   private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
 
+  private IssueChangeEventService issueChangeEventService = mock(IssueChangeEventService.class);
   private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), null);
   private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor();
   private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer();
-  private WsActionTester tester = new WsActionTester(new SetSeverityAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
+  private WsActionTester tester = new WsActionTester(new SetSeverityAction(userSession, dbClient, issueChangeEventService,
+    new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
     new IssueUpdater(dbClient,
       new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, mock(RuleDescriptionFormatter.class)), issueIndexer, new SequenceUuidFactory()),
       mock(NotificationManager.class), issueChangePostProcessor, issuesChangesSerializer),
@@ -102,6 +110,7 @@ public class SetSeverityActionTest {
 
     verify(responseWriter).write(eq(issueDto.getKey()), preloadedSearchResponseDataCaptor.capture(), any(Request.class), any(Response.class));
     verifyContentOfPreloadedSearchResponseData(issueDto);
+    verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any(), any(), any(), any());
 
     IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get();
     assertThat(issueReloaded.getSeverity()).isEqualTo(MINOR);
@@ -111,6 +120,26 @@ public class SetSeverityActionTest {
       .containsExactlyInAnyOrder(issueDto.getComponentUuid());
   }
 
+  @Test
+  public void set_severity_is_not_distributed_for_pull_request() {
+    RuleDto rule = dbTester.rules().insertIssueRule();
+    ComponentDto project = dbTester.components().insertPrivateProject();
+
+    ComponentDto pullRequest = dbTester.components().insertProjectBranch(project, b -> b.setKey("myBranch1")
+      .setBranchType(BranchType.PULL_REQUEST)
+      .setMergeBranchUuid(project.uuid()));
+
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(pullRequest));
+    IssueDto issue = newIssue(rule, pullRequest, file).setType(CODE_SMELL).setSeverity(MAJOR);
+    issueDbTester.insertIssue(issue);
+
+    setUserWithBrowseAndAdministerIssuePermission(issue);
+
+    call(issue.getKey(), MINOR);
+
+    verifyNoInteractions(issueChangeEventService);
+  }
+
   @Test
   public void insert_entry_in_changelog_when_setting_severity() {
     IssueDto issueDto = issueDbTester.insertIssue(i -> i.setSeverity(MAJOR));
index d4b0cfb5cabb6ce57c11cee8c1ae96bd93c2b1c0..979fd7067dcffaf83a2951339cb83042ecafe4db 100644 (file)
@@ -41,6 +41,7 @@ import org.sonar.core.issue.FieldDiffs;
 import org.sonar.core.util.SequenceUuidFactory;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDbTester;
 import org.sonar.db.issue.IssueDto;
@@ -57,6 +58,7 @@ import org.sonar.server.issue.index.IssueIndexer;
 import org.sonar.server.issue.index.IssueIteratorFactory;
 import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer;
 import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.pushapi.issues.IssueChangeEventService;
 import org.sonar.server.rule.DefaultRuleFinder;
 import org.sonar.server.rule.RuleDescriptionFormatter;
 import org.sonar.server.tester.UserSessionRule;
@@ -71,13 +73,16 @@ 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.when;
+import static org.sonar.api.rule.Severity.MAJOR;
 import static org.sonar.api.rules.RuleType.BUG;
 import static org.sonar.api.rules.RuleType.CODE_SMELL;
 import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
 import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
 import static org.sonar.api.web.UserRole.USER;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.issue.IssueTesting.newIssue;
 
 @RunWith(DataProviderRunner.class)
 public class SetTypeActionTest {
@@ -97,10 +102,12 @@ public class SetTypeActionTest {
   private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class);
   private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
 
+  private IssueChangeEventService issueChangeEventService = mock(IssueChangeEventService.class);
   private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), null);
   private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor();
   private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer();
-  private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
+  private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, issueChangeEventService,
+    new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
     new IssueUpdater(dbClient,
       new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, mock(RuleDescriptionFormatter.class)), issueIndexer, new SequenceUuidFactory()),
       mock(NotificationManager.class), issueChangePostProcessor, issuesChangesSerializer), responseWriter, system2));
@@ -124,12 +131,33 @@ public class SetTypeActionTest {
       assertThat(issueChangePostProcessor.calledComponents())
         .extracting(ComponentDto::uuid)
         .containsExactlyInAnyOrder(issueDto.getComponentUuid());
+      verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any(), any(), any(), any());
     } else {
       assertThat(issueChangePostProcessor.wasCalled())
         .isFalse();
     }
   }
 
+  @Test
+  public void set_type_is_not_distributed_for_pull_request() {
+    RuleDto rule = dbTester.rules().insertIssueRule();
+    ComponentDto project = dbTester.components().insertPrivateProject();
+
+    ComponentDto pullRequest = dbTester.components().insertProjectBranch(project, b -> b.setKey("myBranch1")
+      .setBranchType(BranchType.PULL_REQUEST)
+      .setMergeBranchUuid(project.uuid()));
+
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(pullRequest));
+    IssueDto issue = newIssue(rule, pullRequest, file).setType(CODE_SMELL).setSeverity(MAJOR);
+    issueDbTester.insertIssue(issue);
+
+    setUserWithBrowseAndAdministerIssuePermission(issue);
+
+    call(issue.getKey(), BUG.name());
+
+    verifyNoInteractions(issueChangeEventService);
+  }
+
   @Test
   public void insert_entry_in_changelog_when_setting_type() {
     IssueDto issueDto = newIssueWithProject(CODE_SMELL);
index 6ccdff45a30fc7c97cf268fd2b5caf6c896d0421..aef8a7d416cf5fecce95a14a9be40c024c7bb727 100644 (file)
@@ -184,6 +184,9 @@ import org.sonar.server.projectlink.ws.ProjectLinksModule;
 import org.sonar.server.projecttag.ws.ProjectTagsWsModule;
 import org.sonar.server.property.InternalPropertiesImpl;
 import org.sonar.server.pushapi.ServerPushWsModule;
+import org.sonar.server.pushapi.issues.DistributedIssueChangeEventsDistributor;
+import org.sonar.server.pushapi.issues.IssueChangeEventServiceImpl;
+import org.sonar.server.pushapi.issues.StandaloneIssueChangeEventsDistributor;
 import org.sonar.server.pushapi.qualityprofile.DistributedRuleActivatorEventsDistributor;
 import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventServiceImpl;
 import org.sonar.server.pushapi.qualityprofile.StandaloneRuleActivatorEventsDistributor;
@@ -285,6 +288,9 @@ public class PlatformLevel4 extends PlatformLevel {
     addIfCluster(DistributedRuleActivatorEventsDistributor.class);
     addIfStandalone(StandaloneRuleActivatorEventsDistributor.class);
 
+    addIfCluster(DistributedIssueChangeEventsDistributor.class);
+    addIfStandalone(StandaloneIssueChangeEventsDistributor.class);
+
     add(
       RuleDescriptionFormatter.class,
       ClusterVerification.class,
@@ -456,6 +462,7 @@ public class PlatformLevel4 extends PlatformLevel {
       NewIssuesNotificationHandler.newMetadata(),
       MyNewIssuesNotificationHandler.class,
       MyNewIssuesNotificationHandler.newMetadata(),
+      IssueChangeEventServiceImpl.class,
 
       // issues actions
       AssignAction.class,
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
deleted file mode 100644 (file)
index 3a50054..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.core.util;
-
-public interface RuleActivationListener {
-
-  void listen(RuleSetChangedEvent event);
-}
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
deleted file mode 100644 (file)
index 3bb3af5..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.core.util;
-
-import java.io.Serializable;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-
-public class RuleChange implements Serializable {
-  String key;
-  String language;
-  String templateKey;
-  String severity;
-  ParamChange[] params = new ParamChange[0];
-
-  public String getKey() {
-    return key;
-  }
-
-  public RuleChange setKey(String key) {
-    this.key = key;
-    return this;
-  }
-
-  @CheckForNull
-  public String getLanguage() {
-    return language;
-  }
-
-  public RuleChange setLanguage(@Nullable String language) {
-    this.language = language;
-    return this;
-  }
-
-  public String getTemplateKey() {
-    return templateKey;
-  }
-
-  public RuleChange setTemplateKey(String templateKey) {
-    this.templateKey = templateKey;
-    return this;
-  }
-
-  @CheckForNull
-  public String getSeverity() {
-    return severity;
-  }
-
-  public RuleChange setSeverity(@Nullable String severity) {
-    this.severity = severity;
-    return this;
-  }
-
-  public ParamChange[] getParams() {
-    return params;
-  }
-
-  public RuleChange setParams(ParamChange[] params) {
-    this.params = params;
-    return this;
-  }
-}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/RuleSetChangedEvent.java b/sonar-core/src/main/java/org/sonar/core/util/RuleSetChangedEvent.java
deleted file mode 100644 (file)
index a37b763..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.core.util;
-
-import java.io.Serializable;
-
-public class RuleSetChangedEvent implements Serializable {
-
-  private static final String EVENT = "RuleSetChanged";
-
-  private final String[] projects;
-  private final String language;
-  private final RuleChange[] activatedRules;
-  private final String[] deactivatedRules;
-
-  public RuleSetChangedEvent(String[] projects, RuleChange[] activatedRules, String[] deactivatedRules, String language) {
-    this.projects = projects;
-    this.activatedRules = activatedRules;
-    this.deactivatedRules = deactivatedRules;
-    if (activatedRules.length == 0 && deactivatedRules.length == 0) {
-      throw new IllegalArgumentException("Can't create RuleSetChangedEvent without any rules that have changed");
-    }
-    this.language = language;
-  }
-
-  public String getEvent() {
-    return EVENT;
-  }
-
-  public String[] getProjects() {
-    return projects;
-  }
-
-  public String getLanguage() {
-    return language;
-  }
-
-  public RuleChange[] getActivatedRules() {
-    return activatedRules;
-  }
-
-  public String[] getDeactivatedRules() {
-    return deactivatedRules;
-  }
-}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/issue/Issue.java b/sonar-core/src/main/java/org/sonar/core/util/issue/Issue.java
new file mode 100644 (file)
index 0000000..d662f26
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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.issue;
+
+import java.io.Serializable;
+
+public class Issue implements Serializable {
+  private String issueKey;
+  private String branchName;
+
+  public Issue(String issueKey, String branchName) {
+    this.issueKey = issueKey;
+    this.branchName = branchName;
+  }
+
+  public String getIssueKey() {
+    return issueKey;
+  }
+
+  public String getBranchName() {
+    return branchName;
+  }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangeListener.java b/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangeListener.java
new file mode 100644 (file)
index 0000000..6c1bcfe
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.util.issue;
+
+public interface IssueChangeListener {
+  void listen(IssueChangedEvent event);
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangedEvent.java b/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangedEvent.java
new file mode 100644 (file)
index 0000000..0d7c8af
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.util.issue;
+
+import java.io.Serializable;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class IssueChangedEvent implements Serializable {
+  private static final String EVENT = "IssueChangedEvent";
+
+  private final String projectKey;
+  private final Issue[] issues;
+  @CheckForNull
+  private final Boolean resolved;
+  @CheckForNull
+  private final String userSeverity;
+  @CheckForNull
+  private final String userType;
+
+  public IssueChangedEvent(String projectKey, Issue[] issues, @Nullable Boolean resolved, @Nullable String userSeverity,
+    @Nullable String userType) {
+    if (issues.length == 0) {
+      throw new IllegalArgumentException("Can't create IssueChangedEvent without any issues that have changed");
+    }
+    this.projectKey = projectKey;
+    this.issues = issues;
+    this.resolved = resolved;
+    this.userSeverity = userSeverity;
+    this.userType = userType;
+  }
+
+  public String getEvent() {
+    return EVENT;
+  }
+
+  public String getProjectKey() {
+    return projectKey;
+  }
+
+  public Issue[] getIssues() {
+    return issues;
+  }
+
+  @CheckForNull
+  public Boolean getResolved() {
+    return resolved;
+  }
+
+  @CheckForNull
+  public String getUserSeverity() {
+    return userSeverity;
+  }
+
+  @CheckForNull
+  public String getUserType() {
+    return userType;
+  }
+
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/rule/RuleActivationListener.java b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleActivationListener.java
new file mode 100644 (file)
index 0000000..56f3df1
--- /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.rule;
+
+public interface RuleActivationListener {
+
+  void listen(RuleSetChangedEvent event);
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/rule/RuleChange.java b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleChange.java
new file mode 100644 (file)
index 0000000..131a0da
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * 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.rule;
+
+import java.io.Serializable;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.core.util.ParamChange;
+
+public class RuleChange implements Serializable {
+  private String key;
+  private String language;
+  private String templateKey;
+  private String severity;
+  private ParamChange[] params = new ParamChange[0];
+
+  public String getKey() {
+    return key;
+  }
+
+  public RuleChange setKey(String key) {
+    this.key = key;
+    return this;
+  }
+
+  @CheckForNull
+  public String getLanguage() {
+    return language;
+  }
+
+  public RuleChange setLanguage(@Nullable String language) {
+    this.language = language;
+    return this;
+  }
+
+  public String getTemplateKey() {
+    return templateKey;
+  }
+
+  public RuleChange setTemplateKey(String templateKey) {
+    this.templateKey = templateKey;
+    return this;
+  }
+
+  @CheckForNull
+  public String getSeverity() {
+    return severity;
+  }
+
+  public RuleChange setSeverity(@Nullable String severity) {
+    this.severity = severity;
+    return this;
+  }
+
+  public ParamChange[] getParams() {
+    return params;
+  }
+
+  public RuleChange setParams(ParamChange[] params) {
+    this.params = params;
+    return this;
+  }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/util/rule/RuleSetChangedEvent.java b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleSetChangedEvent.java
new file mode 100644 (file)
index 0000000..ad16db9
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.util.rule;
+
+import java.io.Serializable;
+
+public class RuleSetChangedEvent implements Serializable {
+
+  private static final String EVENT = "RuleSetChanged";
+
+  private final String[] projects;
+  private final String language;
+  private final RuleChange[] activatedRules;
+  private final String[] deactivatedRules;
+
+  public RuleSetChangedEvent(String[] projects, RuleChange[] activatedRules, String[] deactivatedRules, String language) {
+    this.projects = projects;
+    this.activatedRules = activatedRules;
+    this.deactivatedRules = deactivatedRules;
+    if (activatedRules.length == 0 && deactivatedRules.length == 0) {
+      throw new IllegalArgumentException("Can't create RuleSetChangedEvent without any rules that have changed");
+    }
+    this.language = language;
+  }
+
+  public String getEvent() {
+    return EVENT;
+  }
+
+  public String[] getProjects() {
+    return projects;
+  }
+
+  public String getLanguage() {
+    return language;
+  }
+
+  public RuleChange[] getActivatedRules() {
+    return activatedRules;
+  }
+
+  public String[] getDeactivatedRules() {
+    return deactivatedRules;
+  }
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/util/RuleSetChangedEventTest.java b/sonar-core/src/test/java/org/sonar/core/util/RuleSetChangedEventTest.java
deleted file mode 100644 (file)
index 174788d..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.core.util;
-
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-public class RuleSetChangedEventTest {
-
-  @Test
-  public void getLanguage_givenNoDeactivatedRules_languageIsCorrectlyIdentified() {
-    String[] projects = {"sonarqube"};
-    RuleChange[] activatedRules = {createRuleChange("java")};
-    String[] deactivatedRules = {};
-    RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");
-
-    String language = event.getLanguage();
-
-    assertThat(language).isEqualTo("java");
-  }
-
-  @Test
-  public void getLanguage_givenNoActivatedRules_languageIsCorrectlyIdentified() {
-    String[] projects = {"sonarqube"};
-    RuleChange[] activatedRules = {};
-    String[] deactivatedRules = {"ruleKey"};
-    RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");
-
-    String language = event.getLanguage();
-
-    assertThat(language).isEqualTo("java");
-  }
-
-  @Test
-  public void getLanguage_givenBothArraysEmpty_throwException() {
-    String[] projects = {"sonarqube"};
-    RuleChange[] activatedRules = {};
-    String[] deactivatedRules = {};
-
-    assertThatThrownBy(() -> new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"))
-      .isInstanceOf(IllegalArgumentException.class);
-  }
-
-  private RuleChange createRuleChange(String language) {
-    RuleChange ruleChange = new RuleChange();
-    ruleChange.setLanguage(language);
-    return ruleChange;
-  }
-}
diff --git a/sonar-core/src/test/java/org/sonar/core/util/issue/IssueChangedEventTest.java b/sonar-core/src/test/java/org/sonar/core/util/issue/IssueChangedEventTest.java
new file mode 100644 (file)
index 0000000..0693c40
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * 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.issue;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class IssueChangedEventTest {
+  private static final String BRANCH_NAME = "branch-name";
+  private static final String ISSUE_KEY = "issue-key";
+  private static final String PROJECT_KEY = "project-key";
+
+  @Test
+  public void issueChangedEvent_instantiation_accepts_nulls() {
+    Issue[] issues = new Issue[]{new Issue(ISSUE_KEY, BRANCH_NAME)};
+    IssueChangedEvent event = new IssueChangedEvent(PROJECT_KEY, issues, null, null, null);
+
+    assertThat(event.getEvent()).isEqualTo("IssueChangedEvent");
+    assertThat(event.getProjectKey()).isEqualTo(PROJECT_KEY);
+    assertThat(event.getResolved()).isNull();
+    assertThat(event.getUserSeverity()).isNull();
+    assertThat(event.getUserType()).isNull();
+    assertThat(event.getIssues()).hasSize(1);
+  }
+
+  @Test
+  public void issueChangedEvent_instantiation_accepts_actual_values() {
+    Issue[] issues = new Issue[]{new Issue(ISSUE_KEY, BRANCH_NAME)};
+    IssueChangedEvent event = new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG");
+
+    assertThat(event.getEvent()).isEqualTo("IssueChangedEvent");
+    assertThat(event.getProjectKey()).isEqualTo(PROJECT_KEY);
+    assertThat(event.getResolved()).isTrue();
+    assertThat(event.getUserSeverity()).isEqualTo("BLOCKER");
+    assertThat(event.getUserType()).isEqualTo("BUG");
+    assertThat(event.getIssues()).hasSize(1);
+  }
+
+  @Test
+  public void issueChangedEvent_instantiation_doesNotAccept_emptyIssues() {
+    Issue[] issues = new Issue[0];
+
+    assertThatThrownBy(() -> new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .withFailMessage("Can't create IssueChangedEvent without any issues that have changed");
+  }
+
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/util/issue/IssueTest.java b/sonar-core/src/test/java/org/sonar/core/util/issue/IssueTest.java
new file mode 100644 (file)
index 0000000..5066a27
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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.issue;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueTest {
+  private static final String BRANCH_NAME = "branch-name";
+  private static final String ISSUE_KEY = "issue-key";
+
+  @Test
+  public void issue_instantiation_accepts_values() {
+    Issue issue = new Issue(ISSUE_KEY, BRANCH_NAME);
+
+    assertThat(issue.getIssueKey()).isEqualTo(ISSUE_KEY);
+    assertThat(issue.getBranchName()).isEqualTo(BRANCH_NAME);
+  }
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/util/rule/RuleSetChangedEventTest.java b/sonar-core/src/test/java/org/sonar/core/util/rule/RuleSetChangedEventTest.java
new file mode 100644 (file)
index 0000000..5c1b52e
--- /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.rule;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class RuleSetChangedEventTest {
+
+  @Test
+  public void getLanguage_givenNoDeactivatedRules_languageIsCorrectlyIdentified() {
+    String[] projects = {"sonarqube"};
+    RuleChange[] activatedRules = {createRuleChange("java")};
+    String[] deactivatedRules = {};
+    RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");
+
+    String language = event.getLanguage();
+
+    assertThat(language).isEqualTo("java");
+  }
+
+  @Test
+  public void getLanguage_givenNoActivatedRules_languageIsCorrectlyIdentified() {
+    String[] projects = {"sonarqube"};
+    RuleChange[] activatedRules = {};
+    String[] deactivatedRules = {"ruleKey"};
+    RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");
+
+    String language = event.getLanguage();
+
+    assertThat(language).isEqualTo("java");
+  }
+
+  @Test
+  public void getLanguage_givenBothArraysEmpty_throwException() {
+    String[] projects = {"sonarqube"};
+    RuleChange[] activatedRules = {};
+    String[] deactivatedRules = {};
+
+    assertThatThrownBy(() -> new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"))
+      .isInstanceOf(IllegalArgumentException.class);
+  }
+
+  private RuleChange createRuleChange(String language) {
+    RuleChange ruleChange = new RuleChange();
+    ruleChange.setLanguage(language);
+    return ruleChange;
+  }
+}