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;
}
+ @Override
+ public void subscribeIssueChangeTopic(IssueChangeListener listener) {
+
+ }
+
+ @Override
+ public void publishEvent(IssueChangedEvent event) {
+
+ }
+
@Override
public void close() {
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 {
void publishEvent(RuleSetChangedEvent event);
+ void subscribeIssueChangeTopic(IssueChangeListener listener);
+
+ void publishEvent(IssueChangedEvent event);
+
@Override
void close();
}
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 {
hzInstance.getTopic("ruleActivated").publish(event);
}
+ @Override
+ public void subscribeIssueChangeTopic(IssueChangeListener listener) {
+ ITopic<IssueChangedEvent> topic = hzInstance.getTopic("issueChanged");
+ MessageListener<IssueChangedEvent> hzListener = message -> listener.listen(message.getMessageObject());
+ topic.addMessageListener(hzListener);
+ }
+
+ @Override
+ public void publishEvent(IssueChangedEvent event) {
+ hzInstance.getTopic("issueChanged").publish(event);
+ }
+
@Override
public void close() {
try {
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;
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)
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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;
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
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;
*/
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 {
--- /dev/null
+/*
+ * 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;
+ }
+
+}
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 {
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;
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) {
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);
}
}
@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) {
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());
}
});
}
- 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 {
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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}");
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.pushapi.qualityprofile;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.util.ParamChange;
+import org.sonar.core.util.rule.RuleChange;
+import org.sonar.core.util.rule.RuleSetChangedEvent;
+import org.sonar.db.DbTester;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.qualityprofile.ActiveRuleDto;
+import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.qualityprofile.QualityProfileTesting;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.server.qualityprofile.ActiveRuleChange;
+
+import static java.util.List.of;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
+import static org.sonar.db.rule.RuleTesting.newCustomRule;
+import static org.sonar.db.rule.RuleTesting.newTemplateRule;
+import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
+
+public class QualityProfileChangeEventServiceImplTest {
+
+ @Rule
+ public DbTester db = DbTester.create();
+
+ RuleActivatorEventsDistributor eventsDistributor = mock(RuleActivatorEventsDistributor.class);
+
+ public final QualityProfileChangeEventServiceImpl underTest = new QualityProfileChangeEventServiceImpl(db.getDbClient(), eventsDistributor);
+
+ @Test
+ public void distributeRuleChangeEvent() {
+ QProfileDto qualityProfileDto = QualityProfileTesting.newQualityProfileDto();
+
+ // Template rule
+ RuleDto templateRule = newTemplateRule(RuleKey.of("xoo", "template-key"));
+ db.rules().insert(templateRule);
+ // Custom rule
+ RuleDto rule1 = newCustomRule(templateRule)
+ .setLanguage("xoo")
+ .setRepositoryKey("repo")
+ .setRuleKey("ruleKey")
+ .setDescriptionFormat(RuleDto.Format.MARKDOWN)
+ .addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", "<div>line1\nline2</div>"));
+ db.rules().insert(rule1);
+
+ ActiveRuleDto activeRuleDto = ActiveRuleDto.createFor(qualityProfileDto, rule1);
+
+ ActiveRuleChange activeRuleChange = new ActiveRuleChange(ACTIVATED, activeRuleDto, rule1);
+ activeRuleChange.setParameter("paramChangeKey", "paramChangeValue");
+
+ Collection<QProfileDto> profiles = Collections.singleton(qualityProfileDto);
+
+ ProjectDto project = db.components().insertPrivateProjectDto();
+ db.qualityProfiles().associateWithProject(project, qualityProfileDto);
+
+ underTest.distributeRuleChangeEvent(profiles, of(activeRuleChange), "xoo");
+
+ ArgumentCaptor<RuleSetChangedEvent> eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class);
+ verify(eventsDistributor).pushEvent(eventCaptor.capture());
+
+ RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue();
+ assertThat(ruleSetChangedEvent).isNotNull();
+ assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent,
+ RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects)
+ .containsExactly("RuleSetChanged", "xoo", new String[]{project.getKey()});
+
+ assertThat(ruleSetChangedEvent.getActivatedRules())
+ .extracting(RuleChange::getKey, RuleChange::getLanguage,
+ RuleChange::getSeverity, RuleChange::getTemplateKey)
+ .containsExactly(tuple("repo:ruleKey", "xoo", null, "xoo:template-key"));
+
+ assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1);
+ ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0];
+ assertThat(actualParamChange)
+ .extracting(ParamChange::getKey, ParamChange::getValue)
+ .containsExactly("paramChangeKey", "paramChangeValue");
+
+ assertThat(ruleSetChangedEvent.getDeactivatedRules()).isEmpty();
+
+ }
+
+ @Test
+ public void publishRuleActivationToSonarLintClients() {
+ ProjectDto projectDao = new ProjectDto();
+ QProfileDto activatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
+ activatedQualityProfile.setLanguage("xoo");
+ db.qualityProfiles().insert(activatedQualityProfile);
+ RuleDto rule1 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo").setRuleKey("ruleKey"));
+ RuleParamDto rule1Param = db.rules().insertRuleParam(rule1);
+
+ ActiveRuleDto activeRule1 = db.qualityProfiles().activateRule(activatedQualityProfile, rule1);
+ ActiveRuleParamDto activeRuleParam1 = ActiveRuleParamDto.createFor(rule1Param).setValue(randomAlphanumeric(20));
+ db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule1, activeRuleParam1);
+ db.getSession().commit();
+
+ QProfileDto deactivatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
+ db.qualityProfiles().insert(deactivatedQualityProfile);
+ RuleDto rule2 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo2").setRuleKey("ruleKey2"));
+ RuleParamDto rule2Param = db.rules().insertRuleParam(rule2);
+
+ ActiveRuleDto activeRule2 = db.qualityProfiles().activateRule(deactivatedQualityProfile, rule2);
+ ActiveRuleParamDto activeRuleParam2 = ActiveRuleParamDto.createFor(rule2Param).setValue(randomAlphanumeric(20));
+ db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule2, activeRuleParam2);
+ db.getSession().commit();
+
+ underTest.publishRuleActivationToSonarLintClients(projectDao, activatedQualityProfile, deactivatedQualityProfile);
+
+ ArgumentCaptor<RuleSetChangedEvent> eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class);
+ verify(eventsDistributor).pushEvent(eventCaptor.capture());
+
+ RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue();
+ assertThat(ruleSetChangedEvent).isNotNull();
+ assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent,
+ RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects)
+ .containsExactly("RuleSetChanged", "xoo", new String[]{null});
+
+ // activated rule
+ assertThat(ruleSetChangedEvent.getActivatedRules())
+ .extracting(RuleChange::getKey, RuleChange::getLanguage,
+ RuleChange::getSeverity, RuleChange::getTemplateKey)
+ .containsExactly(tuple("repo:ruleKey", "xoo", rule1.getSeverityString(), null));
+
+ assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1);
+ ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0];
+ assertThat(actualParamChange)
+ .extracting(ParamChange::getKey, ParamChange::getValue)
+ .containsExactly(activeRuleParam1.getKey(), activeRuleParam1.getValue());
+
+ // deactivated rule
+ assertThat(ruleSetChangedEvent.getDeactivatedRules())
+ .containsExactly("repo2:ruleKey2");
+ }
+
+}
--- /dev/null
+/*
+ * 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\"]}");
+ }
+}
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;
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
}
@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");
verify(sonarLintClient).close();
}
+ @Test
+ public void listen_givenUserNotPermittedToReceiveIssueChangeEvent_closeConnection() {
+ Issue[] issues = new Issue[]{ new Issue("issue-1", "branch-1")};
+ IssueChangedEvent issueChangedEvent = new IssueChangedEvent("project1", issues, true, "BLOCKER", "BUG");
+
+ SonarLintClient sonarLintClient = createSampleSLClient();
+ underTest.registerClient(sonarLintClient);
+ doThrow(new ForbiddenException("Access forbidden")).when(permissionsValidator).validateUserCanReceivePushEventForProjects(anyString(), anySet());
+
+ underTest.listen(issueChangedEvent);
+
+ verify(sonarLintClient).close();
+ }
+
@Test
public void listen_givenUnregisteredClient_closeConnection() throws IOException {
RuleChange[] activatedRules = {};
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() {
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.server.qualityprofile.builtin;
-
-import java.util.Collection;
-import java.util.Collections;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.core.util.ParamChange;
-import org.sonar.core.util.RuleChange;
-import org.sonar.core.util.RuleSetChangedEvent;
-import org.sonar.db.DbTester;
-import org.sonar.db.project.ProjectDto;
-import org.sonar.db.qualityprofile.ActiveRuleDto;
-import org.sonar.db.qualityprofile.ActiveRuleParamDto;
-import org.sonar.db.qualityprofile.QProfileDto;
-import org.sonar.db.qualityprofile.QualityProfileTesting;
-import org.sonar.db.rule.RuleDto;
-import org.sonar.db.rule.RuleParamDto;
-import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventServiceImpl;
-import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor;
-import org.sonar.server.qualityprofile.ActiveRuleChange;
-
-import static java.util.List.of;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.tuple;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
-import static org.sonar.db.rule.RuleTesting.newCustomRule;
-import static org.sonar.db.rule.RuleTesting.newTemplateRule;
-import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
-
-public class QualityProfileChangeEventServiceImplTest {
-
- @Rule
- public DbTester db = DbTester.create();
-
- RuleActivatorEventsDistributor eventsDistributor = mock(RuleActivatorEventsDistributor.class);
-
- public final QualityProfileChangeEventServiceImpl underTest = new QualityProfileChangeEventServiceImpl(db.getDbClient(), eventsDistributor);
-
- @Test
- public void distributeRuleChangeEvent() {
- QProfileDto qualityProfileDto = QualityProfileTesting.newQualityProfileDto();
-
- // Template rule
- RuleDto templateRule = newTemplateRule(RuleKey.of("xoo", "template-key"));
- db.rules().insert(templateRule);
- // Custom rule
- RuleDto rule1 = newCustomRule(templateRule)
- .setLanguage("xoo")
- .setRepositoryKey("repo")
- .setRuleKey("ruleKey")
- .setDescriptionFormat(RuleDto.Format.MARKDOWN)
- .addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", "<div>line1\nline2</div>"));
- db.rules().insert(rule1);
-
- ActiveRuleDto activeRuleDto = ActiveRuleDto.createFor(qualityProfileDto, rule1);
-
- ActiveRuleChange activeRuleChange = new ActiveRuleChange(ACTIVATED, activeRuleDto, rule1);
- activeRuleChange.setParameter("paramChangeKey", "paramChangeValue");
-
- Collection<QProfileDto> profiles = Collections.singleton(qualityProfileDto);
-
- ProjectDto project = db.components().insertPrivateProjectDto();
- db.qualityProfiles().associateWithProject(project, qualityProfileDto);
-
- underTest.distributeRuleChangeEvent(profiles, of(activeRuleChange), "xoo");
-
- ArgumentCaptor<RuleSetChangedEvent> eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class);
- verify(eventsDistributor).pushEvent(eventCaptor.capture());
-
- RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue();
- assertThat(ruleSetChangedEvent).isNotNull();
- assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent,
- RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects)
- .containsExactly("RuleSetChanged", "xoo", new String[]{project.getKey()});
-
- assertThat(ruleSetChangedEvent.getActivatedRules())
- .extracting(RuleChange::getKey, RuleChange::getLanguage,
- RuleChange::getSeverity, RuleChange::getTemplateKey)
- .containsExactly(tuple("repo:ruleKey", "xoo", null, "xoo:template-key"));
-
- assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1);
- ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0];
- assertThat(actualParamChange)
- .extracting(ParamChange::getKey, ParamChange::getValue)
- .containsExactly("paramChangeKey", "paramChangeValue");
-
- assertThat(ruleSetChangedEvent.getDeactivatedRules()).isEmpty();
-
- }
-
- @Test
- public void publishRuleActivationToSonarLintClients() {
- ProjectDto projectDao = new ProjectDto();
- QProfileDto activatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
- activatedQualityProfile.setLanguage("xoo");
- db.qualityProfiles().insert(activatedQualityProfile);
- RuleDto rule1 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo").setRuleKey("ruleKey"));
- RuleParamDto rule1Param = db.rules().insertRuleParam(rule1);
-
- ActiveRuleDto activeRule1 = db.qualityProfiles().activateRule(activatedQualityProfile, rule1);
- ActiveRuleParamDto activeRuleParam1 = ActiveRuleParamDto.createFor(rule1Param).setValue(randomAlphanumeric(20));
- db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule1, activeRuleParam1);
- db.getSession().commit();
-
- QProfileDto deactivatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
- db.qualityProfiles().insert(deactivatedQualityProfile);
- RuleDto rule2 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo2").setRuleKey("ruleKey2"));
- RuleParamDto rule2Param = db.rules().insertRuleParam(rule2);
-
- ActiveRuleDto activeRule2 = db.qualityProfiles().activateRule(deactivatedQualityProfile, rule2);
- ActiveRuleParamDto activeRuleParam2 = ActiveRuleParamDto.createFor(rule2Param).setValue(randomAlphanumeric(20));
- db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule2, activeRuleParam2);
- db.getSession().commit();
-
- underTest.publishRuleActivationToSonarLintClients(projectDao, activatedQualityProfile, deactivatedQualityProfile);
-
- ArgumentCaptor<RuleSetChangedEvent> eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class);
- verify(eventsDistributor).pushEvent(eventCaptor.capture());
-
- RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue();
- assertThat(ruleSetChangedEvent).isNotNull();
- assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent,
- RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects)
- .containsExactly("RuleSetChanged", "xoo", new String[]{null});
-
- // activated rule
- assertThat(ruleSetChangedEvent.getActivatedRules())
- .extracting(RuleChange::getKey, RuleChange::getLanguage,
- RuleChange::getSeverity, RuleChange::getTemplateKey)
- .containsExactly(tuple("repo:ruleKey", "xoo", rule1.getSeverityString(), null));
-
- assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1);
- ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0];
- assertThat(actualParamChange)
- .extracting(ParamChange::getKey, ParamChange::getValue)
- .containsExactly(activeRuleParam1.getKey(), activeRuleParam1.getValue());
-
- // deactivated rule
- assertThat(ruleSetChangedEvent.getDeactivatedRules())
- .containsExactly("repo2:ruleKey2");
- }
-
-}
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;
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;
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;
this.actions = actions;
this.issueChangePostProcessor = issueChangePostProcessor;
this.notificationSerializer = notificationSerializer;
+ this.issueChangeEventService = issueChangeEventService;
}
@Override
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;
}
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());
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()
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;
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;
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;
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);
}
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);
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();
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();
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;
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;
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);
}
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;
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;
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);
}
public class TransitionActionTest {
-
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();
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;
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;
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,
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() {
assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
verifyPostProcessorCalled(file);
+ verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any());
}
@Test
assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
verifyPostProcessorCalled(file);
+ verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any());
}
@Test
// no need to refresh measures
verifyPostProcessorNotCalled();
+ verifyNoInteractions(issueChangeEventService);
}
@Test
// no need to refresh measures
verifyPostProcessorNotCalled();
+ verifyNoInteractions(issueChangeEventService);
}
@Test
assertThat(issueComment.getChangeData()).isEqualTo("type was badly defined");
verifyPostProcessorCalled(file);
+ verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any());
}
@Test
tuple(issue3.getKey(), userToAssign.getUuid(), VULNERABILITY.getDbConstant(), MINOR, NOW));
verifyPostProcessorCalled(file);
+ verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any());
}
@Test
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;
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;
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 {
private System2 system2 = new TestSystem2().setNow(NOW);
-
@Rule
public DbTester db = DbTester.create(system2);
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);
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
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();
private System2 system2 = mock(System2.class);
-
@Rule
public DbTester db = DbTester.create(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.IssueDbTester;
import org.sonar.db.issue.IssueDto;
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;
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 {
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),
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);
.containsExactlyInAnyOrder(issueDto.getComponentUuid());
}
+ @Test
+ public void set_severity_is_not_distributed_for_pull_request() {
+ RuleDto rule = dbTester.rules().insertIssueRule();
+ ComponentDto project = dbTester.components().insertPrivateProject();
+
+ ComponentDto pullRequest = dbTester.components().insertProjectBranch(project, b -> b.setKey("myBranch1")
+ .setBranchType(BranchType.PULL_REQUEST)
+ .setMergeBranchUuid(project.uuid()));
+
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(pullRequest));
+ IssueDto issue = newIssue(rule, pullRequest, file).setType(CODE_SMELL).setSeverity(MAJOR);
+ issueDbTester.insertIssue(issue);
+
+ setUserWithBrowseAndAdministerIssuePermission(issue);
+
+ call(issue.getKey(), MINOR);
+
+ verifyNoInteractions(issueChangeEventService);
+ }
+
@Test
public void insert_entry_in_changelog_when_setting_severity() {
IssueDto issueDto = issueDbTester.insertIssue(i -> i.setSeverity(MAJOR));
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;
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;
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 {
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));
assertThat(issueChangePostProcessor.calledComponents())
.extracting(ComponentDto::uuid)
.containsExactlyInAnyOrder(issueDto.getComponentUuid());
+ verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any(), any(), any(), any());
} else {
assertThat(issueChangePostProcessor.wasCalled())
.isFalse();
}
}
+ @Test
+ public void set_type_is_not_distributed_for_pull_request() {
+ RuleDto rule = dbTester.rules().insertIssueRule();
+ ComponentDto project = dbTester.components().insertPrivateProject();
+
+ ComponentDto pullRequest = dbTester.components().insertProjectBranch(project, b -> b.setKey("myBranch1")
+ .setBranchType(BranchType.PULL_REQUEST)
+ .setMergeBranchUuid(project.uuid()));
+
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(pullRequest));
+ IssueDto issue = newIssue(rule, pullRequest, file).setType(CODE_SMELL).setSeverity(MAJOR);
+ issueDbTester.insertIssue(issue);
+
+ setUserWithBrowseAndAdministerIssuePermission(issue);
+
+ call(issue.getKey(), BUG.name());
+
+ verifyNoInteractions(issueChangeEventService);
+ }
+
@Test
public void insert_entry_in_changelog_when_setting_type() {
IssueDto issueDto = newIssueWithProject(CODE_SMELL);
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;
addIfCluster(DistributedRuleActivatorEventsDistributor.class);
addIfStandalone(StandaloneRuleActivatorEventsDistributor.class);
+ addIfCluster(DistributedIssueChangeEventsDistributor.class);
+ addIfStandalone(StandaloneIssueChangeEventsDistributor.class);
+
add(
RuleDescriptionFormatter.class,
ClusterVerification.class,
NewIssuesNotificationHandler.newMetadata(),
MyNewIssuesNotificationHandler.class,
MyNewIssuesNotificationHandler.newMetadata(),
+ IssueChangeEventServiceImpl.class,
// issues actions
AssignAction.class,
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.core.util;
-
-public interface RuleActivationListener {
-
- void listen(RuleSetChangedEvent event);
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.core.util;
-
-import java.io.Serializable;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-
-public class RuleChange implements Serializable {
- String key;
- String language;
- String templateKey;
- String severity;
- ParamChange[] params = new ParamChange[0];
-
- public String getKey() {
- return key;
- }
-
- public RuleChange setKey(String key) {
- this.key = key;
- return this;
- }
-
- @CheckForNull
- public String getLanguage() {
- return language;
- }
-
- public RuleChange setLanguage(@Nullable String language) {
- this.language = language;
- return this;
- }
-
- public String getTemplateKey() {
- return templateKey;
- }
-
- public RuleChange setTemplateKey(String templateKey) {
- this.templateKey = templateKey;
- return this;
- }
-
- @CheckForNull
- public String getSeverity() {
- return severity;
- }
-
- public RuleChange setSeverity(@Nullable String severity) {
- this.severity = severity;
- return this;
- }
-
- public ParamChange[] getParams() {
- return params;
- }
-
- public RuleChange setParams(ParamChange[] params) {
- this.params = params;
- return this;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.core.util;
-
-import java.io.Serializable;
-
-public class RuleSetChangedEvent implements Serializable {
-
- private static final String EVENT = "RuleSetChanged";
-
- private final String[] projects;
- private final String language;
- private final RuleChange[] activatedRules;
- private final String[] deactivatedRules;
-
- public RuleSetChangedEvent(String[] projects, RuleChange[] activatedRules, String[] deactivatedRules, String language) {
- this.projects = projects;
- this.activatedRules = activatedRules;
- this.deactivatedRules = deactivatedRules;
- if (activatedRules.length == 0 && deactivatedRules.length == 0) {
- throw new IllegalArgumentException("Can't create RuleSetChangedEvent without any rules that have changed");
- }
- this.language = language;
- }
-
- public String getEvent() {
- return EVENT;
- }
-
- public String[] getProjects() {
- return projects;
- }
-
- public String getLanguage() {
- return language;
- }
-
- public RuleChange[] getActivatedRules() {
- return activatedRules;
- }
-
- public String[] getDeactivatedRules() {
- return deactivatedRules;
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.issue;
+
+import java.io.Serializable;
+
+public class Issue implements Serializable {
+ private String issueKey;
+ private String branchName;
+
+ public Issue(String issueKey, String branchName) {
+ this.issueKey = issueKey;
+ this.branchName = branchName;
+ }
+
+ public String getIssueKey() {
+ return issueKey;
+ }
+
+ public String getBranchName() {
+ return branchName;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.issue;
+
+public interface IssueChangeListener {
+ void listen(IssueChangedEvent event);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.issue;
+
+import java.io.Serializable;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class IssueChangedEvent implements Serializable {
+ private static final String EVENT = "IssueChangedEvent";
+
+ private final String projectKey;
+ private final Issue[] issues;
+ @CheckForNull
+ private final Boolean resolved;
+ @CheckForNull
+ private final String userSeverity;
+ @CheckForNull
+ private final String userType;
+
+ public IssueChangedEvent(String projectKey, Issue[] issues, @Nullable Boolean resolved, @Nullable String userSeverity,
+ @Nullable String userType) {
+ if (issues.length == 0) {
+ throw new IllegalArgumentException("Can't create IssueChangedEvent without any issues that have changed");
+ }
+ this.projectKey = projectKey;
+ this.issues = issues;
+ this.resolved = resolved;
+ this.userSeverity = userSeverity;
+ this.userType = userType;
+ }
+
+ public String getEvent() {
+ return EVENT;
+ }
+
+ public String getProjectKey() {
+ return projectKey;
+ }
+
+ public Issue[] getIssues() {
+ return issues;
+ }
+
+ @CheckForNull
+ public Boolean getResolved() {
+ return resolved;
+ }
+
+ @CheckForNull
+ public String getUserSeverity() {
+ return userSeverity;
+ }
+
+ @CheckForNull
+ public String getUserType() {
+ return userType;
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.rule;
+
+public interface RuleActivationListener {
+
+ void listen(RuleSetChangedEvent event);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.rule;
+
+import java.io.Serializable;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.core.util.ParamChange;
+
+public class RuleChange implements Serializable {
+ private String key;
+ private String language;
+ private String templateKey;
+ private String severity;
+ private ParamChange[] params = new ParamChange[0];
+
+ public String getKey() {
+ return key;
+ }
+
+ public RuleChange setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ @CheckForNull
+ public String getLanguage() {
+ return language;
+ }
+
+ public RuleChange setLanguage(@Nullable String language) {
+ this.language = language;
+ return this;
+ }
+
+ public String getTemplateKey() {
+ return templateKey;
+ }
+
+ public RuleChange setTemplateKey(String templateKey) {
+ this.templateKey = templateKey;
+ return this;
+ }
+
+ @CheckForNull
+ public String getSeverity() {
+ return severity;
+ }
+
+ public RuleChange setSeverity(@Nullable String severity) {
+ this.severity = severity;
+ return this;
+ }
+
+ public ParamChange[] getParams() {
+ return params;
+ }
+
+ public RuleChange setParams(ParamChange[] params) {
+ this.params = params;
+ return this;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.rule;
+
+import java.io.Serializable;
+
+public class RuleSetChangedEvent implements Serializable {
+
+ private static final String EVENT = "RuleSetChanged";
+
+ private final String[] projects;
+ private final String language;
+ private final RuleChange[] activatedRules;
+ private final String[] deactivatedRules;
+
+ public RuleSetChangedEvent(String[] projects, RuleChange[] activatedRules, String[] deactivatedRules, String language) {
+ this.projects = projects;
+ this.activatedRules = activatedRules;
+ this.deactivatedRules = deactivatedRules;
+ if (activatedRules.length == 0 && deactivatedRules.length == 0) {
+ throw new IllegalArgumentException("Can't create RuleSetChangedEvent without any rules that have changed");
+ }
+ this.language = language;
+ }
+
+ public String getEvent() {
+ return EVENT;
+ }
+
+ public String[] getProjects() {
+ return projects;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public RuleChange[] getActivatedRules() {
+ return activatedRules;
+ }
+
+ public String[] getDeactivatedRules() {
+ return deactivatedRules;
+ }
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.core.util;
-
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-public class RuleSetChangedEventTest {
-
- @Test
- public void getLanguage_givenNoDeactivatedRules_languageIsCorrectlyIdentified() {
- String[] projects = {"sonarqube"};
- RuleChange[] activatedRules = {createRuleChange("java")};
- String[] deactivatedRules = {};
- RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");
-
- String language = event.getLanguage();
-
- assertThat(language).isEqualTo("java");
- }
-
- @Test
- public void getLanguage_givenNoActivatedRules_languageIsCorrectlyIdentified() {
- String[] projects = {"sonarqube"};
- RuleChange[] activatedRules = {};
- String[] deactivatedRules = {"ruleKey"};
- RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");
-
- String language = event.getLanguage();
-
- assertThat(language).isEqualTo("java");
- }
-
- @Test
- public void getLanguage_givenBothArraysEmpty_throwException() {
- String[] projects = {"sonarqube"};
- RuleChange[] activatedRules = {};
- String[] deactivatedRules = {};
-
- assertThatThrownBy(() -> new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"))
- .isInstanceOf(IllegalArgumentException.class);
- }
-
- private RuleChange createRuleChange(String language) {
- RuleChange ruleChange = new RuleChange();
- ruleChange.setLanguage(language);
- return ruleChange;
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.issue;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class IssueChangedEventTest {
+ private static final String BRANCH_NAME = "branch-name";
+ private static final String ISSUE_KEY = "issue-key";
+ private static final String PROJECT_KEY = "project-key";
+
+ @Test
+ public void issueChangedEvent_instantiation_accepts_nulls() {
+ Issue[] issues = new Issue[]{new Issue(ISSUE_KEY, BRANCH_NAME)};
+ IssueChangedEvent event = new IssueChangedEvent(PROJECT_KEY, issues, null, null, null);
+
+ assertThat(event.getEvent()).isEqualTo("IssueChangedEvent");
+ assertThat(event.getProjectKey()).isEqualTo(PROJECT_KEY);
+ assertThat(event.getResolved()).isNull();
+ assertThat(event.getUserSeverity()).isNull();
+ assertThat(event.getUserType()).isNull();
+ assertThat(event.getIssues()).hasSize(1);
+ }
+
+ @Test
+ public void issueChangedEvent_instantiation_accepts_actual_values() {
+ Issue[] issues = new Issue[]{new Issue(ISSUE_KEY, BRANCH_NAME)};
+ IssueChangedEvent event = new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG");
+
+ assertThat(event.getEvent()).isEqualTo("IssueChangedEvent");
+ assertThat(event.getProjectKey()).isEqualTo(PROJECT_KEY);
+ assertThat(event.getResolved()).isTrue();
+ assertThat(event.getUserSeverity()).isEqualTo("BLOCKER");
+ assertThat(event.getUserType()).isEqualTo("BUG");
+ assertThat(event.getIssues()).hasSize(1);
+ }
+
+ @Test
+ public void issueChangedEvent_instantiation_doesNotAccept_emptyIssues() {
+ Issue[] issues = new Issue[0];
+
+ assertThatThrownBy(() -> new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .withFailMessage("Can't create IssueChangedEvent without any issues that have changed");
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.issue;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueTest {
+ private static final String BRANCH_NAME = "branch-name";
+ private static final String ISSUE_KEY = "issue-key";
+
+ @Test
+ public void issue_instantiation_accepts_values() {
+ Issue issue = new Issue(ISSUE_KEY, BRANCH_NAME);
+
+ assertThat(issue.getIssueKey()).isEqualTo(ISSUE_KEY);
+ assertThat(issue.getBranchName()).isEqualTo(BRANCH_NAME);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.util.rule;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class RuleSetChangedEventTest {
+
+ @Test
+ public void getLanguage_givenNoDeactivatedRules_languageIsCorrectlyIdentified() {
+ String[] projects = {"sonarqube"};
+ RuleChange[] activatedRules = {createRuleChange("java")};
+ String[] deactivatedRules = {};
+ RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");
+
+ String language = event.getLanguage();
+
+ assertThat(language).isEqualTo("java");
+ }
+
+ @Test
+ public void getLanguage_givenNoActivatedRules_languageIsCorrectlyIdentified() {
+ String[] projects = {"sonarqube"};
+ RuleChange[] activatedRules = {};
+ String[] deactivatedRules = {"ruleKey"};
+ RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");
+
+ String language = event.getLanguage();
+
+ assertThat(language).isEqualTo("java");
+ }
+
+ @Test
+ public void getLanguage_givenBothArraysEmpty_throwException() {
+ String[] projects = {"sonarqube"};
+ RuleChange[] activatedRules = {};
+ String[] deactivatedRules = {};
+
+ assertThatThrownBy(() -> new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ private RuleChange createRuleChange(String language) {
+ RuleChange ruleChange = new RuleChange();
+ ruleChange.setLanguage(language);
+ return ruleChange;
+ }
+}