From 7bcccf0899d7bfa9f4d1782b4f5699c1f896502f Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Wed, 28 Jan 2015 18:06:14 +0100 Subject: [PATCH] SONAR-6113 Short-circuit sending of notifications when there are no subscribers --- .../server/computation/issue/IssueCache.java | 3 +- .../step/SendIssueNotificationsStep.java | 81 +++-- .../server/issue/IssueBulkChangeService.java | 19 +- .../org/sonar/server/issue/IssueService.java | 21 +- ...hangesOnMyIssueNotificationDispatcher.java | 2 +- .../notification/IssueChangeNotification.java | 91 ++++++ .../IssueChangesEmailTemplate.java | 2 +- .../notification/IssueNotifications.java | 102 ------- ...ewFalsePositiveNotificationDispatcher.java | 2 +- .../notification/NewIssuesEmailTemplate.java | 2 +- .../notification/NewIssuesNotification.java | 77 +++++ .../NewIssuesNotificationDispatcher.java | 2 +- .../notifications/NotificationService.java | 40 ++- .../server/platform/ServerComponents.java | 184 ++++++++++-- .../step/SendIssueNotificationsStepTest.java | 60 +++- .../issue/IssueBulkChangeServiceTest.java | 38 +-- ...esOnMyIssueNotificationDispatcherTest.java | 8 +- .../IssueChangesEmailTemplateTest.java | 22 +- .../notification/IssueNotificationsTest.java | 279 ++++++++++-------- ...lsePositiveNotificationDispatcherTest.java | 2 +- .../NewIssuesEmailTemplateTest.java | 4 +- .../NewIssuesNotificationDispatcherTest.java | 2 +- .../NewIssuesNotificationTest.java | 43 +++ .../NotificationServiceTest.java | 69 +++-- .../notifications/NotificationTest.java | 2 +- .../org/sonar/core/persistence/DaoUtils.java | 10 + .../org/sonar/core/persistence/MyBatis.java | 80 ++++- .../sonar/core/properties/PropertiesDao.java | 34 +++ .../sonar/core/persistence/DaoUtilsTest.java | 7 + .../core/properties/PropertiesDaoTest.java | 25 +- .../findNotificationSubscribers.xml | 7 +- .../sonar/api/notifications/Notification.java | 10 +- .../notifications/NotificationDispatcher.java | 9 +- 33 files changed, 908 insertions(+), 431 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java delete mode 100644 server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueNotifications.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/issue/IssueCache.java b/server/sonar-server/src/main/java/org/sonar/server/computation/issue/IssueCache.java index 2a0303d6c92..50fab5b12b3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/issue/IssueCache.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/issue/IssueCache.java @@ -34,11 +34,12 @@ import java.io.IOException; */ public class IssueCache extends DiskCache { + // this constructor is used by picocontainer public IssueCache(TempFolder tempFolder, System2 system2) throws IOException { super(tempFolder.newFile("issues", ".dat"), system2); } - IssueCache(File file, System2 system2) { + public IssueCache(File file, System2 system2) { super(file, system2); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java index befe904d961..f500f50030f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java @@ -19,33 +19,35 @@ */ package org.sonar.server.computation.step; -import com.google.common.collect.HashMultiset; -import com.google.common.collect.Multiset; -import org.sonar.api.issue.Issue; +import com.google.common.collect.ImmutableSet; import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.notifications.Notification; -import org.sonar.api.rule.Severity; -import org.sonar.api.utils.DateUtils; import org.sonar.core.component.ComponentDto; import org.sonar.server.computation.ComputationContext; import org.sonar.server.computation.issue.IssueCache; import org.sonar.server.computation.issue.RuleCache; -import org.sonar.server.issue.notification.IssueNotifications; +import org.sonar.server.issue.notification.IssueChangeNotification; +import org.sonar.server.issue.notification.NewIssuesNotification; +import org.sonar.server.notifications.NotificationService; import org.sonar.server.util.CloseableIterator; +import java.util.Set; + /** * Reads issues from disk cache and send related notifications. For performance reasons, * the standard notification DB queue is not used as a temporary storage. Notifications * are directly processed by {@link org.sonar.server.notifications.NotificationService}. */ public class SendIssueNotificationsStep implements ComputationStep { + /** + * Types of the notifications sent by this step + */ + static final Set NOTIF_TYPES = ImmutableSet.of(IssueChangeNotification.TYPE, NewIssuesNotification.TYPE); private final IssueCache issueCache; private final RuleCache rules; - private final IssueNotifications service; + private final NotificationService service; - public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, - IssueNotifications service) { + public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, NotificationService service) { this.issueCache = issueCache; this.rules = rules; this.service = service; @@ -53,39 +55,42 @@ public class SendIssueNotificationsStep implements ComputationStep { @Override public void execute(ComputationContext context) { - NewIssuesStatistics newIssuesStatistics = new NewIssuesStatistics(); + if (service.hasProjectSubscribersForTypes(context.getProject().uuid(), NOTIF_TYPES)) { + doExecute(context); + } + } + + private void doExecute(ComputationContext context) { + NewIssuesNotification.Stats newIssueStats = new NewIssuesNotification.Stats(); CloseableIterator issues = issueCache.traverse(); try { while (issues.hasNext()) { DefaultIssue issue = issues.next(); if (issue.isNew() && issue.resolution() == null) { - newIssuesStatistics.add(issue); + newIssueStats.add(issue); } else if (issue.isChanged() && issue.mustSendNotifications()) { - service.sendChanges(issue, null, rules.ruleName(issue.ruleKey()), - context.getProject(), /* TODO */null, null, true); + IssueChangeNotification changeNotification = new IssueChangeNotification(); + changeNotification.setRuleName(rules.ruleName(issue.ruleKey())); + changeNotification.setIssue(issue); + changeNotification.setProject(context.getProject()); + service.deliver(changeNotification); } } } finally { issues.close(); } - sendNewIssuesStatistics(context, newIssuesStatistics); + sendNewIssuesStatistics(context, newIssueStats); } - private void sendNewIssuesStatistics(ComputationContext context, NewIssuesStatistics newIssuesStatistics) { - if (!newIssuesStatistics.isEmpty()) { + private void sendNewIssuesStatistics(ComputationContext context, NewIssuesNotification.Stats stats) { + if (stats.size() > 0) { ComponentDto project = context.getProject(); - Notification notification = new Notification("new-issues") - .setFieldValue("projectName", project.longName()) - .setFieldValue("projectKey", project.key()) - .setDefaultMessage(newIssuesStatistics.size() + " new issues on " + project.longName() + ".\n") - .setFieldValue("projectDate", DateUtils.formatDateTime(context.getAnalysisDate())) - .setFieldValue("projectUuid", project.uuid()) - .setFieldValue("count", String.valueOf(newIssuesStatistics.size())); - for (String severity : Severity.ALL) { - notification.setFieldValue("count-" + severity, String.valueOf(newIssuesStatistics.issuesWithSeverity(severity))); - } - service.send(notification, true); + NewIssuesNotification notification = new NewIssuesNotification(); + notification.setProject(project); + notification.setAnalysisDate(context.getAnalysisDate()); + notification.setStatistics(project, stats); + service.deliver(notification); } } @@ -94,24 +99,4 @@ public class SendIssueNotificationsStep implements ComputationStep { return "Send issue notifications"; } - static class NewIssuesStatistics { - private final Multiset set = HashMultiset.create(); - - void add(Issue issue) { - set.add(issue.severity()); - } - - int issuesWithSeverity(String severity) { - return set.count(severity); - } - - int size() { - return set.size(); - } - - boolean isEmpty() { - return set.isEmpty(); - } - } - } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java index 46cdbdd63a2..f235a36e71d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java @@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory; import org.sonar.api.issue.Issue; import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.api.notifications.NotificationManager; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.Rule; import org.sonar.core.component.ComponentDto; @@ -36,7 +37,7 @@ import org.sonar.core.issue.db.IssueStorage; import org.sonar.core.persistence.DbSession; import org.sonar.server.db.DbClient; import org.sonar.server.exceptions.BadRequestException; -import org.sonar.server.issue.notification.IssueNotifications; +import org.sonar.server.issue.notification.IssueChangeNotification; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.search.QueryContext; import org.sonar.server.user.UserSession; @@ -62,16 +63,16 @@ public class IssueBulkChangeService { private final IssueService issueService; private final IssueStorage issueStorage; private final DefaultRuleFinder ruleFinder; - private final IssueNotifications issueNotifications; + private final NotificationManager notificationService; private final List actions; public IssueBulkChangeService(DbClient dbClient, IssueService issueService, IssueStorage issueStorage, DefaultRuleFinder ruleFinder, - IssueNotifications issueNotifications, List actions) { + NotificationManager notificationService, List actions) { this.dbClient = dbClient; this.issueService = issueService; this.issueStorage = issueStorage; this.ruleFinder = ruleFinder; - this.issueNotifications = issueNotifications; + this.notificationService = notificationService; this.actions = actions; } @@ -103,10 +104,12 @@ public class IssueBulkChangeService { String projectKey = issue.projectKey(); if (projectKey != null) { Rule rule = repository.rule(issue.ruleKey()); - issueNotifications.sendChanges((DefaultIssue) issue, issueChangeContext.login(), - rule != null ? rule.getName() : null, - repository.project(projectKey), - repository.component(issue.componentKey()), null, false); + notificationService.scheduleForSending(new IssueChangeNotification() + .setIssue((DefaultIssue) issue) + .setChangeAuthorLogin(issueChangeContext.login()) + .setRuleName(rule != null ? rule.getName() : null) + .setProject(repository.project(projectKey)) + .setComponent(repository.component(issue.componentKey()))); } } concernedProjects.add(issue.projectKey()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java index 92012d6ad7b..a35e89e7814 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java @@ -29,6 +29,7 @@ import org.sonar.api.issue.ActionPlan; import org.sonar.api.issue.Issue; import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.api.notifications.NotificationManager; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.rules.Rule; @@ -50,7 +51,7 @@ import org.sonar.server.db.DbClient; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.issue.actionplan.ActionPlanService; import org.sonar.server.issue.index.IssueIndex; -import org.sonar.server.issue.notification.IssueNotifications; +import org.sonar.server.issue.notification.IssueChangeNotification; import org.sonar.server.search.FacetValue; import org.sonar.server.search.IndexClient; import org.sonar.server.search.QueryContext; @@ -80,7 +81,7 @@ public class IssueService implements ServerComponent { private final IssueWorkflow workflow; private final IssueUpdater issueUpdater; private final IssueStorage issueStorage; - private final IssueNotifications issueNotifications; + private final NotificationManager notificationService; private final ActionPlanService actionPlanService; private final RuleFinder ruleFinder; private final IssueDao deprecatedIssueDao; @@ -92,7 +93,7 @@ public class IssueService implements ServerComponent { IssueWorkflow workflow, IssueStorage issueStorage, IssueUpdater issueUpdater, - IssueNotifications issueNotifications, + NotificationManager notificationService, ActionPlanService actionPlanService, RuleFinder ruleFinder, IssueDao deprecatedIssueDao, @@ -105,7 +106,7 @@ public class IssueService implements ServerComponent { this.issueUpdater = issueUpdater; this.actionPlanService = actionPlanService; this.ruleFinder = ruleFinder; - this.issueNotifications = issueNotifications; + this.notificationService = notificationService; this.deprecatedIssueDao = deprecatedIssueDao; this.userFinder = userFinder; this.userIndex = userIndex; @@ -333,11 +334,13 @@ public class IssueService implements ServerComponent { } issueStorage.save(session, issue); Rule rule = getNullableRuleByKey(issue.ruleKey()); - issueNotifications.sendChanges(issue, context.login(), - rule != null ? rule.getName() : null, - dbClient.componentDao().getByKey(session, projectKey), - dbClient.componentDao().getNullableByKey(session, issue.componentKey()), - comment, false); + notificationService.scheduleForSending(new IssueChangeNotification() + .setIssue(issue) + .setChangeAuthorLogin(context.login()) + .setRuleName(rule != null ? rule.getName() : null) + .setProject(dbClient.componentDao().getByKey(session, projectKey)) + .setComponent(dbClient.componentDao().getNullableByKey(session, issue.componentKey())) + .setComment(comment)); } /** diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcher.java index db49b20d35b..e12e2715864 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcher.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcher.java @@ -37,7 +37,7 @@ public class ChangesOnMyIssueNotificationDispatcher extends NotificationDispatch private NotificationManager notificationManager; public ChangesOnMyIssueNotificationDispatcher(NotificationManager notificationManager) { - super("issue-changes"); + super(IssueChangeNotification.TYPE); this.notificationManager = notificationManager; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java new file mode 100644 index 00000000000..2b7c0e659e6 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java @@ -0,0 +1,91 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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 org.sonar.api.component.Component; +import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.issue.internal.FieldDiffs; +import org.sonar.api.notifications.Notification; + +import javax.annotation.Nullable; + +import java.io.Serializable; +import java.util.Map; + +public class IssueChangeNotification extends Notification { + + public static final String TYPE = "issue-changes"; + + public IssueChangeNotification() { + super(TYPE); + } + + public IssueChangeNotification setIssue(DefaultIssue issue) { + setFieldValue("key", issue.key()); + setFieldValue("reporter", issue.reporter()); + setFieldValue("assignee", issue.assignee()); + setFieldValue("message", issue.message()); + setFieldValue("componentKey", issue.componentKey()); + FieldDiffs currentChange = issue.currentChange(); + if (currentChange != null) { + for (Map.Entry entry : currentChange.diffs().entrySet()) { + String type = entry.getKey(); + FieldDiffs.Diff diff = entry.getValue(); + Serializable newValue = diff.newValue(); + Serializable oldValue = diff.oldValue(); + setFieldValue("old." + type, oldValue != null ? oldValue.toString() : null); + setFieldValue("new." + type, newValue != null ? newValue.toString() : null); + } + } + return this; + } + + public IssueChangeNotification setProject(Component project) { + setFieldValue("projectName", project.longName()); + setFieldValue("projectKey", project.key()); + return this; + } + + public IssueChangeNotification setComponent(Component component) { + setFieldValue("componentName", component.longName()); + return this; + } + + public IssueChangeNotification setChangeAuthorLogin(@Nullable String s) { + if (s != null) { + setFieldValue("changeAuthor", s); + } + return this; + } + + public IssueChangeNotification setRuleName(@Nullable String s) { + if (s != null) { + setFieldValue("ruleName", s); + } + return this; + } + + public IssueChangeNotification setComment(@Nullable String s) { + if (s != null) { + setFieldValue("comment", s); + } + return this; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java index d3c34ce96e5..25a9a550f11 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java @@ -47,7 +47,7 @@ public class IssueChangesEmailTemplate extends EmailTemplate { @Override public EmailMessage format(Notification notif) { - if (!"issue-changes".equals(notif.getType())) { + if (!IssueChangeNotification.TYPE.equals(notif.getType())) { return null; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueNotifications.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueNotifications.java deleted file mode 100644 index 25fe9130c6e..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueNotifications.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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 org.sonar.api.ServerComponent; -import org.sonar.api.component.Component; -import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.issue.internal.FieldDiffs; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationManager; -import org.sonar.server.notifications.NotificationService; - -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; - -import java.io.Serializable; -import java.util.Map; - -public class IssueNotifications implements ServerComponent { - - private final NotificationManager asyncService; - private final NotificationService syncService; - - public IssueNotifications(NotificationManager asyncService, NotificationService syncService) { - this.asyncService = asyncService; - this.syncService = syncService; - } - - @CheckForNull - public Notification sendChanges(DefaultIssue issue, @Nullable String changeAuthorLogin, - @Nullable String ruleName, Component project, @Nullable Component component, - @Nullable String comment, boolean synchronous) { - Notification notification = createChangeNotification(issue, changeAuthorLogin, ruleName, project, component, comment); - if (notification != null) { - send(notification, synchronous); - } - return notification; - } - - public void send(Notification notification, boolean synchronous) { - if (synchronous) { - syncService.deliver(notification); - } else { - asyncService.scheduleForSending(notification); - } - } - - @CheckForNull - private Notification createChangeNotification(DefaultIssue issue, @Nullable String changeAuthorLogin, - @Nullable String ruleName, Component project, - @Nullable Component component, @Nullable String comment) { - Notification notification = null; - if (comment != null || issue.mustSendNotifications()) { - FieldDiffs currentChange = issue.currentChange(); - notification = new Notification("issue-changes") - .setFieldValue("projectName", project.longName()) - .setFieldValue("projectKey", project.key()) - .setFieldValue("key", issue.key()) - .setFieldValue("changeAuthor", changeAuthorLogin) - .setFieldValue("reporter", issue.reporter()) - .setFieldValue("assignee", issue.assignee()) - .setFieldValue("message", issue.message()) - .setFieldValue("ruleName", ruleName) - .setFieldValue("componentKey", issue.componentKey()); - if (component != null) { - notification.setFieldValue("componentName", component.longName()); - } - if (comment != null) { - notification.setFieldValue("comment", comment); - } - if (currentChange != null) { - for (Map.Entry entry : currentChange.diffs().entrySet()) { - String type = entry.getKey(); - FieldDiffs.Diff diff = entry.getValue(); - Serializable newValue = diff.newValue(); - Serializable oldValue = diff.oldValue(); - notification.setFieldValue("old." + type, oldValue != null ? oldValue.toString() : null); - notification.setFieldValue("new." + type, newValue != null ? newValue.toString() : null); - } - } - } - return notification; - } - -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewFalsePositiveNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewFalsePositiveNotificationDispatcher.java index bbf9719bc56..be916cfdd42 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewFalsePositiveNotificationDispatcher.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewFalsePositiveNotificationDispatcher.java @@ -40,7 +40,7 @@ public class NewFalsePositiveNotificationDispatcher extends NotificationDispatch private final NotificationManager notifications; public NewFalsePositiveNotificationDispatcher(NotificationManager notifications) { - super("issue-changes"); + super(IssueChangeNotification.TYPE); this.notifications = notifications; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java index 25dcef9bac8..9caba17934a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java @@ -53,7 +53,7 @@ public class NewIssuesEmailTemplate extends EmailTemplate { @Override public EmailMessage format(Notification notification) { - if (!"new-issues".equals(notification.getType())) { + if (!NewIssuesNotification.TYPE.equals(notification.getType())) { return null; } String projectName = notification.getFieldValue(FIELD_PROJECT_NAME); diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java new file mode 100644 index 00000000000..ccade3f7025 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java @@ -0,0 +1,77 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.HashMultiset; +import com.google.common.collect.Multiset; +import org.sonar.api.component.Component; +import org.sonar.api.issue.Issue; +import org.sonar.api.notifications.Notification; +import org.sonar.api.rule.Severity; +import org.sonar.api.utils.DateUtils; +import org.sonar.core.component.ComponentDto; + +import java.util.Date; + +public class NewIssuesNotification extends Notification { + + public static final String TYPE = "new-issues"; + + public NewIssuesNotification() { + super(TYPE); + } + + public NewIssuesNotification setAnalysisDate(Date d) { + setFieldValue("projectDate", DateUtils.formatDateTime(d)); + return this; + } + + public NewIssuesNotification setProject(ComponentDto project) { + setFieldValue("projectName", project.longName()); + setFieldValue("projectKey", project.key()); + setFieldValue("projectUuid", project.uuid()); + return this; + } + + public NewIssuesNotification setStatistics(Component project, Stats stats) { + setDefaultMessage(stats.size() + " new issues on " + project.longName() + ".\n"); + setFieldValue("count", String.valueOf(stats.size())); + for (String severity : Severity.ALL) { + setFieldValue("count-" + severity, String.valueOf(stats.countIssuesWithSeverity(severity))); + } + return this; + } + + public static class Stats { + private final Multiset set = HashMultiset.create(); + + public void add(Issue issue) { + set.add(issue.severity()); + } + + public int countIssuesWithSeverity(String severity) { + return set.count(severity); + } + + public int size() { + return set.size(); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcher.java index f45ef34286c..c8a9726b6b2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcher.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcher.java @@ -34,7 +34,7 @@ public class NewIssuesNotificationDispatcher extends NotificationDispatcher { private final NotificationManager manager; public NewIssuesNotificationDispatcher(NotificationManager manager) { - super("new-issues"); + super(NewIssuesNotification.TYPE); this.manager = manager; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationService.java b/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationService.java index 942918191a8..97764e97a47 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationService.java @@ -21,6 +21,7 @@ package org.sonar.server.notifications; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; import com.google.common.collect.SetMultimap; import org.picocontainer.Startable; import org.slf4j.Logger; @@ -34,11 +35,13 @@ import org.sonar.api.notifications.NotificationChannel; import org.sonar.api.notifications.NotificationDispatcher; import org.sonar.api.utils.TimeProfiler; import org.sonar.core.notification.DefaultNotificationManager; +import org.sonar.server.db.DbClient; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -72,7 +75,8 @@ public class NotificationService implements ServerComponent, Startable { private final long delayInSeconds; private final long delayBeforeReportingStatusInSeconds; private final DefaultNotificationManager manager; - private final NotificationDispatcher[] dispatchers; + private final List dispatchers; + private final DbClient dbClient; private ScheduledExecutorService executorService; private boolean stopping = false; @@ -80,19 +84,19 @@ public class NotificationService implements ServerComponent, Startable { /** * Constructor for {@link NotificationService} */ - public NotificationService(Settings settings, DefaultNotificationManager manager, NotificationDispatcher[] dispatchers) { - delayInSeconds = settings.getLong(PROPERTY_DELAY); - delayBeforeReportingStatusInSeconds = settings.getLong(PROPERTY_DELAY_BEFORE_REPORTING_STATUS); + public NotificationService(Settings settings, DefaultNotificationManager manager, DbClient dbClient, NotificationDispatcher[] dispatchers) { + this.delayInSeconds = settings.getLong(PROPERTY_DELAY); + this.delayBeforeReportingStatusInSeconds = settings.getLong(PROPERTY_DELAY_BEFORE_REPORTING_STATUS); this.manager = manager; - this.dispatchers = dispatchers; + this.dbClient = dbClient; + this.dispatchers = ImmutableList.copyOf(dispatchers); } /** - * Default constructor when no channels. + * Default constructor when no dispatchers. */ - public NotificationService(Settings settings, DefaultNotificationManager manager) { - this(settings, manager, new NotificationDispatcher[0]); - LOG.warn("There is no dispatcher - all notifications will be ignored!"); + public NotificationService(Settings settings, DefaultNotificationManager manager, DbClient dbClient) { + this(settings, manager, dbClient, new NotificationDispatcher[0]); } @Override @@ -204,7 +208,21 @@ public class NotificationService implements ServerComponent, Startable { @VisibleForTesting protected List getDispatchers() { - return Arrays.asList(dispatchers); + return dispatchers; } + /** + * Returns true if at least one user is subscribed to at least one notifications with given types. + * Subscription can be globally or on the specific project. + */ + public boolean hasProjectSubscribersForTypes(String projectUuid, Set notificationTypes) { + Collection dispatcherKeys = new ArrayList<>(); + for (NotificationDispatcher dispatcher : dispatchers) { + if (notificationTypes.contains(dispatcher.getType())) { + dispatcherKeys.add(dispatcher.getKey()); + } + } + + return dbClient.propertiesDao().hasProjectNotificationSubscribersForDispatchers(projectUuid, dispatcherKeys); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java index 34123541de4..c059d2a5cac 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java @@ -53,7 +53,12 @@ import org.sonar.core.measure.db.MeasureFilterDao; import org.sonar.core.metric.DefaultMetricFinder; import org.sonar.core.notification.DefaultNotificationManager; import org.sonar.core.permission.PermissionFacade; -import org.sonar.core.persistence.*; +import org.sonar.core.persistence.DaoUtils; +import org.sonar.core.persistence.DatabaseVersion; +import org.sonar.core.persistence.DefaultDatabase; +import org.sonar.core.persistence.MyBatis; +import org.sonar.core.persistence.SemaphoreUpdater; +import org.sonar.core.persistence.SemaphoresImpl; import org.sonar.core.profiling.Profiling; import org.sonar.core.purge.PurgeProfiler; import org.sonar.core.qualitygate.db.ProjectQgateAssociationDao; @@ -78,7 +83,12 @@ import org.sonar.server.activity.index.ActivityNormalizer; import org.sonar.server.activity.ws.ActivitiesWebService; import org.sonar.server.activity.ws.ActivityMapping; import org.sonar.server.authentication.ws.AuthenticationWs; -import org.sonar.server.batch.*; +import org.sonar.server.batch.BatchIndex; +import org.sonar.server.batch.BatchWs; +import org.sonar.server.batch.GlobalRepositoryAction; +import org.sonar.server.batch.IssuesAction; +import org.sonar.server.batch.ProjectRepositoryAction; +import org.sonar.server.batch.ProjectRepositoryLoader; import org.sonar.server.charts.ChartFactory; import org.sonar.server.component.ComponentCleanerService; import org.sonar.server.component.ComponentService; @@ -86,12 +96,20 @@ import org.sonar.server.component.DefaultComponentFinder; import org.sonar.server.component.DefaultRubyComponentService; import org.sonar.server.component.db.ComponentDao; import org.sonar.server.component.db.SnapshotDao; -import org.sonar.server.component.ws.*; +import org.sonar.server.component.ws.ComponentAppAction; +import org.sonar.server.component.ws.ComponentsWs; +import org.sonar.server.component.ws.EventsWs; +import org.sonar.server.component.ws.ProjectsWs; +import org.sonar.server.component.ws.ResourcesWs; import org.sonar.server.computation.AnalysisReportQueue; import org.sonar.server.computation.AnalysisReportQueueCleaner; import org.sonar.server.computation.ComputationThreadLauncher; import org.sonar.server.computation.db.AnalysisReportDao; -import org.sonar.server.computation.ws.*; +import org.sonar.server.computation.ws.ComputationWebService; +import org.sonar.server.computation.ws.HistoryWsAction; +import org.sonar.server.computation.ws.IsQueueEmptyWebService; +import org.sonar.server.computation.ws.QueueWsAction; +import org.sonar.server.computation.ws.SubmitReportWsAction; import org.sonar.server.config.ws.PropertiesWs; import org.sonar.server.dashboard.db.DashboardDao; import org.sonar.server.dashboard.db.WidgetDao; @@ -103,7 +121,14 @@ import org.sonar.server.db.DbClient; import org.sonar.server.db.EmbeddedDatabaseFactory; import org.sonar.server.db.migrations.DatabaseMigrations; import org.sonar.server.db.migrations.DatabaseMigrator; -import org.sonar.server.debt.*; +import org.sonar.server.debt.DebtCharacteristicsXMLImporter; +import org.sonar.server.debt.DebtModelBackup; +import org.sonar.server.debt.DebtModelLookup; +import org.sonar.server.debt.DebtModelOperations; +import org.sonar.server.debt.DebtModelPluginRepository; +import org.sonar.server.debt.DebtModelService; +import org.sonar.server.debt.DebtModelXMLExporter; +import org.sonar.server.debt.DebtRulesXMLImporter; import org.sonar.server.design.FileDesignWidget; import org.sonar.server.duplication.ws.DuplicationsJsonWriter; import org.sonar.server.duplication.ws.DuplicationsParser; @@ -111,7 +136,22 @@ import org.sonar.server.duplication.ws.DuplicationsWs; import org.sonar.server.es.EsClient; import org.sonar.server.es.IndexCreator; import org.sonar.server.es.IndexRegistry; -import org.sonar.server.issue.*; +import org.sonar.server.issue.ActionService; +import org.sonar.server.issue.AddTagsAction; +import org.sonar.server.issue.AssignAction; +import org.sonar.server.issue.CommentAction; +import org.sonar.server.issue.InternalRubyIssueService; +import org.sonar.server.issue.IssueBulkChangeService; +import org.sonar.server.issue.IssueChangelogFormatter; +import org.sonar.server.issue.IssueChangelogService; +import org.sonar.server.issue.IssueCommentService; +import org.sonar.server.issue.IssueQueryService; +import org.sonar.server.issue.IssueService; +import org.sonar.server.issue.PlanAction; +import org.sonar.server.issue.RemoveTagsAction; +import org.sonar.server.issue.ServerIssueStorage; +import org.sonar.server.issue.SetSeverityAction; +import org.sonar.server.issue.TransitionAction; import org.sonar.server.issue.actionplan.ActionPlanService; import org.sonar.server.issue.actionplan.ActionPlanWs; import org.sonar.server.issue.db.IssueDao; @@ -122,8 +162,16 @@ import org.sonar.server.issue.index.IssueAuthorizationIndexer; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexDefinition; import org.sonar.server.issue.index.IssueIndexer; -import org.sonar.server.issue.notification.*; -import org.sonar.server.issue.ws.*; +import org.sonar.server.issue.notification.ChangesOnMyIssueNotificationDispatcher; +import org.sonar.server.issue.notification.IssueChangesEmailTemplate; +import org.sonar.server.issue.notification.NewFalsePositiveNotificationDispatcher; +import org.sonar.server.issue.notification.NewIssuesEmailTemplate; +import org.sonar.server.issue.notification.NewIssuesNotificationDispatcher; +import org.sonar.server.issue.ws.ComponentTagsAction; +import org.sonar.server.issue.ws.IssueActionsWriter; +import org.sonar.server.issue.ws.IssueShowAction; +import org.sonar.server.issue.ws.IssuesWs; +import org.sonar.server.issue.ws.SetTagsAction; import org.sonar.server.language.ws.LanguageWs; import org.sonar.server.language.ws.ListAction; import org.sonar.server.measure.MeasureFilterEngine; @@ -144,35 +192,117 @@ import org.sonar.server.platform.ws.L10nWs; import org.sonar.server.platform.ws.RestartHandler; import org.sonar.server.platform.ws.ServerWs; import org.sonar.server.platform.ws.SystemWs; -import org.sonar.server.plugins.*; +import org.sonar.server.plugins.InstalledPluginReferentialFactory; +import org.sonar.server.plugins.PluginDownloader; +import org.sonar.server.plugins.ServerExtensionInstaller; +import org.sonar.server.plugins.ServerPluginJarInstaller; +import org.sonar.server.plugins.ServerPluginJarsInstaller; +import org.sonar.server.plugins.ServerPluginRepository; +import org.sonar.server.plugins.UpdateCenterClient; +import org.sonar.server.plugins.UpdateCenterMatrixFactory; import org.sonar.server.properties.ProjectSettingsFactory; import org.sonar.server.qualitygate.QgateProjectFinder; import org.sonar.server.qualitygate.QualityGates; import org.sonar.server.qualitygate.RegisterQualityGates; -import org.sonar.server.qualitygate.ws.*; -import org.sonar.server.qualityprofile.*; +import org.sonar.server.qualitygate.ws.QGatesAppAction; +import org.sonar.server.qualitygate.ws.QGatesCopyAction; +import org.sonar.server.qualitygate.ws.QGatesCreateAction; +import org.sonar.server.qualitygate.ws.QGatesCreateConditionAction; +import org.sonar.server.qualitygate.ws.QGatesDeleteConditionAction; +import org.sonar.server.qualitygate.ws.QGatesDeselectAction; +import org.sonar.server.qualitygate.ws.QGatesDestroyAction; +import org.sonar.server.qualitygate.ws.QGatesListAction; +import org.sonar.server.qualitygate.ws.QGatesRenameAction; +import org.sonar.server.qualitygate.ws.QGatesSearchAction; +import org.sonar.server.qualitygate.ws.QGatesSelectAction; +import org.sonar.server.qualitygate.ws.QGatesSetAsDefaultAction; +import org.sonar.server.qualitygate.ws.QGatesShowAction; +import org.sonar.server.qualitygate.ws.QGatesUnsetDefaultAction; +import org.sonar.server.qualitygate.ws.QGatesUpdateConditionAction; +import org.sonar.server.qualitygate.ws.QGatesWs; +import org.sonar.server.qualityprofile.BuiltInProfiles; +import org.sonar.server.qualityprofile.QProfileBackuper; +import org.sonar.server.qualityprofile.QProfileCopier; +import org.sonar.server.qualityprofile.QProfileExporters; +import org.sonar.server.qualityprofile.QProfileFactory; +import org.sonar.server.qualityprofile.QProfileLoader; +import org.sonar.server.qualityprofile.QProfileLookup; +import org.sonar.server.qualityprofile.QProfileProjectLookup; +import org.sonar.server.qualityprofile.QProfileProjectOperations; +import org.sonar.server.qualityprofile.QProfileReset; +import org.sonar.server.qualityprofile.QProfileService; +import org.sonar.server.qualityprofile.QProfiles; +import org.sonar.server.qualityprofile.RegisterQualityProfiles; +import org.sonar.server.qualityprofile.RuleActivator; +import org.sonar.server.qualityprofile.RuleActivatorContextFactory; import org.sonar.server.qualityprofile.db.ActiveRuleDao; import org.sonar.server.qualityprofile.index.ActiveRuleIndex; import org.sonar.server.qualityprofile.index.ActiveRuleNormalizer; -import org.sonar.server.qualityprofile.ws.*; -import org.sonar.server.rule.*; +import org.sonar.server.qualityprofile.ws.BulkRuleActivationActions; +import org.sonar.server.qualityprofile.ws.ProfilesWs; +import org.sonar.server.qualityprofile.ws.QProfileRestoreBuiltInAction; +import org.sonar.server.qualityprofile.ws.QProfilesWs; +import org.sonar.server.qualityprofile.ws.RuleActivationActions; +import org.sonar.server.rule.DefaultRuleFinder; +import org.sonar.server.rule.DeprecatedRulesDefinitionLoader; +import org.sonar.server.rule.RegisterRules; +import org.sonar.server.rule.RubyRuleService; +import org.sonar.server.rule.RuleCreator; +import org.sonar.server.rule.RuleDefinitionsLoader; +import org.sonar.server.rule.RuleDeleter; +import org.sonar.server.rule.RuleOperations; +import org.sonar.server.rule.RuleRepositories; +import org.sonar.server.rule.RuleService; +import org.sonar.server.rule.RuleUpdater; import org.sonar.server.rule.db.RuleDao; import org.sonar.server.rule.index.RuleIndex; import org.sonar.server.rule.index.RuleNormalizer; -import org.sonar.server.rule.ws.*; +import org.sonar.server.rule.ws.ActiveRuleCompleter; +import org.sonar.server.rule.ws.AppAction; +import org.sonar.server.rule.ws.DeleteAction; +import org.sonar.server.rule.ws.RepositoriesAction; +import org.sonar.server.rule.ws.RuleMapping; +import org.sonar.server.rule.ws.RulesWebService; import org.sonar.server.rule.ws.SearchAction; import org.sonar.server.rule.ws.TagsAction; -import org.sonar.server.search.*; +import org.sonar.server.rule.ws.UpdateAction; +import org.sonar.server.search.IndexClient; +import org.sonar.server.search.IndexQueue; +import org.sonar.server.search.IndexSynchronizer; +import org.sonar.server.search.SearchClient; +import org.sonar.server.search.SearchHealth; import org.sonar.server.source.HtmlSourceDecorator; import org.sonar.server.source.SourceService; import org.sonar.server.source.index.SourceLineIndex; import org.sonar.server.source.index.SourceLineIndexDefinition; import org.sonar.server.source.index.SourceLineIndexer; -import org.sonar.server.source.ws.*; +import org.sonar.server.source.ws.HashAction; +import org.sonar.server.source.ws.IndexAction; +import org.sonar.server.source.ws.LinesAction; +import org.sonar.server.source.ws.RawAction; +import org.sonar.server.source.ws.ScmAction; +import org.sonar.server.source.ws.ScmWriter; import org.sonar.server.source.ws.ShowAction; -import org.sonar.server.startup.*; +import org.sonar.server.source.ws.SourcesWs; +import org.sonar.server.startup.CopyRequirementsFromCharacteristicsToRules; +import org.sonar.server.startup.GeneratePluginIndex; +import org.sonar.server.startup.JdbcDriverDeployer; +import org.sonar.server.startup.LogServerId; +import org.sonar.server.startup.RegisterDashboards; +import org.sonar.server.startup.RegisterDebtModel; +import org.sonar.server.startup.RegisterMetrics; +import org.sonar.server.startup.RegisterNewMeasureFilters; +import org.sonar.server.startup.RegisterPermissionTemplates; +import org.sonar.server.startup.RegisterServletFilters; +import org.sonar.server.startup.RenameDeprecatedPropertyKeys; +import org.sonar.server.startup.ServerMetadataPersister; import org.sonar.server.test.CoverageService; -import org.sonar.server.test.ws.*; +import org.sonar.server.test.ws.CoverageShowAction; +import org.sonar.server.test.ws.CoverageWs; +import org.sonar.server.test.ws.TestsCoveredFilesAction; +import org.sonar.server.test.ws.TestsShowAction; +import org.sonar.server.test.ws.TestsTestCasesAction; +import org.sonar.server.test.ws.TestsWs; import org.sonar.server.text.MacroInterpreter; import org.sonar.server.text.RubyTextService; import org.sonar.server.ui.JRubyI18n; @@ -180,7 +310,14 @@ import org.sonar.server.ui.JRubyProfiling; import org.sonar.server.ui.PageDecorations; import org.sonar.server.ui.Views; import org.sonar.server.updatecenter.ws.UpdateCenterWs; -import org.sonar.server.user.*; +import org.sonar.server.user.DefaultUserService; +import org.sonar.server.user.DoPrivileged; +import org.sonar.server.user.GroupMembershipFinder; +import org.sonar.server.user.GroupMembershipService; +import org.sonar.server.user.NewUserNotifier; +import org.sonar.server.user.SecurityRealmFactory; +import org.sonar.server.user.UserService; +import org.sonar.server.user.UserUpdater; import org.sonar.server.user.db.GroupDao; import org.sonar.server.user.db.UserDao; import org.sonar.server.user.db.UserGroupDao; @@ -190,7 +327,13 @@ import org.sonar.server.user.index.UserIndexer; import org.sonar.server.user.ws.FavoritesWs; import org.sonar.server.user.ws.UserPropertiesWs; import org.sonar.server.user.ws.UsersWs; -import org.sonar.server.util.*; +import org.sonar.server.util.BooleanTypeValidation; +import org.sonar.server.util.FloatTypeValidation; +import org.sonar.server.util.IntegerTypeValidation; +import org.sonar.server.util.StringListTypeValidation; +import org.sonar.server.util.StringTypeValidation; +import org.sonar.server.util.TextTypeValidation; +import org.sonar.server.util.TypeValidations; import org.sonar.server.view.index.ViewIndex; import org.sonar.server.view.index.ViewIndexDefinition; import org.sonar.server.view.index.ViewIndexer; @@ -538,7 +681,6 @@ class ServerComponents { pico.addSingleton(IssueService.class); pico.addSingleton(IssueActionsWriter.class); pico.addSingleton(IssueQueryService.class); - pico.addSingleton(IssueNotifications.class); pico.addSingleton(NewIssuesEmailTemplate.class); pico.addSingleton(IssueChangesEmailTemplate.class); pico.addSingleton(ChangesOnMyIssueNotificationDispatcher.class); diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java index d22c2f399d3..273e14f4ec5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java @@ -19,27 +19,57 @@ */ package org.sonar.server.computation.step; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.notifications.Notification; +import org.sonar.api.rule.Severity; +import org.sonar.api.utils.System2; +import org.sonar.server.computation.ComputationContext; +import org.sonar.server.computation.issue.IssueCache; +import org.sonar.server.computation.issue.RuleCache; +import org.sonar.server.issue.notification.IssueChangeNotification; +import org.sonar.server.issue.notification.NewIssuesNotification; +import org.sonar.server.notifications.NotificationService; -import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; public class SendIssueNotificationsStepTest { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + RuleCache ruleCache = mock(RuleCache.class); + NotificationService notifService = mock(NotificationService.class); + ComputationContext context = mock(ComputationContext.class, Mockito.RETURNS_DEEP_STUBS); + + @Test + public void do_not_send_notifications_if_no_subscribers() throws Exception { + IssueCache issueCache = new IssueCache(temp.newFile(), System2.INSTANCE); + when(context.getProject().uuid()).thenReturn("PROJECT_UUID"); + when(notifService.hasProjectSubscribersForTypes("PROJECT_UUID", SendIssueNotificationsStep.NOTIF_TYPES)).thenReturn(false); + + SendIssueNotificationsStep step = new SendIssueNotificationsStep(issueCache, ruleCache, notifService); + step.execute(context); + + verify(notifService, never()).deliver(any(Notification.class)); + } + @Test - public void new_issue_statistics() { - SendIssueNotificationsStep.NewIssuesStatistics sut = new SendIssueNotificationsStep.NewIssuesStatistics(); - assertThat(sut.isEmpty()).isTrue(); - assertThat(sut.size()).isEqualTo(0); - - sut.add(new DefaultIssue().setSeverity("MINOR")); - sut.add(new DefaultIssue().setSeverity("BLOCKER")); - sut.add(new DefaultIssue().setSeverity("MINOR")); - - assertThat(sut.isEmpty()).isFalse(); - assertThat(sut.size()).isEqualTo(3); - assertThat(sut.issuesWithSeverity("INFO")).isEqualTo(0); - assertThat(sut.issuesWithSeverity("MINOR")).isEqualTo(2); - assertThat(sut.issuesWithSeverity("BLOCKER")).isEqualTo(1); + public void send_notifications_if_subscribers() throws Exception { + IssueCache issueCache = new IssueCache(temp.newFile(), System2.INSTANCE); + issueCache.newAppender().append(new DefaultIssue().setSeverity(Severity.BLOCKER)).close(); + + when(context.getProject().uuid()).thenReturn("PROJECT_UUID"); + when(notifService.hasProjectSubscribersForTypes("PROJECT_UUID", SendIssueNotificationsStep.NOTIF_TYPES)).thenReturn(true); + + SendIssueNotificationsStep step = new SendIssueNotificationsStep(issueCache, ruleCache, notifService); + step.execute(context); + + verify(notifService).deliver(any(NewIssuesNotification.class)); + verify(notifService, atLeastOnce()).deliver(any(IssueChangeNotification.class)); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java index 40df25eea5c..39cb75ea3fd 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.sonar.api.issue.Issue; import org.sonar.api.issue.condition.Condition; import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.notifications.NotificationManager; import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Scopes; import org.sonar.api.rules.Rule; @@ -38,7 +39,7 @@ import org.sonar.server.db.DbClient; import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.issue.db.IssueDao; -import org.sonar.server.issue.notification.IssueNotifications; +import org.sonar.server.issue.notification.IssueChangeNotification; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.rule.RuleTesting; import org.sonar.server.search.QueryContext; @@ -56,15 +57,8 @@ import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyListOf; import static org.mockito.Matchers.anyMap; -import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class IssueBulkChangeServiceTest { @@ -76,7 +70,7 @@ public class IssueBulkChangeServiceTest { IssueStorage issueStorage = mock(IssueStorage.class); DefaultRuleFinder ruleFinder = mock(DefaultRuleFinder.class); ComponentDao componentDao = mock(ComponentDao.class); - IssueNotifications issueNotifications = mock(IssueNotifications.class); + NotificationManager notificationService = mock(NotificationManager.class); IssueBulkChangeService service; @@ -122,7 +116,7 @@ public class IssueBulkChangeServiceTest { when(issueDao.selectByKeys(dbSession, newArrayList(issue.key()))).thenReturn(newArrayList(issueDto)); actions = newArrayList(); - service = new IssueBulkChangeService(dbClient, issueService, issueStorage, ruleFinder, issueNotifications, actions); + service = new IssueBulkChangeService(dbClient, issueService, issueStorage, ruleFinder, notificationService, actions); } @Test @@ -140,8 +134,8 @@ public class IssueBulkChangeServiceTest { verify(issueStorage).save(eq(issue)); verifyNoMoreInteractions(issueStorage); - verify(issueNotifications).sendChanges(eq(issue), anyString(), eq("the rule name"), eq(project), eq(file), eq((String) null), eq(false)); - verifyNoMoreInteractions(issueNotifications); + verify(notificationService).scheduleForSending(any(IssueChangeNotification.class)); + verifyNoMoreInteractions(notificationService); } @Test @@ -159,7 +153,7 @@ public class IssueBulkChangeServiceTest { verify(issueStorage).save(eq(issue)); verifyNoMoreInteractions(issueStorage); - verifyZeroInteractions(issueNotifications); + verifyZeroInteractions(notificationService); } @Test @@ -244,8 +238,8 @@ public class IssueBulkChangeServiceTest { verify(issueStorage, times(1)).save(eq(issue)); verifyNoMoreInteractions(issueStorage); - verify(issueNotifications).sendChanges(eq(issue), anyString(), eq("the rule name"), eq(project), eq(file), eq((String) null), eq(false)); - verifyNoMoreInteractions(issueNotifications); + verify(notificationService).scheduleForSending(any(IssueChangeNotification.class)); + verifyNoMoreInteractions(notificationService); } @Test @@ -262,7 +256,7 @@ public class IssueBulkChangeServiceTest { assertThat(result.issuesNotChanged()).hasSize(1); verifyZeroInteractions(issueStorage); - verifyZeroInteractions(issueNotifications); + verifyZeroInteractions(notificationService); } @Test @@ -279,7 +273,7 @@ public class IssueBulkChangeServiceTest { assertThat(result.issuesNotChanged()).isEmpty(); verifyZeroInteractions(issueStorage); - verifyZeroInteractions(issueNotifications); + verifyZeroInteractions(notificationService); } @Test @@ -296,7 +290,7 @@ public class IssueBulkChangeServiceTest { assertThat(result.issuesNotChanged()).hasSize(1); verifyZeroInteractions(issueStorage); - verifyZeroInteractions(issueNotifications); + verifyZeroInteractions(notificationService); } @Test @@ -319,7 +313,7 @@ public class IssueBulkChangeServiceTest { assertThat(result.issuesNotChanged()).hasSize(1); verifyZeroInteractions(issueStorage); - verifyZeroInteractions(issueNotifications); + verifyZeroInteractions(notificationService); } @Test @@ -338,7 +332,7 @@ public class IssueBulkChangeServiceTest { assertThat(e).isInstanceOf(UnauthorizedException.class); } verifyZeroInteractions(issueStorage); - verifyZeroInteractions(issueNotifications); + verifyZeroInteractions(notificationService); } @Test @@ -355,7 +349,7 @@ public class IssueBulkChangeServiceTest { assertThat(e).isInstanceOf(BadRequestException.class).hasMessage("The action : 'unknown' is unknown"); } verifyZeroInteractions(issueStorage); - verifyZeroInteractions(issueNotifications); + verifyZeroInteractions(notificationService); } class MockAction extends Action { diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java index 48b9d5f60ad..75a84e8c5df 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java @@ -78,7 +78,7 @@ public class ChangesOnMyIssueNotificationDispatcherTest { recipients.put("godin", twitterChannel); when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients); - Notification notification = new Notification("issue-changes").setFieldValue("projectKey", "struts") + Notification notification = new IssueChangeNotification().setFieldValue("projectKey", "struts") .setFieldValue("changeAuthor", "olivier") .setFieldValue("reporter", "simon") .setFieldValue("assignee", "freddy"); @@ -99,15 +99,15 @@ public class ChangesOnMyIssueNotificationDispatcherTest { when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients); // change author is the reporter - dispatcher.performDispatch(new Notification("issue-changes").setFieldValue("projectKey", "struts") + dispatcher.performDispatch(new IssueChangeNotification().setFieldValue("projectKey", "struts") .setFieldValue("changeAuthor", "simon").setFieldValue("reporter", "simon"), context); // change author is the assignee - dispatcher.performDispatch(new Notification("issue-changes").setFieldValue("projectKey", "struts") + dispatcher.performDispatch(new IssueChangeNotification().setFieldValue("projectKey", "struts") .setFieldValue("changeAuthor", "simon").setFieldValue("assignee", "simon"), context); // no change author - dispatcher.performDispatch(new Notification("issue-changes").setFieldValue("projectKey", "struts") + dispatcher.performDispatch(new IssueChangeNotification().setFieldValue("projectKey", "struts") .setFieldValue("new.resolution", "FIXED"), context); verifyNoMoreInteractions(context); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java index 8e51bf1be43..f7023431f43 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java @@ -31,6 +31,7 @@ import org.sonar.api.config.EmailSettings; import org.sonar.api.notifications.Notification; import org.sonar.api.user.User; import org.sonar.api.user.UserFinder; +import org.sonar.core.component.ComponentDto; import org.sonar.plugins.emailnotifications.api.EmailMessage; import static org.assertj.core.api.Assertions.assertThat; @@ -71,9 +72,9 @@ public class IssueChangesEmailTemplateTest { String message = email.getMessage(); String expected = Resources.toString(Resources.getResource( - "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt"), + "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt"), Charsets.UTF_8 - ); + ); expected = StringUtils.remove(expected, '\r'); assertThat(message).isEqualTo(expected); assertThat(email.getFrom()).isNull(); @@ -91,9 +92,9 @@ public class IssueChangesEmailTemplateTest { String message = email.getMessage(); String expected = Resources.toString(Resources.getResource( - "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt"), + "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt"), Charsets.UTF_8 - ); + ); expected = StringUtils.remove(expected, '\r'); assertThat(message).isEqualTo(expected); assertThat(email.getFrom()).isNull(); @@ -110,9 +111,9 @@ public class IssueChangesEmailTemplateTest { String message = email.getMessage(); String expected = Resources.toString(Resources.getResource( - "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt"), + "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt"), Charsets.UTF_8 - ); + ); expected = StringUtils.remove(expected, '\r'); assertThat(message).isEqualTo(expected); } @@ -145,17 +146,16 @@ public class IssueChangesEmailTemplateTest { when(user.name()).thenReturn("Simon"); when(userFinder.findByLogin("simon")).thenReturn(user); - Notification notification = new Notification("issue-changes") - .setFieldValue("projectName", "Struts") - .setFieldValue("projectKey", "org.apache:struts") - .setFieldValue("changeAuthor", "simon"); + Notification notification = new IssueChangeNotification() + .setChangeAuthorLogin("simon") + .setProject(new ComponentDto().setLongName("Struts").setKey("org.apache:struts")); EmailMessage message = template.format(notification); assertThat(message.getFrom()).isEqualTo("Simon"); } private Notification generateNotification() { - Notification notification = new Notification("issue-changes") + Notification notification = new IssueChangeNotification() .setFieldValue("projectName", "Struts") .setFieldValue("projectKey", "org.apache:struts") .setFieldValue("componentName", "Action") diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueNotificationsTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueNotificationsTest.java index 251ff4dea5f..fb3177cd528 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueNotificationsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueNotificationsTest.java @@ -17,133 +17,152 @@ * 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 org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; -import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.issue.internal.IssueChangeContext; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationManager; -import org.sonar.api.resources.File; -import org.sonar.api.resources.Project; -import org.sonar.core.component.ResourceComponent; -import org.sonar.server.notifications.NotificationService; - -import java.util.Date; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -@RunWith(MockitoJUnitRunner.class) -public class IssueNotificationsTest { - - @Mock - NotificationManager manager; - - IssueNotifications issueNotifications; - - @Before - public void setUp() throws Exception { - issueNotifications = new IssueNotifications(manager, mock(NotificationService.class)); - } - - // @Test - // public void should_send_new_issues() throws Exception { - // Date date = DateUtils.parseDateTime("2013-05-18T13:00:03+0200"); - // Project project = new Project("struts").setAnalysisDate(date); - // IssuesBySeverity issuesBySeverity = mock(IssuesBySeverity.class); - // when(issuesBySeverity.size()).thenReturn(42); - // when(issuesBySeverity.issues("MINOR")).thenReturn(10); - // Notification notification = issueNotifications.sendNewIssues(project, issuesBySeverity); - // - // assertThat(notification.getFieldValue("count")).isEqualTo("42"); - // assertThat(notification.getFieldValue("count-MINOR")).isEqualTo("10"); - // assertThat(DateUtils.parseDateTime(notification.getFieldValue("projectDate"))).isEqualTo(date); - // Mockito.verify(manager).scheduleForSending(notification); - // } - - @Test - public void should_send_changes() throws Exception { - IssueChangeContext context = IssueChangeContext.createScan(new Date()); - DefaultIssue issue = new DefaultIssue() - .setMessage("the message") - .setKey("ABCDE") - .setAssignee("freddy") - .setFieldChange(context, "resolution", null, "FIXED") - .setFieldChange(context, "status", "OPEN", "RESOLVED") - .setFieldChange(context, "assignee", "simon", null) - .setSendNotifications(true) - .setComponentKey("struts:Action") - .setProjectKey("struts"); - - Notification notification = issueNotifications.sendChanges(issue, "charlie", null, new Project("struts"), null, null, false); - - assertThat(notification.getFieldValue("message")).isEqualTo("the message"); - assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE"); - assertThat(notification.getFieldValue("componentKey")).isEqualTo("struts:Action"); - assertThat(notification.getFieldValue("componentName")).isNull(); - assertThat(notification.getFieldValue("old.resolution")).isNull(); - assertThat(notification.getFieldValue("new.resolution")).isEqualTo("FIXED"); - assertThat(notification.getFieldValue("old.status")).isEqualTo("OPEN"); - assertThat(notification.getFieldValue("new.status")).isEqualTo("RESOLVED"); - assertThat(notification.getFieldValue("old.assignee")).isEqualTo("simon"); - assertThat(notification.getFieldValue("new.assignee")).isNull(); - Mockito.verify(manager).scheduleForSending(notification); - } - - @Test - public void should_send_changes_with_comment() throws Exception { - DefaultIssue issue = new DefaultIssue() - .setMessage("the message") - .setKey("ABCDE") - .setAssignee("freddy") - .setComponentKey("struts:Action") - .setProjectKey("struts"); - Notification notification = issueNotifications.sendChanges(issue, "charlie", null, new Project("struts"), null, "I don't know how to fix it?", false); - - assertThat(notification.getFieldValue("message")).isEqualTo("the message"); - assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE"); - assertThat(notification.getFieldValue("comment")).isEqualTo("I don't know how to fix it?"); - Mockito.verify(manager).scheduleForSending(notification); - } - - @Test - public void should_send_changes_with_component_name() throws Exception { - IssueChangeContext context = IssueChangeContext.createScan(new Date()); - DefaultIssue issue = new DefaultIssue() - .setMessage("the message") - .setKey("ABCDE") - .setAssignee("freddy") - .setFieldChange(context, "resolution", null, "FIXED") - .setSendNotifications(true) - .setComponentKey("struts:Action.java") - .setProjectKey("struts"); - Notification notification = issueNotifications.sendChanges(issue, "charlie", null, new Project("struts"), - new ResourceComponent(File.create("Action.java", "Action.java", null, false).setEffectiveKey("struts:Action.java")), null, false); - - assertThat(notification.getFieldValue("message")).isEqualTo("the message"); - assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE"); - assertThat(notification.getFieldValue("componentKey")).isEqualTo("struts:Action.java"); - assertThat(notification.getFieldValue("componentName")).isEqualTo("Action.java"); - assertThat(notification.getFieldValue("old.resolution")).isNull(); - assertThat(notification.getFieldValue("new.resolution")).isEqualTo("FIXED"); - Mockito.verify(manager).scheduleForSending(notification); - } - - @Test - public void should_not_send_changes_if_no_diffs() throws Exception { - DefaultIssue issue = new DefaultIssue() - .setMessage("the message") - .setKey("ABCDE") - .setComponentKey("struts:Action") - .setProjectKey("struts"); - issueNotifications.sendChanges(issue, "charlie", null, new Project("struts"), null, null, false); - - Mockito.verifyZeroInteractions(manager); - } -} +///* +// * SonarQube, open source software quality management tool. +// * Copyright (C) 2008-2014 SonarSource +// * mailto:contact AT sonarsource DOT com +// * +// * SonarQube 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. +// * +// * SonarQube 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 org.junit.Before; +//import org.junit.Test; +//import org.junit.runner.RunWith; +//import org.mockito.Mock; +//import org.mockito.Mockito; +//import org.mockito.runners.MockitoJUnitRunner; +//import org.sonar.api.issue.internal.DefaultIssue; +//import org.sonar.api.issue.internal.IssueChangeContext; +//import org.sonar.api.notifications.Notification; +//import org.sonar.api.notifications.NotificationManager; +//import org.sonar.api.resources.File; +//import org.sonar.api.resources.Project; +//import org.sonar.core.component.ResourceComponent; +//import org.sonar.server.notifications.NotificationService; +// +//import java.util.Date; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.mockito.Mockito.mock; +// +//@RunWith(MockitoJUnitRunner.class) +//public class IssueNotificationsTest { +// +// @Mock +// NotificationManager manager; +// +// IssueNotifications issueNotifications; +// +// @Before +// public void setUp() throws Exception { +// issueNotifications = new IssueNotifications(manager, mock(NotificationService.class)); +// } +// +// // @Test +// // public void should_send_new_issues() throws Exception { +// // Date date = DateUtils.parseDateTime("2013-05-18T13:00:03+0200"); +// // Project project = new Project("struts").setAnalysisDate(date); +// // IssuesBySeverity issuesBySeverity = mock(IssuesBySeverity.class); +// // when(issuesBySeverity.size()).thenReturn(42); +// // when(issuesBySeverity.issues("MINOR")).thenReturn(10); +// // Notification notification = issueNotifications.sendNewIssues(project, issuesBySeverity); +// // +// // assertThat(notification.getFieldValue("count")).isEqualTo("42"); +// // assertThat(notification.getFieldValue("count-MINOR")).isEqualTo("10"); +// // assertThat(DateUtils.parseDateTime(notification.getFieldValue("projectDate"))).isEqualTo(date); +// // Mockito.verify(manager).scheduleForSending(notification); +// // } +// +// @Test +// public void should_send_changes() throws Exception { +// IssueChangeContext context = IssueChangeContext.createScan(new Date()); +// DefaultIssue issue = new DefaultIssue() +// .setMessage("the message") +// .setKey("ABCDE") +// .setAssignee("freddy") +// .setFieldChange(context, "resolution", null, "FIXED") +// .setFieldChange(context, "status", "OPEN", "RESOLVED") +// .setFieldChange(context, "assignee", "simon", null) +// .setSendNotifications(true) +// .setComponentKey("struts:Action") +// .setProjectKey("struts"); +// +// Notification notification = issueNotifications.sendChanges(issue, "charlie", null, new Project("struts"), null, null, false); +// +// assertThat(notification.getFieldValue("message")).isEqualTo("the message"); +// assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE"); +// assertThat(notification.getFieldValue("componentKey")).isEqualTo("struts:Action"); +// assertThat(notification.getFieldValue("componentName")).isNull(); +// assertThat(notification.getFieldValue("old.resolution")).isNull(); +// assertThat(notification.getFieldValue("new.resolution")).isEqualTo("FIXED"); +// assertThat(notification.getFieldValue("old.status")).isEqualTo("OPEN"); +// assertThat(notification.getFieldValue("new.status")).isEqualTo("RESOLVED"); +// assertThat(notification.getFieldValue("old.assignee")).isEqualTo("simon"); +// assertThat(notification.getFieldValue("new.assignee")).isNull(); +// Mockito.verify(manager).scheduleForSending(notification); +// } +// +// @Test +// public void should_send_changes_with_comment() throws Exception { +// DefaultIssue issue = new DefaultIssue() +// .setMessage("the message") +// .setKey("ABCDE") +// .setAssignee("freddy") +// .setComponentKey("struts:Action") +// .setProjectKey("struts"); +// Notification notification = issueNotifications.sendChanges(issue, "charlie", null, new Project("struts"), null, "I don't know how to fix it?", false); +// +// assertThat(notification.getFieldValue("message")).isEqualTo("the message"); +// assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE"); +// assertThat(notification.getFieldValue("comment")).isEqualTo("I don't know how to fix it?"); +// Mockito.verify(manager).scheduleForSending(notification); +// } +// +// @Test +// public void should_send_changes_with_component_name() throws Exception { +// IssueChangeContext context = IssueChangeContext.createScan(new Date()); +// DefaultIssue issue = new DefaultIssue() +// .setMessage("the message") +// .setKey("ABCDE") +// .setAssignee("freddy") +// .setFieldChange(context, "resolution", null, "FIXED") +// .setSendNotifications(true) +// .setComponentKey("struts:Action.java") +// .setProjectKey("struts"); +// Notification notification = issueNotifications.sendChanges(issue, "charlie", null, new Project("struts"), +// new ResourceComponent(File.create("Action.java", "Action.java", null, false).setEffectiveKey("struts:Action.java")), null, false); +// +// assertThat(notification.getFieldValue("message")).isEqualTo("the message"); +// assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE"); +// assertThat(notification.getFieldValue("componentKey")).isEqualTo("struts:Action.java"); +// assertThat(notification.getFieldValue("componentName")).isEqualTo("Action.java"); +// assertThat(notification.getFieldValue("old.resolution")).isNull(); +// assertThat(notification.getFieldValue("new.resolution")).isEqualTo("FIXED"); +// Mockito.verify(manager).scheduleForSending(notification); +// } +// +// @Test +// public void should_not_send_changes_if_no_diffs() throws Exception { +// DefaultIssue issue = new DefaultIssue() +// .setMessage("the message") +// .setKey("ABCDE") +// .setComponentKey("struts:Action") +// .setProjectKey("struts"); +// issueNotifications.sendChanges(issue, "charlie", null, new Project("struts"), null, null, false); +// +// Mockito.verifyZeroInteractions(manager); +// } +//} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewFalsePositiveNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewFalsePositiveNotificationDispatcherTest.java index f44b04bfaac..ac434d62372 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewFalsePositiveNotificationDispatcherTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewFalsePositiveNotificationDispatcherTest.java @@ -79,7 +79,7 @@ public class NewFalsePositiveNotificationDispatcherTest { recipients.put("godin", twitterChannel); when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients); - Notification notification = new Notification("issue-changes").setFieldValue("projectKey", "struts") + Notification notification = new IssueChangeNotification().setFieldValue("projectKey", "struts") .setFieldValue("changeAuthor", "godin") .setFieldValue("new.resolution", "FALSE-POSITIVE") .setFieldValue("assignee", "freddy"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java index f658a8f19c9..c436301c8cc 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java @@ -73,7 +73,7 @@ public class NewIssuesEmailTemplateTest { */ @Test public void shouldFormatCommentAdded() { - Notification notification = new Notification("new-issues") + Notification notification = new NewIssuesNotification() .setFieldValue("count", "32") .setFieldValue("count-INFO", "1") .setFieldValue("count-MINOR", "3") @@ -108,7 +108,7 @@ public class NewIssuesEmailTemplateTest { @Test public void shouldNotAddFooterIfMissingProperties() { - Notification notification = new Notification("new-issues") + Notification notification = new NewIssuesNotification() .setFieldValue("count", "32") .setFieldValue("projectName", "Struts"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java index c6fc60cf03f..04a3b1ae3fb 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java @@ -70,7 +70,7 @@ public class NewIssuesNotificationDispatcherTest { recipients.put("user2", twitterChannel); when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients); - Notification notification = new Notification("new-issues").setFieldValue("projectKey", "struts"); + Notification notification = new NewIssuesNotification().setFieldValue("projectKey", "struts"); dispatcher.performDispatch(notification, context); verify(context).addUser("user1", emailChannel); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java new file mode 100644 index 00000000000..4c208547406 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java @@ -0,0 +1,43 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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 org.junit.Test; +import org.sonar.api.issue.internal.DefaultIssue; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NewIssuesNotificationTest { + + @Test + public void stats() { + NewIssuesNotification.Stats sut = new NewIssuesNotification.Stats(); + assertThat(sut.size()).isEqualTo(0); + + sut.add(new DefaultIssue().setSeverity("MINOR")); + sut.add(new DefaultIssue().setSeverity("BLOCKER")); + sut.add(new DefaultIssue().setSeverity("MINOR")); + + assertThat(sut.size()).isEqualTo(3); + assertThat(sut.countIssuesWithSeverity("INFO")).isEqualTo(0); + assertThat(sut.countIssuesWithSeverity("MINOR")).isEqualTo(2); + assertThat(sut.countIssuesWithSeverity("BLOCKER")).isEqualTo(1); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationServiceTest.java index 5572f77cffc..98f07a6048c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationServiceTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationServiceTest.java @@ -19,6 +19,7 @@ */ package org.sonar.server.notifications; +import com.google.common.collect.Sets; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -27,6 +28,10 @@ import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.NotificationChannel; import org.sonar.api.notifications.NotificationDispatcher; import org.sonar.core.notification.DefaultNotificationManager; +import org.sonar.core.properties.PropertiesDao; +import org.sonar.server.db.DbClient; + +import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.any; @@ -39,26 +44,32 @@ public class NotificationServiceTest { private static String CREATOR_EVGENY = "evgeny"; private static String ASSIGNEE_SIMON = "simon"; - private final DefaultNotificationManager manager = mock(DefaultNotificationManager.class); - private final Notification notification = mock(Notification.class); - private final NotificationChannel emailChannel = mock(NotificationChannel.class); - private final NotificationChannel gtalkChannel = mock(NotificationChannel.class); - private final NotificationDispatcher commentOnReviewAssignedToMe = mock(NotificationDispatcher.class); - private final NotificationDispatcher commentOnReviewCreatedByMe = mock(NotificationDispatcher.class); + DefaultNotificationManager manager = mock(DefaultNotificationManager.class); + Notification notification = mock(Notification.class); + NotificationChannel emailChannel = mock(NotificationChannel.class); + NotificationChannel gtalkChannel = mock(NotificationChannel.class); + NotificationDispatcher commentOnIssueAssignedToMe = mock(NotificationDispatcher.class); + NotificationDispatcher commentOnIssueCreatedByMe = mock(NotificationDispatcher.class); + NotificationDispatcher qualityGateChange = mock(NotificationDispatcher.class); + DbClient dbClient = mock(DbClient.class); private NotificationService service; private void setUpMocks() { when(emailChannel.getKey()).thenReturn("email"); when(gtalkChannel.getKey()).thenReturn("gtalk"); - when(commentOnReviewAssignedToMe.getKey()).thenReturn("comment on review assigned to me"); - when(commentOnReviewCreatedByMe.getKey()).thenReturn("comment on review created by me"); + when(commentOnIssueAssignedToMe.getKey()).thenReturn("CommentOnIssueAssignedToMe"); + when(commentOnIssueAssignedToMe.getType()).thenReturn("issue-changes"); + when(commentOnIssueCreatedByMe.getKey()).thenReturn("CommentOnIssueCreatedByMe"); + when(commentOnIssueCreatedByMe.getType()).thenReturn("issue-changes"); + when(qualityGateChange.getKey()).thenReturn("QGateChange"); + when(qualityGateChange.getType()).thenReturn("qgate-changes"); when(manager.getFromQueue()).thenReturn(notification).thenReturn(null); Settings settings = new Settings().setProperty("sonar.notifications.delay", 1L); service = new NotificationService(settings, manager, - new NotificationDispatcher[] {commentOnReviewAssignedToMe, commentOnReviewCreatedByMe}); + dbClient, new NotificationDispatcher[] {commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange}); } /** @@ -74,8 +85,8 @@ public class NotificationServiceTest { @Test public void scenario1() { setUpMocks(); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnReviewAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnReviewCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); service.start(); verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); @@ -98,8 +109,8 @@ public class NotificationServiceTest { @Test public void scenario2() { setUpMocks(); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnReviewAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_EVGENY, gtalkChannel)).when(commentOnReviewCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_EVGENY, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); service.start(); verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); @@ -124,7 +135,7 @@ public class NotificationServiceTest { public void scenario3() { setUpMocks(); doAnswer(addUser(ASSIGNEE_SIMON, new NotificationChannel[] {emailChannel, gtalkChannel})) - .when(commentOnReviewAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + .when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); service.start(); verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); @@ -161,8 +172,8 @@ public class NotificationServiceTest { public void shouldNotStopWhenException() { setUpMocks(); when(manager.getFromQueue()).thenThrow(new RuntimeException("Unexpected exception")).thenReturn(notification).thenReturn(null); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnReviewAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnReviewCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); service.start(); verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); @@ -174,7 +185,7 @@ public class NotificationServiceTest { @Test public void shouldNotAddNullAsUser() { setUpMocks(); - doAnswer(addUser(null, gtalkChannel)).when(commentOnReviewCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(null, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); service.start(); service.stop(); @@ -184,17 +195,17 @@ public class NotificationServiceTest { } @Test - public void shouldReturnDispatcherList() { + public void getDispatchers() { setUpMocks(); - assertThat(service.getDispatchers()).containsOnly(commentOnReviewAssignedToMe, commentOnReviewCreatedByMe); + assertThat(service.getDispatchers()).containsOnly(commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange); } @Test - public void shouldReturnNoDispatcher() { + public void getDispatchers_empty() { Settings settings = new Settings().setProperty("sonar.notifications.delay", 1L); - service = new NotificationService(settings, manager); + service = new NotificationService(settings, manager, dbClient); assertThat(service.getDispatchers()).hasSize(0); } @@ -213,6 +224,22 @@ public class NotificationServiceTest { service.stop(); } + @Test + public void hasProjectSubscribersForType() { + setUpMocks(); + + PropertiesDao dao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(dao); + + // no subscribers + when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(false); + assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isFalse(); + + // has subscribers on one dispatcher (among the two) + when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(true); + assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isTrue(); + } + private static Answer addUser(final String user, final NotificationChannel channel) { return addUser(user, new NotificationChannel[] {channel}); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationTest.java b/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationTest.java index 00bc619be41..2c7c9194f9c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationTest.java @@ -48,7 +48,7 @@ public class NotificationTest { public void shouldReturnToStringIfDefaultMessageNotSet() { notification = new Notification("alerts").setFieldValue("alertCount", "42"); System.out.println(notification); - assertThat(notification.getDefaultMessage()).contains("type=alerts"); + assertThat(notification.getDefaultMessage()).contains("type='alerts'"); assertThat(notification.getDefaultMessage()).contains("fields={alertCount=42}"); } diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java b/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java index abd7d3fb3d6..92f36946428 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java @@ -112,4 +112,14 @@ public final class DaoUtils { return results; } + public static String repeatCondition(String sql, int count, String separator) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append(sql); + if (i < count-1) { + sb.append(" ").append(separator).append(" "); + } + } + return sb.toString(); + } } diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java b/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java index a3c29f4e0c0..0390d6fabdf 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java @@ -25,7 +25,11 @@ import com.google.common.io.Closeables; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.logging.LogFactory; import org.apache.ibatis.mapping.Environment; -import org.apache.ibatis.session.*; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.apache.ibatis.type.JdbcType; import org.slf4j.LoggerFactory; @@ -44,7 +48,14 @@ import org.sonar.core.component.db.SnapshotMapper; import org.sonar.core.computation.db.AnalysisReportDto; import org.sonar.core.computation.db.AnalysisReportMapper; import org.sonar.core.config.Logback; -import org.sonar.core.dashboard.*; +import org.sonar.core.dashboard.ActiveDashboardDto; +import org.sonar.core.dashboard.ActiveDashboardMapper; +import org.sonar.core.dashboard.DashboardDto; +import org.sonar.core.dashboard.DashboardMapper; +import org.sonar.core.dashboard.WidgetDto; +import org.sonar.core.dashboard.WidgetMapper; +import org.sonar.core.dashboard.WidgetPropertyDto; +import org.sonar.core.dashboard.WidgetPropertyMapper; import org.sonar.core.dependency.DependencyDto; import org.sonar.core.dependency.DependencyMapper; import org.sonar.core.dependency.ResourceSnapshotDto; @@ -53,11 +64,33 @@ import org.sonar.core.duplication.DuplicationMapper; import org.sonar.core.duplication.DuplicationUnitDto; import org.sonar.core.graph.jdbc.GraphDto; import org.sonar.core.graph.jdbc.GraphDtoMapper; -import org.sonar.core.issue.db.*; -import org.sonar.core.measure.db.*; +import org.sonar.core.issue.db.ActionPlanDto; +import org.sonar.core.issue.db.ActionPlanMapper; +import org.sonar.core.issue.db.ActionPlanStatsDto; +import org.sonar.core.issue.db.ActionPlanStatsMapper; +import org.sonar.core.issue.db.BatchIssueDto; +import org.sonar.core.issue.db.IssueChangeDto; +import org.sonar.core.issue.db.IssueChangeMapper; +import org.sonar.core.issue.db.IssueDto; +import org.sonar.core.issue.db.IssueFilterDto; +import org.sonar.core.issue.db.IssueFilterFavouriteDto; +import org.sonar.core.issue.db.IssueFilterFavouriteMapper; +import org.sonar.core.issue.db.IssueFilterMapper; +import org.sonar.core.issue.db.IssueMapper; +import org.sonar.core.measure.db.MeasureDto; +import org.sonar.core.measure.db.MeasureFilterDto; +import org.sonar.core.measure.db.MeasureFilterMapper; +import org.sonar.core.measure.db.MeasureMapper; +import org.sonar.core.measure.db.MetricDto; +import org.sonar.core.measure.db.MetricMapper; import org.sonar.core.notification.db.NotificationQueueDto; import org.sonar.core.notification.db.NotificationQueueMapper; -import org.sonar.core.permission.*; +import org.sonar.core.permission.GroupWithPermissionDto; +import org.sonar.core.permission.PermissionTemplateDto; +import org.sonar.core.permission.PermissionTemplateGroupDto; +import org.sonar.core.permission.PermissionTemplateMapper; +import org.sonar.core.permission.PermissionTemplateUserDto; +import org.sonar.core.permission.UserWithPermissionDto; import org.sonar.core.persistence.dialect.Dialect; import org.sonar.core.persistence.migration.v44.Migration44Mapper; import org.sonar.core.persistence.migration.v45.Migration45Mapper; @@ -67,9 +100,22 @@ import org.sonar.core.properties.PropertyDto; import org.sonar.core.purge.IdUuidPair; import org.sonar.core.purge.PurgeMapper; import org.sonar.core.purge.PurgeableSnapshotDto; -import org.sonar.core.qualitygate.db.*; -import org.sonar.core.qualityprofile.db.*; -import org.sonar.core.resource.*; +import org.sonar.core.qualitygate.db.ProjectQgateAssociationDto; +import org.sonar.core.qualitygate.db.ProjectQgateAssociationMapper; +import org.sonar.core.qualitygate.db.QualityGateConditionDto; +import org.sonar.core.qualitygate.db.QualityGateConditionMapper; +import org.sonar.core.qualitygate.db.QualityGateDto; +import org.sonar.core.qualitygate.db.QualityGateMapper; +import org.sonar.core.qualityprofile.db.ActiveRuleDto; +import org.sonar.core.qualityprofile.db.ActiveRuleMapper; +import org.sonar.core.qualityprofile.db.ActiveRuleParamDto; +import org.sonar.core.qualityprofile.db.QualityProfileDto; +import org.sonar.core.qualityprofile.db.QualityProfileMapper; +import org.sonar.core.resource.ResourceDto; +import org.sonar.core.resource.ResourceIndexDto; +import org.sonar.core.resource.ResourceIndexerMapper; +import org.sonar.core.resource.ResourceKeyUpdaterMapper; +import org.sonar.core.resource.ResourceMapper; import org.sonar.core.rule.RuleDto; import org.sonar.core.rule.RuleMapper; import org.sonar.core.rule.RuleParamDto; @@ -79,7 +125,21 @@ import org.sonar.core.technicaldebt.db.CharacteristicMapper; import org.sonar.core.technicaldebt.db.RequirementMigrationDto; import org.sonar.core.template.LoadedTemplateDto; import org.sonar.core.template.LoadedTemplateMapper; -import org.sonar.core.user.*; +import org.sonar.core.user.AuthorDto; +import org.sonar.core.user.AuthorMapper; +import org.sonar.core.user.GroupDto; +import org.sonar.core.user.GroupMapper; +import org.sonar.core.user.GroupMembershipDto; +import org.sonar.core.user.GroupMembershipMapper; +import org.sonar.core.user.GroupRoleDto; +import org.sonar.core.user.RoleMapper; +import org.sonar.core.user.UserDto; +import org.sonar.core.user.UserGroupDto; +import org.sonar.core.user.UserGroupMapper; +import org.sonar.core.user.UserMapper; +import org.sonar.core.user.UserRoleDto; + +import javax.annotation.Nullable; import java.io.InputStream; @@ -98,7 +158,7 @@ public class MyBatis implements BatchComponent, ServerComponent { this.queue = queue; } - public static void closeQuietly(SqlSession session) { + public static void closeQuietly(@Nullable SqlSession session) { if (session != null) { try { session.close(); diff --git a/sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java b/sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java index 0ee8f546523..3f080f74beb 100644 --- a/sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java +++ b/sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java @@ -22,17 +22,24 @@ package org.sonar.core.properties; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import org.apache.commons.dbutils.DbUtils; import org.apache.commons.lang.StringUtils; import org.apache.ibatis.session.SqlSession; import org.sonar.api.BatchComponent; import org.sonar.api.ServerComponent; import org.sonar.api.resources.Scopes; import org.sonar.core.persistence.DaoComponent; +import org.sonar.core.persistence.DaoUtils; import org.sonar.core.persistence.DbSession; import org.sonar.core.persistence.MyBatis; import javax.annotation.Nullable; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -74,6 +81,33 @@ public class PropertiesDao implements BatchComponent, ServerComponent, DaoCompon } } + public boolean hasProjectNotificationSubscribersForDispatchers(String projectUuid, Collection dispatcherKeys) { + DbSession session = mybatis.openSession(false); + Connection connection = session.getConnection(); + PreparedStatement pstmt = null; + ResultSet rs = null; + String sql = "SELECT count(*) FROM properties pp " + + "left outer join projects pj on pp.resource_id = pj.id " + + "where pp.user_id is not null and (pp.resource_id is null or pj.uuid=?) " + + "and (" + DaoUtils.repeatCondition("pp.prop_key like ?", dispatcherKeys.size(), "or") + ")"; + try { + pstmt = connection.prepareStatement(sql); + pstmt.setString(1, projectUuid); + int index = 2; + for (String dispatcherKey : dispatcherKeys) { + pstmt.setString(index, "notification." + dispatcherKey + ".%"); + index++; + } + rs = pstmt.executeQuery(); + return rs.next() && rs.getInt(1) > 0; + } catch (SQLException e) { + throw new IllegalStateException("Fail to execute SQL request: " + sql, e); + } finally { + DbUtils.closeQuietly(connection, pstmt, rs); + MyBatis.closeQuietly(session); + } + } + public List selectGlobalProperties() { SqlSession session = mybatis.openSession(false); try { diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/DaoUtilsTest.java b/sonar-core/src/test/java/org/sonar/core/persistence/DaoUtilsTest.java index 7ef1161ef17..db891b609ec 100644 --- a/sonar-core/src/test/java/org/sonar/core/persistence/DaoUtilsTest.java +++ b/sonar-core/src/test/java/org/sonar/core/persistence/DaoUtilsTest.java @@ -33,4 +33,11 @@ public class DaoUtilsTest { assertThat(daoClasses).isNotEmpty(); } + + @Test + public void repeatCondition() throws Exception { + assertThat(DaoUtils.repeatCondition("uuid=?", 1, "or")).isEqualTo("uuid=?"); + assertThat(DaoUtils.repeatCondition("uuid=?", 3, "or")).isEqualTo("uuid=? or uuid=? or uuid=?"); + + } } diff --git a/sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java b/sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java index ab643acf4d6..c92c655006a 100644 --- a/sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java @@ -29,6 +29,7 @@ import org.junit.rules.ExpectedException; import org.sonar.core.persistence.AbstractDaoTestCase; import org.sonar.core.persistence.DbSession; +import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -39,8 +40,9 @@ public class PropertiesDaoTest extends AbstractDaoTestCase { @Rule public ExpectedException thrown = ExpectedException.none(); - private DbSession session; - private PropertiesDao dao; + + DbSession session; + PropertiesDao dao; @Before public void createDao() { @@ -103,6 +105,25 @@ public class PropertiesDaoTest extends AbstractDaoTestCase { assertThat(users).containsOnly("eric", "simon"); } + @Test + public void hasNotificationSubscribers() throws Exception { + setupData("findNotificationSubscribers"); + + // Nobody is subscribed + assertThat(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_A", Arrays.asList("NotSexyDispatcher"))).isFalse(); + + // Global subscribers + assertThat(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_A", Arrays.asList("DispatcherWithGlobalSubscribers"))).isTrue(); + + // Project subscribers + assertThat(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_A", Arrays.asList("DispatcherWithProjectSubscribers"))).isTrue(); + assertThat(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_B", Arrays.asList("DispatcherWithProjectSubscribers"))).isFalse(); + + // Global + Project subscribers + assertThat(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_A", Arrays.asList("DispatcherWithGlobalAndProjectSubscribers"))).isTrue(); + assertThat(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_B", Arrays.asList("DispatcherWithGlobalAndProjectSubscribers"))).isTrue(); + } + @Test public void selectGlobalProperties() { setupData("selectGlobalProperties"); diff --git a/sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/findNotificationSubscribers.xml b/sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/findNotificationSubscribers.xml index 78f7e4864dc..92f5da7324d 100644 --- a/sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/findNotificationSubscribers.xml +++ b/sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/findNotificationSubscribers.xml @@ -10,8 +10,9 @@ login="simon" /> - + + + + + +