Browse Source

SONAR-16647 - Move previous SSE events to DB queue

tags/9.6.0.59041
Belen Pruvost 1 year ago
parent
commit
7ed0c0a19b
39 changed files with 414 additions and 1175 deletions
  1. 11
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/pushevent/PushEventDto.java
  2. 3
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/pushevent/PushEventMapper.xml
  3. 2
    1
      server/sonar-db-dao/src/schema/schema-sq.ddl
  4. 65
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v96/AddLanguageColumnToPushEventsTable.java
  5. 1
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v96/DbVersion96.java
  6. 56
    0
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v96/AddLanguageColumnToPushEventsTableTest.java
  7. 7
    0
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v96/AddLanguageColumnToPushEventsTableTest/schema.sql
  8. 0
    24
      server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java
  9. 0
    12
      server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
  10. 0
    30
      server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
  11. 0
    35
      server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java
  12. 0
    45
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java
  13. 0
    75
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java
  14. 30
    5
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java
  15. 0
    30
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java
  16. 0
    42
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java
  17. 0
    45
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java
  18. 37
    15
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java
  19. 0
    30
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java
  20. 0
    94
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java
  21. 0
    42
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java
  22. 9
    3
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/scheduler/polling/PushEventPollScheduler.java
  23. 6
    79
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java
  24. 11
    1
      server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushEvent.java
  25. 0
    48
      server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java
  26. 0
    62
      server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java
  27. 75
    53
      server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java
  28. 0
    41
      server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java
  29. 35
    55
      server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java
  30. 0
    81
      server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java
  31. 53
    105
      server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java
  32. 0
    24
      server/sonar-webserver-pushapi/src/test/resources/org/sonar/server/pushapi/sonarlint/rule-change-event-data.json
  33. 0
    10
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  34. 0
    24
      sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangeListener.java
  35. 1
    1
      sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangedEvent.java
  36. 0
    25
      sonar-core/src/main/java/org/sonar/core/util/rule/RuleActivationListener.java
  37. 2
    15
      sonar-core/src/main/java/org/sonar/core/util/rule/RuleSetChangedEvent.java
  38. 2
    2
      sonar-core/src/test/java/org/sonar/core/util/issue/IssueChangedEventTest.java
  39. 8
    21
      sonar-core/src/test/java/org/sonar/core/util/rule/RuleSetChangedEventTest.java

+ 11
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/pushevent/PushEventDto.java View File

@@ -25,6 +25,7 @@ public class PushEventDto {
private String uuid;
private String name;
private String projectUuid;
private String language;
private byte[] payload;
private Long createdAt;

@@ -59,6 +60,16 @@ public class PushEventDto {
return this;
}

@CheckForNull
public String getLanguage() {
return language;
}

public PushEventDto setLanguage(String language) {
this.language = language;
return this;
}

public byte[] getPayload() {
return payload;
}

+ 3
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/pushevent/PushEventMapper.xml View File

@@ -6,6 +6,7 @@
pe.uuid as uuid,
pe.name as name,
pe.project_uuid as projectUuid,
pe.language,
pe.payload as payload,
pe.created_at as createdAt
</sql>
@@ -15,6 +16,7 @@
uuid,
name,
project_uuid,
language,
payload,
created_at
)
@@ -22,6 +24,7 @@
#{uuid,jdbcType=VARCHAR},
#{name,jdbcType=VARCHAR},
#{projectUuid,jdbcType=VARCHAR},
#{language,jdbcType=VARCHAR},
#{payload,jdbcType=BLOB},
#{createdAt,jdbcType=BIGINT}
)

+ 2
- 1
server/sonar-db-dao/src/schema/schema-sq.ddl View File

@@ -745,7 +745,8 @@ CREATE TABLE "PUSH_EVENTS"(
"NAME" CHARACTER VARYING(40) NOT NULL,
"PROJECT_UUID" CHARACTER VARYING(40) NOT NULL,
"PAYLOAD" BINARY LARGE OBJECT NOT NULL,
"CREATED_AT" BIGINT NOT NULL
"CREATED_AT" BIGINT NOT NULL,
"LANGUAGE" CHARACTER VARYING(20)
);
ALTER TABLE "PUSH_EVENTS" ADD CONSTRAINT "PK_PUSH_EVENTS" PRIMARY KEY("UUID");
CREATE INDEX "IDX_PUSH_EVEN_CREA_UUID_PROJ" ON "PUSH_EVENTS"("CREATED_AT" NULLS FIRST, "UUID" NULLS FIRST, "PROJECT_UUID" NULLS FIRST);

+ 65
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v96/AddLanguageColumnToPushEventsTable.java View File

@@ -0,0 +1,65 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.platform.db.migration.version.v96;

import java.sql.Connection;
import java.sql.SQLException;
import org.sonar.db.Database;
import org.sonar.db.DatabaseUtils;
import org.sonar.server.platform.db.migration.def.ColumnDef;
import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
import org.sonar.server.platform.db.migration.step.DdlChange;

import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
import static org.sonar.server.platform.db.migration.version.v96.CreatePushEventsTable.PUSH_EVENTS_TABLE_NAME;

public class AddLanguageColumnToPushEventsTable extends DdlChange {

static final String COLUMN_LANGUAGE = "language";

public AddLanguageColumnToPushEventsTable(Database db) {
super(db);
}

@Override
public void execute(Context context) throws SQLException {
try (Connection connection = getDatabase().getDataSource().getConnection()) {
createLanguageColumn(context, connection);
}
}

private void createLanguageColumn(Context context, Connection connection) {
VarcharColumnDef contextKeyColumn = newVarcharColumnDefBuilder()
.setColumnName(COLUMN_LANGUAGE)
.setIsNullable(true)
.setLimit(20).build();
createColumnIfNotExists(context, connection, contextKeyColumn);
}

private void createColumnIfNotExists(Context context, Connection connection, ColumnDef columnDef) {
if (!DatabaseUtils.tableColumnExists(connection, PUSH_EVENTS_TABLE_NAME, columnDef.getName())) {
context.execute(new AddColumnsBuilder(getDialect(), PUSH_EVENTS_TABLE_NAME)
.addColumn(columnDef)
.build());
}
}

}

+ 1
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v96/DbVersion96.java View File

@@ -40,6 +40,7 @@ public class DbVersion96 implements DbVersion {
.add(6509, "Drop column sonarlint_ad_seen in 'users'", DropSonarlintAdSeenColumnInUsersTable.class)
.add(6510, "Create table 'push_events'", CreatePushEventsTable.class)
.add(6511, "Create index 'idx_push_even_crea_uuid_proj' on 'push_events'", CreateIndexForPushEvents.class)
.add(6512, "Add column 'language' to 'push_events'", AddLanguageColumnToPushEventsTable.class)
;
}
}

+ 56
- 0
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v96/AddLanguageColumnToPushEventsTableTest.java View File

@@ -0,0 +1,56 @@
/*
* 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.platform.db.migration.version.v96;

import java.sql.SQLException;
import java.sql.Types;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.db.CoreDbTester;

import static org.sonar.db.CoreDbTester.createForSchema;
import static org.sonar.server.platform.db.migration.version.v96.AddLanguageColumnToPushEventsTable.COLUMN_LANGUAGE;
import static org.sonar.server.platform.db.migration.version.v96.CreatePushEventsTable.PUSH_EVENTS_TABLE_NAME;

public class AddLanguageColumnToPushEventsTableTest {
@Rule
public final CoreDbTester db = createForSchema(AddLanguageColumnToPushEventsTableTest.class, "schema.sql");

private final AddLanguageColumnToPushEventsTable underTest = new AddLanguageColumnToPushEventsTable(db.database());

@Test
public void column_education_principles_should_be_added() throws SQLException {
db.assertColumnDoesNotExist(PUSH_EVENTS_TABLE_NAME, COLUMN_LANGUAGE);

underTest.execute();

db.assertColumnDefinition(PUSH_EVENTS_TABLE_NAME, COLUMN_LANGUAGE, Types.VARCHAR, 20, true);
}

@Test
public void migration_should_be_reentrant() throws SQLException {
db.assertColumnDoesNotExist(PUSH_EVENTS_TABLE_NAME, COLUMN_LANGUAGE);

underTest.execute();
underTest.execute();

db.assertColumnDefinition(PUSH_EVENTS_TABLE_NAME, COLUMN_LANGUAGE, Types.VARCHAR, 20, true);
}
}

+ 7
- 0
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v96/AddLanguageColumnToPushEventsTableTest/schema.sql View File

@@ -0,0 +1,7 @@
CREATE TABLE "PUSH_EVENTS"(
"UUID" CHARACTER VARYING(40) NOT NULL,
"PROJECT_UUID" CHARACTER VARYING(40) NOT NULL,
"PAYLOAD" CHARACTER LARGE OBJECT NOT NULL,
"CREATED_AT" BIGINT NOT NULL
);
ALTER TABLE "PUSH_EVENTS" ADD CONSTRAINT "PK_PUSH_EVENTS" PRIMARY KEY("UUID");

+ 0
- 24
server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java View File

@@ -40,10 +40,6 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.sonar.application.config.TestAppSettings;
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;
@@ -198,26 +194,6 @@ public class AppNodesClusterHostsConsistencyTest {
callback.onComplete((Map<Member, T>) hostsPerMember);
}

@Override
public void subscribeRuleActivationTopic(RuleActivationListener listener) {

}

@Override
public void publishEvent(RuleSetChangedEvent event) {

}

@Override
public void subscribeIssueChangeTopic(IssueChangeListener listener) {

}

@Override
public void publishEvent(IssueChangedEvent event) {

}

@Override
public void close() {


+ 0
- 12
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java View File

@@ -26,10 +26,6 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
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 {
@@ -110,14 +106,6 @@ public interface HazelcastMember extends AutoCloseable {
*/
<T> void callAsync(DistributedCall<T> callable, MemberSelector memberSelector, DistributedCallback<T> callback);

void subscribeRuleActivationTopic(RuleActivationListener listener);

void publishEvent(RuleSetChangedEvent event);

void subscribeIssueChangeTopic(IssueChangeListener listener);

void publishEvent(IssueChangedEvent event);

@Override
void close();
}

+ 0
- 30
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java View File

@@ -27,8 +27,6 @@ import com.hazelcast.core.HazelcastInstanceNotActiveException;
import com.hazelcast.core.IExecutorService;
import com.hazelcast.core.MultiExecutionCallback;
import com.hazelcast.cp.IAtomicReference;
import com.hazelcast.topic.ITopic;
import com.hazelcast.topic.MessageListener;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -39,10 +37,6 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
import org.slf4j.LoggerFactory;
import org.sonar.core.util.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 {

@@ -131,30 +125,6 @@ class HazelcastMemberImpl implements HazelcastMember {
});
}

@Override
public void subscribeRuleActivationTopic(RuleActivationListener listener) {
ITopic<RuleSetChangedEvent> topic = hzInstance.getTopic("ruleActivated");
MessageListener<RuleSetChangedEvent> hzListener = message -> listener.listen(message.getMessageObject());
topic.addMessageListener(hzListener);
}

@Override
public void publishEvent(RuleSetChangedEvent event) {
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 {

+ 0
- 35
server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java View File

@@ -21,8 +21,6 @@ package org.sonar.process.cluster.hz;

import com.hazelcast.cluster.Member;
import com.hazelcast.cluster.memberselector.MemberSelectors;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.topic.ITopic;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
@@ -36,17 +34,10 @@ import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.sonar.core.util.issue.IssueChangeListener;
import org.sonar.core.util.rule.RuleActivationListener;
import org.sonar.process.NetworkUtilsImpl;
import org.sonar.process.ProcessId;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class HazelcastMemberImplTest {

@@ -115,32 +106,6 @@ public class HazelcastMemberImplTest {
assertThat(failures.get(0)).hasMessageContaining("BOOM");
}

@Test
public void subscribeRuleActivationTopic_listenerAdded() {
RuleActivationListener listener = mock(RuleActivationListener.class);
HazelcastInstance hzInstance = mock(HazelcastInstance.class);
ITopic<Object> topic = mock(ITopic.class);
when(hzInstance.getTopic(any())).thenReturn(topic);
HazelcastMemberImpl underTest = new HazelcastMemberImpl(hzInstance);

underTest.subscribeRuleActivationTopic(listener);

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)

+ 0
- 45
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java View File

@@ -1,45 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.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);
}
}

+ 0
- 75
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java View File

@@ -1,75 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.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;
}

}

+ 30
- 5
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java View File

@@ -19,6 +19,8 @@
*/
package org.sonar.server.pushapi.issues;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
@@ -31,9 +33,13 @@ 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.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.pushevent.PushEventDto;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.elasticsearch.common.Strings.isNullOrEmpty;
import static org.sonar.api.issue.DefaultTransitions.CONFIRM;
import static org.sonar.api.issue.DefaultTransitions.FALSE_POSITIVE;
@@ -43,6 +49,9 @@ import static org.sonar.db.component.BranchType.BRANCH;

@ServerSide
public class IssueChangeEventServiceImpl implements IssueChangeEventService {
private static final Gson GSON = new GsonBuilder().create();

private static final String EVENT_NAME = "IssueChanged";
private static final String FALSE_POSITIVE_KEY = "FALSE-POSITIVE";
private static final String WONT_FIX_KEY = "WONTFIX";

@@ -50,10 +59,10 @@ public class IssueChangeEventServiceImpl implements IssueChangeEventService {
private static final String SEVERITY_KEY = "severity";
private static final String TYPE_KEY = "type";

private final IssueChangeEventsDistributor eventsDistributor;
private final DbClient dbClient;

public IssueChangeEventServiceImpl(IssueChangeEventsDistributor eventsDistributor) {
this.eventsDistributor = eventsDistributor;
public IssueChangeEventServiceImpl(DbClient dbClient) {
this.dbClient = dbClient;
}

@Override
@@ -69,7 +78,8 @@ public class IssueChangeEventServiceImpl implements IssueChangeEventService {

IssueChangedEvent event = new IssueChangedEvent(projectKey, new Issue[]{changedIssue},
resolved, severity, type);
eventsDistributor.pushEvent(event);

persistEvent(event, issue.projectUuid());
}

@Override
@@ -96,7 +106,7 @@ public class IssueChangeEventServiceImpl implements IssueChangeEventService {
IssueChangedEvent event = getIssueChangedEvent(projectKey, issuesInProject, issueChanges);

if (event != null) {
eventsDistributor.pushEvent(event);
persistEvent(event, entry.getValue().projectUuid());
}
}
}
@@ -151,4 +161,19 @@ public class IssueChangeEventServiceImpl implements IssueChangeEventService {
return transitionOrStatus.equals(WONT_FIX) || transitionOrStatus.equals(FALSE_POSITIVE) ||
transitionOrStatus.equals(FALSE_POSITIVE_KEY) || transitionOrStatus.equals(WONT_FIX_KEY);
}

private void persistEvent(IssueChangedEvent event, String entry) {
try (DbSession dbSession = dbClient.openSession(false)) {
PushEventDto eventDto = new PushEventDto()
.setName(EVENT_NAME)
.setProjectUuid(entry)
.setPayload(serializeIssueToPushEvent(event));
dbClient.pushEventDao().insert(dbSession, eventDto);
dbSession.commit();
}
}

private static byte[] serializeIssueToPushEvent(IssueChangedEvent event) {
return GSON.toJson(event).getBytes(UTF_8);
}
}

+ 0
- 30
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java View File

@@ -1,30 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.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);
}

+ 0
- 42
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java View File

@@ -1,42 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.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));
}
}

+ 0
- 45
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java View File

@@ -1,45 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.pushapi.qualityprofile;

import org.sonar.api.server.ServerSide;
import org.sonar.core.util.rule.RuleActivationListener;
import org.sonar.core.util.rule.RuleSetChangedEvent;
import org.sonar.process.cluster.hz.HazelcastMember;

@ServerSide
public class DistributedRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {

private HazelcastMember hazelcastMember;

public DistributedRuleActivatorEventsDistributor(HazelcastMember hazelcastMember) {
this.hazelcastMember = hazelcastMember;
}

@Override
public void subscribe(RuleActivationListener listener) {
hazelcastMember.subscribeRuleActivationTopic(listener);
}

@Override
public void pushEvent(RuleSetChangedEvent event) {
hazelcastMember.publishEvent(event);
}
}

+ 37
- 15
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java View File

@@ -19,8 +19,11 @@
*/
package org.sonar.server.pushapi.qualityprofile;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -39,6 +42,7 @@ import org.sonar.core.util.rule.RuleSetChangedEvent;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.pushevent.PushEventDto;
import org.sonar.db.qualityprofile.ActiveRuleDto;
import org.sonar.db.qualityprofile.ActiveRuleParamDto;
import org.sonar.db.qualityprofile.OrgActiveRuleDto;
@@ -47,6 +51,7 @@ import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.server.qualityprofile.ActiveRuleChange;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.function.Predicate.not;
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
@@ -54,17 +59,18 @@ import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED;

@ServerSide
public class QualityProfileChangeEventServiceImpl implements QualityProfileChangeEventService {
private static final Gson GSON = new GsonBuilder().create();
private static final String EVENT_NAME = "RuleSetChanged";

private final DbClient dbClient;
private final RuleActivatorEventsDistributor eventsDistributor;

public QualityProfileChangeEventServiceImpl(DbClient dbClient, RuleActivatorEventsDistributor eventsDistributor) {
public QualityProfileChangeEventServiceImpl(DbClient dbClient) {
this.dbClient = dbClient;
this.eventsDistributor = eventsDistributor;
}

@Override
public void publishRuleActivationToSonarLintClients(ProjectDto project, @Nullable QProfileDto activatedProfile, @Nullable QProfileDto deactivatedProfile) {
public void publishRuleActivationToSonarLintClients(ProjectDto project, @Nullable QProfileDto activatedProfile,
@Nullable QProfileDto deactivatedProfile) {
List<RuleChange> activatedRules = new ArrayList<>();
Set<String> deactivatedRules = new HashSet<>();

@@ -81,9 +87,8 @@ public class QualityProfileChangeEventServiceImpl implements QualityProfileChang
}

String language = activatedProfile != null ? activatedProfile.getLanguage() : deactivatedProfile.getLanguage();
RuleSetChangedEvent event = new RuleSetChangedEvent(new String[] {project.getKey()}, activatedRules.toArray(new RuleChange[0]), deactivatedRules.toArray(new String[0]),
language);
eventsDistributor.pushEvent(event);

persistPushEvent(project.getKey(), activatedRules.toArray(new RuleChange[0]), deactivatedRules, language, project.getUuid());
}

private List<RuleChange> createRuleChanges(@NotNull QProfileDto profileDto) {
@@ -193,15 +198,29 @@ public class QualityProfileChangeEventServiceImpl implements QualityProfileChang
.map(RuleKey::toString)
.collect(Collectors.toSet());

Set<String> projectKeys = getProjectKeys(profiles);
Map<String, String> projectKeyAndUuids = getProjectKeyAndUuids(profiles);

if (activatedRules.isEmpty() && deactivatedRules.isEmpty()) {
return;
}

RuleSetChangedEvent event = new RuleSetChangedEvent(projectKeys.toArray(new String[0]), activatedRules.toArray(new RuleChange[0]), deactivatedRules.toArray(new String[0]),
language);
eventsDistributor.pushEvent(event);
for (Map.Entry<String, String> entry : projectKeyAndUuids.entrySet()) {
persistPushEvent(entry.getKey(), activatedRules.toArray(new RuleChange[0]), deactivatedRules, language, entry.getValue());
}
}

private void persistPushEvent(String projectKey, RuleChange[] activatedRules, Set<String> deactivatedRules, String language, String projectUuid) {
RuleSetChangedEvent event = new RuleSetChangedEvent(projectKey, activatedRules, deactivatedRules.toArray(new String[0]));

try (DbSession dbSession = dbClient.openSession(false)) {
PushEventDto eventDto = new PushEventDto()
.setName(EVENT_NAME)
.setProjectUuid(projectUuid)
.setLanguage(language)
.setPayload(serializeIssueToPushEvent(event));
dbClient.pushEventDao().insert(dbSession, eventDto);
dbSession.commit();
}
}

private Optional<String> templateKey(ActiveRuleChange arc) {
@@ -219,17 +238,20 @@ public class QualityProfileChangeEventServiceImpl implements QualityProfileChang
return Optional.empty();
}

private Set<String> getProjectKeys(Collection<QProfileDto> profiles) {
Set<String> projectKeys = new HashSet<>();
private Map<String, String> getProjectKeyAndUuids(Collection<QProfileDto> profiles) {
Map<String, String> projectKeyAndUuids = new HashMap<>();
try (DbSession dbSession = dbClient.openSession(false)) {
for (QProfileDto profileDto : profiles) {
List<ProjectQprofileAssociationDto> associationDtos = dbClient.qualityProfileDao().selectSelectedProjects(dbSession, profileDto, null);
for (ProjectQprofileAssociationDto associationDto : associationDtos) {
projectKeys.add(associationDto.getProjectKey());
projectKeyAndUuids.put(associationDto.getProjectKey(), associationDto.getProjectUuid());
}
}
return projectKeys;
return projectKeyAndUuids;
}
}

private static byte[] serializeIssueToPushEvent(RuleSetChangedEvent event) {
return GSON.toJson(event).getBytes(UTF_8);
}
}

+ 0
- 30
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java View File

@@ -1,30 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.pushapi.qualityprofile;

import org.sonar.core.util.rule.RuleActivationListener;
import org.sonar.core.util.rule.RuleSetChangedEvent;

public interface RuleActivatorEventsDistributor {

void subscribe(RuleActivationListener listener);

void pushEvent(RuleSetChangedEvent event);
}

+ 0
- 94
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java View File

@@ -1,94 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.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;
}

}

+ 0
- 42
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java View File

@@ -1,42 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.pushapi.qualityprofile;

import java.util.ArrayList;
import java.util.List;
import org.sonar.api.server.ServerSide;
import org.sonar.core.util.rule.RuleActivationListener;
import org.sonar.core.util.rule.RuleSetChangedEvent;

@ServerSide
public class StandaloneRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor {

private List<RuleActivationListener> listeners = new ArrayList<>();

@Override
public void subscribe(RuleActivationListener listener) {
listeners.add(listener);
}

@Override
public void pushEvent(RuleSetChangedEvent event) {
listeners.forEach(l -> l.listen(event));
}
}

+ 9
- 3
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/scheduler/polling/PushEventPollScheduler.java View File

@@ -49,6 +49,7 @@ public class PushEventPollScheduler implements Startable {
private static final Logger LOG = Loggers.get(PushEventPollScheduler.class);

private static final String INITIAL_DELAY_IN_SECONDS = "sonar.pushevents.polling.initial.delay";
private static final String LAST_TIMESTAMP_IN_SECONDS = "sonar.pushevents.polling.last.timestamp";
private static final String PERIOD_IN_SECONDS = "sonar.pushevents.polling.period";
private static final String PAGE_SIZE = "sonar.pushevents.polling.page.size";

@@ -91,7 +92,7 @@ public class PushEventPollScheduler implements Startable {
}

if (lastPullTimestamp == null) {
lastPullTimestamp = system2.now();
lastPullTimestamp = getLastPullTimestamp();
}

var projectKeys = getClientsProjectKeys(clients);
@@ -120,7 +121,8 @@ public class PushEventPollScheduler implements Startable {
LOG.debug("Could not find key for project with uuid [{}]", pushEventDto.getProjectUuid());
return Optional.empty();
}
return Optional.of(new SonarLintPushEvent(pushEventDto.getName(), pushEventDto.getPayload(), resolvedProjectKey));
return Optional.of(new SonarLintPushEvent(pushEventDto.getName(), pushEventDto.getPayload(), resolvedProjectKey,
pushEventDto.getLanguage()));
}

private static Set<String> getClientsProjectKeys(List<SonarLintClient> clients) {
@@ -154,8 +156,12 @@ public class PushEventPollScheduler implements Startable {
return config.getLong(PERIOD_IN_SECONDS).orElse(40L);
}

public long getLastPullTimestamp() {
// execute every 40 seconds
return config.getLong(LAST_TIMESTAMP_IN_SECONDS).orElse(system2.now());
}

public long getPageSize() {
// 20 events per 40 seconds
return config.getLong(PAGE_SIZE).orElse(20L);
}


+ 6
- 79
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java View File

@@ -24,62 +24,32 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
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.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 org.sonar.server.pushapi.qualityprofile.RuleSetChangeBroadcastUtils;

@ServerSide
public class SonarLintClientsRegistry implements RuleActivationListener, IssueChangeListener {
public class SonarLintClientsRegistry {

private static final Logger LOG = Loggers.get(SonarLintClientsRegistry.class);

private final SonarLintClientPermissionsValidator sonarLintClientPermissionsValidator;
private final List<SonarLintClient> clients = new CopyOnWriteArrayList<>();
private final RuleActivatorEventsDistributor ruleEventsDistributor;
private final IssueChangeEventsDistributor issueChangeEventsDistributor;

private boolean registeredToEvents = false;

public SonarLintClientsRegistry(IssueChangeEventsDistributor issueChangeEventsDistributor,
RuleActivatorEventsDistributor ruleActivatorEventsDistributor, SonarLintClientPermissionsValidator permissionsValidator) {
this.issueChangeEventsDistributor = issueChangeEventsDistributor;
public SonarLintClientsRegistry(SonarLintClientPermissionsValidator permissionsValidator) {
this.sonarLintClientPermissionsValidator = permissionsValidator;
this.ruleEventsDistributor = ruleActivatorEventsDistributor;
}

public void registerClient(SonarLintClient sonarLintClient) {
ensureListeningToEvents();
clients.add(sonarLintClient);
sonarLintClient.scheduleHeartbeat();
sonarLintClient.addListener(new SonarLintClientEventsListener(sonarLintClient));
LOG.debug("Registering new SonarLint client");
}

private synchronized void ensureListeningToEvents() {
if (registeredToEvents) {
return;
}
try {
ruleEventsDistributor.subscribe(this);
issueChangeEventsDistributor.subscribe(this);
registeredToEvents = true;
} catch (RuntimeException e) {
LOG.warn("Can not listen to rule activation or issue events for server push. Web Server might not have started fully yet.", e);
}
}

public void unregisterClient(SonarLintClient client) {
client.close();
clients.remove(client);
@@ -94,18 +64,8 @@ public class SonarLintClientsRegistry implements RuleActivationListener, IssueCh
return clients.size();
}

@Override
public void listen(RuleSetChangedEvent ruleSetChangedEvent) {
broadcastMessage(ruleSetChangedEvent, RuleSetChangeBroadcastUtils.getFilterForEvent(ruleSetChangedEvent));
}

@Override
public void listen(IssueChangedEvent issueChangedEvent) {
broadcastMessage(issueChangedEvent, IssueChangeBroadcastUtils.getFilterForEvent(issueChangedEvent));
}

public void broadcastMessage(SonarLintPushEvent event) {
clients.stream().filter(client -> client.getClientProjectKeys().contains(event.getProjectKey()))
clients.stream().filter(client -> isRelevantEvent(event, client))
.forEach(c -> {
Set<String> clientProjectKeys = new HashSet<>(c.getClientProjectKeys());
clientProjectKeys.retainAll(Set.of(event.getProjectKey()));
@@ -122,42 +82,9 @@ public class SonarLintClientsRegistry implements RuleActivationListener, IssueCh
});
}

public void broadcastMessage(RuleSetChangedEvent event, Predicate<SonarLintClient> filter) {
clients.stream().filter(filter).forEach(c -> {
Set<String> projectKeysInterestingForClient = new HashSet<>(c.getClientProjectKeys());
projectKeysInterestingForClient.retainAll(Set.of(event.getProjects()));
try {
sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient);
RuleSetChangedEvent personalizedEvent = new RuleSetChangedEvent(projectKeysInterestingForClient.toArray(String[]::new), event.getActivatedRules(),
event.getDeactivatedRules(), event.getLanguage());
String message = RuleSetChangeBroadcastUtils.getMessage(personalizedEvent);
c.writeAndFlush(message);
} catch (ForbiddenException forbiddenException) {
logClientUnauthenticated(forbiddenException);
unregisterClient(c);
} catch (IllegalStateException | IOException e) {
logUnexpectedError(e);
unregisterClient(c);
}
});
}

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) {
logClientUnauthenticated(forbiddenException);
unregisterClient(c);
} catch (IllegalStateException | IOException e) {
logUnexpectedError(e);
unregisterClient(c);
}
});
private static boolean isRelevantEvent(SonarLintPushEvent event, SonarLintClient client) {
return client.getClientProjectKeys().contains(event.getProjectKey())
&& (!event.getName().equals("RuleSetChanged") || client.getLanguages().contains(event.getLanguage()));
}

private static void logUnexpectedError(Exception e) {

+ 11
- 1
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushEvent.java View File

@@ -19,6 +19,9 @@
*/
package org.sonar.server.pushapi.sonarlint;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

import static java.nio.charset.StandardCharsets.UTF_8;

public class SonarLintPushEvent {
@@ -26,17 +29,24 @@ public class SonarLintPushEvent {
private final String name;
private final byte[] data;
private final String projectKey;
private final String language;

public SonarLintPushEvent(String name, byte[] data, String projectKey) {
public SonarLintPushEvent(String name, byte[] data, String projectKey, @Nullable String language) {
this.name = name;
this.data = data;
this.projectKey = projectKey;
this.language = language;
}

public String getProjectKey() {
return projectKey;
}

@CheckForNull
public String getLanguage() {
return language;
}

public String getName() {
return name;
}

+ 0
- 48
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java View File

@@ -1,48 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.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);
}
}

+ 0
- 62
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java View File

@@ -1,62 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.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}");
}
}

+ 75
- 53
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java View File

@@ -19,34 +19,30 @@
*/
package org.sonar.server.pushapi.issues;

import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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.pushevent.PushEventDto;
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;
@@ -64,9 +60,7 @@ public class IssueChangeEventServiceImplTest {
@Rule
public DbTester db = DbTester.create();

IssueChangeEventsDistributor eventsDistributor = mock(IssueChangeEventsDistributor.class);

public final IssueChangeEventServiceImpl underTest = new IssueChangeEventServiceImpl(eventsDistributor);
public final IssueChangeEventServiceImpl underTest = new IssueChangeEventServiceImpl(db.getDbClient());

@Test
public void distributeIssueChangeEvent_singleIssueChange_severityChange() {
@@ -74,9 +68,9 @@ public class IssueChangeEventServiceImplTest {
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()));
IssueDto issue = db.issues().insert(rule, project, componentDto, i -> i.setSeverity(MAJOR.name()));

assertIssueDistribution(project, branch, issue, BLOCKER.name(), null, null, null, 1);
assertPushEventIsPersisted(project, branch, issue, BLOCKER.name(), null, null, null, 1);
}

@Test
@@ -85,9 +79,9 @@ public class IssueChangeEventServiceImplTest {
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()));
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);
assertPushEventIsPersisted(project, branch, issue, null, Common.RuleType.BUG.name(), null, null, 1);
}

@Test
@@ -96,16 +90,16 @@ public class IssueChangeEventServiceImplTest {
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);
IssueDto issue = db.issues().insert(rule, project, componentDto, i -> i.setSeverity(MAJOR.name()));
assertPushEventIsPersisted(project, branch, issue, null, null, WONT_FIX, true, 1);
assertPushEventIsPersisted(project, branch, issue, null, null, REOPEN, false, 2);
assertPushEventIsPersisted(project, branch, issue, null, null, FALSE_POSITIVE, true, 3);
assertPushEventIsPersisted(project, branch, issue, null, null, REOPEN, false, 4);
assertPushEventIsPersisted(project, branch, issue, null, null, RESOLVE, false, 5);
assertPushEventIsPersisted(project, branch, issue, null, null, REOPEN, false, 6);
assertNoIssueDistribution(project, branch, issue, null, null, CONFIRM, 7);
assertNoIssueDistribution(project, branch, issue, null, null, UNCONFIRM, 8);
}

@Test
@@ -114,9 +108,9 @@ public class IssueChangeEventServiceImplTest {
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()));
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);
assertPushEventIsPersisted(project, branch, issue, BLOCKER.name(), Common.RuleType.BUG.name(), WONT_FIX, true, 1);
}

@Test
@@ -126,17 +120,17 @@ public class IssueChangeEventServiceImplTest {
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));
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));
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));
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)
@@ -159,18 +153,35 @@ public class IssueChangeEventServiceImplTest {

underTest.distributeIssueChangeEvent(issues, projectsByUuid, branchesByProjectUuid);

ArgumentCaptor<IssueChangedEvent> eventCaptor = ArgumentCaptor.forClass(IssueChangedEvent.class);
verify(eventsDistributor, times(2)).pushEvent(eventCaptor.capture());
Deque<PushEventDto> issueChangedEvents = db.getDbClient().pushEventDao()
.selectChunkByProjectUuids(db.getSession(), Set.of(project1.getUuid(), project2.getUuid()),
1l, null, 3);

List<IssueChangedEvent> issueChangedEvents = eventCaptor.getAllValues();
assertThat(issueChangedEvents).hasSize(2);

assertThat(issueChangedEvents)
.extracting(IssueChangedEvent::getEvent, IssueChangedEvent::getProjectKey,
IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType, IssueChangedEvent::getResolved)
.extracting(PushEventDto::getName, PushEventDto::getProjectUuid)
.containsExactlyInAnyOrder(
tuple("IssueChangedEvent", project1.getKey(), CRITICAL.name(), CODE_SMELL.name(), false),
tuple("IssueChangedEvent", project2.getKey(), CRITICAL.name(), CODE_SMELL.name(), true));
tuple("IssueChanged", project1.getUuid()),
tuple("IssueChanged", project2.getUuid()));

Optional<PushEventDto> project1Event = issueChangedEvents.stream().filter(e -> e.getProjectUuid().equals(project1.getUuid())).findFirst();
Optional<PushEventDto> project2Event = issueChangedEvents.stream().filter(e -> e.getProjectUuid().equals(project2.getUuid())).findFirst();

assertThat(project1Event).isPresent();
assertThat(project2Event).isPresent();

String firstPayload = new String(project1Event.get().getPayload(), StandardCharsets.UTF_8);
assertThat(firstPayload)
.contains("\"userSeverity\":\"" + CRITICAL.name() + "\"",
"\"userType\":\"" + CODE_SMELL.name() + "\"",
"\"resolved\":" + false);

String secondPayload = new String(project2Event.get().getPayload(), StandardCharsets.UTF_8);
assertThat(secondPayload)
.contains("\"userSeverity\":\"" + CRITICAL.name() + "\"",
"\"userType\":\"" + CODE_SMELL.name() + "\"",
"\"resolved\":" + true);
}

@Test
@@ -183,7 +194,7 @@ public class IssueChangeEventServiceImplTest {
.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));
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)
@@ -198,29 +209,40 @@ public class IssueChangeEventServiceImplTest {

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) {
private void assertNoIssueDistribution(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity,
@Nullable String type, @Nullable String transition, int page) {
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);
Deque<PushEventDto> events = db.getDbClient().pushEventDao()
.selectChunkByProjectUuids(db.getSession(), Set.of(project.getUuid()), 1l, null, page);
assertThat(events).hasSizeLessThan(page);
}

private void assertNoIssueDistribution(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity,
@Nullable String type, @Nullable String transition) {
private void assertPushEventIsPersisted(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity,
@Nullable String type, @Nullable String transition, Boolean resolved, int page) {
underTest.distributeIssueChangeEvent(issue.toDefaultIssue(), severity, type, transition, branch, project.getKey());

ArgumentCaptor<IssueChangedEvent> eventCaptor = ArgumentCaptor.forClass(IssueChangedEvent.class);
verifyNoMoreInteractions(eventsDistributor);
Deque<PushEventDto> events = db.getDbClient().pushEventDao()
.selectChunkByProjectUuids(db.getSession(), Set.of(project.getUuid()), 1l, null, page);
assertThat(events).isNotEmpty();
assertThat(events).extracting(PushEventDto::getName, PushEventDto::getProjectUuid)
.contains(tuple("IssueChanged", project.getUuid()));

String payload = new String(events.getLast().getPayload(), StandardCharsets.UTF_8);
if (severity != null) {
assertThat(payload).contains("\"userSeverity\":\"" + severity + "\"");
}

if (type != null) {
assertThat(payload).contains("\"userType\":\"" + type + "\"");
}

if (resolved != null) {
assertThat(payload).contains("\"resolved\":" + resolved);
}

}

}

+ 0
- 41
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java View File

@@ -1,41 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.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);
}
}

+ 35
- 55
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java View File

@@ -19,17 +19,17 @@
*/
package org.sonar.server.pushapi.qualityprofile;

import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.Set;
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.pushevent.PushEventDto;
import org.sonar.db.qualityprofile.ActiveRuleDto;
import org.sonar.db.qualityprofile.ActiveRuleParamDto;
import org.sonar.db.qualityprofile.QProfileDto;
@@ -37,14 +37,11 @@ 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 org.sonarqube.ws.Common;

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;
@@ -54,9 +51,7 @@ public class QualityProfileChangeEventServiceImplTest {
@Rule
public DbTester db = DbTester.create();

RuleActivatorEventsDistributor eventsDistributor = mock(RuleActivatorEventsDistributor.class);

public final QualityProfileChangeEventServiceImpl underTest = new QualityProfileChangeEventServiceImpl(db.getDbClient(), eventsDistributor);
public final QualityProfileChangeEventServiceImpl underTest = new QualityProfileChangeEventServiceImpl(db.getDbClient());

@Test
public void distributeRuleChangeEvent() {
@@ -84,33 +79,27 @@ public class QualityProfileChangeEventServiceImplTest {

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"));
Deque<PushEventDto> events = db.getDbClient().pushEventDao()
.selectChunkByProjectUuids(db.getSession(), Set.of(project.getUuid()), 1l, null, 1);

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(events).isNotEmpty().hasSize(1);
assertThat(events.getFirst())
.extracting(PushEventDto::getName, PushEventDto::getLanguage)
.contains("RuleSetChanged", "xoo");

assertThat(ruleSetChangedEvent.getDeactivatedRules()).isEmpty();
String ruleSetChangedEvent = new String(events.getFirst().getPayload(), StandardCharsets.UTF_8);

assertThat(ruleSetChangedEvent)
.contains("\"activatedRules\":[{\"key\":\"repo:ruleKey\"," +
"\"language\":\"xoo\"," +
"\"templateKey\":\"xoo:template-key\"," +
"\"params\":[{\"key\":\"paramChangeKey\",\"value\":\"paramChangeValue\"}]}]," +
"\"deactivatedRules\":[]");
}

@Test
public void publishRuleActivationToSonarLintClients() {
ProjectDto projectDao = new ProjectDto();
ProjectDto projectDao = new ProjectDto().setUuid("project-uuid");
QProfileDto activatedQualityProfile = QualityProfileTesting.newQualityProfileDto();
activatedQualityProfile.setLanguage("xoo");
db.qualityProfiles().insert(activatedQualityProfile);
@@ -134,30 +123,21 @@ public class QualityProfileChangeEventServiceImplTest {

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");
Deque<PushEventDto> events = db.getDbClient().pushEventDao()
.selectChunkByProjectUuids(db.getSession(), Set.of(projectDao.getUuid()), 1l, null, 1);

assertThat(events).isNotEmpty().hasSize(1);
assertThat(events.getFirst())
.extracting(PushEventDto::getName, PushEventDto::getLanguage)
.contains("RuleSetChanged", "xoo");

String ruleSetChangedEvent = new String(events.getFirst().getPayload(), StandardCharsets.UTF_8);

assertThat(ruleSetChangedEvent)
.contains("\"activatedRules\":[{\"key\":\"repo:ruleKey\"," +
"\"language\":\"xoo\",\"severity\":\"" + Common.Severity.forNumber(rule1.getSeverity()).name() + "\"," +
"\"params\":[{\"key\":\"" + activeRuleParam1.getKey() + "\",\"value\":\"" + activeRuleParam1.getValue() + "\"}]}]," +
"\"deactivatedRules\":[\"repo2:ruleKey2\"]");
}

}

+ 0
- 81
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java View File

@@ -1,81 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.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\"]}");
}
}

+ 53
- 105
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java View File

@@ -28,15 +28,7 @@ import javax.servlet.ServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.sonar.api.rule.Severity;
import org.sonar.core.util.ParamChange;
import org.sonar.core.util.issue.Issue;
import org.sonar.core.util.issue.IssueChangedEvent;
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;
import static org.mockito.ArgumentMatchers.any;
@@ -52,6 +44,7 @@ import static org.mockito.Mockito.when;
import static org.sonar.test.EventAssert.assertThatEvent;

public class SonarLintClientsRegistryTest {
private static final String EVENT_NAME = "RuleSetChanged";

private final AsyncContext defaultAsyncContext = mock(AsyncContext.class);

@@ -62,14 +55,12 @@ public class SonarLintClientsRegistryTest {
private final ServletOutputStream outputStream = mock(ServletOutputStream.class);

private final SonarLintClientPermissionsValidator permissionsValidator = mock(SonarLintClientPermissionsValidator.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(issueChangeEventsDistributor, ruleEventsDistributor, permissionsValidator);
underTest = new SonarLintClientsRegistry(permissionsValidator);
}

@Test
@@ -100,7 +91,7 @@ public class SonarLintClientsRegistryTest {
}

@Test
public void listen_givenOneClientInterestedInJavaEvents_sendOneJavaEvent() throws IOException {
public void listen_givenOneClientInterestedInJavaEvents_sendAllJavaEvents() throws IOException {
Set<String> javaLanguageKey = Set.of("java");
when(defaultAsyncContext.getResponse()).thenReturn(response);
when(response.getOutputStream()).thenReturn(outputStream);
@@ -108,19 +99,36 @@ public class SonarLintClientsRegistryTest {

underTest.registerClient(sonarLintClient);

RuleChange javaRule = createRuleChange();
SonarLintPushEvent event1 = new SonarLintPushEvent(EVENT_NAME, "data".getBytes(StandardCharsets.UTF_8), "project1", "java");
SonarLintPushEvent event2 = new SonarLintPushEvent(EVENT_NAME, "data".getBytes(StandardCharsets.UTF_8), "project2", "java");
SonarLintPushEvent event3 = new SonarLintPushEvent(EVENT_NAME, "data".getBytes(StandardCharsets.UTF_8), "project3", "java");

RuleChange[] activatedRules = {javaRule};
String[] deactivatedRules = {"repo2:rule-key2"};
RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules, "java");
underTest.listen(ruleSetChangedEvent);
underTest.broadcastMessage(event1);

ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
verify(outputStream).write(captor.capture());
String message = new String(captor.getValue());
assertThatEvent(message)
.hasType("RuleSetChanged")
.hasJsonData(getClass().getResource("rule-change-event-data.json"));
.hasType(EVENT_NAME);

clearInvocations(outputStream);

underTest.broadcastMessage(event2);

verify(outputStream).write(captor.capture());
message = new String(captor.getValue());
assertThatEvent(message)
.hasType(EVENT_NAME);

clearInvocations(outputStream);


underTest.broadcastMessage(event3);

verify(outputStream).write(captor.capture());
message = new String(captor.getValue());
assertThatEvent(message)
.hasType(EVENT_NAME);
}

@Test
@@ -132,10 +140,9 @@ public class SonarLintClientsRegistryTest {

underTest.registerClient(sonarLintClient);

RuleChange[] activatedRules = {};
String[] deactivatedRules = {"repo:rule-key"};
RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules, "java");
underTest.listen(ruleSetChangedEvent);
SonarLintPushEvent event = new SonarLintPushEvent(EVENT_NAME, "data".getBytes(StandardCharsets.UTF_8), "project1", "java");

underTest.broadcastMessage(event);

verifyNoInteractions(outputStream);
}
@@ -144,74 +151,56 @@ public class SonarLintClientsRegistryTest {
public void listen_givenOneClientInterestedInProjA_DontCheckPermissionsForProjB() throws IOException {
when(defaultAsyncContext.getResponse()).thenReturn(response);
when(response.getOutputStream()).thenReturn(outputStream);
Set<String> clientProjectKeys = Set.of("projA");
Set<String> eventProjectKeys = Set.of("projA", "projB");
SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, clientProjectKeys, Set.of("java"), USER_UUID);
SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, Set.of("projA"), Set.of("java"), USER_UUID);

underTest.registerClient(sonarLintClient);

RuleChange[] activatedRules = {};
String[] deactivatedRules = {"repo:rule-key"};
RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(eventProjectKeys.toArray(String[]::new), activatedRules, deactivatedRules, "java");
underTest.listen(ruleSetChangedEvent);
SonarLintPushEvent event1 = new SonarLintPushEvent(EVENT_NAME, "data".getBytes(StandardCharsets.UTF_8), "projA", "java");
SonarLintPushEvent event2 = new SonarLintPushEvent(EVENT_NAME, "data".getBytes(StandardCharsets.UTF_8), "projB", "java");

ArgumentCaptor<Set<String>> argument = ArgumentCaptor.forClass(Set.class);
verify(permissionsValidator).validateUserCanReceivePushEventForProjects(anyString(), argument.capture());
assertThat(argument.getValue()).isEqualTo(clientProjectKeys);
}

@Test
public void listen_givenUserNotPermittedToReceiveRuleSetChangedEvent_closeConnection() {
RuleChange[] activatedRules = {};
String[] deactivatedRules = {"repo:rule-key"};
RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules, "java");

SonarLintClient sonarLintClient = createSampleSLClient();
underTest.registerClient(sonarLintClient);
doThrow(new ForbiddenException("Access forbidden")).when(permissionsValidator).validateUserCanReceivePushEventForProjects(anyString(), anySet());
underTest.broadcastMessage(event1);
underTest.broadcastMessage(event2);

underTest.listen(ruleSetChangedEvent);

verify(sonarLintClient).close();
verify(permissionsValidator, times(1)).validateUserCanReceivePushEventForProjects(anyString(), argument.capture());
assertThat(argument.getValue()).hasSize(1).contains("projA");
}

@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");

public void listen_givenUserNotPermittedToReceiveEvent_closeConnection() {
SonarLintClient sonarLintClient = createSampleSLClient();
underTest.registerClient(sonarLintClient);
doThrow(new ForbiddenException("Access forbidden")).when(permissionsValidator).validateUserCanReceivePushEventForProjects(anyString(), anySet());

underTest.listen(issueChangedEvent);
SonarLintPushEvent event = new SonarLintPushEvent(EVENT_NAME, "data".getBytes(StandardCharsets.UTF_8), "project1", "java");

underTest.broadcastMessage(event);

verify(sonarLintClient).close();
}

@Test
public void listen_givenUnregisteredClient_closeConnection() throws IOException {
RuleChange[] activatedRules = {};
String[] deactivatedRules = {"repo:rule-key"};
RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules, "java");

SonarLintClient sonarLintClient = createSampleSLClient();
underTest.registerClient(sonarLintClient);
doThrow(new IOException("Broken pipe")).when(sonarLintClient).writeAndFlush(anyString());

underTest.listen(ruleSetChangedEvent);
SonarLintPushEvent event = new SonarLintPushEvent(EVENT_NAME, "data".getBytes(StandardCharsets.UTF_8), "project1", "java");

underTest.broadcastMessage(event);

underTest.registerClient(sonarLintClient);
doThrow(new IllegalStateException("Things went wrong")).when(sonarLintClient).writeAndFlush(anyString());

underTest.listen(ruleSetChangedEvent);
underTest.broadcastMessage(event);

verify(sonarLintClient, times(2)).close();
}

@Test
public void broadcast_push_event_to_clients() throws IOException {
SonarLintPushEvent event = new SonarLintPushEvent("event", "data".getBytes(StandardCharsets.UTF_8), "project2");
SonarLintPushEvent event = new SonarLintPushEvent("event", "data".getBytes(StandardCharsets.UTF_8), "project2", null);

SonarLintClient sonarLintClient = createSampleSLClient();
underTest.registerClient(sonarLintClient);
@@ -224,7 +213,7 @@ public class SonarLintClientsRegistryTest {

@Test
public void broadcast_skips_push_if_event_project_does_not_match_with_client() throws IOException {
SonarLintPushEvent event = new SonarLintPushEvent("event", "data".getBytes(StandardCharsets.UTF_8), "project4");
SonarLintPushEvent event = new SonarLintPushEvent("event", "data".getBytes(StandardCharsets.UTF_8), "project4", null);

SonarLintClient sonarLintClient = createSampleSLClient();
underTest.registerClient(sonarLintClient);
@@ -238,7 +227,7 @@ public class SonarLintClientsRegistryTest {

@Test
public void broadcast_givenUserNotPermittedToReceiveSonarLintPushEvent_closeConnection() {
SonarLintPushEvent event = new SonarLintPushEvent("event", "data".getBytes(StandardCharsets.UTF_8), "project1");
SonarLintPushEvent event = new SonarLintPushEvent("event", "data".getBytes(StandardCharsets.UTF_8), "project1", null);

SonarLintClient sonarLintClient = createSampleSLClient();
underTest.registerClient(sonarLintClient);
@@ -251,7 +240,7 @@ public class SonarLintClientsRegistryTest {

@Test
public void broadcast_givenUnregisteredClient_closeConnection() throws IOException {
SonarLintPushEvent event = new SonarLintPushEvent("event", "data".getBytes(StandardCharsets.UTF_8), "project1");
SonarLintPushEvent event = new SonarLintPushEvent("event", "data".getBytes(StandardCharsets.UTF_8), "project1", null);

SonarLintClient sonarLintClient = createSampleSLClient();
underTest.registerClient(sonarLintClient);
@@ -268,42 +257,11 @@ public class SonarLintClientsRegistryTest {
}

@Test
public void registerClient_whenCalledFirstTime_registerAlsoToListenToEvents() {
underTest.registerClient(createSampleSLClient());

verify(ruleEventsDistributor).subscribe(underTest);
verify(issueChangeEventsDistributor).subscribe(underTest);
}

@Test
public void registerClient_whenCalledSecondTime_doNotRegisterToEvents() {
underTest.registerClient(createSampleSLClient());
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());
verify(ruleEventsDistributor).subscribe(underTest);
}

@Test
public void registerClient_whenExceptionAndCalledSecondTime_registerToIssueChangeEvents() {
doThrow(new RuntimeException()).when(issueChangeEventsDistributor).subscribe(any());
underTest.registerClient(createSampleSLClient());
clearInvocations(issueChangeEventsDistributor);
public void registerClient_whenCalledFirstTime_addsAsyncListenerToClient() {
SonarLintClient client = mock(SonarLintClient.class);
underTest.registerClient(client);

underTest.registerClient(createSampleSLClient());
verify(issueChangeEventsDistributor).subscribe(underTest);
verify(client).addListener(any());
}

private SonarLintClient createSampleSLClient() {
@@ -313,14 +271,4 @@ public class SonarLintClientsRegistryTest {
when(mock.getUserUuid()).thenReturn("userUuid");
return mock;
}

private RuleChange createRuleChange() {
RuleChange javaRule = new RuleChange();
javaRule.setLanguage("java");
javaRule.setParams(new ParamChange[] {new ParamChange("param-key", "param-value")});
javaRule.setTemplateKey("repo:template-key");
javaRule.setSeverity(Severity.CRITICAL);
javaRule.setKey("repo:rule-key");
return javaRule;
}
}

+ 0
- 24
server/sonar-webserver-pushapi/src/test/resources/org/sonar/server/pushapi/sonarlint/rule-change-event-data.json View File

@@ -1,24 +0,0 @@
{
"projects": [
"project2",
"project1",
"project3"
],
"activatedRules": [
{
"key": "repo:rule-key",
"templateKey": "repo:template-key",
"severity": "CRITICAL",
"language": "java",
"params": [
{
"value": "param-value",
"key": "param-key"
}
]
}
],
"deactivatedRules": [
"repo2:rule-key2"
]
}

+ 0
- 10
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -185,12 +185,8 @@ import org.sonar.server.projectlink.ws.ProjectLinksModule;
import org.sonar.server.projecttag.ws.ProjectTagsWsModule;
import org.sonar.server.property.InternalPropertiesImpl;
import org.sonar.server.pushapi.ServerPushModule;
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;
import org.sonar.server.qualitygate.ProjectsInWarningModule;
import org.sonar.server.qualitygate.QualityGateModule;
import org.sonar.server.qualitygate.notification.QGChangeNotificationHandler;
@@ -285,12 +281,6 @@ public class PlatformLevel4 extends PlatformLevel {

addIfCluster(new NodeHealthModule());

addIfCluster(DistributedRuleActivatorEventsDistributor.class);
addIfStandalone(StandaloneRuleActivatorEventsDistributor.class);

addIfCluster(DistributedIssueChangeEventsDistributor.class);
addIfStandalone(StandaloneIssueChangeEventsDistributor.class);

add(
RuleDescriptionFormatter.class,
ClusterVerification.class,

+ 0
- 24
sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangeListener.java View File

@@ -1,24 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.core.util.issue;

public interface IssueChangeListener {
void listen(IssueChangedEvent event);
}

+ 1
- 1
sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangedEvent.java View File

@@ -24,7 +24,7 @@ import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

public class IssueChangedEvent implements Serializable {
private static final String EVENT = "IssueChangedEvent";
private static final String EVENT = "IssueChanged";

private final String projectKey;
private final Issue[] issues;

+ 0
- 25
sonar-core/src/main/java/org/sonar/core/util/rule/RuleActivationListener.java View File

@@ -1,25 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.core.util.rule;

public interface RuleActivationListener {

void listen(RuleSetChangedEvent event);
}

+ 2
- 15
sonar-core/src/main/java/org/sonar/core/util/rule/RuleSetChangedEvent.java View File

@@ -22,36 +22,23 @@ 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;
public RuleSetChangedEvent(String projectKey, RuleChange[] activatedRules, String[] deactivatedRules) {
this.projects = new String[]{projectKey};
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;
}

+ 2
- 2
sonar-core/src/test/java/org/sonar/core/util/issue/IssueChangedEventTest.java View File

@@ -34,7 +34,7 @@ public class IssueChangedEventTest {
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.getEvent()).isEqualTo("IssueChanged");
assertThat(event.getProjectKey()).isEqualTo(PROJECT_KEY);
assertThat(event.getResolved()).isNull();
assertThat(event.getUserSeverity()).isNull();
@@ -47,7 +47,7 @@ public class IssueChangedEventTest {
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.getEvent()).isEqualTo("IssueChanged");
assertThat(event.getProjectKey()).isEqualTo(PROJECT_KEY);
assertThat(event.getResolved()).isTrue();
assertThat(event.getUserSeverity()).isEqualTo("BLOCKER");

+ 8
- 21
sonar-core/src/test/java/org/sonar/core/util/rule/RuleSetChangedEventTest.java View File

@@ -27,36 +27,23 @@ 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 = {};
public void getDeactivatedAndActivatedRules() {
String project = "sonarqube";
RuleChange[] activatedRules = { new RuleChange()};
String[] deactivatedRules = {"ruleKey"};
RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java");

String language = event.getLanguage();
RuleSetChangedEvent event = new RuleSetChangedEvent(project, activatedRules, deactivatedRules);

assertThat(language).isEqualTo("java");
assertThat(event.getActivatedRules()).isEqualTo(activatedRules);
assertThat(event.getDeactivatedRules()).isEqualTo(deactivatedRules);
}

@Test
public void getLanguage_givenBothArraysEmpty_throwException() {
String[] projects = {"sonarqube"};
String project = "sonarqube";
RuleChange[] activatedRules = {};
String[] deactivatedRules = {};

assertThatThrownBy(() -> new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"))
assertThatThrownBy(() -> new RuleSetChangedEvent(project, activatedRules, deactivatedRules))
.isInstanceOf(IllegalArgumentException.class);
}


Loading…
Cancel
Save