]> source.dussan.org Git - sonarqube.git/blob
e0ae3f266cfd9230684bb5691b38bdba1d211a8d
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 package org.sonar.ce.task.projectanalysis.step;
21
22 import com.google.common.collect.ImmutableSet;
23 import java.time.Instant;
24 import java.time.temporal.ChronoUnit;
25 import java.util.Date;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.Set;
31 import java.util.function.Predicate;
32 import java.util.stream.Collectors;
33 import javax.annotation.CheckForNull;
34 import org.sonar.api.issue.Issue;
35 import org.sonar.api.notifications.Notification;
36 import org.sonar.api.rules.RuleType;
37 import org.sonar.api.utils.Duration;
38 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
39 import org.sonar.ce.task.projectanalysis.analysis.Branch;
40 import org.sonar.ce.task.projectanalysis.component.Component;
41 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
42 import org.sonar.ce.task.projectanalysis.issue.ProtoIssueCache;
43 import org.sonar.ce.task.projectanalysis.notification.NotificationFactory;
44 import org.sonar.ce.task.step.ComputationStep;
45 import org.sonar.core.issue.DefaultIssue;
46 import org.sonar.core.util.CloseableIterator;
47 import org.sonar.db.DbClient;
48 import org.sonar.db.DbSession;
49 import org.sonar.db.component.BranchType;
50 import org.sonar.db.user.UserDto;
51 import org.sonar.server.issue.notification.IssuesChangesNotification;
52 import org.sonar.server.issue.notification.MyNewIssuesNotification;
53 import org.sonar.server.issue.notification.NewIssuesNotification;
54 import org.sonar.server.issue.notification.NewIssuesStatistics;
55 import org.sonar.server.notification.NotificationService;
56
57 import static java.util.Collections.singleton;
58 import static java.util.stream.Collectors.toList;
59 import static java.util.stream.Collectors.toMap;
60 import static java.util.stream.StreamSupport.stream;
61 import static org.sonar.core.util.stream.MoreCollectors.toSet;
62 import static org.sonar.db.component.BranchType.PULL_REQUEST;
63
64 /**
65  * Reads issues from disk cache and send related notifications. For performance reasons,
66  * the standard notification DB queue is not used as a temporary storage. Notifications
67  * are directly processed by {@link NotificationService}.
68  */
69 public class SendIssueNotificationsStep implements ComputationStep {
70   /**
71    * Types of the notifications sent by this step
72    */
73   static final Set<Class<? extends Notification>> NOTIF_TYPES = ImmutableSet.of(NewIssuesNotification.class, MyNewIssuesNotification.class, IssuesChangesNotification.class);
74
75   private final ProtoIssueCache protoIssueCache;
76   private final TreeRootHolder treeRootHolder;
77   private final NotificationService service;
78   private final AnalysisMetadataHolder analysisMetadataHolder;
79   private final NotificationFactory notificationFactory;
80   private final DbClient dbClient;
81
82   public SendIssueNotificationsStep(ProtoIssueCache protoIssueCache, TreeRootHolder treeRootHolder,
83     NotificationService service, AnalysisMetadataHolder analysisMetadataHolder,
84     NotificationFactory notificationFactory, DbClient dbClient) {
85     this.protoIssueCache = protoIssueCache;
86     this.treeRootHolder = treeRootHolder;
87     this.service = service;
88     this.analysisMetadataHolder = analysisMetadataHolder;
89     this.notificationFactory = notificationFactory;
90     this.dbClient = dbClient;
91   }
92
93   @Override
94   public void execute(ComputationStep.Context context) {
95     BranchType branchType = analysisMetadataHolder.getBranch().getType();
96     if (branchType == PULL_REQUEST) {
97       return;
98     }
99
100     Component project = treeRootHolder.getRoot();
101     NotificationStatistics notificationStatistics = new NotificationStatistics();
102     if (service.hasProjectSubscribersForTypes(analysisMetadataHolder.getProject().getUuid(), NOTIF_TYPES)) {
103       doExecute(notificationStatistics, project);
104     }
105     notificationStatistics.dumpTo(context);
106   }
107
108   private void doExecute(NotificationStatistics notificationStatistics, Component project) {
109     long analysisDate = analysisMetadataHolder.getAnalysisDate();
110     Predicate<DefaultIssue> onCurrentAnalysis = i -> i.isNew() && i.creationDate().getTime() >= truncateToSeconds(analysisDate);
111     NewIssuesStatistics newIssuesStats = new NewIssuesStatistics(onCurrentAnalysis);
112     Map<String, UserDto> assigneesByUuid;
113     try (DbSession dbSession = dbClient.openSession(false)) {
114       Iterable<DefaultIssue> iterable = protoIssueCache::traverse;
115       Set<String> assigneeUuids = stream(iterable.spliterator(), false).map(DefaultIssue::assignee).filter(Objects::nonNull).collect(Collectors.toSet());
116       assigneesByUuid = dbClient.userDao().selectByUuids(dbSession, assigneeUuids).stream().collect(toMap(UserDto::getUuid, dto -> dto));
117     }
118
119     try (CloseableIterator<DefaultIssue> issues = protoIssueCache.traverse()) {
120       processIssues(newIssuesStats, issues, assigneesByUuid, notificationStatistics);
121     }
122     if (newIssuesStats.hasIssuesOnCurrentAnalysis()) {
123       sendNewIssuesNotification(newIssuesStats, project, assigneesByUuid, analysisDate, notificationStatistics);
124       sendMyNewIssuesNotification(newIssuesStats, project, assigneesByUuid, analysisDate, notificationStatistics);
125     }
126   }
127
128   /**
129    * Truncated the analysis date to seconds before comparing it to {@link Issue#creationDate()} is required because
130    * {@link DefaultIssue#setCreationDate(Date)} does it.
131    */
132   private static long truncateToSeconds(long analysisDate) {
133     Instant instant = new Date(analysisDate).toInstant();
134     instant = instant.truncatedTo(ChronoUnit.SECONDS);
135     return Date.from(instant).getTime();
136   }
137
138   private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues,
139     Map<String, UserDto> assigneesByUuid, NotificationStatistics notificationStatistics) {
140     int batchSize = 1000;
141     Set<DefaultIssue> changedIssuesToNotify = new HashSet<>(batchSize);
142     while (issues.hasNext()) {
143       DefaultIssue issue = issues.next();
144       if (issue.type() != RuleType.SECURITY_HOTSPOT) {
145         if (issue.isNew() && issue.resolution() == null) {
146           newIssuesStats.add(issue);
147         } else if (issue.isChanged() && issue.mustSendNotifications()) {
148           changedIssuesToNotify.add(issue);
149         }
150       }
151
152       if (changedIssuesToNotify.size() >= batchSize) {
153         sendIssuesChangesNotification(changedIssuesToNotify, assigneesByUuid, notificationStatistics);
154         changedIssuesToNotify.clear();
155       }
156     }
157
158     if (!changedIssuesToNotify.isEmpty()) {
159       sendIssuesChangesNotification(changedIssuesToNotify, assigneesByUuid, notificationStatistics);
160     }
161   }
162
163   private void sendIssuesChangesNotification(Set<DefaultIssue> issues, Map<String, UserDto> assigneesByUuid, NotificationStatistics notificationStatistics) {
164     IssuesChangesNotification notification = notificationFactory.newIssuesChangesNotification(issues, assigneesByUuid);
165
166     notificationStatistics.issueChangesDeliveries += service.deliverEmails(singleton(notification));
167     notificationStatistics.issueChanges++;
168
169     // compatibility with old API
170     notificationStatistics.issueChangesDeliveries += service.deliver(notification);
171   }
172
173   private void sendNewIssuesNotification(NewIssuesStatistics statistics, Component project, Map<String, UserDto> assigneesByUuid,
174     long analysisDate, NotificationStatistics notificationStatistics) {
175     NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics();
176     NewIssuesNotification notification = notificationFactory
177       .newNewIssuesNotification(assigneesByUuid)
178       .setProject(project.getKey(), project.getName(), getBranchName(), getPullRequest())
179       .setProjectVersion(project.getProjectAttributes().getProjectVersion())
180       .setAnalysisDate(new Date(analysisDate))
181       .setStatistics(project.getName(), globalStatistics)
182       .setDebt(Duration.create(globalStatistics.effort().getOnCurrentAnalysis()));
183     notificationStatistics.newIssuesDeliveries += service.deliverEmails(singleton(notification));
184     notificationStatistics.newIssues++;
185
186     // compatibility with old API
187     notificationStatistics.newIssuesDeliveries += service.deliver(notification);
188   }
189
190   private void sendMyNewIssuesNotification(NewIssuesStatistics statistics, Component project, Map<String, UserDto> assigneesByUuid, long analysisDate,
191     NotificationStatistics notificationStatistics) {
192     Map<String, UserDto> userDtoByUuid = loadUserDtoByUuid(statistics);
193     Set<MyNewIssuesNotification> myNewIssuesNotifications = statistics.getAssigneesStatistics().entrySet()
194       .stream()
195       .filter(e -> e.getValue().hasIssuesOnCurrentAnalysis())
196       .map(e -> {
197         String assigneeUuid = e.getKey();
198         NewIssuesStatistics.Stats assigneeStatistics = e.getValue();
199         MyNewIssuesNotification myNewIssuesNotification = notificationFactory
200           .newMyNewIssuesNotification(assigneesByUuid)
201           .setAssignee(userDtoByUuid.get(assigneeUuid));
202         myNewIssuesNotification
203           .setProject(project.getKey(), project.getName(), getBranchName(), getPullRequest())
204           .setProjectVersion(project.getProjectAttributes().getProjectVersion())
205           .setAnalysisDate(new Date(analysisDate))
206           .setStatistics(project.getName(), assigneeStatistics)
207           .setDebt(Duration.create(assigneeStatistics.effort().getOnCurrentAnalysis()));
208
209         return myNewIssuesNotification;
210       })
211       .collect(toSet(statistics.getAssigneesStatistics().size()));
212
213     notificationStatistics.myNewIssuesDeliveries += service.deliverEmails(myNewIssuesNotifications);
214     notificationStatistics.myNewIssues += myNewIssuesNotifications.size();
215
216     // compatibility with old API
217     myNewIssuesNotifications
218       .forEach(e -> notificationStatistics.myNewIssuesDeliveries += service.deliver(e));
219   }
220
221   private Map<String, UserDto> loadUserDtoByUuid(NewIssuesStatistics statistics) {
222     List<Map.Entry<String, NewIssuesStatistics.Stats>> entriesWithIssuesOnLeak = statistics.getAssigneesStatistics().entrySet()
223       .stream().filter(e -> e.getValue().hasIssuesOnCurrentAnalysis()).collect(toList());
224     List<String> assigneeUuids = entriesWithIssuesOnLeak.stream().map(Map.Entry::getKey).collect(toList());
225     try (DbSession dbSession = dbClient.openSession(false)) {
226       return dbClient.userDao().selectByUuids(dbSession, assigneeUuids).stream().collect(toMap(UserDto::getUuid, u -> u));
227     }
228   }
229
230   @Override
231   public String getDescription() {
232     return "Send issue notifications";
233   }
234
235   @CheckForNull
236   private String getBranchName() {
237     Branch branch = analysisMetadataHolder.getBranch();
238     return branch.isMain() || branch.getType() == PULL_REQUEST ? null : branch.getName();
239   }
240
241   @CheckForNull
242   private String getPullRequest() {
243     Branch branch = analysisMetadataHolder.getBranch();
244     return branch.getType() == PULL_REQUEST ? analysisMetadataHolder.getPullRequestKey() : null;
245   }
246
247   private static class NotificationStatistics {
248     private int issueChanges = 0;
249     private int issueChangesDeliveries = 0;
250     private int newIssues = 0;
251     private int newIssuesDeliveries = 0;
252     private int myNewIssues = 0;
253     private int myNewIssuesDeliveries = 0;
254
255     private void dumpTo(ComputationStep.Context context) {
256       context.getStatistics()
257         .add("newIssuesNotifs", newIssues)
258         .add("newIssuesDeliveries", newIssuesDeliveries)
259         .add("myNewIssuesNotifs", myNewIssues)
260         .add("myNewIssuesDeliveries", myNewIssuesDeliveries)
261         .add("changesNotifs", issueChanges)
262         .add("changesDeliveries", issueChangesDeliveries);
263     }
264   }
265 }