You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SendIssueNotificationsStep.java 12KB

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