123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- /*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
- package org.sonar.server.issue.notification;
-
- import com.google.common.collect.ImmutableList;
- import com.google.common.collect.ImmutableSortedSet;
- import com.google.common.collect.ListMultimap;
- import com.google.common.collect.Lists;
- import com.google.common.collect.SetMultimap;
- import java.io.UnsupportedEncodingException;
- import java.util.Collection;
- import java.util.Comparator;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Locale;
- import java.util.Optional;
- import java.util.Set;
- import java.util.SortedSet;
- import java.util.function.BiConsumer;
- import java.util.function.Consumer;
- import javax.annotation.Nullable;
- import org.sonar.api.config.EmailSettings;
- import org.sonar.api.rules.RuleType;
- import org.sonar.core.i18n.I18n;
- import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
- import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
- import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
-
- import static java.net.URLEncoder.encode;
- import static java.nio.charset.StandardCharsets.UTF_8;
- import static java.util.function.Function.identity;
- import static org.sonar.core.util.stream.MoreCollectors.index;
- import static org.sonar.server.issue.notification.RuleGroup.ISSUES;
- import static org.sonar.server.issue.notification.RuleGroup.SECURITY_HOTSPOTS;
- import static org.sonar.server.issue.notification.RuleGroup.formatIssueOrHotspot;
- import static org.sonar.server.issue.notification.RuleGroup.formatIssuesOrHotspots;
- import static org.sonar.server.issue.notification.RuleGroup.resolveGroup;
-
- public abstract class IssueChangesEmailTemplate implements EmailTemplate {
-
- private static final Comparator<Rule> RULE_COMPARATOR = Comparator.comparing(r -> r.getKey().toString());
- private static final Comparator<Project> PROJECT_COMPARATOR = Comparator.comparing(Project::getProjectName)
- .thenComparing(t -> t.getBranchName().orElse(""));
- private static final Comparator<ChangedIssue> CHANGED_ISSUE_KEY_COMPARATOR = Comparator.comparing(ChangedIssue::getKey, Comparator.naturalOrder());
-
- /**
- * Assuming:
- * <ul>
- * <li>UUID length of 40 chars</li>
- * <li>a max URL length of 2083 chars</li>
- * </ul>
- * This leaves ~850 chars for the rest of the URL (including other parameters such as the project key and the branch),
- * which is reasonable to stay safe from the max URL length supported by some browsers and network devices.
- */
- private static final int MAX_ISSUES_BY_LINK = 40;
- private static final String URL_ENCODED_COMMA = urlEncode(",");
-
- private final I18n i18n;
- private final EmailSettings settings;
-
- protected IssueChangesEmailTemplate(I18n i18n, EmailSettings settings) {
- this.i18n = i18n;
- this.settings = settings;
- }
-
- /**
- * Adds "projectName" or "projectName, branchName" if branchName is non null
- */
- protected static void toString(StringBuilder sb, Project project) {
- Optional<String> branchName = project.getBranchName();
- if (branchName.isPresent()) {
- sb.append(project.getProjectName()).append(", ").append(branchName.get());
- } else {
- sb.append(project.getProjectName());
- }
- }
-
- static String toUrlParams(Project project) {
- return "id=" + urlEncode(project.getKey()) +
- project.getBranchName().map(branchName -> "&branch=" + urlEncode(branchName)).orElse("");
- }
-
- void addIssuesByProjectThenRule(StringBuilder sb, SetMultimap<Project, ChangedIssue> issuesByProject) {
- issuesByProject.keySet().stream()
- .sorted(PROJECT_COMPARATOR)
- .forEach(project -> {
- String encodedProjectParams = toUrlParams(project);
- paragraph(sb, s -> toString(s, project));
- addIssuesByRule(sb, issuesByProject.get(project), projectIssuePageHref(encodedProjectParams));
- });
- }
-
- void addIssuesAndHotspotsByProjectThenRule(StringBuilder sb, SetMultimap<Project, ChangedIssue> issuesByProject) {
- issuesByProject.keySet().stream()
- .sorted(PROJECT_COMPARATOR)
- .forEach(project -> {
- String encodedProjectParams = toUrlParams(project);
- paragraph(sb, s -> toString(s, project));
-
- Set<ChangedIssue> changedIssues = issuesByProject.get(project);
- ListMultimap<RuleGroup, ChangedIssue> issuesAndHotspots = changedIssues.stream()
- .collect(index(changedIssue -> resolveGroup(changedIssue.getRule().getRuleType()), identity()));
-
- List<ChangedIssue> issues = issuesAndHotspots.get(ISSUES);
- List<ChangedIssue> hotspots = issuesAndHotspots.get(SECURITY_HOTSPOTS);
-
- boolean hasSecurityHotspots = !hotspots.isEmpty();
- boolean hasOtherIssues = !issues.isEmpty();
-
- if (hasOtherIssues) {
- addIssuesByRule(sb, issues, projectIssuePageHref(encodedProjectParams));
- }
-
- if (hasSecurityHotspots && hasOtherIssues) {
- paragraph(sb, stringBuilder -> {
- });
- }
-
- if (hasSecurityHotspots) {
- addIssuesByRule(sb, hotspots, securityHotspotPageHref(encodedProjectParams));
- }
- });
- }
-
- void addIssuesByRule(StringBuilder sb, Collection<ChangedIssue> changedIssues, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref) {
- ListMultimap<Rule, ChangedIssue> issuesByRule = changedIssues.stream()
- .collect(index(ChangedIssue::getRule, t -> t));
-
- Iterator<Rule> rules = issuesByRule.keySet().stream()
- .sorted(RULE_COMPARATOR)
- .iterator();
- if (!rules.hasNext()) {
- return;
- }
-
- sb.append("<ul>");
- while (rules.hasNext()) {
- Rule rule = rules.next();
- Collection<ChangedIssue> issues = issuesByRule.get(rule);
-
- sb.append("<li>").append("Rule ").append(" <em>").append(rule.getName()).append("</em> - ");
- appendIssueLinks(sb, issuePageHref, issues, rule.getRuleType());
- sb.append("</li>");
- }
- sb.append("</ul>");
- }
-
- private static void appendIssueLinks(StringBuilder sb, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref, Collection<ChangedIssue> issues,
- @Nullable RuleType ruleType) {
- SortedSet<ChangedIssue> sortedIssues = ImmutableSortedSet.copyOf(CHANGED_ISSUE_KEY_COMPARATOR, issues);
- int issueCount = issues.size();
- if (issueCount == 1) {
- link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See the single ").append(formatIssueOrHotspot(ruleType)));
- } else if (issueCount <= MAX_ISSUES_BY_LINK) {
- link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See all ").append(issueCount).append(" ").append(formatIssuesOrHotspots(ruleType)));
- } else {
- sb.append("See ").append(formatIssuesOrHotspots(ruleType));
- List<List<ChangedIssue>> issueGroups = Lists.partition(ImmutableList.copyOf(sortedIssues), MAX_ISSUES_BY_LINK);
- Iterator<List<ChangedIssue>> issueGroupsIterator = issueGroups.iterator();
- int[] groupIndex = new int[] {0};
- while (issueGroupsIterator.hasNext()) {
- List<ChangedIssue> issueGroup = issueGroupsIterator.next();
- sb.append(' ');
- link(sb, s -> issuePageHref.accept(s, issueGroup), issueGroupLabel(sb, groupIndex, issueGroup));
- groupIndex[0]++;
- }
- }
- }
-
- BiConsumer<StringBuilder, Collection<ChangedIssue>> projectIssuePageHref(String projectParams) {
- return (s, issues) -> {
- s.append(settings.getServerBaseURL()).append("/project/issues?").append(projectParams)
- .append("&issues=");
-
- Iterator<ChangedIssue> issueIterator = issues.iterator();
- while (issueIterator.hasNext()) {
- s.append(urlEncode(issueIterator.next().getKey()));
- if (issueIterator.hasNext()) {
- s.append(URL_ENCODED_COMMA);
- }
- }
-
- if (issues.size() == 1) {
- s.append("&open=").append(urlEncode(issues.iterator().next().getKey()));
- }
- };
- }
-
- BiConsumer<StringBuilder, Collection<ChangedIssue>> securityHotspotPageHref(String projectParams) {
- return (s, issues) -> {
- s.append(settings.getServerBaseURL()).append("/security_hotspots?").append(projectParams)
- .append("&hotspots=");
-
- Iterator<ChangedIssue> issueIterator = issues.iterator();
- while (issueIterator.hasNext()) {
- s.append(urlEncode(issueIterator.next().getKey()));
- if (issueIterator.hasNext()) {
- s.append(URL_ENCODED_COMMA);
- }
- }
- };
- }
-
- private static Consumer<StringBuilder> issueGroupLabel(StringBuilder sb, int[] groupIndex, List<ChangedIssue> issueGroup) {
- return s -> {
- int firstIssueNumber = (groupIndex[0] * MAX_ISSUES_BY_LINK) + 1;
- if (issueGroup.size() == 1) {
- sb.append(firstIssueNumber);
- } else {
- sb.append(firstIssueNumber).append("-").append(firstIssueNumber + issueGroup.size() - 1);
- }
- };
- }
-
- void addFooter(StringBuilder sb, String notificationI18nKey) {
- paragraph(sb, s -> s.append(" "));
- paragraph(sb, s -> {
- s.append("<small>");
- s.append("You received this email because you are subscribed to ")
- .append('"').append(i18n.message(Locale.ENGLISH, notificationI18nKey, notificationI18nKey)).append('"')
- .append(" notifications from SonarQube.");
- s.append(" Click ");
- link(s, s1 -> s1.append(settings.getServerBaseURL()).append("/account/notifications"), s1 -> s1.append("here"));
- s.append(" to edit your email preferences.");
- s.append("</small>");
- });
- }
-
- protected static void paragraph(StringBuilder sb, Consumer<StringBuilder> content) {
- sb.append("<p>");
- content.accept(sb);
- sb.append("</p>");
- }
-
- protected static void link(StringBuilder sb, Consumer<StringBuilder> link, Consumer<StringBuilder> content) {
- sb.append("<a href=\"");
- link.accept(sb);
- sb.append("\">");
- content.accept(sb);
- sb.append("</a>");
- }
-
- private static String urlEncode(String str) {
- try {
- return encode(str, UTF_8.name());
- } catch (UnsupportedEncodingException e) {
- throw new IllegalStateException(e);
- }
- }
-
- protected static String issueOrIssues(Collection<?> collection) {
- if (collection.size() > 1) {
- return "issues";
- }
- return "issue";
- }
-
- }
|