3 * Copyright (C) 2009-2017 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.computation.task.projectanalysis.step;
22 import com.google.common.collect.ImmutableMap;
23 import com.google.common.collect.ImmutableSet;
24 import java.time.Instant;
25 import java.time.temporal.ChronoUnit;
26 import java.util.Date;
28 import java.util.Optional;
30 import java.util.function.Predicate;
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;
52 import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
55 * Reads issues from disk cache and send related notifications. For performance reasons,
56 * the standard notification DB queue is not used as a temporary storage. Notifications
57 * are directly processed by {@link NotificationService}.
59 public class SendIssueNotificationsStep implements ComputationStep {
61 * Types of the notifications sent by this step
63 static final Set<String> NOTIF_TYPES = ImmutableSet.of(IssueChangeNotification.TYPE, NewIssuesNotification.TYPE, MyNewIssuesNotification.MY_NEW_ISSUES_NOTIF_TYPE);
65 private final IssueCache issueCache;
66 private final RuleRepository rules;
67 private final TreeRootHolder treeRootHolder;
68 private final NotificationService service;
69 private final AnalysisMetadataHolder analysisMetadataHolder;
70 private final NewIssuesNotificationFactory newIssuesNotificationFactory;
71 private Map<String, Component> componentsByDbKey;
73 public SendIssueNotificationsStep(IssueCache issueCache, RuleRepository rules, TreeRootHolder treeRootHolder,
74 NotificationService service, AnalysisMetadataHolder analysisMetadataHolder,
75 NewIssuesNotificationFactory newIssuesNotificationFactory) {
76 this.issueCache = issueCache;
78 this.treeRootHolder = treeRootHolder;
79 this.service = service;
80 this.analysisMetadataHolder = analysisMetadataHolder;
81 this.newIssuesNotificationFactory = newIssuesNotificationFactory;
85 public void execute() {
86 Component project = treeRootHolder.getRoot();
87 if (service.hasProjectSubscribersForTypes(project.getUuid(), NOTIF_TYPES)) {
92 private void doExecute(Component project) {
93 long analysisDate = analysisMetadataHolder.getAnalysisDate();
94 Predicate<DefaultIssue> isOnLeakPredicate = i -> i.isNew() && i.creationDate().getTime() >= truncateToSeconds(analysisDate);
95 NewIssuesStatistics newIssuesStats = new NewIssuesStatistics(isOnLeakPredicate);
96 try (CloseableIterator<DefaultIssue> issues = issueCache.traverse()) {
97 processIssues(newIssuesStats, issues, project);
99 if (newIssuesStats.hasIssuesOnLeak()) {
100 sendNewIssuesNotification(newIssuesStats, project, analysisDate);
101 sendNewIssuesNotificationToAssignees(newIssuesStats, project, analysisDate);
106 * Truncated the analysis date to seconds before comparing it to {@link Issue#creationDate()} is required because
107 * {@link DefaultIssue#setCreationDate(Date)} does it.
109 private static long truncateToSeconds(long analysisDate) {
110 Instant instant = new Date(analysisDate).toInstant();
111 instant = instant.truncatedTo(ChronoUnit.SECONDS);
112 return Date.from(instant).getTime();
115 private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues, Component project) {
116 while (issues.hasNext()) {
117 DefaultIssue issue = issues.next();
118 if (issue.isNew() && issue.resolution() == null) {
119 newIssuesStats.add(issue);
120 } else if (issue.isChanged() && issue.mustSendNotifications()) {
121 sendIssueChangeNotification(issue, project);
126 private void sendIssueChangeNotification(DefaultIssue issue, Component project) {
127 IssueChangeNotification changeNotification = new IssueChangeNotification();
128 changeNotification.setRuleName(rules.getByKey(issue.ruleKey()).getName());
129 changeNotification.setIssue(issue);
130 changeNotification.setProject(project.getPublicKey(), project.getName(), getBranchName());
131 getComponentKey(issue).ifPresent(c -> changeNotification.setComponent(c.getPublicKey(), c.getName()));
132 service.deliver(changeNotification);
135 private void sendNewIssuesNotification(NewIssuesStatistics statistics, Component project, long analysisDate) {
136 NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics();
137 NewIssuesNotification notification = newIssuesNotificationFactory
138 .newNewIssuesNotication()
139 .setProject(project.getPublicKey(), project.getName(), getBranchName())
140 .setProjectVersion(project.getReportAttributes().getVersion())
141 .setAnalysisDate(new Date(analysisDate))
142 .setStatistics(project.getName(), globalStatistics)
143 .setDebt(Duration.create(globalStatistics.effort().getOnLeak()));
144 service.deliver(notification);
147 private void sendNewIssuesNotificationToAssignees(NewIssuesStatistics statistics, Component project, long analysisDate) {
148 statistics.getAssigneesStatistics().entrySet()
150 .filter(e -> e.getValue().hasIssuesOnLeak())
152 String assignee = e.getKey();
153 NewIssuesStatistics.Stats assigneeStatistics = e.getValue();
154 MyNewIssuesNotification myNewIssuesNotification = newIssuesNotificationFactory
155 .newMyNewIssuesNotification()
156 .setAssignee(assignee);
157 myNewIssuesNotification
158 .setProject(project.getPublicKey(), project.getName(), getBranchName())
159 .setProjectVersion(project.getReportAttributes().getVersion())
160 .setAnalysisDate(new Date(analysisDate))
161 .setStatistics(project.getName(), assigneeStatistics)
162 .setDebt(Duration.create(assigneeStatistics.effort().getOnLeak()));
164 service.deliver(myNewIssuesNotification);
168 private Optional<Component> getComponentKey(DefaultIssue issue) {
169 if (componentsByDbKey == null) {
170 final ImmutableMap.Builder<String, Component> builder = ImmutableMap.builder();
171 new DepthTraversalTypeAwareCrawler(
172 new TypeAwareVisitorAdapter(CrawlerDepthLimit.LEAVES, POST_ORDER) {
174 public void visitAny(Component component) {
175 builder.put(component.getKey(), component);
177 }).visit(this.treeRootHolder.getRoot());
178 this.componentsByDbKey = builder.build();
180 return Optional.ofNullable(componentsByDbKey.get(issue.componentKey()));
184 public String getDescription() {
185 return "Send issue notifications";
188 private String getBranchName() {
189 return analysisMetadataHolder.getBranch().filter(b -> !b.isMain()).map(Branch::getName).orElse(null);