3 * Copyright (C) 2009-2022 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.pushapi.sonarlint;
22 import java.io.IOException;
23 import java.util.HashSet;
24 import java.util.List;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.function.Predicate;
28 import javax.servlet.AsyncEvent;
29 import javax.servlet.AsyncListener;
30 import org.sonar.api.server.ServerSide;
31 import org.sonar.api.utils.log.Logger;
32 import org.sonar.api.utils.log.Loggers;
33 import org.sonar.core.util.issue.IssueChangeListener;
34 import org.sonar.core.util.issue.IssueChangedEvent;
35 import org.sonar.core.util.rule.RuleActivationListener;
36 import org.sonar.core.util.rule.RuleSetChangedEvent;
37 import org.sonar.server.exceptions.ForbiddenException;
38 import org.sonar.server.pushapi.issues.IssueChangeBroadcastUtils;
39 import org.sonar.server.pushapi.issues.IssueChangeEventsDistributor;
40 import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor;
41 import org.sonar.server.pushapi.qualityprofile.RuleSetChangeBroadcastUtils;
44 public class SonarLintClientsRegistry implements RuleActivationListener, IssueChangeListener {
46 private static final Logger LOG = Loggers.get(SonarLintClientsRegistry.class);
48 private final SonarLintClientPermissionsValidator sonarLintClientPermissionsValidator;
49 private final List<SonarLintClient> clients = new CopyOnWriteArrayList<>();
50 private final RuleActivatorEventsDistributor ruleEventsDistributor;
51 private final IssueChangeEventsDistributor issueChangeEventsDistributor;
53 private boolean registeredToEvents = false;
55 public SonarLintClientsRegistry(IssueChangeEventsDistributor issueChangeEventsDistributor,
56 RuleActivatorEventsDistributor ruleActivatorEventsDistributor, SonarLintClientPermissionsValidator permissionsValidator) {
57 this.issueChangeEventsDistributor = issueChangeEventsDistributor;
58 this.sonarLintClientPermissionsValidator = permissionsValidator;
59 this.ruleEventsDistributor = ruleActivatorEventsDistributor;
62 public void registerClient(SonarLintClient sonarLintClient) {
63 ensureListeningToEvents();
64 clients.add(sonarLintClient);
65 sonarLintClient.scheduleHeartbeat();
66 sonarLintClient.addListener(new SonarLintClientEventsListener(sonarLintClient));
67 LOG.debug("Registering new SonarLint client");
70 private synchronized void ensureListeningToEvents() {
71 if (registeredToEvents) {
75 ruleEventsDistributor.subscribe(this);
76 issueChangeEventsDistributor.subscribe(this);
77 registeredToEvents = true;
78 } catch (RuntimeException e) {
79 LOG.warn("Can not listen to rule activation or issue events for server push. Web Server might not have started fully yet.", e);
83 public void unregisterClient(SonarLintClient client) {
85 clients.remove(client);
86 LOG.debug("Removing SonarLint client");
89 public long countConnectedClients() {
90 return clients.size();
94 public void listen(RuleSetChangedEvent ruleSetChangedEvent) {
95 broadcastMessage(ruleSetChangedEvent, RuleSetChangeBroadcastUtils.getFilterForEvent(ruleSetChangedEvent));
99 public void listen(IssueChangedEvent issueChangedEvent) {
100 broadcastMessage(issueChangedEvent, IssueChangeBroadcastUtils.getFilterForEvent(issueChangedEvent));
103 public void broadcastMessage(RuleSetChangedEvent event, Predicate<SonarLintClient> filter) {
104 clients.stream().filter(filter).forEach(c -> {
105 Set<String> projectKeysInterestingForClient = new HashSet<>(c.getClientProjectKeys());
106 projectKeysInterestingForClient.retainAll(Set.of(event.getProjects()));
108 sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient);
109 RuleSetChangedEvent personalizedEvent = new RuleSetChangedEvent(projectKeysInterestingForClient.toArray(String[]::new), event.getActivatedRules(),
110 event.getDeactivatedRules(), event.getLanguage());
111 String message = RuleSetChangeBroadcastUtils.getMessage(personalizedEvent);
112 c.writeAndFlush(message);
113 } catch (ForbiddenException forbiddenException) {
114 LOG.debug("Client is no longer authenticated: " + forbiddenException.getMessage());
116 } catch (IllegalStateException | IOException e) {
117 LOG.error("Unable to send message to a client: " + e.getMessage());
123 public void broadcastMessage(IssueChangedEvent event, Predicate<SonarLintClient> filter) {
124 clients.stream().filter(filter).forEach(c -> {
125 Set<String> projectKeysInterestingForClient = new HashSet<>(c.getClientProjectKeys());
126 projectKeysInterestingForClient.retainAll(Set.of(event.getProjectKey()));
128 sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient);
129 String message = IssueChangeBroadcastUtils.getMessage(event);
130 c.writeAndFlush(message);
131 } catch (ForbiddenException forbiddenException) {
132 LOG.debug("Client is no longer authenticated: " + forbiddenException.getMessage());
134 } catch (IllegalStateException | IOException e) {
135 LOG.error("Unable to send message to a client: " + e.getMessage());
141 class SonarLintClientEventsListener implements AsyncListener {
142 private final SonarLintClient client;
144 public SonarLintClientEventsListener(SonarLintClient sonarLintClient) {
145 this.client = sonarLintClient;
149 public void onComplete(AsyncEvent event) {
150 unregisterClient(client);
154 public void onError(AsyncEvent event) {
155 unregisterClient(client);
159 public void onStartAsync(AsyncEvent event) {
160 // nothing to do on start
164 public void onTimeout(AsyncEvent event) {
165 unregisterClient(client);