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.Collections;
24 import java.util.HashSet;
25 import java.util.List;
27 import java.util.concurrent.CopyOnWriteArrayList;
28 import java.util.function.Predicate;
29 import javax.servlet.AsyncEvent;
30 import javax.servlet.AsyncListener;
31 import org.json.JSONArray;
32 import org.json.JSONObject;
33 import org.sonar.api.server.ServerSide;
34 import org.sonar.api.utils.log.Logger;
35 import org.sonar.api.utils.log.Loggers;
36 import org.sonar.core.util.ParamChange;
37 import org.sonar.core.util.RuleActivationListener;
38 import org.sonar.core.util.RuleChange;
39 import org.sonar.core.util.RuleSetChangedEvent;
40 import org.sonar.server.exceptions.ForbiddenException;
41 import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor;
43 import static java.util.Arrays.asList;
46 public class SonarLintClientsRegistry implements RuleActivationListener {
48 private static final Logger LOG = Loggers.get(SonarLintClientsRegistry.class);
50 private final SonarLintClientPermissionsValidator sonarLintClientPermissionsValidator;
51 private final List<SonarLintClient> clients = new CopyOnWriteArrayList<>();
52 private final RuleActivatorEventsDistributor eventsDistributor;
54 private boolean registeredToEvents = false;
56 public SonarLintClientsRegistry(RuleActivatorEventsDistributor ruleActivatorEventsDistributor, SonarLintClientPermissionsValidator permissionsValidator) {
57 this.sonarLintClientPermissionsValidator = permissionsValidator;
58 this.eventsDistributor = ruleActivatorEventsDistributor;
61 public void registerClient(SonarLintClient sonarLintClient) {
62 ensureListeningToEvents();
63 clients.add(sonarLintClient);
64 sonarLintClient.scheduleHeartbeat();
65 sonarLintClient.addListener(new SonarLintClientEventsListener(sonarLintClient));
66 LOG.debug("Registering new SonarLint client");
69 private synchronized void ensureListeningToEvents() {
70 if (registeredToEvents) {
74 eventsDistributor.subscribe(this);
75 registeredToEvents = true;
76 } catch (RuntimeException e) {
77 LOG.warn("Can not listen to rule activation events for server push. Web Server might not have started fully yet.", e);
81 public void unregisterClient(SonarLintClient client) {
83 clients.remove(client);
84 LOG.debug("Removing SonarLint client");
87 public long countConnectedClients() {
88 return clients.size();
92 public void listen(RuleSetChangedEvent ruleSetChangedEvent) {
93 broadcastMessage(ruleSetChangedEvent, getFilterForEvent(ruleSetChangedEvent));
96 private static Predicate<SonarLintClient> getFilterForEvent(RuleSetChangedEvent ruleSetChangedEvent) {
97 List<String> affectedProjects = asList(ruleSetChangedEvent.getProjects());
99 Set<String> clientProjectKeys = client.getClientProjectKeys();
100 Set<String> languages = client.getLanguages();
101 return !Collections.disjoint(clientProjectKeys, affectedProjects) && languages.contains(ruleSetChangedEvent.getLanguage());
105 public void broadcastMessage(RuleSetChangedEvent event, Predicate<SonarLintClient> filter) {
106 clients.stream().filter(filter).forEach(c -> {
107 Set<String> projectKeysInterestingForClient = new HashSet<>(c.getClientProjectKeys());
108 projectKeysInterestingForClient.retainAll(Set.of(event.getProjects()));
110 sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient);
111 RuleSetChangedEvent personalizedEvent = new RuleSetChangedEvent(projectKeysInterestingForClient.toArray(String[]::new), event.getActivatedRules(),
112 event.getDeactivatedRules(), event.getLanguage());
113 String message = getMessage(personalizedEvent);
114 c.writeAndFlush(message);
115 } catch (ForbiddenException forbiddenException) {
116 LOG.debug("Client is no longer authenticated: " + forbiddenException.getMessage());
118 } catch (IllegalStateException | IOException e) {
119 LOG.error("Unable to send message to a client: " + e.getMessage());
124 private static String getMessage(RuleSetChangedEvent ruleSetChangedEvent) {
125 return "event: " + ruleSetChangedEvent.getEvent() + "\n"
126 + "data: " + toJson(ruleSetChangedEvent);
129 private static String toJson(RuleSetChangedEvent ruleSetChangedEvent) {
130 JSONObject data = new JSONObject();
131 data.put("projects", ruleSetChangedEvent.getProjects());
133 JSONArray activatedRulesJson = new JSONArray();
134 for (RuleChange rule : ruleSetChangedEvent.getActivatedRules()) {
135 activatedRulesJson.put(toJson(rule));
137 data.put("activatedRules", activatedRulesJson);
139 JSONArray deactivatedRulesJson = new JSONArray();
140 for (String ruleKey : ruleSetChangedEvent.getDeactivatedRules()) {
141 deactivatedRulesJson.put(ruleKey);
143 data.put("deactivatedRules", deactivatedRulesJson);
145 return data.toString();
148 private static JSONObject toJson(RuleChange rule) {
149 JSONObject ruleJson = new JSONObject();
150 ruleJson.put("key", rule.getKey());
151 ruleJson.put("language", rule.getLanguage());
152 ruleJson.put("severity", rule.getSeverity());
153 ruleJson.put("templateKey", rule.getTemplateKey());
155 JSONArray params = new JSONArray();
156 for (ParamChange paramChange : rule.getParams()) {
157 params.put(toJson(paramChange));
159 ruleJson.put("params", params);
163 private static JSONObject toJson(ParamChange paramChange) {
164 JSONObject param = new JSONObject();
165 param.put("key", paramChange.getKey());
166 param.put("value", paramChange.getValue());
170 class SonarLintClientEventsListener implements AsyncListener {
171 private final SonarLintClient client;
173 public SonarLintClientEventsListener(SonarLintClient sonarLintClient) {
174 this.client = sonarLintClient;
178 public void onComplete(AsyncEvent event) {
179 unregisterClient(client);
183 public void onError(AsyncEvent event) {
184 unregisterClient(client);
188 public void onStartAsync(AsyncEvent event) {
189 // nothing to do on start
193 public void onTimeout(AsyncEvent event) {
194 unregisterClient(client);