Browse Source

SONAR-4283 notifications on new issues and issue changes

tags/3.6
Simon Brandhof 11 years ago
parent
commit
26a782fc42
49 changed files with 1143 additions and 1236 deletions
  1. 17
    18
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
  2. 83
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/ChangesOnMyIssueNotificationDispatcher.java
  3. 111
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/IssueChangesEmailTemplate.java
  4. 32
    21
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/NewFalsePositiveNotificationDispatcher.java
  5. 16
    8
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/NewIssuesEmailTemplate.java
  6. 17
    8
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/NewIssuesNotificationDispatcher.java
  7. 17
    15
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/SendIssueNotificationsPostJob.java
  8. 23
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/package-info.java
  9. 18
    9
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/notifications/alerts/NewAlerts.java
  10. 0
    72
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/notifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMe.java
  11. 0
    58
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/notifications/violations/NewViolationsOnFirstDifferentialPeriod.java
  12. 2
    2
      plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
  13. 115
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java
  14. 98
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/IssueChangesEmailTemplateTest.java
  15. 95
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/NewFalsePositiveNotificationDispatcherTest.java
  16. 11
    13
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/NewIssuesEmailTemplateTest.java
  17. 3
    3
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/NewIssuesNotificationDispatcherTest.java
  18. 17
    18
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/SendIssueNotificationsPostJobTest.java
  19. 0
    118
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/notifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMeTest.java
  20. 0
    120
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/notifications/reviews/NewFalsePositiveReviewTest.java
  21. 0
    84
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/notifications/violations/NewViolationsOnFirstDifferentialPeriodTest.java
  22. 0
    2
      plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java
  23. 0
    128
      plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/templates/reviews/ReviewEmailTemplate.java
  24. 1
    1
      plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/EmailNotificationsPluginTest.java
  25. 0
    489
      plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/templates/reviews/ReviewEmailTemplateTest.java
  26. 2
    0
      sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java
  27. 105
    0
      sonar-core/src/main/java/org/sonar/core/issue/IssueNotifications.java
  28. 14
    1
      sonar-core/src/main/java/org/sonar/core/notification/DefaultNotificationManager.java
  29. 10
    0
      sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java
  30. 2
    0
      sonar-core/src/main/java/org/sonar/core/properties/PropertiesMapper.java
  31. 12
    0
      sonar-core/src/main/resources/org/sonar/core/properties/PropertiesMapper.xml
  32. 40
    0
      sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueQueryResultTest.java
  33. 93
    0
      sonar-core/src/test/java/org/sonar/core/issue/IssueNotificationsTest.java
  34. 14
    1
      sonar-core/src/test/java/org/sonar/core/notification/DefaultNotificationManagerTest.java
  35. 27
    3
      sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java
  36. 50
    0
      sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/findNotificationSubscribers.xml
  37. 3
    1
      sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueQueryResult.java
  38. 7
    5
      sonar-plugin-api/src/main/java/org/sonar/api/notifications/Notification.java
  39. 7
    13
      sonar-plugin-api/src/main/java/org/sonar/api/notifications/NotificationDispatcher.java
  40. 7
    6
      sonar-plugin-api/src/main/java/org/sonar/api/notifications/NotificationDispatcherMetadata.java
  41. 1
    0
      sonar-plugin-api/src/main/java/org/sonar/api/notifications/NotificationManager.java
  42. 22
    1
      sonar-plugin-api/src/main/java/org/sonar/api/resources/Project.java
  43. 5
    0
      sonar-server/src/main/java/org/sonar/server/issue/DefaultIssueQueryResult.java
  44. 2
    4
      sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
  45. 27
    13
      sonar-server/src/main/java/org/sonar/server/issue/IssueService.java
  46. 2
    0
      sonar-server/src/main/java/org/sonar/server/platform/Platform.java
  47. 2
    0
      sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb
  48. 1
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/issue/_plan_form.html.erb
  49. 12
    0
      sonar-server/src/main/webapp/WEB-INF/db/migrate/403_replace_review_notifications.rb

+ 17
- 18
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java View File

@@ -23,7 +23,6 @@ package org.sonar.plugins.core;
import com.google.common.collect.ImmutableList;
import org.sonar.api.*;
import org.sonar.api.checks.NoSonarFilter;
import org.sonar.api.notifications.NotificationDispatcherMetadata;
import org.sonar.api.resources.Java;
import org.sonar.core.timemachine.Periods;
import org.sonar.plugins.core.batch.IndexProjectPostJob;
@@ -33,18 +32,25 @@ import org.sonar.plugins.core.charts.XradarChart;
import org.sonar.plugins.core.colorizers.JavaColorizerFormat;
import org.sonar.plugins.core.dashboards.*;
import org.sonar.plugins.core.issue.*;
import org.sonar.plugins.core.issue.notification.*;
import org.sonar.plugins.core.measurefilters.MyFavouritesFilter;
import org.sonar.plugins.core.measurefilters.ProjectFilter;
import org.sonar.plugins.core.notifications.alerts.NewAlerts;
import org.sonar.plugins.core.notifications.reviews.ChangesInReviewAssignedToMeOrCreatedByMe;
import org.sonar.plugins.core.notifications.reviews.NewFalsePositiveReview;
import org.sonar.plugins.core.notifications.violations.NewViolationsOnFirstDifferentialPeriod;
import org.sonar.plugins.core.security.ApplyProjectRolesDecorator;
import org.sonar.plugins.core.sensors.*;
import org.sonar.plugins.core.timemachine.*;
import org.sonar.plugins.core.web.Lcom4Viewer;
import org.sonar.plugins.core.web.TestsViewer;
import org.sonar.plugins.core.widgets.*;
import org.sonar.plugins.core.widgets.issues.ActionPlansWidget;
import org.sonar.plugins.core.widgets.issues.*;
import org.sonar.plugins.core.widgets.reviews.FalsePositiveReviewsWidget;
import org.sonar.plugins.core.widgets.reviews.MyReviewsWidget;
import org.sonar.plugins.core.widgets.reviews.ProjectReviewsWidget;
import org.sonar.plugins.core.widgets.reviews.ReviewsPerDeveloperWidget;

import java.util.List;

@@ -406,12 +412,17 @@ public final class CorePlugin extends SonarPlugin {
ActionPlansWidget.class,
UnresolvedIssuesPerAssigneeWidget.class,
UnresolvedIssuesStatusesWidget.class,

// issue notifications
SendIssueNotificationsPostJob.class,
NewIssuesEmailTemplate.class,
IssueChangesEmailTemplate.class,
ChangesOnMyIssueNotificationDispatcher.class,
ChangesOnMyIssueNotificationDispatcher.newMetadata(),
NewIssuesNotificationDispatcher.class,
NewIssuesNotificationPostJob.class,
NotificationDispatcherMetadata.create("NewIssues")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
NewIssuesNotificationDispatcher.newMetadata(),
NewFalsePositiveNotificationDispatcher.class,
NewFalsePositiveNotificationDispatcher.newMetadata(),

// batch
ProfileSensor.class,
@@ -450,19 +461,7 @@ public final class CorePlugin extends SonarPlugin {

// Notify alerts on my favourite projects
NewAlerts.class,
NotificationDispatcherMetadata.create("NewAlerts")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
// Notify reviews changes
ChangesInReviewAssignedToMeOrCreatedByMe.class,
NotificationDispatcherMetadata.create("ChangesInReviewAssignedToMeOrCreatedByMe")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
// Notify new false positive resolution
NewFalsePositiveReview.class,
NotificationDispatcherMetadata.create("NewFalsePositiveReview")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true))
NewAlerts.newMetadata()
);
}
}

+ 83
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/ChangesOnMyIssueNotificationDispatcher.java View File

@@ -0,0 +1,83 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.issue.notification;

import com.google.common.base.Objects;
import com.google.common.collect.Multimap;
import org.sonar.api.notifications.*;

import javax.annotation.Nullable;
import java.util.Collection;

/**
* This dispatcher means: "notify me when a change is done on an issue that is assigned to me or reported by me".
*
* @since 3.6, but the feature exists since 2.10 ("review-changed" notification)
*/
public class ChangesOnMyIssueNotificationDispatcher extends NotificationDispatcher {

public static final String KEY = "ChangesOnMyIssue";
private NotificationManager notificationManager;

public ChangesOnMyIssueNotificationDispatcher(NotificationManager notificationManager) {
super("issue-changes");
this.notificationManager = notificationManager;
}

@Override
public String getKey() {
return KEY;
}

public static NotificationDispatcherMetadata newMetadata() {
return NotificationDispatcherMetadata.create(KEY)
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
}

@Override
public void dispatch(Notification notification, Context context) {
String projectKey = notification.getFieldValue("projectKey");
Multimap<String, NotificationChannel> subscribedRecipients = notificationManager.findNotificationSubscribers(this, projectKey);

// See available fields in the class IssueNotifications.

// All the following users can be null
String changeAuthor = notification.getFieldValue("changeAuthor");
String reporter = notification.getFieldValue("reporter");
String assignee = notification.getFieldValue("assignee");

if (!Objects.equal(changeAuthor, reporter)) {
addUserToContextIfSubscribed(context, reporter, subscribedRecipients);
}
if (!Objects.equal(changeAuthor, assignee)) {
addUserToContextIfSubscribed(context, assignee, subscribedRecipients);
}
}

private void addUserToContextIfSubscribed(Context context, @Nullable String user, Multimap<String, NotificationChannel> subscribedRecipients) {
if (user != null) {
Collection<NotificationChannel> channels = subscribedRecipients.get(user);
for (NotificationChannel channel : channels) {
context.addUser(user, channel);
}
}
}
}

+ 111
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/IssueChangesEmailTemplate.java View File

@@ -0,0 +1,111 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.issue.notification;

import org.apache.commons.lang.StringUtils;
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.plugins.emailnotifications.api.EmailMessage;
import org.sonar.plugins.emailnotifications.api.EmailTemplate;

import javax.annotation.Nullable;

/**
* Creates email message for notification "issue-changes".
*
* @since 3.6
*/
public class IssueChangesEmailTemplate extends EmailTemplate {

private final EmailSettings settings;
private final UserFinder userFinder;

public IssueChangesEmailTemplate(EmailSettings settings, UserFinder userFinder) {
this.settings = settings;
this.userFinder = userFinder;
}

@Override
public EmailMessage format(Notification notif) {
if (!"issue-changes".equals(notif.getType())) {
return null;
}
String issueKey = notif.getFieldValue("key");
String author = notif.getFieldValue("changeAuthor");

StringBuilder sb = new StringBuilder();
appendField(sb, "Project", null, notif.getFieldValue("projectName"));
appendField(sb, "Component", null, StringUtils.defaultString(notif.getFieldValue("componentName"), notif.getFieldValue("componentKey")));
appendField(sb, "Rule", null, notif.getFieldValue("ruleName"));
appendField(sb, "Message", null, notif.getFieldValue("message"));
sb.append('\n');

appendField(sb, "Assignee", notif.getFieldValue("old.assignee"), notif.getFieldValue("new.assignee"));
appendField(sb, "Severity", notif.getFieldValue("old.severity"), notif.getFieldValue("new.severity"));
appendField(sb, "Resolution", notif.getFieldValue("old.resolution"), notif.getFieldValue("new.resolution"));
appendField(sb, "Status", notif.getFieldValue("old.status"), notif.getFieldValue("new.status"));
appendField(sb, "Message", notif.getFieldValue("old.message"), notif.getFieldValue("new.message"));

appendFooter(sb, notif);

EmailMessage message = new EmailMessage()
.setMessageId("issue-changes/" + issueKey)
.setSubject("Issue #" + issueKey)
.setMessage(sb.toString());
if (author != null) {
message.setFrom(getUserFullName(author));
}
return message;
}

private void appendField(StringBuilder sb, String name, @Nullable String oldValue, @Nullable String newValue) {
if (oldValue != null || newValue != null) {
sb.append(name).append(": ");
if (newValue != null) {
sb.append(newValue);
}
if (oldValue != null) {
sb.append(" (was ").append(oldValue).append(")");
}
sb.append('\n');
}
}

private void appendFooter(StringBuilder sb, Notification notification) {
String issueKey = notification.getFieldValue("key");
sb.append("\n")
.append("See it in Sonar: ").append(settings.getServerBaseURL()).append("/issue/show/").append(issueKey).append('\n');
}

private String getUserFullName(@Nullable String login) {
if (login == null) {
return null;
}
User user = userFinder.findByLogin(login);
if (user == null) {
// most probably user was deleted
return login;
}
return StringUtils.defaultIfBlank(user.name(), login);
}

}

plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/notifications/reviews/NewFalsePositiveReview.java → plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/NewFalsePositiveNotificationDispatcher.java View File

@@ -18,50 +18,61 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package org.sonar.plugins.core.notifications.reviews;
package org.sonar.plugins.core.issue.notification;

import com.google.common.base.Objects;
import com.google.common.collect.Multimap;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.core.review.ReviewDto;
import org.sonar.api.issue.Issue;
import org.sonar.api.notifications.*;

import java.util.Collection;
import java.util.Map;

/**
* This dispatcher means: "notify me when someone resolve a review as false positive".
*
* This dispatcher means: "notify me when someone resolves an issue as false positive".
*
* @since 3.6
*/
public class NewFalsePositiveReview extends NotificationDispatcher {
public class NewFalsePositiveNotificationDispatcher extends NotificationDispatcher {

public static final String KEY = "NewFalsePositiveIssue";

private NotificationManager notificationManager;
private final NotificationManager notifications;

public NewFalsePositiveNotificationDispatcher(NotificationManager notifications) {
super("issue-changes");
this.notifications = notifications;
}

@Override
public String getKey() {
return KEY;
}

public NewFalsePositiveReview(NotificationManager notificationManager) {
super("review-changed");
this.notificationManager = notificationManager;
public static NotificationDispatcherMetadata newMetadata() {
return NotificationDispatcherMetadata.create(KEY)
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
}

@Override
public void dispatch(Notification notification, Context context) {
String newResolution = notification.getFieldValue("new.resolution");
if (StringUtils.equals(newResolution, ReviewDto.RESOLUTION_FALSE_POSITIVE)) {
String author = notification.getFieldValue("author");
int projectId = Integer.parseInt(notification.getFieldValue("projectId"));
Multimap<String, NotificationChannel> subscribedRecipients = notificationManager.findSubscribedRecipientsForDispatcher(this, projectId);
if (Objects.equal(newResolution, Issue.RESOLUTION_FALSE_POSITIVE)) {
String author = notification.getFieldValue("changeAuthor");
String projectKey = notification.getFieldValue("projectKey");
Multimap<String, NotificationChannel> subscribedRecipients = notifications.findNotificationSubscribers(this, projectKey);
notify(author, context, subscribedRecipients);
}
}

private void notify(String author, Context context, Multimap<String, NotificationChannel> subscribedRecipients) {
for (Map.Entry<String, Collection<NotificationChannel>> channelsByRecipients : subscribedRecipients.asMap().entrySet()) {
String userLogin = channelsByRecipients.getKey();
if (!StringUtils.equals(author, userLogin)) {
String login = channelsByRecipients.getKey();
// Do not notify the person that resolved the issue
if (!Objects.equal(author, login)) {
for (NotificationChannel channel : channelsByRecipients.getValue()) {
context.addUser(userLogin, channel);
context.addUser(login, channel);
}
}
}

plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/NewIssuesEmailTemplate.java → plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/NewIssuesEmailTemplate.java View File

@@ -17,16 +17,20 @@
* 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.plugins.core.issue;
package org.sonar.plugins.core.issue.notification;

import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification;
import org.sonar.api.utils.DateUtils;
import org.sonar.plugins.emailnotifications.api.EmailMessage;
import org.sonar.plugins.emailnotifications.api.EmailTemplate;

import java.net.URLEncoder;
import java.util.Date;

/**
* Creates email message for notification "new-violations".
*
* Creates email message for notification "new-issues".
*
* @since 2.10
*/
public class NewIssuesEmailTemplate extends EmailTemplate {
@@ -51,18 +55,22 @@ public class NewIssuesEmailTemplate extends EmailTemplate {
appendFooter(sb, notification);

EmailMessage message = new EmailMessage()
.setMessageId("new-issues/" + notification.getFieldValue("projectId"))
.setSubject("New issues for project " + projectName)
.setMessage(sb.toString());
.setMessageId("new-issues/" + notification.getFieldValue("projectKey"))
.setSubject("New issues for project " + projectName)
.setMessage(sb.toString());

return message;
}

private void appendFooter(StringBuilder sb, Notification notification) {
String projectKey = notification.getFieldValue("projectKey");
String dateString = notification.getFieldValue("projectDate");
Date date = DateUtils.parseDateTime(dateString);
String url = String.format("%s/issues/search?componentRoots=%s&createdAfter=%s", settings.getServerBaseURL(), URLEncoder.encode(projectKey), DateUtils.formatDate(date));
sb.append("\n")
.append("See it in Sonar: ").append(settings.getServerBaseURL()).append("/drilldown/measures/").append(projectKey)
.append("?metric=new_violations\n");
.append("See it in Sonar: ")
.append(url)
.append("\n");
}

}

plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/NewIssuesNotificationDispatcher.java → plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/NewIssuesNotificationDispatcher.java View File

@@ -17,24 +17,22 @@
* 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.plugins.core.issue;
package org.sonar.plugins.core.issue.notification;

import com.google.common.collect.Multimap;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.notifications.*;

import java.util.Collection;
import java.util.Map;

/**
* This dispatcher means: "notify me when new issues are introduced during project scan".
*
*
* @since 2.14
*/
public class NewIssuesNotificationDispatcher extends NotificationDispatcher {

public static final String KEY = "NewIssues";
private final NotificationManager manager;

public NewIssuesNotificationDispatcher(NotificationManager manager) {
@@ -42,10 +40,21 @@ public class NewIssuesNotificationDispatcher extends NotificationDispatcher {
this.manager = manager;
}

@Override
public String getKey() {
return KEY;
}

public static NotificationDispatcherMetadata newMetadata() {
return NotificationDispatcherMetadata.create(KEY)
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
}

@Override
public void dispatch(Notification notification, Context context) {
int projectId = Integer.parseInt(notification.getFieldValue("projectId"));
Multimap<String, NotificationChannel> subscribedRecipients = manager.findSubscribedRecipientsForDispatcher(this, projectId);
String projectKey = notification.getFieldValue("projectKey");
Multimap<String, NotificationChannel> subscribedRecipients = manager.findNotificationSubscribers(this, projectKey);

for (Map.Entry<String, Collection<NotificationChannel>> channelsByRecipients : subscribedRecipients.asMap().entrySet()) {
String userLogin = channelsByRecipients.getKey();

plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/NewIssuesNotificationPostJob.java → plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/SendIssueNotificationsPostJob.java View File

@@ -17,27 +17,31 @@
* 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.plugins.core.issue;
package org.sonar.plugins.core.issue.notification;

import org.sonar.api.batch.PostJob;
import org.sonar.api.batch.SensorContext;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.resources.Project;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.RuleFinder;
import org.sonar.batch.issue.IssueCache;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.issue.IssueNotifications;

/**
* @since 3.6
*/
public class NewIssuesNotificationPostJob implements PostJob {
public class SendIssueNotificationsPostJob implements PostJob {

private final IssueCache issueCache;
private final NotificationManager notifications;
private final IssueNotifications notifications;
private final RuleFinder ruleFinder;

public NewIssuesNotificationPostJob(IssueCache issueCache, NotificationManager notifications) {
public SendIssueNotificationsPostJob(IssueCache issueCache, IssueNotifications notifications, RuleFinder ruleFinder) {
this.issueCache = issueCache;
this.notifications = notifications;
this.ruleFinder = ruleFinder;
}

@Override
@@ -49,20 +53,18 @@ public class NewIssuesNotificationPostJob implements PostJob {

private void sendNotifications(Project project) {
int newIssues = 0;
IssueChangeContext context = IssueChangeContext.createScan(project.getAnalysisDate());
for (DefaultIssue issue : issueCache.all()) {
if (issue.isNew()) {
if (issue.isNew() && issue.resolution() == null) {
newIssues++;
}
if (issue.isChanged() && issue.diffs() != null) {
Rule rule = ruleFinder.findByKey(issue.ruleKey());
notifications.sendChanges(issue, context, rule, project, null);
}
}
if (newIssues > 0) {
Notification notification = new Notification("new-issues")
.setDefaultMessage(newIssues + " new issues on " + project.getLongName() + ".")
.setFieldValue("count", String.valueOf(newIssues))
.setFieldValue("projectName", project.getLongName())
.setFieldValue("projectKey", project.getKey())
.setFieldValue("projectId", String.valueOf(project.getId()));
notifications.scheduleForSending(notification);
notifications.sendNewIssues(project, newIssues);
}
}

}

+ 23
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/notification/package-info.java View File

@@ -0,0 +1,23 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.
*/
@ParametersAreNonnullByDefault
package org.sonar.plugins.core.issue.notification;

import javax.annotation.ParametersAreNonnullByDefault;

+ 18
- 9
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/notifications/alerts/NewAlerts.java View File

@@ -20,32 +20,41 @@
package org.sonar.plugins.core.notifications.alerts;

import com.google.common.collect.Multimap;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.notifications.*;

import java.util.Collection;
import java.util.Map;

/**
* This dispatcher means: "notify me each new alert event".
*
*
* @since 3.5
*/
public class NewAlerts extends NotificationDispatcher {

private NotificationManager notificationManager;
public static final String KEY = "NewAlerts";
private final NotificationManager notifications;

public NewAlerts(NotificationManager notificationManager) {
public NewAlerts(NotificationManager notifications) {
super("alerts");
this.notificationManager = notificationManager;
this.notifications = notifications;
}

@Override
public String getKey() {
return KEY;
}

public static NotificationDispatcherMetadata newMetadata() {
return NotificationDispatcherMetadata.create(KEY)
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
}

@Override
public void dispatch(Notification notification, Context context) {
int projectId = Integer.parseInt(notification.getFieldValue("projectId"));
Multimap<String, NotificationChannel> subscribedRecipients = notificationManager.findSubscribedRecipientsForDispatcher(this, projectId);
Multimap<String, NotificationChannel> subscribedRecipients = notifications.findSubscribedRecipientsForDispatcher(this, projectId);

for (Map.Entry<String, Collection<NotificationChannel>> channelsByRecipients : subscribedRecipients.asMap().entrySet()) {
String userLogin = channelsByRecipients.getKey();

+ 0
- 72
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/notifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMe.java View File

@@ -1,72 +0,0 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.notifications.reviews;

import com.google.common.collect.Multimap;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.api.notifications.NotificationManager;

import java.util.Collection;

/**
* This dispatcher means: "notify me when someone changes review assigned to me or created by me".
*
* @since 2.10
*/
public class ChangesInReviewAssignedToMeOrCreatedByMe extends NotificationDispatcher {

private NotificationManager notificationManager;

public ChangesInReviewAssignedToMeOrCreatedByMe(NotificationManager notificationManager) {
super("review-changed");
this.notificationManager = notificationManager;
}

@Override
public void dispatch(Notification notification, Context context) {
int projectId = Integer.parseInt(notification.getFieldValue("projectId"));
Multimap<String, NotificationChannel> subscribedRecipients = notificationManager.findSubscribedRecipientsForDispatcher(this, projectId);

String author = notification.getFieldValue("author");
String creator = notification.getFieldValue("creator");
String oldAssignee = notification.getFieldValue("old.assignee");
String assignee = notification.getFieldValue("assignee");
if (creator != null && !StringUtils.equals(author, creator)) {
addUserToContextIfSubscribed(context, creator, subscribedRecipients);
}
if (oldAssignee != null && !StringUtils.equals(author, oldAssignee)) {
addUserToContextIfSubscribed(context, oldAssignee, subscribedRecipients);
}
if (assignee != null && !StringUtils.equals(author, assignee)) {
addUserToContextIfSubscribed(context, assignee, subscribedRecipients);
}
}

private void addUserToContextIfSubscribed(Context context, String user, Multimap<String, NotificationChannel> subscribedRecipients) {
Collection<NotificationChannel> channels = subscribedRecipients.get(user);
for (NotificationChannel channel : channels) {
context.addUser(user, channel);
}
}

}

+ 0
- 58
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/notifications/violations/NewViolationsOnFirstDifferentialPeriod.java View File

@@ -1,58 +0,0 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.notifications.violations;

import com.google.common.collect.Multimap;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.api.notifications.NotificationManager;

import java.util.Collection;
import java.util.Map;

/**
* This dispatcher means: "notify me when new violations are introduced during the first differential period".
*
* @since 2.14
*/
public class NewViolationsOnFirstDifferentialPeriod extends NotificationDispatcher {

private NotificationManager notificationManager;

public NewViolationsOnFirstDifferentialPeriod(NotificationManager notificationManager) {
super("new-violations");
this.notificationManager = notificationManager;
}

@Override
public void dispatch(Notification notification, Context context) {
int projectId = Integer.parseInt(notification.getFieldValue("projectId"));
Multimap<String, NotificationChannel> subscribedRecipients = notificationManager.findSubscribedRecipientsForDispatcher(this, projectId);

for (Map.Entry<String, Collection<NotificationChannel>> channelsByRecipients : subscribedRecipients.asMap().entrySet()) {
String userLogin = channelsByRecipients.getKey();
for (NotificationChannel channel : channelsByRecipients.getValue()) {
context.addUser(userLogin, channel);
}
}
}

}

+ 2
- 2
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1591,10 +1591,10 @@ server_id_configuration.does_not_match_organisation_pattern=Organisation does no
#
#------------------------------------------------------------------------------
notification.channel.EmailNotificationChannel=Email
notification.dispatcher.ChangesInReviewAssignedToMeOrCreatedByMe=Changes in reviews assigned to me or created by me
notification.dispatcher.ChangesOnMyIssue=Changes in issues assigned to me or reported by me
notification.dispatcher.NewIssues=New issues
notification.dispatcher.NewAlerts=New alerts
notification.dispatcher.NewFalsePositiveReview=New false positive issue
notification.dispatcher.NewFalsePositiveIssue=New false positive issue


#------------------------------------------------------------------------------

+ 115
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java View File

@@ -0,0 +1,115 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.issue.notification;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.sonar.api.notifications.*;

import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class ChangesOnMyIssueNotificationDispatcherTest {

@Mock
NotificationManager notifications;

@Mock
NotificationDispatcher.Context context;

@Mock
NotificationChannel emailChannel;

@Mock
NotificationChannel twitterChannel;

ChangesOnMyIssueNotificationDispatcher dispatcher;

@Before
public void setUp() {
dispatcher = new ChangesOnMyIssueNotificationDispatcher(notifications);
}

@Test
public void test_metadata() throws Exception {
NotificationDispatcherMetadata metadata = ChangesOnMyIssueNotificationDispatcher.newMetadata();
assertThat(metadata.getDispatcherKey()).isEqualTo(dispatcher.getKey());
assertThat(metadata.getProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION)).isEqualTo("true");
assertThat(metadata.getProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION)).isEqualTo("true");
}

@Test
public void should_not_dispatch_if_other_notification_type() throws Exception {
Notification notification = new Notification("other-notif");
dispatcher.performDispatch(notification, context);

verify(context, never()).addUser(any(String.class), any(NotificationChannel.class));
}

@Test
public void should_dispatch_to_reporter_and_assignee() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("simon", emailChannel);
recipients.put("freddy", twitterChannel);
recipients.put("godin", twitterChannel);
when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients);

Notification notification = new Notification("issue-changes").setFieldValue("projectKey", "struts")
.setFieldValue("changeAuthor", "olivier")
.setFieldValue("reporter", "simon")
.setFieldValue("assignee", "freddy");
dispatcher.performDispatch(notification, context);

verify(context).addUser("simon", emailChannel);
verify(context).addUser("freddy", twitterChannel);
verify(context, never()).addUser("godin", twitterChannel);
verifyNoMoreInteractions(context);
}

@Test
public void should_not_dispatch_to_author_of_changes() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("simon", emailChannel);
recipients.put("freddy", twitterChannel);
recipients.put("godin", twitterChannel);
when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients);

// change author is the reporter
dispatcher.performDispatch(new Notification("issue-changes").setFieldValue("projectKey", "struts")
.setFieldValue("changeAuthor", "simon").setFieldValue("reporter", "simon"), context);

// change author is the assignee
dispatcher.performDispatch(new Notification("issue-changes").setFieldValue("projectKey", "struts")
.setFieldValue("changeAuthor", "simon").setFieldValue("assignee", "simon"), context);

// no change author
dispatcher.performDispatch(new Notification("issue-changes").setFieldValue("projectKey", "struts")
.setFieldValue("new.resolution", "FIXED"), context);

verifyNoMoreInteractions(context);
}
}

+ 98
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/IssueChangesEmailTemplateTest.java View File

@@ -0,0 +1,98 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.issue.notification;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
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.plugins.emailnotifications.api.EmailMessage;

import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class IssueChangesEmailTemplateTest {

@Mock
UserFinder userFinder;

IssueChangesEmailTemplate template;

@Before
public void setUp() {
EmailSettings settings = mock(EmailSettings.class);
when(settings.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org");
template = new IssueChangesEmailTemplate(settings, userFinder);
}

@Test
public void should_ignore_non_issue_changes() {
Notification notification = new Notification("other");
EmailMessage message = template.format(notification);
assertThat(message).isNull();
}

@Test
public void should_format_change() {
Notification notification = new Notification("issue-changes")
.setFieldValue("projectName", "Struts")
.setFieldValue("projectKey", "org.apache:struts")
.setFieldValue("key", "ABCDE")
.setFieldValue("ruleName", "Avoid Cycles")
.setFieldValue("message", "Has 3 cycles")
.setFieldValue("old.assignee", "simon")
.setFieldValue("new.assignee", "louis")
.setFieldValue("new.resolution", "FALSE-POSITIVE")
.setFieldValue("new.status", "RESOLVED");

EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("issue-changes/ABCDE");
assertThat(message.getSubject()).isEqualTo("Issue #ABCDE");
assertThat(message.getMessage()).contains("Rule: Avoid Cycles");
assertThat(message.getMessage()).contains("Message: Has 3 cycles");
assertThat(message.getMessage()).contains("Assignee: louis (was simon)");
assertThat(message.getMessage()).contains("Resolution: FALSE-POSITIVE");
assertThat(message.getMessage()).contains("See it in Sonar: http://nemo.sonarsource.org/issue/show/ABCDE");
assertThat(message.getFrom()).isNull();
}

@Test
public void notification_sender_should_be_the_author_of_change() {
User user = mock(User.class);
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");

EmailMessage message = template.format(notification);
assertThat(message.getFrom()).isEqualTo("Simon");
}

}

+ 95
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/NewFalsePositiveNotificationDispatcherTest.java View File

@@ -0,0 +1,95 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.issue.notification;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.sonar.api.notifications.*;

import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class NewFalsePositiveNotificationDispatcherTest {
@Mock
NotificationManager notifications;

@Mock
NotificationDispatcher.Context context;

@Mock
NotificationChannel emailChannel;

@Mock
NotificationChannel twitterChannel;

NewFalsePositiveNotificationDispatcher dispatcher;

@Before
public void setUp() {
dispatcher = new NewFalsePositiveNotificationDispatcher(notifications);
}

@Test
public void test_metadata() throws Exception {
NotificationDispatcherMetadata metadata = NewFalsePositiveNotificationDispatcher.newMetadata();
assertThat(metadata.getDispatcherKey()).isEqualTo(dispatcher.getKey());
assertThat(metadata.getProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION)).isEqualTo("true");
assertThat(metadata.getProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION)).isEqualTo("true");
}

@Test
public void should_not_dispatch_if_other_notification_type() throws Exception {
Notification notification = new Notification("other");
dispatcher.performDispatch(notification, context);

verify(context, never()).addUser(any(String.class), any(NotificationChannel.class));
}

@Test
public void should_dispatch_to_subscribers() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("simon", emailChannel);
recipients.put("freddy", twitterChannel);
recipients.put("godin", twitterChannel);
when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients);

Notification notification = new Notification("issue-changes").setFieldValue("projectKey", "struts")
.setFieldValue("changeAuthor", "godin")
.setFieldValue("new.resolution", "FALSE-POSITIVE")
.setFieldValue("assignee", "freddy");
dispatcher.performDispatch(notification, context);

verify(context).addUser("simon", emailChannel);
verify(context).addUser("freddy", twitterChannel);
// do not notify the person who flagged the issue as false-positive
verify(context, never()).addUser("godin", twitterChannel);
verifyNoMoreInteractions(context);
}

}

plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/NewIssuesEmailTemplateTest.java → plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/NewIssuesEmailTemplateTest.java View File

@@ -17,7 +17,7 @@
* 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.plugins.core.issue;
package org.sonar.plugins.core.issue.notification;

import org.junit.Before;
import org.junit.Test;
@@ -25,9 +25,7 @@ import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification;
import org.sonar.plugins.emailnotifications.api.EmailMessage;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@@ -46,7 +44,7 @@ public class NewIssuesEmailTemplateTest {
public void shouldNotFormatIfNotCorrectNotification() {
Notification notification = new Notification("other-notif");
EmailMessage message = template.format(notification);
assertThat(message, nullValue());
assertThat(message).isNull();
}

/**
@@ -64,18 +62,18 @@ public class NewIssuesEmailTemplateTest {
public void shouldFormatCommentAdded() {
Notification notification = new Notification("new-issues")
.setFieldValue("count", "32")
.setFieldValue("projectName", "Foo")
.setFieldValue("projectKey", "org.sonar.foo:foo")
.setFieldValue("projectId", "45");
.setFieldValue("projectName", "Struts")
.setFieldValue("projectKey", "org.apache:struts")
.setFieldValue("projectDate", "2010-05-18T15:50:45+0100");

EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("new-issues/45"));
assertThat(message.getSubject(), is("New issues for project Foo"));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts");
assertThat(message.getSubject()).isEqualTo("New issues for project Struts");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Struts\n" +
"32 new issues\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/drilldown/measures/org.sonar.foo:foo?metric=new_violations\n"));
"See it in Sonar: http://nemo.sonarsource.org/issues/search?componentRoots=org.apache%3Astruts&createdAfter=2010-05-18\n");
}

}

plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/NewIssuesNotificationDispatcherTest.java → plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/NewIssuesNotificationDispatcherTest.java View File

@@ -17,7 +17,7 @@
* 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.plugins.core.issue;
package org.sonar.plugins.core.issue.notification;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
@@ -68,9 +68,9 @@ public class NewIssuesNotificationDispatcherTest {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("user1", emailChannel);
recipients.put("user2", twitterChannel);
when(notifications.findSubscribedRecipientsForDispatcher(dispatcher, 34)).thenReturn(recipients);
when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients);

Notification notification = new Notification("new-issues").setFieldValue("projectId", "34");
Notification notification = new Notification("new-issues").setFieldValue("projectKey", "struts");
dispatcher.performDispatch(notification, context);

verify(context).addUser("user1", emailChannel);

plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/NewIssuesNotificationPostJobTest.java → plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/SendIssueNotificationsPostJobTest.java View File

@@ -17,27 +17,26 @@
* 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.plugins.core.issue;
package org.sonar.plugins.core.issue.notification;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.sonar.api.batch.SensorContext;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.resources.Project;
import org.sonar.api.rules.RuleFinder;
import org.sonar.api.utils.DateUtils;
import org.sonar.batch.issue.IssueCache;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueNotifications;

import java.util.Arrays;

import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class NewIssuesNotificationPostJobTest {
public class SendIssueNotificationsPostJobTest {
@Mock
Project project;

@@ -45,7 +44,10 @@ public class NewIssuesNotificationPostJobTest {
IssueCache issueCache;

@Mock
NotificationManager notifications;
IssueNotifications notifications;

@Mock
RuleFinder ruleFinder;

@Mock
SensorContext sensorContext;
@@ -53,41 +55,38 @@ public class NewIssuesNotificationPostJobTest {
@Test
public void should_not_send_notif_if_past_scan() throws Exception {
when(project.isLatestAnalysis()).thenReturn(false);
when(project.getAnalysisDate()).thenReturn(DateUtils.parseDate("2013-05-18"));

NewIssuesNotificationPostJob job = new NewIssuesNotificationPostJob(issueCache, notifications);
SendIssueNotificationsPostJob job = new SendIssueNotificationsPostJob(issueCache, notifications, ruleFinder);
job.executeOn(project, sensorContext);

verifyZeroInteractions(notifications, issueCache, sensorContext);
verifyZeroInteractions(notifications, issueCache, ruleFinder, sensorContext);
}

@Test
public void should_send_notif_if_new_issues() throws Exception {
when(project.isLatestAnalysis()).thenReturn(true);
when(project.getAnalysisDate()).thenReturn(DateUtils.parseDate("2013-05-18"));
when(issueCache.all()).thenReturn(Arrays.asList(
new DefaultIssue().setNew(true),
new DefaultIssue().setNew(false)
));

NewIssuesNotificationPostJob job = new NewIssuesNotificationPostJob(issueCache, notifications);
SendIssueNotificationsPostJob job = new SendIssueNotificationsPostJob(issueCache, notifications, ruleFinder);
job.executeOn(project, sensorContext);

verify(notifications).scheduleForSending(argThat(new ArgumentMatcher<Notification>() {
@Override
public boolean matches(Object o) {
Notification n = (Notification) o;
return n.getType().equals("new-issues") && n.getFieldValue("count").equals("1");
}
}));
verify(notifications).sendNewIssues(project, 1);
}

@Test
public void should_not_send_notif_if_no_new_issues() throws Exception {
when(project.isLatestAnalysis()).thenReturn(true);
when(project.getAnalysisDate()).thenReturn(DateUtils.parseDate("2013-05-18"));
when(issueCache.all()).thenReturn(Arrays.asList(
new DefaultIssue().setNew(false)
));

NewIssuesNotificationPostJob job = new NewIssuesNotificationPostJob(issueCache, notifications);
SendIssueNotificationsPostJob job = new SendIssueNotificationsPostJob(issueCache, notifications, ruleFinder);
job.executeOn(project, sensorContext);

verifyZeroInteractions(notifications);

+ 0
- 118
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/notifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMeTest.java View File

@@ -1,118 +0,0 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.notifications.reviews;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.api.notifications.NotificationManager;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

public class ChangesInReviewAssignedToMeOrCreatedByMeTest {

@Mock
private NotificationManager notificationManager;

@Mock
private NotificationDispatcher.Context context;

@Mock
private NotificationChannel emailChannel;

@Mock
private NotificationChannel twitterChannel;

private ChangesInReviewAssignedToMeOrCreatedByMe dispatcher;

@Before
public void before() {
MockitoAnnotations.initMocks(this);

dispatcher = new ChangesInReviewAssignedToMeOrCreatedByMe(notificationManager);
}

@Test
public void should_not_dispatch_if_not_new_violations_notification() throws Exception {
Notification notification = new Notification("other-notif");
dispatcher.performDispatch(notification, context);

verify(context, never()).addUser(any(String.class), any(NotificationChannel.class));
}

@Test
public void should_dispatch_to_creator_and_assignee() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("simon", emailChannel);
recipients.put("freddy", twitterChannel);
recipients.put("godin", twitterChannel);
when(notificationManager.findSubscribedRecipientsForDispatcher(dispatcher, 42)).thenReturn(recipients);

Notification notification = new Notification("review-changed")
.setFieldValue("projectId", "42")
.setFieldValue("author", "olivier")
.setFieldValue("creator", "simon")
.setFieldValue("old.assignee", "godin")
.setFieldValue("assignee", "freddy");
dispatcher.performDispatch(notification, context);

verify(context).addUser("simon", emailChannel);
verify(context).addUser("freddy", twitterChannel);
verify(context).addUser("godin", twitterChannel);
verifyNoMoreInteractions(context);
}

@Test
public void should_not_dispatch_to_author_of_changes() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("simon", emailChannel);
recipients.put("freddy", twitterChannel);
recipients.put("godin", twitterChannel);
when(notificationManager.findSubscribedRecipientsForDispatcher(dispatcher, 42)).thenReturn(recipients);

dispatcher.performDispatch(new Notification("review-changed").setFieldValue("projectId", "42")
.setFieldValue("author", "simon").setFieldValue("creator", "simon"), context);
dispatcher.performDispatch(new Notification("review-changed").setFieldValue("projectId", "42")
.setFieldValue("author", "simon").setFieldValue("assignee", "simon"), context);
dispatcher.performDispatch(new Notification("review-changed").setFieldValue("projectId", "42")
.setFieldValue("author", "simon").setFieldValue("old.assignee", "simon"), context);

verifyNoMoreInteractions(context);
}

@Test
public void should_not_dispatch_when_other_notification_type() {
Notification notification = new Notification("other");
dispatcher.performDispatch(notification, context);

verifyNoMoreInteractions(context);
}

}

+ 0
- 120
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/notifications/reviews/NewFalsePositiveReviewTest.java View File

@@ -1,120 +0,0 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.notifications.reviews;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.api.notifications.NotificationManager;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

public class NewFalsePositiveReviewTest {

@Mock
private NotificationManager notificationManager;

@Mock
private NotificationDispatcher.Context context;

@Mock
private NotificationChannel emailChannel;

@Mock
private NotificationChannel twitterChannel;

private NotificationDispatcher dispatcher;

@Before
public void before() {
MockitoAnnotations.initMocks(this);

dispatcher = new NewFalsePositiveReview(notificationManager);
}

@Test
public void should_not_dispatch_if_not_reviews_notification() throws Exception {
Notification notification = new Notification("other-notif");
dispatcher.performDispatch(notification, context);

verify(context, never()).addUser(any(String.class), any(NotificationChannel.class));
}

@Test
public void should_dispatch_false_positive_resolution_to_every_subscribers() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("user1", emailChannel);
recipients.put("user2", twitterChannel);
when(notificationManager.findSubscribedRecipientsForDispatcher(dispatcher, 42)).thenReturn(recipients);

dispatcher.performDispatch(new Notification("review-changed")
.setFieldValue("projectId", "42")
.setFieldValue("author", "user3")
.setFieldValue("new.resolution", "FALSE-POSITIVE"),
context);

verify(context).addUser("user1", emailChannel);
verify(context).addUser("user2", twitterChannel);
verifyNoMoreInteractions(context);
}

@Test
public void should_not_dispatch_to_author_of_changes() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("user1", emailChannel);
recipients.put("user2", twitterChannel);
when(notificationManager.findSubscribedRecipientsForDispatcher(dispatcher, 42)).thenReturn(recipients);

dispatcher.performDispatch(new Notification("review-changed")
.setFieldValue("projectId", "42")
.setFieldValue("author", "user1")
.setFieldValue("new.resolution", "FALSE-POSITIVE"),
context);

verify(context).addUser("user2", twitterChannel);
verify(context, never()).addUser("user1", emailChannel);
verifyNoMoreInteractions(context);
}

@Test
public void should_not_dispatch_other_than_false_positive_resolution() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("user", emailChannel);
when(notificationManager.findSubscribedRecipientsForDispatcher(dispatcher, 42)).thenReturn(recipients);

dispatcher.performDispatch(new Notification("review-changed")
.setFieldValue("projectId", "42")
.setFieldValue("author", "user2")
.setFieldValue("new.assignee", "user"), context);

verify(context, never()).addUser(any(String.class), any(NotificationChannel.class));
}
}

+ 0
- 84
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/notifications/violations/NewViolationsOnFirstDifferentialPeriodTest.java View File

@@ -1,84 +0,0 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.notifications.violations;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.api.notifications.NotificationManager;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

public class NewViolationsOnFirstDifferentialPeriodTest {

@Mock
private NotificationManager notificationManager;

@Mock
private NotificationDispatcher.Context context;

@Mock
private NotificationChannel emailChannel;

@Mock
private NotificationChannel twitterChannel;

private NewViolationsOnFirstDifferentialPeriod dispatcher;

@Before
public void init() {
MockitoAnnotations.initMocks(this);
dispatcher = new NewViolationsOnFirstDifferentialPeriod(notificationManager);
}

@Test
public void shouldNotDispatchIfNotNewViolationsNotification() throws Exception {
Notification notification = new Notification("other-notif");
dispatcher.performDispatch(notification, context);

verify(context, never()).addUser(any(String.class), any(NotificationChannel.class));
}

@Test
public void shouldDispatchToUsersWhoHaveSubscribedAndFlaggedProjectAsFavourite() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("user1", emailChannel);
recipients.put("user2", twitterChannel);
when(notificationManager.findSubscribedRecipientsForDispatcher(dispatcher, 34)).thenReturn(recipients);

Notification notification = new Notification("new-violations").setFieldValue("projectId", "34");
dispatcher.performDispatch(notification, context);

verify(context).addUser("user1", emailChannel);
verify(context).addUser("user2", twitterChannel);
verifyNoMoreInteractions(context);
}

}

+ 0
- 2
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java View File

@@ -22,7 +22,6 @@ package org.sonar.plugins.emailnotifications;
import com.google.common.collect.ImmutableList;
import org.sonar.api.SonarPlugin;
import org.sonar.plugins.emailnotifications.templates.alerts.AlertsEmailTemplate;
import org.sonar.plugins.emailnotifications.templates.reviews.ReviewEmailTemplate;

import java.util.List;

@@ -32,7 +31,6 @@ public class EmailNotificationsPlugin extends SonarPlugin {
EmailNotificationChannel.class,

// Email templates
ReviewEmailTemplate.class,
AlertsEmailTemplate.class);
}
}

+ 0
- 128
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/templates/reviews/ReviewEmailTemplate.java View File

@@ -1,128 +0,0 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.emailnotifications.templates.reviews;

import org.apache.commons.lang.StringUtils;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.database.model.User;
import org.sonar.api.notifications.Notification;
import org.sonar.api.security.UserFinder;
import org.sonar.plugins.emailnotifications.api.EmailMessage;
import org.sonar.plugins.emailnotifications.api.EmailTemplate;

/**
* Creates email message for notification "review-changed".
*
* @since 2.10
*/
public class ReviewEmailTemplate extends EmailTemplate {

private EmailSettings configuration;
private UserFinder userFinder;

public ReviewEmailTemplate(EmailSettings configuration, UserFinder userFinder) {
this.configuration = configuration;
this.userFinder = userFinder;
}

@Override
public EmailMessage format(Notification notification) {
if (!"review-changed".equals(notification.getType())) {
return null;
}
String reviewId = notification.getFieldValue("reviewId");
String author = notification.getFieldValue("author");
StringBuilder sb = new StringBuilder();

append(sb, "Project", null, notification.getFieldValue("project"));
append(sb, "Resource", null, notification.getFieldValue("resource"));
sb.append('\n');
append(sb, null, null, notification.getFieldValue("title"));
sb.append('\n');
append(sb, "Status", notification.getFieldValue("old.status"), notification.getFieldValue("new.status"));
append(sb, "Resolution", notification.getFieldValue("old.resolution"), notification.getFieldValue("new.resolution"));
append(sb, "Assignee", getUserFullName(notification.getFieldValue("old.assignee")), getUserFullName(notification.getFieldValue("new.assignee")));
appendComment(sb, notification);
appendFooter(sb, notification);

EmailMessage message = new EmailMessage()
.setMessageId("review/" + reviewId)
.setSubject("Review #" + reviewId + ("FALSE-POSITIVE".equals(notification.getFieldValue("new.resolution")) ? " - False Positive" : ""))
.setMessage(sb.toString());
if (author != null) {
message.setFrom(getUserFullName(author));
}
return message;
}

private void append(StringBuilder sb, String name, String oldValue, String newValue) {
if (oldValue != null || newValue != null) {
if (name != null) {
sb.append(name).append(": ");
}
if (newValue != null) {
sb.append(newValue);
}
if (oldValue != null) {
sb.append(" (was ").append(oldValue).append(")");
}
sb.append('\n');
}
}

private void appendComment(StringBuilder sb, Notification notification) {
String newComment = notification.getFieldValue("new.comment");
String oldComment = notification.getFieldValue("old.comment");

if (newComment != null) {
// comment was added or modified
sb.append("Comment:\n ").append(newComment).append('\n');
if (oldComment != null) {
// comment was modified
sb.append("Was:\n ").append(oldComment).append('\n');
}
} else if (oldComment != null) {
// comment was deleted
sb.append("Comment deleted, was:\n ").append(oldComment).append('\n');
}
}

private void appendFooter(StringBuilder sb, Notification notification) {
String reviewId = notification.getFieldValue("reviewId");
sb.append("\n")
.append("See it in Sonar: ").append(configuration.getServerBaseURL()).append("/reviews/view/").append(reviewId).append('\n');
}

/**
* Visibility has been relaxed for tests.
*/
String getUserFullName(String login) {
if (login == null) {
return null;
}
User user = userFinder.findByLogin(login);
if (user == null) {
// most probably user was deleted
return login;
}
return StringUtils.defaultIfBlank(user.getName(), login);
}

}

+ 1
- 1
plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/EmailNotificationsPluginTest.java View File

@@ -26,6 +26,6 @@ import static org.fest.assertions.Assertions.assertThat;
public class EmailNotificationsPluginTest {
@Test
public void should_get_extensions() {
assertThat(new EmailNotificationsPlugin().getExtensions()).hasSize(3);
assertThat(new EmailNotificationsPlugin().getExtensions()).hasSize(2);
}
}

+ 0
- 489
plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/templates/reviews/ReviewEmailTemplateTest.java View File

@@ -1,489 +0,0 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.emailnotifications.templates.reviews;

import org.junit.Before;
import org.junit.Test;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.database.model.User;
import org.sonar.api.notifications.Notification;
import org.sonar.api.security.UserFinder;
import org.sonar.plugins.emailnotifications.api.EmailMessage;

import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ReviewEmailTemplateTest {

private ReviewEmailTemplate template;

@Before
public void setUp() {
EmailSettings configuration = mock(EmailSettings.class);
when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org");
UserFinder userFinder = mock(UserFinder.class);
when(userFinder.findByLogin(eq("freddy.mallet"))).thenReturn(new User().setName("Freddy Mallet"));
when(userFinder.findByLogin(eq("simon.brandhof"))).thenReturn(new User().setName("Simon Brandhof"));
when(userFinder.findByLogin(eq("evgeny.mandrikov"))).thenReturn(new User().setName("Evgeny Mandrikov"));
template = new ReviewEmailTemplate(configuration, userFinder);
}

/**
* <pre>
* Subject: Review #1
* From: Freddy Mallet
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Comment:
* This is my first comment
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_comment_added() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("author", "freddy.mallet")
.setFieldValue("old.comment", null)
.setFieldValue("new.comment", "This is my first comment");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isEqualTo("Freddy Mallet");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Comment:\n" +
" This is my first comment\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Freddy Mallet
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Comment:
* This is another comment
* Was:
* This is my first comment
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_commentedited() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("author", "freddy.mallet")
.setFieldValue("old.comment", "This is my first comment")
.setFieldValue("new.comment", "This is another comment");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isEqualTo("Freddy Mallet");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Comment:\n" +
" This is another comment\n" +
"Was:\n" +
" This is my first comment\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Freddy Mallet
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Comment deleted, was:
* This is deleted comment
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_comment_deleted() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("old.comment", "This is deleted comment")
.setFieldValue("new.comment", null)
.setFieldValue("author", "freddy.mallet");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isEqualTo("Freddy Mallet");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Comment deleted, was:\n" +
" This is deleted comment\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Freddy Mallet
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Assignee: Evgeny Mandrikov
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_assigneed() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("author", "freddy.mallet")
.setFieldValue("old.assignee", null)
.setFieldValue("new.assignee", "evgeny.mandrikov");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isEqualTo("Freddy Mallet");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Assignee: Evgeny Mandrikov\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Freddy Mallet
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Assignee: Simon Brandhof (was Evgeny Mandrikov)
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_assigneed_to_another_person() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("author", "freddy.mallet")
.setFieldValue("old.assignee", "evgeny.mandrikov")
.setFieldValue("new.assignee", "simon.brandhof");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isEqualTo("Freddy Mallet");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Assignee: Simon Brandhof (was Evgeny Mandrikov)\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Freddy Mallet
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Assignee: (was Simon Brandhof)
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_unassigned() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("author", "freddy.mallet")
.setFieldValue("old.assignee", "simon.brandhof")
.setFieldValue("new.assignee", null);
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isEqualTo("Freddy Mallet");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Assignee: (was Simon Brandhof)\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Sonar
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Status: CLOSED (was OPEN)
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_closed() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("old.status", "OPEN")
.setFieldValue("new.status", "CLOSED");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isNull();
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Status: CLOSED (was OPEN)\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Simon Brandhof
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Status: REOPENED (was RESOLVED)
* Resolution: (was FIXED)
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_reopened() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("old.resolution", "FIXED")
.setFieldValue("new.resolution", null)
.setFieldValue("old.status", "RESOLVED")
.setFieldValue("new.status", "REOPENED");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isNull();
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Status: REOPENED (was RESOLVED)\n" +
"Resolution: (was FIXED)\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Simon Brandhof
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Status: RESOLVED (was OPEN)
* Resolution: FIXED
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_resolved_as_fixed() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("author", "simon.brandhof")
.setFieldValue("old.status", "OPEN")
.setFieldValue("old.resolution", null)
.setFieldValue("new.status", "RESOLVED")
.setFieldValue("new.resolution", "FIXED");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1");
assertThat(message.getFrom()).isEqualTo("Simon Brandhof");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Status: RESOLVED (was OPEN)\n" +
"Resolution: FIXED\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

/**
* <pre>
* Subject: Review #1
* From: Simon Brandhof
*
* Project: Sonar
* Resource: org.sonar.server.ui.DefaultPages
*
* Utility classes should not have a public or default constructor.
*
* Status: RESOLVED (was REOPENED)
* Resolution: FALSE-POSITIVE
* Comment:
* Because!
*
* See it in Sonar: http://nemo.sonarsource.org/review/view/1
* </pre>
*/
@Test
public void should_format_resolved_as_false_positive() {
Notification notification = new Notification("review-changed")
.setFieldValue("reviewId", "1")
.setFieldValue("project", "Sonar")
.setFieldValue("resource", "org.sonar.server.ui.DefaultPages")
.setFieldValue("title", "Utility classes should not have a public or default constructor.")
.setFieldValue("author", "freddy.mallet")
.setFieldValue("old.status", "REOPENED")
.setFieldValue("old.resolution", null)
.setFieldValue("new.status", "RESOLVED")
.setFieldValue("new.resolution", "FALSE-POSITIVE")
.setFieldValue("new.comment", "Because!");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId()).isEqualTo("review/1");
assertThat(message.getSubject()).isEqualTo("Review #1 - False Positive");
assertThat(message.getFrom()).isEqualTo("Freddy Mallet");
assertThat(message.getMessage()).isEqualTo("" +
"Project: Sonar\n" +
"Resource: org.sonar.server.ui.DefaultPages\n" +
"\n" +
"Utility classes should not have a public or default constructor.\n" +
"\n" +
"Status: RESOLVED (was REOPENED)\n" +
"Resolution: FALSE-POSITIVE\n" +
"Comment:\n" +
" Because!\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/reviews/view/1\n");
}

@Test
public void should_not_format() {
Notification notification = new Notification("other");
EmailMessage message = template.format(notification);
assertThat(message).isNull();
}

@Test
public void should_return_full_name_or_login() {
assertThat(template.getUserFullName("freddy.mallet")).isEqualTo("Freddy Mallet");
assertThat(template.getUserFullName("deleted")).isEqualTo("deleted");
assertThat(template.getUserFullName(null)).isNull();
}

}

+ 2
- 0
sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java View File

@@ -43,6 +43,7 @@ import org.sonar.batch.scan.maven.MavenPluginExecutor;
import org.sonar.batch.source.HighlightableBuilder;
import org.sonar.batch.source.SymbolizableBuilder;
import org.sonar.core.component.ScanGraph;
import org.sonar.core.issue.IssueNotifications;
import org.sonar.core.issue.IssueUpdater;
import org.sonar.core.issue.workflow.FunctionExecutor;
import org.sonar.core.issue.workflow.IssueWorkflow;
@@ -99,6 +100,7 @@ public class ProjectScanContainer extends ComponentContainer {
IssueCache.class,
ScanIssueStorage.class,
IssuePersister.class,
IssueNotifications.class,

// tests
TestPlanPerspectiveLoader.class,

+ 105
- 0
sonar-core/src/main/java/org/sonar/core/issue/IssueNotifications.java View File

@@ -0,0 +1,105 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.core.issue;

import org.sonar.api.BatchComponent;
import org.sonar.api.ServerComponent;
import org.sonar.api.component.Component;
import org.sonar.api.issue.IssueQueryResult;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.resources.Project;
import org.sonar.api.rules.Rule;
import org.sonar.api.utils.DateUtils;
import org.sonar.core.i18n.RuleI18nManager;

import javax.annotation.Nullable;
import java.util.Locale;
import java.util.Map;

/**
* Send notifications related to issues.
*
* @since 3.6
*/
public class IssueNotifications implements BatchComponent, ServerComponent {

private final NotificationManager notificationsManager;
private final RuleI18nManager ruleI18n;

public IssueNotifications(NotificationManager notificationsManager, RuleI18nManager ruleI18n) {
this.notificationsManager = notificationsManager;
this.ruleI18n = ruleI18n;
}

public Notification sendNewIssues(Project project, int newIssues) {
Notification notification = newNotification(project, "new-issues")
.setDefaultMessage(newIssues + " new issues on " + project.getLongName() + ".")
.setFieldValue("projectDate", DateUtils.formatDateTime(project.getAnalysisDate()))
.setFieldValue("count", String.valueOf(newIssues));
notificationsManager.scheduleForSending(notification);
return notification;
}

public Notification sendChanges(DefaultIssue issue, IssueChangeContext context, IssueQueryResult queryResult) {
return sendChanges(issue, context, queryResult.rule(issue), queryResult.project(issue), queryResult.component(issue));
}

public Notification sendChanges(DefaultIssue issue, IssueChangeContext context, Rule rule, Component project, @Nullable Component component) {
Notification notification = newNotification(project, "issue-changes");
notification.setFieldValue("key", issue.key());
notification.setFieldValue("changeAuthor", context.login());
notification.setFieldValue("reporter", issue.reporter());
notification.setFieldValue("assignee", issue.assignee());
notification.setFieldValue("message", issue.message());
notification.setFieldValue("ruleName", ruleName(rule));
notification.setFieldValue("componentKey", issue.componentKey());
if (component != null) {
notification.setFieldValue("componentName", component.name());
}

for (Map.Entry<String, FieldDiffs.Diff> entry : issue.diffs().diffs().entrySet()) {
String type = entry.getKey();
FieldDiffs.Diff diff = entry.getValue();
notification.setFieldValue("old." + type, diff.oldValue() != null ? diff.oldValue().toString() : null);
notification.setFieldValue("new." + type, diff.newValue() != null ? diff.newValue().toString() : null);
}
notificationsManager.scheduleForSending(notification);
return notification;
}

private String ruleName(@Nullable Rule rule) {
// this code should definitely be shared in api
if (rule == null) {
return null;
}
String name = ruleI18n.getName(rule.getRepositoryKey(), rule.getKey(), Locale.ENGLISH);
if (name == null) {
name = rule.getName();
}
return name;
}

private Notification newNotification(Component project, String key) {
return new Notification(key)
.setFieldValue("projectName", project.longName())
.setFieldValue("projectKey", project.key());
}
}

+ 14
- 1
sonar-core/src/main/java/org/sonar/core/notification/DefaultNotificationManager.java View File

@@ -31,6 +31,7 @@ import org.sonar.api.notifications.NotificationManager;
import org.sonar.core.properties.PropertiesDao;
import org.sonar.jpa.session.DatabaseSessionFactory;

import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -97,7 +98,7 @@ public class DefaultNotificationManager implements NotificationManager {
/**
* {@inheritDoc}
*/
public Multimap<String, NotificationChannel> findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, Integer resourceId) {
public Multimap<String, NotificationChannel> findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, @Nullable Integer resourceId) {
String dispatcherKey = dispatcher.getKey();

SetMultimap<String, NotificationChannel> recipients = HashMultimap.create();
@@ -116,6 +117,18 @@ public class DefaultNotificationManager implements NotificationManager {
return recipients;
}

@Override
public Multimap<String, NotificationChannel> findNotificationSubscribers(NotificationDispatcher dispatcher, @Nullable String componentKey) {
String dispatcherKey = dispatcher.getKey();

SetMultimap<String, NotificationChannel> recipients = HashMultimap.create();
for (NotificationChannel channel : notificationChannels) {
addUsersToRecipientListForChannel(propertiesDao.findNotificationSubscribers(dispatcherKey, channel.getKey(), componentKey), recipients, channel);
}

return recipients;
}

@VisibleForTesting
protected List<NotificationChannel> getChannels() {
return Arrays.asList(notificationChannels);

+ 10
- 0
sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java View File

@@ -59,6 +59,16 @@ public class PropertiesDao implements BatchComponent, ServerComponent {
}
}

public List<String> findNotificationSubscribers(String notificationDispatcherKey, String notificationChannelKey, @Nullable String componentKey) {
SqlSession session = mybatis.openSession();
PropertiesMapper mapper = session.getMapper(PropertiesMapper.class);
try {
return mapper.findNotificationSubscribers("notification." + notificationDispatcherKey + "." + notificationChannelKey, componentKey);
} finally {
MyBatis.closeQuietly(session);
}
}

public List<PropertyDto> selectGlobalProperties() {
SqlSession session = mybatis.openSession();
PropertiesMapper mapper = session.getMapper(PropertiesMapper.class);

+ 2
- 0
sonar-core/src/main/java/org/sonar/core/properties/PropertiesMapper.java View File

@@ -29,6 +29,8 @@ public interface PropertiesMapper {

List<String> findUsersForNotification(@Param("notifKey") String notificationKey, @Nullable @Param("rId") Long resourceId);

List<String> findNotificationSubscribers(@Param("propKey") String propertyKey, @Nullable @Param("componentKey") String componentKey);

List<PropertyDto> selectGlobalProperties();

List<PropertyDto> selectProjectProperties(String resourceKey);

+ 12
- 0
sonar-core/src/main/resources/org/sonar/core/properties/PropertiesMapper.xml View File

@@ -15,6 +15,18 @@
</if>
</select>

<select id="findNotificationSubscribers" parameterType="map" resultType="String">
SELECT U.login
FROM properties P, users U
WHERE P.user_id = U.id AND P.prop_key = #{propKey} AND P.text_value LIKE 'true'
AND (
P.resource_id is null
<if test="componentKey != null">
OR P.resource_id in (select id from projects where kee=#{componentKey})
</if>
)
</select>

<select id="selectGlobalProperties" resultType="Property">
select p.id as id, p.prop_key as "key", p.text_value as value, p.resource_id as resourceId, p.user_id as userId
from properties p

+ 40
- 0
sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueQueryResultTest.java View File

@@ -0,0 +1,40 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.core.issue;

import org.junit.Test;
import org.sonar.api.issue.Issue;

import java.util.Arrays;

import static org.fest.assertions.Assertions.assertThat;

public class DefaultIssueQueryResultTest {
@Test
public void test_first_issue() {
DefaultIssueQueryResult result = new DefaultIssueQueryResult();
assertThat(result.first()).isNull();

Issue first = new DefaultIssue();
Issue second = new DefaultIssue();
result.setIssues(Arrays.asList(first, second));
assertThat(result.first()).isSameAs(first);
}
}

+ 93
- 0
sonar-core/src/test/java/org/sonar/core/issue/IssueNotificationsTest.java View File

@@ -0,0 +1,93 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.core.issue;

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.component.Component;
import org.sonar.api.issue.Issue;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.resources.Project;
import org.sonar.api.utils.DateUtils;
import org.sonar.core.i18n.RuleI18nManager;

import java.util.Arrays;
import java.util.Date;

import static org.fest.assertions.Assertions.assertThat;

@RunWith(MockitoJUnitRunner.class)
public class IssueNotificationsTest {

@Mock
NotificationManager manager;

@Mock
RuleI18nManager ruleI18n;

IssueNotifications issueNotifications;

@Before
public void setUp() throws Exception {
issueNotifications = new IssueNotifications(manager, ruleI18n);
}

@Test
public void sendNewIssues() throws Exception {
Date date = DateUtils.parseDateTime("2013-05-18T00:00:03+0200");
Project project = new Project("struts").setAnalysisDate(date);
Notification notification = issueNotifications.sendNewIssues(project, 42);

assertThat(notification.getFieldValue("count")).isEqualTo("42");
assertThat(notification.getFieldValue("projectDate")).isEqualTo("2013-05-18T00:00:03+0200");
Mockito.verify(manager).scheduleForSending(notification);
}

@Test
public void sendChanges() throws Exception {
IssueChangeContext context = IssueChangeContext.createScan(new Date());
DefaultIssue issue = new DefaultIssue()
.setMessage("the message")
.setKey("ABCDE")
.setAssignee("freddy")
.setFieldDiff(context, "resolution", null, "FIXED")
.setFieldDiff(context, "status", "OPEN", "RESOLVED")
.setComponentKey("struts:Action")
.setProjectKey("struts");
DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult();
queryResult.setIssues(Arrays.<Issue>asList(issue));
queryResult.addProjects(Arrays.<Component>asList(new Project("struts")));

Notification notification = issueNotifications.sendChanges(issue, context, queryResult);

assertThat(notification.getFieldValue("message")).isEqualTo("the message");
assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE");
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");
Mockito.verify(manager).scheduleForSending(notification);
}
}

+ 14
- 1
sonar-core/src/test/java/org/sonar/core/notification/DefaultNotificationManagerTest.java View File

@@ -116,7 +116,7 @@ public class DefaultNotificationManagerTest extends AbstractDbUnitTestCase {
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", null)).thenReturn(Lists.newArrayList("user3"));
when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", null)).thenReturn(Lists.newArrayList("user4"));

Multimap<String, NotificationChannel> multiMap = manager.findSubscribedRecipientsForDispatcher(dispatcher, null);
Multimap<String, NotificationChannel> multiMap = manager.findSubscribedRecipientsForDispatcher(dispatcher, (Integer)null);
assertThat(multiMap.entries()).hasSize(3);

Map<String, Collection<NotificationChannel>> map = multiMap.asMap();
@@ -126,4 +126,17 @@ public class DefaultNotificationManagerTest extends AbstractDbUnitTestCase {
assertThat(map.get("user4")).isNull();
}

@Test
public void findNotificationSubscribers() {
when(propertiesDao.findNotificationSubscribers("NewViolations", "Email", "struts")).thenReturn(Lists.newArrayList("user1", "user2"));
when(propertiesDao.findNotificationSubscribers("NewViolations", "Twitter", "struts")).thenReturn(Lists.newArrayList("user2"));

Multimap<String, NotificationChannel> multiMap = manager.findNotificationSubscribers(dispatcher, "struts");
assertThat(multiMap.entries()).hasSize(3);

Map<String, Collection<NotificationChannel>> map = multiMap.asMap();
assertThat(map.get("user1")).containsOnly(emailChannel);
assertThat(map.get("user2")).containsOnly(emailChannel, twitterChannel);
assertThat(map.get("other")).isNull();
}
}

+ 27
- 3
sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java View File

@@ -49,10 +49,10 @@ public class PropertiesDaoTest extends AbstractDaoTestCase {
setupData("shouldFindUsersForNotification");

List<String> users = dao.findUsersForNotification("NewViolations", "Email", null);
assertThat(users).hasSize(0);
assertThat(users).isEmpty();

users = dao.findUsersForNotification("NewViolations", "Email", 78L);
assertThat(users).hasSize(0);
assertThat(users).isEmpty();

users = dao.findUsersForNotification("NewViolations", "Email", 45L);
assertThat(users).hasSize(1);
@@ -63,13 +63,37 @@ public class PropertiesDaoTest extends AbstractDaoTestCase {
assertThat(users).containsOnly("user3");

users = dao.findUsersForNotification("NewViolations", "Twitter", 78L);
assertThat(users).hasSize(0);
assertThat(users).isEmpty();

users = dao.findUsersForNotification("NewViolations", "Twitter", 56L);
assertThat(users).hasSize(2);
assertThat(users).containsOnly("user1", "user3");
}

@Test
public void findNotificationSubscribers() {
setupData("findNotificationSubscribers");

// Nobody is subscribed
List<String> users = dao.findNotificationSubscribers("NotSexyDispatcher", "Email", "org.apache:struts");
assertThat(users).isEmpty();

// Global subscribers
users = dao.findNotificationSubscribers("DispatcherWithGlobalSubscribers", "Email", "org.apache:struts");
assertThat(users).containsOnly("simon");

users = dao.findNotificationSubscribers("DispatcherWithGlobalSubscribers", "Email", null);
assertThat(users).containsOnly("simon");

// Project subscribers
users = dao.findNotificationSubscribers("DispatcherWithProjectSubscribers", "Email", "org.apache:struts");
assertThat(users).containsOnly("eric");

// Global + Project subscribers
users = dao.findNotificationSubscribers("DispatcherWithGlobalAndProjectSubscribers", "Email", "org.apache:struts");
assertThat(users).containsOnly("eric", "simon");
}

@Test
public void selectGlobalProperties() {
setupData("selectGlobalProperties");

+ 50
- 0
sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/findNotificationSubscribers.xml View File

@@ -0,0 +1,50 @@
<dataset>

<users
id="1"
login="eric"
/>

<users
id="2"
login="simon"
/>

<projects id="42" kee="org.apache:struts"/>

<properties
id="1"
prop_key="notification.DispatcherWithGlobalSubscribers.Email"
text_value="true"
resource_id="[null]"
user_id="2"/>

<properties
id="2"
prop_key="notification.DispatcherWithProjectSubscribers.Email"
text_value="true"
resource_id="42"
user_id="1"/>

<properties
id="3"
prop_key="notification.DispatcherWithGlobalAndProjectSubscribers.Email"
text_value="true"
resource_id="56"
user_id="1"/>

<properties
id="4"
prop_key="notification.DispatcherWithGlobalAndProjectSubscribers.Email"
text_value="true"
resource_id="42"
user_id="1"/>

<properties
id="5"
prop_key="notification.DispatcherWithGlobalAndProjectSubscribers.Email"
text_value="true"
resource_id="[null]"
user_id="2"/>

</dataset>

+ 3
- 1
sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueQueryResult.java View File

@@ -25,7 +25,6 @@ import org.sonar.api.user.User;
import org.sonar.api.utils.Paging;

import javax.annotation.CheckForNull;

import java.util.Collection;
import java.util.List;

@@ -35,6 +34,9 @@ import java.util.List;
public interface IssueQueryResult {
List<Issue> issues();

@CheckForNull
Issue first();

Rule rule(Issue issue);

/**

+ 7
- 5
sonar-plugin-api/src/main/java/org/sonar/api/notifications/Notification.java View File

@@ -23,8 +23,11 @@ import com.google.common.collect.Maps;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
* <p>
@@ -44,10 +47,8 @@ public class Notification implements Serializable {

private static final String DEFAULT_MESSAGE_KEY = "default_message";

private String type;

private HashMap<String, String> fields = Maps.newHashMap(); // NOSONAR false-positive due to serialization : usage of HashMap instead of
// Map
private final String type;
private final Map<String, String> fields = Maps.newHashMap();

/**
* <p>
@@ -105,7 +106,7 @@ public class Notification implements Serializable {
* @param value the value of the field
* @return the notification itself
*/
public Notification setFieldValue(String field, String value) {
public Notification setFieldValue(String field, @Nullable String value) {
fields.put(field, value);
return this;
}
@@ -116,6 +117,7 @@ public class Notification implements Serializable {
* @param field the field
* @return the value of the field
*/
@CheckForNull
public String getFieldValue(String field) {
return fields.get(field);
}

+ 7
- 13
sonar-plugin-api/src/main/java/org/sonar/api/notifications/NotificationDispatcher.java View File

@@ -29,9 +29,9 @@ import org.sonar.api.ServerExtension;
* </p>
* For example:
* <ul>
* <li>notify me by email when someone comments on review created by me</li>
* <li>notify me by twitter when someone comments on review assigned to me</li>
* <li>notify me by Jabber when someone mentions me in comment for review</li>
* <li>notify me by email when someone comments an issue reported by me</li>
* <li>notify me by twitter when someone comments an issue assigned to me</li>
* <li>notify me by Jabber when someone mentions me in an issue comment</li>
* <li>send me by SMS when there are system notifications (like password reset, account creation, ...)</li>
* </ul>
*
@@ -74,7 +74,7 @@ public abstract class NotificationDispatcher implements ServerExtension {

/**
* Creates a new generic dispatcher, used for any kind of notification.
* <br/>
* <p/>
* Should be avoided and replaced by the other constructor - as it is easier to understand that a
* dispatcher listens for a specific type of notification.
*/
@@ -83,9 +83,9 @@ public abstract class NotificationDispatcher implements ServerExtension {
}

/**
* Returns the unique key of this dispatcher.
*
* @return the key
* The unique key of this dispatcher. By default it's the class name without the package prefix.
* <p/>
* The related label in l10n bundles is 'notification.dispatcher.<key>', for example 'notification.dispatcher.NewFalsePositive'.
*/
public String getKey() {
return getClass().getSimpleName();
@@ -95,9 +95,6 @@ public abstract class NotificationDispatcher implements ServerExtension {
* <p>
* Performs the dispatch.
* </p>
*
* @param notification the notification that will be sent
* @param the context linked to this notification
*/
public final void performDispatch(Notification notification, Context context) {
if (StringUtils.equals(notification.getType(), notificationType) || StringUtils.equals("", notificationType)) {
@@ -110,9 +107,6 @@ public abstract class NotificationDispatcher implements ServerExtension {
* Implements the logic that defines which users will receive the notification.
* </p>
* The purpose of this method is to populate the context object with users, based on the type of notification and the content of the notification.
*
* @param notification the notification that will be sent
* @param the context linked to this notification
*/
public abstract void dispatch(Notification notification, Context context);


+ 7
- 6
sonar-plugin-api/src/main/java/org/sonar/api/notifications/NotificationDispatcherMetadata.java View File

@@ -27,12 +27,10 @@ import java.util.Map;
/**
* <p>
* Notification dispatchers (see {@link NotificationDispatcher}) can define their own metadata class in order
* to tell more about them.
* <br/>
* Instances of those classes must be passed to Pico container (generally in the
* {@link SonarPlugin#getExtensions()} method implementation).
* </p>
*
* to tell more about them.
* <p/>
* Instances of these classes must be declared in {@link org.sonar.api.SonarPlugin#getExtensions()}.
*
* @since 3.5
*/
public final class NotificationDispatcherMetadata implements ServerExtension {
@@ -50,6 +48,9 @@ public final class NotificationDispatcherMetadata implements ServerExtension {

/**
* Creates a new metadata instance for the given dispatcher.
* <p/>
* By default the key is the class name without package. It can be changed by overriding
* {@link org.sonar.api.notifications.NotificationDispatcher#getKey()}.
*/
public static NotificationDispatcherMetadata create(String dispatcherKey) {
return new NotificationDispatcherMetadata(dispatcherKey);

+ 1
- 0
sonar-plugin-api/src/main/java/org/sonar/api/notifications/NotificationManager.java View File

@@ -60,4 +60,5 @@ public interface NotificationManager extends ServerComponent, BatchComponent {
*/
Multimap<String, NotificationChannel> findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, @Nullable Integer resourceId);

Multimap<String, NotificationChannel> findNotificationSubscribers(NotificationDispatcher dispatcher, @Nullable String componentKey);
}

+ 22
- 1
sonar-plugin-api/src/main/java/org/sonar/api/resources/Project.java View File

@@ -26,6 +26,7 @@ import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.maven.project.MavenProject;
import org.sonar.api.CoreProperties;
import org.sonar.api.component.Component;

import java.util.ArrayList;
import java.util.Date;
@@ -36,7 +37,7 @@ import java.util.List;
*
* @since 1.10
*/
public class Project extends Resource {
public class Project extends Resource implements Component {

public static final String SCOPE = Scopes.PROJECT;

@@ -459,4 +460,24 @@ public class Project extends Resource {
.append("qualifier", getQualifier())
.toString();
}

@Override
public String key() {
return getKey();
}

@Override
public String name() {
return getName();
}

@Override
public String longName() {
return getLongName();
}

@Override
public String qualifier() {
return getQualifier();
}
}

+ 5
- 0
sonar-server/src/main/java/org/sonar/server/issue/DefaultIssueQueryResult.java View File

@@ -101,6 +101,11 @@ public class DefaultIssueQueryResult implements IssueQueryResult {
return issues;
}

@Override
public Issue first() {
return issues != null && !issues.isEmpty() ? issues.get(0) : null;
}

@Override
public Rule rule(Issue issue) {
return rulesByKey.get(issue.ruleKey());

+ 2
- 4
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java View File

@@ -40,7 +40,6 @@ import org.sonar.server.platform.UserSession;
import org.sonar.server.util.RubyUtils;

import javax.annotation.Nullable;

import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
@@ -141,7 +140,7 @@ public class InternalRubyIssueService implements ServerComponent {
}

if (result.ok()) {
DefaultIssue issue = (DefaultIssue)new DefaultIssueBuilder()
DefaultIssue issue = (DefaultIssue) new DefaultIssueBuilder()
.componentKey(componentKey)
.line(RubyUtils.toInteger(params.get("line")))
.message(params.get("message"))
@@ -160,8 +159,7 @@ public class InternalRubyIssueService implements ServerComponent {
return result;
}

public Collection<ActionPlan> findOpenActionPlans(String issueKey) {
String componentKey = issueService.loadIssue(issueKey).componentKey();
public Collection<ActionPlan> findOpenActionPlansForComponent(String componentKey) {
return actionPlanService.findOpenByComponentKey(componentKey);
}


+ 27
- 13
sonar-server/src/main/java/org/sonar/server/issue/IssueService.java View File

@@ -22,11 +22,14 @@ package org.sonar.server.issue;
import com.google.common.base.Strings;
import org.sonar.api.ServerComponent;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueQuery;
import org.sonar.api.issue.IssueQueryResult;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.RuleFinder;
import org.sonar.api.web.UserRole;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.issue.IssueNotifications;
import org.sonar.core.issue.IssueUpdater;
import org.sonar.core.issue.db.IssueStorage;
import org.sonar.core.issue.workflow.IssueWorkflow;
@@ -34,7 +37,7 @@ import org.sonar.core.issue.workflow.Transition;
import org.sonar.server.platform.UserSession;

import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -50,19 +53,22 @@ public class IssueService implements ServerComponent {
private final IssueStorage issueStorage;
private final ActionPlanService actionPlanService;
private final RuleFinder ruleFinder;
private final IssueNotifications issueNotifications;

public IssueService(DefaultIssueFinder finder,
IssueWorkflow workflow,
IssueStorage issueStorage,
IssueUpdater issueUpdater,
ActionPlanService actionPlanService,
RuleFinder ruleFinder) {
RuleFinder ruleFinder,
IssueNotifications issueNotifications) {
this.finder = finder;
this.workflow = workflow;
this.issueStorage = issueStorage;
this.issueUpdater = issueUpdater;
this.actionPlanService = actionPlanService;
this.ruleFinder = ruleFinder;
this.issueNotifications = issueNotifications;
}

/**
@@ -71,7 +77,7 @@ public class IssueService implements ServerComponent {
* Never return null, but return an empty list if the issue does not exist.
*/
public List<Transition> listTransitions(String issueKey) {
return listTransitions(loadIssue(issueKey));
return listTransitions(loadIssue(issueKey).first());
}

/**
@@ -86,22 +92,26 @@ public class IssueService implements ServerComponent {

public Issue doTransition(String issueKey, String transition, UserSession userSession) {
verifyLoggedIn(userSession);
DefaultIssue issue = loadIssue(issueKey);
IssueQueryResult queryResult = loadIssue(issueKey);
DefaultIssue issue = (DefaultIssue) queryResult.first();
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login());
if (workflow.doTransition(issue, transition, context)) {
issueStorage.save(issue);
issueNotifications.sendChanges(issue, context, queryResult);
}
return issue;
}

public Issue assign(String issueKey, @Nullable String assigneeLogin, UserSession userSession) {
public Issue assign(String issueKey, @Nullable String assignee, UserSession userSession) {
verifyLoggedIn(userSession);
DefaultIssue issue = loadIssue(issueKey);
IssueQueryResult queryResult = loadIssue(issueKey);
DefaultIssue issue = (DefaultIssue) queryResult.first();

// TODO check that assignee exists
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login());
if (issueUpdater.assign(issue, assigneeLogin, context)) {
if (issueUpdater.assign(issue, assignee, context)) {
issueStorage.save(issue);
issueNotifications.sendChanges(issue, context, queryResult);
}
return issue;
}
@@ -112,21 +122,24 @@ public class IssueService implements ServerComponent {
}

verifyLoggedIn(userSession);
DefaultIssue issue = loadIssue(issueKey);
IssueQueryResult queryResult = loadIssue(issueKey);
DefaultIssue issue = (DefaultIssue) queryResult.first();
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login());
if (issueUpdater.plan(issue, actionPlanKey, context)) {
issueStorage.save(issue);
issueNotifications.sendChanges(issue, context, queryResult);
}
return issue;
}

public Issue setSeverity(String issueKey, String severity, UserSession userSession) {
verifyLoggedIn(userSession);
DefaultIssue issue = loadIssue(issueKey);
IssueQueryResult queryResult = loadIssue(issueKey);
DefaultIssue issue = (DefaultIssue) queryResult.first();
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login());
if (issueUpdater.setManualSeverity(issue, severity, context)) {
issueStorage.save(issue);
issueNotifications.sendChanges(issue, context, queryResult);
}
return issue;
}
@@ -150,11 +163,12 @@ public class IssueService implements ServerComponent {
}


public DefaultIssue loadIssue(String issueKey) {
return finder.findByKey(issueKey, UserRole.USER);
public IssueQueryResult loadIssue(String issueKey) {
IssueQuery query = IssueQuery.builder().issueKeys(Arrays.asList(issueKey)).requiredRole(UserRole.USER).build();
return finder.find(query);
}

public List<String> listStatus(){
public List<String> listStatus() {
return workflow.statusKeys();
}


+ 2
- 0
sonar-server/src/main/java/org/sonar/server/platform/Platform.java View File

@@ -40,6 +40,7 @@ import org.sonar.core.config.Logback;
import org.sonar.core.i18n.GwtI18n;
import org.sonar.core.i18n.I18nManager;
import org.sonar.core.i18n.RuleI18nManager;
import org.sonar.core.issue.IssueNotifications;
import org.sonar.core.issue.IssueUpdater;
import org.sonar.core.issue.workflow.FunctionExecutor;
import org.sonar.core.issue.workflow.IssueWorkflow;
@@ -267,6 +268,7 @@ public final class Platform {
servicesContainer.addSingleton(PublicRubyIssueService.class);
servicesContainer.addSingleton(InternalRubyIssueService.class);
servicesContainer.addSingleton(ActionPlanService.class);
servicesContainer.addSingleton(IssueNotifications.class);

// rules
servicesContainer.addSingleton(RubyRuleService.class);

+ 2
- 0
sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb View File

@@ -50,6 +50,8 @@ class IssueController < ApplicationController
@issue_result = Api.issues.find(params[:issue])
@issue = @issue_result.issues().get(0)

bad_request('Unknown issue') unless @issue

action_type = params[:id]
render :partial => "issue/#{action_type}_form"
end

+ 1
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/issue/_plan_form.html.erb View File

@@ -1,6 +1,6 @@
<%
plans_select_box_id = "plans-#{params[:issue]}"
plans = Internal.issues.findOpenActionPlans(params[:issue])
plans = Internal.issues.findOpenActionPlansForComponent(@issue.componentKey())


if plans.empty?

+ 12
- 0
sonar-server/src/main/webapp/WEB-INF/db/migrate/403_replace_review_notifications.rb View File

@@ -20,6 +20,7 @@

#
# Sonar 3.6
# See SONAR-4283
#
class ReplaceReviewNotifications < ActiveRecord::Migration

@@ -31,5 +32,16 @@ class ReplaceReviewNotifications < ActiveRecord::Migration
prop.prop_key = prop.prop_key.gsub(/NewViolationsOnFirstDifferentialPeriod/, 'NewIssues')
prop.save
end

Property.find(:all, :conditions => ['prop_key like ?', 'notification.ChangesInReviewAssignedToMeOrCreatedByMe.%']).each do |prop|
prop.prop_key = prop.prop_key.gsub(/ChangesInReviewAssignedToMeOrCreatedByMe/, 'ChangesOnMyIssue')
prop.save
end

Property.find(:all, :conditions => ['prop_key like ?', 'notification.NewFalsePositiveReview.%']).each do |prop|
prop.prop_key = prop.prop_key.gsub(/NewFalsePositiveReview/, 'NewFalsePositiveIssue')
prop.save
end

end
end

Loading…
Cancel
Save