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 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2018 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.server.computation.task.projectanalysis.step;
  21. import com.google.common.collect.ImmutableMap;
  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.Map;
  27. import java.util.Optional;
  28. import java.util.Set;
  29. import java.util.function.Predicate;
  30. import javax.annotation.CheckForNull;
  31. import org.sonar.api.issue.Issue;
  32. import org.sonar.api.utils.Duration;
  33. import org.sonar.core.issue.DefaultIssue;
  34. import org.sonar.core.util.CloseableIterator;
  35. import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder;
  36. import org.sonar.server.computation.task.projectanalysis.analysis.Branch;
  37. import org.sonar.server.computation.task.projectanalysis.component.Component;
  38. import org.sonar.server.computation.task.projectanalysis.component.CrawlerDepthLimit;
  39. import org.sonar.server.computation.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
  40. import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder;
  41. import org.sonar.server.computation.task.projectanalysis.component.TypeAwareVisitorAdapter;
  42. import org.sonar.server.computation.task.projectanalysis.issue.IssueCache;
  43. import org.sonar.server.computation.task.projectanalysis.issue.RuleRepository;
  44. import org.sonar.server.computation.task.step.ComputationStep;
  45. import org.sonar.server.issue.notification.IssueChangeNotification;
  46. import org.sonar.server.issue.notification.MyNewIssuesNotification;
  47. import org.sonar.server.issue.notification.NewIssuesNotification;
  48. import org.sonar.server.issue.notification.NewIssuesNotificationFactory;
  49. import org.sonar.server.issue.notification.NewIssuesStatistics;
  50. import org.sonar.server.notification.NotificationService;
  51. import static org.sonar.db.component.BranchType.PULL_REQUEST;
  52. import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
  53. /**
  54. * Reads issues from disk cache and send related notifications. For performance reasons,
  55. * the standard notification DB queue is not used as a temporary storage. Notifications
  56. * are directly processed by {@link NotificationService}.
  57. */
  58. public class SendIssueNotificationsStep implements ComputationStep {
  59. /**
  60. * Types of the notifications sent by this step
  61. */
  62. static final Set<String> NOTIF_TYPES = ImmutableSet.of(IssueChangeNotification.TYPE, NewIssuesNotification.TYPE, MyNewIssuesNotification.MY_NEW_ISSUES_NOTIF_TYPE);
  63. private final IssueCache issueCache;
  64. private final RuleRepository rules;
  65. private final TreeRootHolder treeRootHolder;
  66. private final NotificationService service;
  67. private final AnalysisMetadataHolder analysisMetadataHolder;
  68. private final NewIssuesNotificationFactory newIssuesNotificationFactory;
  69. private Map<String, Component> componentsByDbKey;
  70. public SendIssueNotificationsStep(IssueCache issueCache, RuleRepository rules, TreeRootHolder treeRootHolder,
  71. NotificationService service, AnalysisMetadataHolder analysisMetadataHolder,
  72. NewIssuesNotificationFactory newIssuesNotificationFactory) {
  73. this.issueCache = issueCache;
  74. this.rules = rules;
  75. this.treeRootHolder = treeRootHolder;
  76. this.service = service;
  77. this.analysisMetadataHolder = analysisMetadataHolder;
  78. this.newIssuesNotificationFactory = newIssuesNotificationFactory;
  79. }
  80. @Override
  81. public void execute() {
  82. Component project = treeRootHolder.getRoot();
  83. if (service.hasProjectSubscribersForTypes(project.getUuid(), NOTIF_TYPES)) {
  84. doExecute(project);
  85. }
  86. }
  87. private void doExecute(Component project) {
  88. long analysisDate = analysisMetadataHolder.getAnalysisDate();
  89. Predicate<DefaultIssue> isOnLeakPredicate = i -> i.isNew() && i.creationDate().getTime() >= truncateToSeconds(analysisDate);
  90. NewIssuesStatistics newIssuesStats = new NewIssuesStatistics(isOnLeakPredicate);
  91. try (CloseableIterator<DefaultIssue> issues = issueCache.traverse()) {
  92. processIssues(newIssuesStats, issues, project);
  93. }
  94. if (newIssuesStats.hasIssuesOnLeak()) {
  95. sendNewIssuesNotification(newIssuesStats, project, analysisDate);
  96. sendNewIssuesNotificationToAssignees(newIssuesStats, project, analysisDate);
  97. }
  98. }
  99. /**
  100. * Truncated the analysis date to seconds before comparing it to {@link Issue#creationDate()} is required because
  101. * {@link DefaultIssue#setCreationDate(Date)} does it.
  102. */
  103. private static long truncateToSeconds(long analysisDate) {
  104. Instant instant = new Date(analysisDate).toInstant();
  105. instant = instant.truncatedTo(ChronoUnit.SECONDS);
  106. return Date.from(instant).getTime();
  107. }
  108. private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues, Component project) {
  109. while (issues.hasNext()) {
  110. DefaultIssue issue = issues.next();
  111. if (issue.isNew() && issue.resolution() == null) {
  112. newIssuesStats.add(issue);
  113. } else if (issue.isChanged() && issue.mustSendNotifications()) {
  114. sendIssueChangeNotification(issue, project);
  115. }
  116. }
  117. }
  118. private void sendIssueChangeNotification(DefaultIssue issue, Component project) {
  119. IssueChangeNotification changeNotification = new IssueChangeNotification();
  120. changeNotification.setRuleName(rules.getByKey(issue.ruleKey()).getName());
  121. changeNotification.setIssue(issue);
  122. changeNotification.setProject(project.getPublicKey(), project.getName(), getBranchName(), getPullRequest());
  123. getComponentKey(issue).ifPresent(c -> changeNotification.setComponent(c.getPublicKey(), c.getName()));
  124. service.deliver(changeNotification);
  125. }
  126. private void sendNewIssuesNotification(NewIssuesStatistics statistics, Component project, long analysisDate) {
  127. NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics();
  128. NewIssuesNotification notification = newIssuesNotificationFactory
  129. .newNewIssuesNotication()
  130. .setProject(project.getPublicKey(), project.getName(), getBranchName(), getPullRequest())
  131. .setProjectVersion(project.getReportAttributes().getVersion())
  132. .setAnalysisDate(new Date(analysisDate))
  133. .setStatistics(project.getName(), globalStatistics)
  134. .setDebt(Duration.create(globalStatistics.effort().getOnLeak()));
  135. service.deliver(notification);
  136. }
  137. private void sendNewIssuesNotificationToAssignees(NewIssuesStatistics statistics, Component project, long analysisDate) {
  138. statistics.getAssigneesStatistics().entrySet()
  139. .stream()
  140. .filter(e -> e.getValue().hasIssuesOnLeak())
  141. .forEach(e -> {
  142. String assignee = e.getKey();
  143. NewIssuesStatistics.Stats assigneeStatistics = e.getValue();
  144. MyNewIssuesNotification myNewIssuesNotification = newIssuesNotificationFactory
  145. .newMyNewIssuesNotification()
  146. .setAssignee(assignee);
  147. myNewIssuesNotification
  148. .setProject(project.getPublicKey(), project.getName(), getBranchName(), getPullRequest())
  149. .setProjectVersion(project.getReportAttributes().getVersion())
  150. .setAnalysisDate(new Date(analysisDate))
  151. .setStatistics(project.getName(), assigneeStatistics)
  152. .setDebt(Duration.create(assigneeStatistics.effort().getOnLeak()));
  153. service.deliver(myNewIssuesNotification);
  154. });
  155. }
  156. private Optional<Component> getComponentKey(DefaultIssue issue) {
  157. if (componentsByDbKey == null) {
  158. final ImmutableMap.Builder<String, Component> builder = ImmutableMap.builder();
  159. new DepthTraversalTypeAwareCrawler(
  160. new TypeAwareVisitorAdapter(CrawlerDepthLimit.LEAVES, POST_ORDER) {
  161. @Override
  162. public void visitAny(Component component) {
  163. builder.put(component.getKey(), component);
  164. }
  165. }).visit(this.treeRootHolder.getRoot());
  166. this.componentsByDbKey = builder.build();
  167. }
  168. return Optional.ofNullable(componentsByDbKey.get(issue.componentKey()));
  169. }
  170. @Override
  171. public String getDescription() {
  172. return "Send issue notifications";
  173. }
  174. @CheckForNull
  175. private String getBranchName() {
  176. Branch branch = analysisMetadataHolder.getBranch();
  177. return branch.isMain() || branch.getType() == PULL_REQUEST ? null : branch.getName();
  178. }
  179. @CheckForNull
  180. private String getPullRequest() {
  181. Branch branch = analysisMetadataHolder.getBranch();
  182. return branch.getType() == PULL_REQUEST ? analysisMetadataHolder.getPullRequestId() : null;
  183. }
  184. }