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.server.issue.notification;
22 import java.util.Arrays;
23 import java.util.Comparator;
24 import java.util.Date;
25 import java.util.List;
27 import java.util.Objects;
28 import java.util.Optional;
29 import java.util.function.ToIntFunction;
30 import javax.annotation.CheckForNull;
31 import javax.annotation.Nullable;
32 import org.sonar.api.notifications.Notification;
33 import org.sonar.api.rule.RuleKey;
34 import org.sonar.api.rules.RuleType;
35 import org.sonar.api.utils.DateUtils;
36 import org.sonar.api.utils.Duration;
37 import org.sonar.api.utils.Durations;
38 import org.sonar.core.util.stream.MoreCollectors;
39 import org.sonar.server.issue.notification.NewIssuesStatistics.Metric;
41 import static java.util.Objects.requireNonNull;
42 import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_BRANCH;
43 import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PROJECT_VERSION;
44 import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PULL_REQUEST;
45 import static org.sonar.server.issue.notification.NewIssuesEmailTemplate.FIELD_PROJECT_DATE;
46 import static org.sonar.server.issue.notification.NewIssuesEmailTemplate.FIELD_PROJECT_KEY;
47 import static org.sonar.server.issue.notification.NewIssuesEmailTemplate.FIELD_PROJECT_NAME;
48 import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.RULE_TYPE;
50 public class NewIssuesNotification extends Notification {
52 public static final String TYPE = "new-issues";
53 private static final long serialVersionUID = -6305871981920103093L;
54 private static final String COUNT = ".count";
55 private static final String LABEL = ".label";
56 private static final String DOT = ".";
58 private final transient DetailsSupplier detailsSupplier;
59 private final transient Durations durations;
61 public NewIssuesNotification(Durations durations, DetailsSupplier detailsSupplier) {
62 this(TYPE, durations, detailsSupplier);
65 protected NewIssuesNotification(String type, Durations durations, DetailsSupplier detailsSupplier) {
67 this.durations = durations;
68 this.detailsSupplier = detailsSupplier;
71 public interface DetailsSupplier {
73 * @throws NullPointerException if {@code ruleKey} is {@code null}
75 Optional<RuleDefinition> getRuleDefinitionByRuleKey(RuleKey ruleKey);
78 * @throws NullPointerException if {@code uuid} is {@code null}
80 Optional<String> getComponentNameByUuid(String uuid);
83 * @throws NullPointerException if {@code uuid} is {@code null}
85 Optional<String> getUserNameByUuid(String uuid);
88 public record RuleDefinition(String name, String language) {
89 public RuleDefinition(String name, @Nullable String language) {
90 this.name = requireNonNull(name, "name can't be null");
91 this.language = language;
95 public String toString() {
96 return "RuleDefinition{" + name + " (" + language + ')' + '}';
100 public NewIssuesNotification setAnalysisDate(Date d) {
101 setFieldValue(FIELD_PROJECT_DATE, DateUtils.formatDateTime(d));
105 public NewIssuesNotification setProject(String projectKey, String projectName, @Nullable String branchName, @Nullable String pullRequest) {
106 setFieldValue(FIELD_PROJECT_NAME, projectName);
107 setFieldValue(FIELD_PROJECT_KEY, projectKey);
108 if (branchName != null) {
109 setFieldValue(FIELD_BRANCH, branchName);
111 if (pullRequest != null) {
112 setFieldValue(FIELD_PULL_REQUEST, pullRequest);
118 public String getProjectKey() {
119 return getFieldValue(FIELD_PROJECT_KEY);
122 public NewIssuesNotification setProjectVersion(@Nullable String version) {
123 if (version != null) {
124 setFieldValue(FIELD_PROJECT_VERSION, version);
129 public NewIssuesNotification setStatistics(String projectName, NewIssuesStatistics.Stats stats) {
130 setDefaultMessage(stats.getDistributedMetricStats(RULE_TYPE).getOnCurrentAnalysis() + " new issues on " + projectName + ".\n");
132 setRuleTypeStatistics(stats);
133 setAssigneesStatistics(stats);
134 setTagsStatistics(stats);
135 setComponentsStatistics(stats);
136 setRuleStatistics(stats);
141 private void setRuleStatistics(NewIssuesStatistics.Stats stats) {
142 Metric metric = Metric.RULE;
143 List<Map.Entry<String, MetricStatsInt>> fiveBiggest = fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getOnCurrentAnalysis);
145 for (Map.Entry<String, MetricStatsInt> ruleStats : fiveBiggest) {
146 String ruleKey = ruleStats.getKey();
147 RuleDefinition rule = detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.parse(ruleKey))
148 .orElseThrow(() -> new IllegalStateException(String.format("Rule with key '%s' does not exist", ruleKey)));
149 String name = rule.name() + " (" + rule.language() + ")";
150 setFieldValue(metric + DOT + i + LABEL, name);
151 setFieldValue(metric + DOT + i + COUNT, String.valueOf(ruleStats.getValue().getOnCurrentAnalysis()));
156 private void setComponentsStatistics(NewIssuesStatistics.Stats stats) {
157 Metric metric = Metric.COMPONENT;
159 List<Map.Entry<String, MetricStatsInt>> fiveBiggest = fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getOnCurrentAnalysis);
160 for (Map.Entry<String, MetricStatsInt> componentStats : fiveBiggest) {
161 String uuid = componentStats.getKey();
162 String componentName = detailsSupplier.getComponentNameByUuid(uuid)
163 .orElseThrow(() -> new IllegalStateException(String.format("Component with uuid '%s' not found", uuid)));
164 setFieldValue(metric + DOT + i + LABEL, componentName);
165 setFieldValue(metric + DOT + i + COUNT, String.valueOf(componentStats.getValue().getOnCurrentAnalysis()));
170 private void setTagsStatistics(NewIssuesStatistics.Stats stats) {
171 Metric metric = Metric.TAG;
173 for (Map.Entry<String, MetricStatsInt> tagStats : fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getOnCurrentAnalysis)) {
174 setFieldValue(metric + DOT + i + COUNT, String.valueOf(tagStats.getValue().getOnCurrentAnalysis()));
175 setFieldValue(metric + DOT + i + LABEL, tagStats.getKey());
180 private void setAssigneesStatistics(NewIssuesStatistics.Stats stats) {
181 Metric metric = Metric.ASSIGNEE;
182 List<Map.Entry<String, MetricStatsInt>> entries = fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getOnCurrentAnalysis);
185 for (Map.Entry<String, MetricStatsInt> assigneeStats : entries) {
186 String assigneeUuid = assigneeStats.getKey();
187 String name = detailsSupplier.getUserNameByUuid(assigneeUuid).orElse(assigneeUuid);
188 setFieldValue(metric + DOT + i + LABEL, name);
189 setFieldValue(metric + DOT + i + COUNT, String.valueOf(assigneeStats.getValue().getOnCurrentAnalysis()));
194 private static List<Map.Entry<String, MetricStatsInt>> fiveBiggest(DistributedMetricStatsInt distributedMetricStatsInt, ToIntFunction<MetricStatsInt> biggerCriteria) {
195 Comparator<Map.Entry<String, MetricStatsInt>> comparator = Comparator.comparingInt(a -> biggerCriteria.applyAsInt(a.getValue()));
196 return distributedMetricStatsInt.getForLabels()
199 .filter(i -> biggerCriteria.applyAsInt(i.getValue()) > 0)
200 .sorted(comparator.reversed())
202 .collect(MoreCollectors.toList(5));
205 public NewIssuesNotification setDebt(Duration debt) {
206 setFieldValue(Metric.EFFORT + COUNT, durations.format(debt));
210 private void setRuleTypeStatistics(NewIssuesStatistics.Stats stats) {
211 DistributedMetricStatsInt distributedMetricStats = stats.getDistributedMetricStats(RULE_TYPE);
212 setFieldValue(RULE_TYPE + COUNT, String.valueOf(distributedMetricStats.getOnCurrentAnalysis()));
213 Arrays.stream(RuleType.values())
214 .forEach(ruleType -> setFieldValue(
215 RULE_TYPE + DOT + ruleType + COUNT,
216 String.valueOf(distributedMetricStats.getForLabel(ruleType.name()).map(MetricStatsInt::getOnCurrentAnalysis).orElse(0))));
220 public boolean equals(Object obj) {
221 return super.equals(obj);
225 public int hashCode() {
226 return super.hashCode();