aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorBelen Pruvost <belen.pruvost@sonarsource.com>2022-05-11 22:17:29 +0200
committersonartech <sonartech@sonarsource.com>2022-05-16 20:03:56 +0000
commite774fdb3e5fbb4d6153b4379fc0af5606d1fcab3 (patch)
tree050163f52efa6f4bfff54ebcacde7bfd6cc20971 /server
parent67ce5b054754e4556ad4613d34311d7408d8cb52 (diff)
downloadsonarqube-e774fdb3e5fbb4d6153b4379fc0af5606d1fcab3.tar.gz
sonarqube-e774fdb3e5fbb4d6153b4379fc0af5606d1fcab3.zip
SONAR-16372 - Push IssueChangedEvents
Diffstat (limited to 'server')
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java16
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java10
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java18
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java17
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java45
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java75
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventService.java35
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java154
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java30
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java42
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/package-info.java21
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java4
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java4
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java4
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java94
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java4
-rw-r--r--server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java105
-rw-r--r--server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java48
-rw-r--r--server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java62
-rw-r--r--server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java226
-rw-r--r--server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java41
-rw-r--r--server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java (renamed from server/sonar-webserver-pushapi/src/test/java/org/sonar/server/qualityprofile/builtin/QualityProfileChangeEventServiceImplTest.java)8
-rw-r--r--server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java81
-rw-r--r--server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java55
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java34
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java17
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java23
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java17
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java17
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java1
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java11
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java30
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java1
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java31
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java30
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java7
36 files changed, 1292 insertions, 126 deletions
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java
index 3c4b5235f5c..13581a4d364 100644
--- a/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java
+++ b/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java
@@ -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;
@@ -207,6 +209,16 @@ public class AppNodesClusterHostsConsistencyTest {
}
@Override
+ public void subscribeIssueChangeTopic(IssueChangeListener listener) {
+
+ }
+
+ @Override
+ public void publishEvent(IssueChangedEvent event) {
+
+ }
+
+ @Override
public void close() {
}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
index 2f0ec8cb8e0..833864b8e96 100644
--- a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
@@ -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();
}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
index 89a9b75f994..bb6a541087d 100644
--- a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
@@ -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 {
@@ -142,6 +144,18 @@ class HazelcastMemberImpl implements HazelcastMember {
}
@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 {
hzInstance.shutdown();
diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java
index 86418d5f637..032143d68d3 100644
--- a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java
@@ -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
index 00000000000..ae5bbc7e5dc
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java
@@ -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
index 00000000000..b784f0eead5
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java
@@ -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
index 00000000000..7decf7279f5
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventService.java
@@ -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
index 00000000000..54dc1c71ba1
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java
@@ -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
index 00000000000..486cc87068f
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java
@@ -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
index 00000000000..53aa3f34054
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java
@@ -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
index 00000000000..497a7e13420
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/package-info.java
@@ -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;
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java
index c5b4b9fae9d..a45ff9ce87f 100644
--- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java
@@ -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
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java
index 568dc5eb9f3..76fc0799986 100644
--- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java
@@ -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;
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java
index 0d8a6c26c8b..8914610b28a 100644
--- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java
@@ -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
index 00000000000..159d67fa593
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java
@@ -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;
+ }
+
+}
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java
index 3207154ac2e..64a5d15e10a 100644
--- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java
@@ -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 {
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java
index 88d87433773..d2508ada331 100644
--- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java
+++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java
@@ -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
index 00000000000..b08a831cb47
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java
@@ -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
index 00000000000..8e65c839b02
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java
@@ -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
index 00000000000..91d543e2e73
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java
@@ -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
index 00000000000..b3b9e0b84d9
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java
@@ -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/qualityprofile/builtin/QualityProfileChangeEventServiceImplTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java
index a67e8ded258..1b4c10e0fc9 100644
--- 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/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java
@@ -17,7 +17,7 @@
* 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;
+package org.sonar.server.pushapi.qualityprofile;
import java.util.Collection;
import java.util.Collections;
@@ -26,8 +26,8 @@ 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.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;
@@ -36,8 +36,6 @@ 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;
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
index 00000000000..e36a5df6fc5
--- /dev/null
+++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java
@@ -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\"]}");
+ }
+}
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java
index 2147bf50ecf..48cbb218411 100644
--- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java
+++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java
@@ -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");
@@ -169,6 +173,20 @@ public class SonarLintClientsRegistryTest {
}
@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 = {};
String[] deactivatedRules = {"repo:rule-key"};
@@ -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-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
index 9a170755d84..c45a8e54d47 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
@@ -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()
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
index cfd1f718017..2776f28ca5e 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
@@ -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);
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
index dee115bf149..494c9bd4828 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
@@ -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();
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
index 1b998ed234c..0150ad31962 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
@@ -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);
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
index 3e08765ee6a..aee2c8a7fb4 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
@@ -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);
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java
index 2d671139ae4..d9115d8fd0c 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java
@@ -51,7 +51,6 @@ import static org.sonar.db.rule.RuleTesting.newRule;
public class TransitionActionTest {
-
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java
index 9797f60a958..f4ff321ce5b 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java
@@ -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
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
index 8fefd3eaa06..babaff80e60 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
@@ -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,12 +127,32 @@ 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();
ComponentDto file = db.components().insertComponent(newFileDto(project));
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java
index e334d80a7a3..565fb7fd3e8 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java
@@ -68,7 +68,6 @@ public class IssueUpdaterTest {
private System2 system2 = mock(System2.class);
-
@Rule
public DbTester db = DbTester.create(system2);
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java
index 3936efae996..fc7c9791577 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java
@@ -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);
@@ -112,6 +121,26 @@ public class SetSeverityActionTest {
}
@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));
setUserWithBrowseAndAdministerIssuePermission(issueDto);
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
index d4b0cfb5cab..979fd7067dc 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
@@ -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,6 +131,7 @@ 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();
@@ -131,6 +139,26 @@ public class SetTypeActionTest {
}
@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);
setUserWithBrowseAndAdministerIssuePermission(issueDto);
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 6ccdff45a30..aef8a7d416c 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -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,