3 * Copyright (C) 2009-2023 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.ce.task.projectanalysis.step;
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;
29 import java.util.Objects;
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;
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;
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}.
69 public class SendIssueNotificationsStep implements ComputationStep {
71 * Types of the notifications sent by this step
73 static final Set<Class<? extends Notification>> NOTIF_TYPES = ImmutableSet.of(NewIssuesNotification.class, MyNewIssuesNotification.class, IssuesChangesNotification.class);
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;
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;
94 public void execute(ComputationStep.Context context) {
95 BranchType branchType = analysisMetadataHolder.getBranch().getType();
96 if (branchType == PULL_REQUEST) {
100 Component project = treeRootHolder.getRoot();
101 NotificationStatistics notificationStatistics = new NotificationStatistics();
102 if (service.hasProjectSubscribersForTypes(analysisMetadataHolder.getProject().getUuid(), NOTIF_TYPES)) {
103 doExecute(notificationStatistics, project);
105 notificationStatistics.dumpTo(context);
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));
119 try (CloseableIterator<DefaultIssue> issues = protoIssueCache.traverse()) {
120 processIssues(newIssuesStats, issues, assigneesByUuid, notificationStatistics);
122 if (newIssuesStats.hasIssuesOnCurrentAnalysis()) {
123 sendNewIssuesNotification(newIssuesStats, project, assigneesByUuid, analysisDate, notificationStatistics);
124 sendMyNewIssuesNotification(newIssuesStats, project, assigneesByUuid, analysisDate, notificationStatistics);
129 * Truncated the analysis date to seconds before comparing it to {@link Issue#creationDate()} is required because
130 * {@link DefaultIssue#setCreationDate(Date)} does it.
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();
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);
152 if (changedIssuesToNotify.size() >= batchSize) {
153 sendIssuesChangesNotification(changedIssuesToNotify, assigneesByUuid, notificationStatistics);
154 changedIssuesToNotify.clear();
158 if (!changedIssuesToNotify.isEmpty()) {
159 sendIssuesChangesNotification(changedIssuesToNotify, assigneesByUuid, notificationStatistics);
163 private void sendIssuesChangesNotification(Set<DefaultIssue> issues, Map<String, UserDto> assigneesByUuid, NotificationStatistics notificationStatistics) {
164 IssuesChangesNotification notification = notificationFactory.newIssuesChangesNotification(issues, assigneesByUuid);
166 notificationStatistics.issueChangesDeliveries += service.deliverEmails(singleton(notification));
167 notificationStatistics.issueChanges++;
169 // compatibility with old API
170 notificationStatistics.issueChangesDeliveries += service.deliver(notification);
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++;
186 // compatibility with old API
187 notificationStatistics.newIssuesDeliveries += service.deliver(notification);
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()
195 .filter(e -> e.getValue().hasIssuesOnCurrentAnalysis())
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()));
209 return myNewIssuesNotification;
211 .collect(toSet(statistics.getAssigneesStatistics().size()));
213 notificationStatistics.myNewIssuesDeliveries += service.deliverEmails(myNewIssuesNotifications);
214 notificationStatistics.myNewIssues += myNewIssuesNotifications.size();
216 // compatibility with old API
217 myNewIssuesNotifications
218 .forEach(e -> notificationStatistics.myNewIssuesDeliveries += service.deliver(e));
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));
231 public String getDescription() {
232 return "Send issue notifications";
236 private String getBranchName() {
237 Branch branch = analysisMetadataHolder.getBranch();
238 return branch.isMain() || branch.getType() == PULL_REQUEST ? null : branch.getName();
242 private String getPullRequest() {
243 Branch branch = analysisMetadataHolder.getBranch();
244 return branch.getType() == PULL_REQUEST ? analysisMetadataHolder.getPullRequestKey() : null;
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;
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);