From: Fabrice Bellingard Date: Fri, 3 Feb 2012 16:09:26 +0000 (+0100) Subject: SONAR-2747 Send email when new violations appear on favourite project X-Git-Tag: 2.14~183 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=01906e4e61dcde195d2368c092f8471c0894079f;p=sonarqube.git SONAR-2747 Send email when new violations appear on favourite project The email is sent only if: * the user has set the project as a favourite * this is a "last analysis" (= no 'sonar.projectDate' specified) * 'since last analysis' period was not removed in the admin page * there are new violations (obviously...) --- diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewViolationsDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewViolationsDecorator.java index 64005d358bd..48b97f0a60e 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewViolationsDecorator.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewViolationsDecorator.java @@ -37,6 +37,8 @@ import org.sonar.api.measures.MeasureUtils; import org.sonar.api.measures.MeasuresFilters; import org.sonar.api.measures.Metric; import org.sonar.api.measures.RuleMeasure; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationManager; import org.sonar.api.resources.Project; import org.sonar.api.resources.Resource; import org.sonar.api.resources.ResourceUtils; @@ -55,9 +57,11 @@ import com.google.common.collect.Sets; public class NewViolationsDecorator implements Decorator { private TimeMachineConfiguration timeMachineConfiguration; + private NotificationManager notificationManager; - public NewViolationsDecorator(TimeMachineConfiguration timeMachineConfiguration) { + public NewViolationsDecorator(TimeMachineConfiguration timeMachineConfiguration, NotificationManager notificationManager) { this.timeMachineConfiguration = timeMachineConfiguration; + this.notificationManager = notificationManager; } public boolean shouldExecuteOnProject(Project project) { @@ -75,15 +79,19 @@ public class NewViolationsDecorator implements Decorator { CoreMetrics.NEW_INFO_VIOLATIONS); } + @SuppressWarnings("rawtypes") public void decorate(Resource resource, DecoratorContext context) { if (shouldDecorateResource(resource, context)) { computeNewViolations(context); computeNewViolationsPerSeverity(context); computeNewViolationsPerRule(context); } + if (ResourceUtils.isRootProject(resource)) { + notifyNewViolations((Project) resource, context); + } } - private boolean shouldDecorateResource(Resource resource, DecoratorContext context) { + private boolean shouldDecorateResource(Resource resource, DecoratorContext context) { return (StringUtils.equals(Scopes.PROJECT, resource.getScope()) || StringUtils.equals(Scopes.DIRECTORY, resource.getScope()) || StringUtils .equals(Scopes.FILE, resource.getScope())) && !ResourceUtils.isUnitTestClass(resource) @@ -198,6 +206,23 @@ public class NewViolationsDecorator implements Decorator { return metric; } + protected void notifyNewViolations(Project project, DecoratorContext context) { + Integer lastAnalysisPeriodIndex = timeMachineConfiguration.getLastAnalysisPeriodIndex(); + if (lastAnalysisPeriodIndex != null) { + Double newViolationsCount = context.getMeasure(CoreMetrics.NEW_VIOLATIONS).getVariation(lastAnalysisPeriodIndex); + if (newViolationsCount != null && newViolationsCount > 0) { + // Maybe we should check if this is the first analysis or not? + Notification notification = new Notification("new-violations") + .setFieldValue("count", String.valueOf(newViolationsCount.intValue())) + .setFieldValue("projectName", project.getLongName()) + .setFieldValue("projectKey", project.getKey()) + .setFieldValue("projectId", String.valueOf(project.getId())) + .setFieldValue("period", lastAnalysisPeriodIndex.toString()); + notificationManager.scheduleForSending(notification); + } + } + } + @Override public String toString() { return getClass().getSimpleName(); diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewViolationsDecoratorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewViolationsDecoratorTest.java index 8dc10282cb3..d38f59e30eb 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewViolationsDecoratorTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewViolationsDecoratorTest.java @@ -19,7 +19,21 @@ */ package org.sonar.plugins.core.timemachine; -import com.google.common.collect.Lists; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.time.DateUtils; import org.hamcrest.BaseMatcher; @@ -31,8 +45,11 @@ import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Measure; import org.sonar.api.measures.Metric; import org.sonar.api.measures.RuleMeasure; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationManager; import org.sonar.api.resources.File; import org.sonar.api.resources.Project; +import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Resource; import org.sonar.api.rules.Rule; import org.sonar.api.rules.RulePriority; @@ -40,14 +57,7 @@ import org.sonar.api.rules.Violation; import org.sonar.batch.components.PastSnapshot; import org.sonar.batch.components.TimeMachineConfiguration; -import java.util.Arrays; -import java.util.Date; -import java.util.List; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.argThat; -import static org.mockito.Mockito.*; +import com.google.common.collect.Lists; public class NewViolationsDecoratorTest { private Rule rule1; @@ -57,10 +67,12 @@ public class NewViolationsDecoratorTest { private NewViolationsDecorator decorator; private DecoratorContext context; private Resource resource; + private NotificationManager notificationManager; private Date rightNow; private Date tenDaysAgo; private Date fiveDaysAgo; + private TimeMachineConfiguration timeMachineConfiguration; @Before public void setUp() { @@ -76,14 +88,15 @@ public class NewViolationsDecoratorTest { when(pastSnapshot2.getIndex()).thenReturn(2); when(pastSnapshot2.getTargetDate()).thenReturn(tenDaysAgo); - TimeMachineConfiguration timeMachineConfiguration = mock(TimeMachineConfiguration.class); + timeMachineConfiguration = mock(TimeMachineConfiguration.class); when(timeMachineConfiguration.getProjectPastSnapshots()).thenReturn(Arrays.asList(pastSnapshot, pastSnapshot2)); context = mock(DecoratorContext.class); resource = new File("com/foo/bar"); when(context.getResource()).thenReturn(resource); - decorator = new NewViolationsDecorator(timeMachineConfiguration); + notificationManager = mock(NotificationManager.class); + decorator = new NewViolationsDecorator(timeMachineConfiguration, notificationManager); rule1 = Rule.create().setRepositoryKey("rule1").setKey("rule1").setName("name1"); rule2 = Rule.create().setRepositoryKey("rule2").setKey("rule2").setName("name2"); @@ -154,6 +167,69 @@ public class NewViolationsDecoratorTest { verify(context).saveMeasure(argThat(new IsVariationRuleMeasure(CoreMetrics.NEW_MINOR_VIOLATIONS, rule3, 0.0, 1.0))); } + @Test + public void shouldNotNotifyIfNotLastestAnalysis() { + Project project = mock(Project.class); + when(project.isLatestAnalysis()).thenReturn(false); + assertThat(decorator.shouldExecuteOnProject(project), is(false)); + } + + @Test + public void shouldNotNotifyIfNotRootProject() throws Exception { + Project project = mock(Project.class); + when(project.getQualifier()).thenReturn(Qualifiers.MODULE); + + decorator.decorate(project, context); + + verify(notificationManager, never()).scheduleForSending(any(Notification.class)); + } + + @Test + public void shouldNotNotifyIfNoPeriodForLastAnalysis() throws Exception { + Project project = new Project("key"); + when(timeMachineConfiguration.getLastAnalysisPeriodIndex()).thenReturn(null); + + decorator.notifyNewViolations(project, context); + + verify(notificationManager, never()).scheduleForSending(any(Notification.class)); + } + + @Test + public void shouldNotNotifyIfNoNewViolations() throws Exception { + Project project = new Project("key"); + when(timeMachineConfiguration.getLastAnalysisPeriodIndex()).thenReturn(1); + Measure m = new Measure(CoreMetrics.NEW_VIOLATIONS); + when(context.getMeasure(CoreMetrics.NEW_VIOLATIONS)).thenReturn(m); + + // NULL is returned here + decorator.notifyNewViolations(project, context); + verify(notificationManager, never()).scheduleForSending(any(Notification.class)); + + // 0 will be returned now + m.setVariation1(0.0); + decorator.notifyNewViolations(project, context); + verify(notificationManager, never()).scheduleForSending(any(Notification.class)); + } + + @Test + public void shouldNotifyUserAboutNewViolations() throws Exception { + Project project = new Project("key").setName("LongName"); + project.setId(45); + when(timeMachineConfiguration.getLastAnalysisPeriodIndex()).thenReturn(2); + Measure m = new Measure(CoreMetrics.NEW_VIOLATIONS).setVariation2(32.0); + when(context.getMeasure(CoreMetrics.NEW_VIOLATIONS)).thenReturn(m); + + decorator.decorate(project, context); + + Notification notification = new Notification("new-violations") + .setFieldValue("count", "32") + .setFieldValue("projectName", "LongName") + .setFieldValue("projectKey", "key") + .setFieldValue("projectId", "45") + .setFieldValue("period", "2"); + verify(notificationManager, times(1)).scheduleForSending(eq(notification)); + } + private List createViolations() { List violations = Lists.newLinkedList(); violations.add(Violation.create(rule1, resource).setSeverity(RulePriority.CRITICAL).setCreatedAt(rightNow)); diff --git a/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java b/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java index 85ad80b6fc3..9c8965c2366 100644 --- a/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java +++ b/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java @@ -23,18 +23,24 @@ import java.util.Arrays; import java.util.List; import org.sonar.api.SonarPlugin; +import org.sonar.plugins.emailnotifications.newviolations.NewViolationsEmailTemplate; +import org.sonar.plugins.emailnotifications.newviolations.NewViolationsOnMyFavouriteProject; import org.sonar.plugins.emailnotifications.reviews.ChangesInReviewAssignedToMeOrCreatedByMe; import org.sonar.plugins.emailnotifications.reviews.ReviewEmailTemplate; public class EmailNotificationsPlugin extends SonarPlugin { + @SuppressWarnings({"rawtypes", "unchecked"}) public List getExtensions() { return Arrays.asList( EmailConfiguration.class, EmailNotificationChannel.class, ReviewEmailTemplate.class, - ChangesInReviewAssignedToMeOrCreatedByMe.class); + ChangesInReviewAssignedToMeOrCreatedByMe.class, + + NewViolationsEmailTemplate.class, + NewViolationsOnMyFavouriteProject.class); } } diff --git a/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsEmailTemplate.java b/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsEmailTemplate.java new file mode 100644 index 00000000000..d0a874f943f --- /dev/null +++ b/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsEmailTemplate.java @@ -0,0 +1,72 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.emailnotifications.newviolations; + +import org.sonar.api.notifications.Notification; +import org.sonar.plugins.emailnotifications.EmailConfiguration; +import org.sonar.plugins.emailnotifications.api.EmailMessage; +import org.sonar.plugins.emailnotifications.api.EmailTemplate; + +/** + * Creates email message for notification "new-violations". + * + * @since 2.10 + */ +public class NewViolationsEmailTemplate extends EmailTemplate { + + private EmailConfiguration configuration; + + public NewViolationsEmailTemplate(EmailConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public EmailMessage format(Notification notification) { + if (!"new-violations".equals(notification.getType())) { + return null; + } + StringBuilder sb = new StringBuilder(); + + String projectName = notification.getFieldValue("projectName"); + appendLine(sb, "Project", projectName); + appendLine(sb, "New violations on last analysis", notification.getFieldValue("count")); + appendFooter(sb, notification); + + EmailMessage message = new EmailMessage() + .setMessageId("new-violations/" + notification.getFieldValue("projectId")) + .setSubject("New violations for project " + projectName) + .setMessage(sb.toString()); + + return message; + } + + private void appendLine(StringBuilder sb, String name, String value) { + sb.append(name).append(": ").append(value).append('\n'); + } + + private void appendFooter(StringBuilder sb, Notification notification) { + String projectKey = notification.getFieldValue("projectKey"); + String period = notification.getFieldValue("period"); + sb.append("\n--\n") + .append("See it in Sonar: ").append(configuration.getServerBaseURL()).append("/drilldown/measures/").append(projectKey) + .append("?metric=new_violations&period=").append(period).append('\n'); + } + +} diff --git a/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsOnMyFavouriteProject.java b/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsOnMyFavouriteProject.java new file mode 100644 index 00000000000..4f2d637658b --- /dev/null +++ b/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsOnMyFavouriteProject.java @@ -0,0 +1,53 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.emailnotifications.newviolations; + +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationDispatcher; +import org.sonar.core.properties.PropertiesDao; + +/** + * This dispatcher means: "notify me when new violations are introduced on projects that I flagged as favourite". + * + * @since 2.14 + */ +public class NewViolationsOnMyFavouriteProject extends NotificationDispatcher { + + private PropertiesDao propertiesDao; + + public NewViolationsOnMyFavouriteProject(PropertiesDao propertiesDao) { + this.propertiesDao = propertiesDao; + } + + @Override + public void dispatch(Notification notification, Context context) { + if (StringUtils.equals(notification.getType(), "new-violations")) { + Integer projectId = Integer.parseInt(notification.getFieldValue("projectId")); + List userLogins = propertiesDao.findUserIdsForFavouriteResource(projectId); + for (String userLogin : userLogins) { + context.addUser(userLogin); + } + } + } + +} diff --git a/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMe.java b/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMe.java index 725636edb75..047b6269d1c 100644 --- a/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMe.java +++ b/plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMe.java @@ -24,7 +24,7 @@ import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.NotificationDispatcher; /** - * This dispatcher means: "notify me when when someone changes review assigned to me or created by me". + * This dispatcher means: "notify me when someone changes review assigned to me or created by me". * * @since 2.10 */ diff --git a/plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsOnMyFavouriteProjectTest.java b/plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsOnMyFavouriteProjectTest.java new file mode 100644 index 00000000000..ed2de225074 --- /dev/null +++ b/plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsOnMyFavouriteProjectTest.java @@ -0,0 +1,63 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.emailnotifications.newviolations; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationDispatcher; +import org.sonar.core.properties.PropertiesDao; + +import com.google.common.collect.Lists; + +public class NewViolationsOnMyFavouriteProjectTest { + + @Test + public void shouldNotDispatchIfNotNewViolationsNotification() throws Exception { + NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class); + NewViolationsOnMyFavouriteProject dispatcher = new NewViolationsOnMyFavouriteProject(null); + Notification notification = new Notification("other-notif"); + dispatcher.dispatch(notification, context); + + verify(context, never()).addUser(any(String.class)); + } + + @Test + public void shouldDispatchToUsersWhoHaveFlaggedProjectAsFavourite() { + NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(propertiesDao.findUserIdsForFavouriteResource(34)).thenReturn(Lists.newArrayList("user1", "user2")); + NewViolationsOnMyFavouriteProject dispatcher = new NewViolationsOnMyFavouriteProject(propertiesDao); + + Notification notification = new Notification("new-violations").setFieldValue("projectId", "34"); + dispatcher.dispatch(notification, context); + + verify(context).addUser("user1"); + verify(context).addUser("user2"); + verifyNoMoreInteractions(context); + } + +} diff --git a/plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsTemplateTest.java b/plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsTemplateTest.java new file mode 100644 index 00000000000..ed065d1ccee --- /dev/null +++ b/plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsTemplateTest.java @@ -0,0 +1,89 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.emailnotifications.newviolations; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.notifications.Notification; +import org.sonar.plugins.emailnotifications.EmailConfiguration; +import org.sonar.plugins.emailnotifications.api.EmailMessage; + +public class NewViolationsTemplateTest { + + private NewViolationsEmailTemplate template; + + @Before + public void setUp() { + EmailConfiguration configuration = mock(EmailConfiguration.class); + when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org"); + template = new NewViolationsEmailTemplate(configuration); + } + + @Test + public void shouldNotFormatIfNotCorrectNotification() { + Notification notification = new Notification("other-notif"); + EmailMessage message = template.format(notification); + assertThat(message, nullValue()); + } + + /** + *
+   * 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
+   * 
+ */ + @Test + public void shouldFormatCommentAdded() { + Notification notification = new Notification("new-violations") + .setFieldValue("count", "32") + .setFieldValue("projectName", "Foo") + .setFieldValue("projectKey", "org.sonar.foo:foo") + .setFieldValue("projectId", "45") + .setFieldValue("period", "2"); + + EmailMessage message = template.format(notification); + assertThat(message.getMessageId(), is("new-violations/45")); + assertThat(message.getSubject(), is("New violations for project Foo")); + assertThat(message.getMessage(), is("" + + "Project: Foo\n" + + "New violations on last analysis: 32\n" + + "\n" + + "--\n" + + "See it in Sonar: http://nemo.sonarsource.org/drilldown/measures/org.sonar.foo:foo?metric=new_violations&period=2\n")); + } + +} diff --git a/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties b/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties index 77fbccad043..1de8b496d0e 100644 --- a/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties +++ b/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties @@ -1068,6 +1068,7 @@ server_id_configuration.generation_error=Organisation and/or IP address are not #------------------------------------------------------------------------------ notification.channel.EmailNotificationChannel=Email notification.dispatcher.ChangesInReviewAssignedToMeOrCreatedByMe=Changes in review assigned to me or created by me +notification.dispatcher.NewViolationsOnMyFavouriteProject=New violations on my favourite projects #------------------------------------------------------------------------------ diff --git a/sonar-batch/src/main/java/org/sonar/batch/components/TimeMachineConfiguration.java b/sonar-batch/src/main/java/org/sonar/batch/components/TimeMachineConfiguration.java index 749e3b36de8..59a07f5eaa8 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/components/TimeMachineConfiguration.java +++ b/sonar-batch/src/main/java/org/sonar/batch/components/TimeMachineConfiguration.java @@ -19,7 +19,11 @@ */ package org.sonar.batch.components; -import com.google.common.collect.Lists; +import java.util.Date; +import java.util.List; + +import javax.persistence.Query; + import org.apache.commons.configuration.Configuration; import org.apache.commons.lang.StringUtils; import org.slf4j.LoggerFactory; @@ -30,12 +34,8 @@ import org.sonar.api.database.model.Snapshot; import org.sonar.api.resources.Project; import org.sonar.api.resources.Qualifiers; import org.sonar.api.utils.Logs; -import org.sonar.batch.components.PastSnapshot; -import org.sonar.batch.components.PastSnapshotFinder; -import javax.persistence.Query; -import java.util.Date; -import java.util.List; +import com.google.common.collect.Lists; public class TimeMachineConfiguration implements BatchExtension { @@ -46,7 +46,8 @@ public class TimeMachineConfiguration implements BatchExtension { private List projectPastSnapshots; private DatabaseSession session; - public TimeMachineConfiguration(DatabaseSession session, Project project, Configuration configuration, PastSnapshotFinder pastSnapshotFinder) { + public TimeMachineConfiguration(DatabaseSession session, Project project, Configuration configuration, + PastSnapshotFinder pastSnapshotFinder) { this.session = session; this.project = project; this.configuration = configuration; @@ -69,7 +70,8 @@ public class TimeMachineConfiguration implements BatchExtension { } private Snapshot buildProjectSnapshot() { - Query query = session.createNativeQuery("select p.id from projects p where p.kee=:resourceKey and p.qualifier<>:lib and p.enabled=:enabled"); + Query query = session + .createNativeQuery("select p.id from projects p where p.kee=:resourceKey and p.qualifier<>:lib and p.enabled=:enabled"); query.setParameter("resourceKey", project.getKey()); query.setParameter("lib", Qualifiers.LIBRARY); query.setParameter("enabled", Boolean.TRUE); @@ -110,4 +112,26 @@ public class TimeMachineConfiguration implements BatchExtension { public boolean isFileVariationEnabled() { return configuration.getBoolean("sonar.enableFileVariation", Boolean.FALSE); } + + /** + * Returns the index corresponding to the 'previous_analysis' period (which is '1' by default). + * + * @return the index of 'previous_analysis' period, or NULL is users have modified the periods and haven't set a 'previous_analysis' one. + */ + public Integer getLastAnalysisPeriodIndex() { + // period1 is the default for 'previous_analysis' + String period1 = configuration.getString(CoreProperties.TIMEMACHINE_PERIOD_PREFIX + "1"); + if (StringUtils.isBlank(period1) || CoreProperties.TIMEMACHINE_MODE_PREVIOUS_ANALYSIS.equals(period1)) { + return 1; + } + // else search for the other periods + for (int index = 2; index < 6; index++) { + if (CoreProperties.TIMEMACHINE_MODE_PREVIOUS_ANALYSIS.equals(configuration + .getString(CoreProperties.TIMEMACHINE_PERIOD_PREFIX + index))) { + return index; + } + } + // if we're here, this means that we have not found the 'previous_analysis' mode + return null; + } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/components/TimeMachineConfigurationTest.java b/sonar-batch/src/test/java/org/sonar/batch/components/TimeMachineConfigurationTest.java index ab1c621cdde..7d4be9f7802 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/components/TimeMachineConfigurationTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/components/TimeMachineConfigurationTest.java @@ -19,6 +19,15 @@ */ package org.sonar.batch.components; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + import org.apache.commons.configuration.PropertiesConfiguration; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; @@ -26,29 +35,25 @@ import org.junit.Test; import org.sonar.api.CoreProperties; import org.sonar.api.database.model.Snapshot; import org.sonar.api.resources.Project; -import org.sonar.batch.components.PastSnapshotFinder; -import org.sonar.batch.components.TimeMachineConfiguration; import org.sonar.jpa.test.AbstractDbUnitTestCase; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.argThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; - public class TimeMachineConfigurationTest extends AbstractDbUnitTestCase { @Test public void shouldSkipTendencies() { PropertiesConfiguration conf = new PropertiesConfiguration(); conf.setProperty(CoreProperties.SKIP_TENDENCIES_PROPERTY, true); - assertThat(new TimeMachineConfiguration(getSession(), new Project("my:project"), conf, mock(PastSnapshotFinder.class)).skipTendencies(), is(true)); + assertThat( + new TimeMachineConfiguration(getSession(), new Project("my:project"), conf, mock(PastSnapshotFinder.class)).skipTendencies(), + is(true)); } @Test public void shouldNotSkipTendenciesByDefault() { PropertiesConfiguration conf = new PropertiesConfiguration(); - assertThat(new TimeMachineConfiguration(getSession(), new Project("my:project"), conf, mock(PastSnapshotFinder.class)).skipTendencies(), is(false)); + assertThat( + new TimeMachineConfiguration(getSession(), new Project("my:project"), conf, mock(PastSnapshotFinder.class)).skipTendencies(), + is(false)); } @Test @@ -79,4 +84,24 @@ public class TimeMachineConfigurationTest extends AbstractDbUnitTestCase { verifyZeroInteractions(pastSnapshotFinder); } + + @Test + public void shouldReturnLastAnalysisIndexIfSet() { + PropertiesConfiguration conf = new PropertiesConfiguration(); + TimeMachineConfiguration timeMachineConfiguration = new TimeMachineConfiguration(getSession(), new Project("my:project"), conf, + mock(PastSnapshotFinder.class)); + + // Nothing set, so period for 'previous_analysis' is 1 by default + assertThat(timeMachineConfiguration.getLastAnalysisPeriodIndex(), is(1)); + + // period1 has been replaced and 'previous_analysis' not set elsewhere... + conf.setProperty(CoreProperties.TIMEMACHINE_PERIOD_PREFIX + 1, "Version 1"); + conf.setProperty(CoreProperties.TIMEMACHINE_PERIOD_PREFIX + 2, "Version 2"); + assertThat(timeMachineConfiguration.getLastAnalysisPeriodIndex(), is(nullValue())); + + // 'previous_analysis' has now been set on period 4 + conf.setProperty(CoreProperties.TIMEMACHINE_PERIOD_PREFIX + 4, CoreProperties.TIMEMACHINE_MODE_PREVIOUS_ANALYSIS); + assertThat(timeMachineConfiguration.getLastAnalysisPeriodIndex(), is(4)); + } + } diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java b/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java index a6d536ca07f..a750228cc37 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java @@ -22,6 +22,7 @@ package org.sonar.core.persistence; import org.sonar.core.dashboard.ActiveDashboardDao; import org.sonar.core.dashboard.DashboardDao; import org.sonar.core.duplication.DuplicationDao; +import org.sonar.core.properties.PropertiesDao; import org.sonar.core.purge.PurgeDao; import org.sonar.core.resource.ResourceDao; import org.sonar.core.resource.ResourceIndexerDao; @@ -44,6 +45,7 @@ public final class DaoUtils { DashboardDao.class, DuplicationDao.class, LoadedTemplateDao.class, + PropertiesDao.class, PurgeDao.class, ResourceIndexerDao.class, ResourceDao.class, diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java b/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java index b42b27d54d5..3629a399df3 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java @@ -19,22 +19,41 @@ */ package org.sonar.core.persistence; +import java.io.IOException; +import java.io.InputStream; + import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.logging.LogFactory; import org.apache.ibatis.mapping.Environment; -import org.apache.ibatis.session.*; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.slf4j.LoggerFactory; import org.sonar.api.BatchComponent; import org.sonar.api.ServerComponent; -import org.sonar.core.dashboard.*; +import org.sonar.core.dashboard.ActiveDashboardDto; +import org.sonar.core.dashboard.ActiveDashboardMapper; +import org.sonar.core.dashboard.DashboardDto; +import org.sonar.core.dashboard.DashboardMapper; +import org.sonar.core.dashboard.WidgetDto; +import org.sonar.core.dashboard.WidgetMapper; +import org.sonar.core.dashboard.WidgetPropertyDto; +import org.sonar.core.dashboard.WidgetPropertyMapper; import org.sonar.core.duplication.DuplicationMapper; import org.sonar.core.duplication.DuplicationUnitDto; +import org.sonar.core.properties.PropertiesMapper; import org.sonar.core.purge.PurgeMapper; import org.sonar.core.purge.PurgeableSnapshotDto; -import org.sonar.core.resource.*; +import org.sonar.core.resource.ResourceDto; +import org.sonar.core.resource.ResourceIndexDto; +import org.sonar.core.resource.ResourceIndexerMapper; +import org.sonar.core.resource.ResourceMapper; +import org.sonar.core.resource.SnapshotDto; import org.sonar.core.review.ReviewDto; import org.sonar.core.review.ReviewMapper; import org.sonar.core.rule.RuleDto; @@ -42,9 +61,6 @@ import org.sonar.core.rule.RuleMapper; import org.sonar.core.template.LoadedTemplateDto; import org.sonar.core.template.LoadedTemplateMapper; -import java.io.IOException; -import java.io.InputStream; - public class MyBatis implements BatchComponent, ServerComponent { private Database database; @@ -80,6 +96,7 @@ public class MyBatis implements BatchComponent, ServerComponent { loadMapper(conf, DashboardMapper.class); loadMapper(conf, DuplicationMapper.class); loadMapper(conf, LoadedTemplateMapper.class); + loadMapper(conf, PropertiesMapper.class); loadMapper(conf, PurgeMapper.class); loadMapper(conf, ResourceMapper.class); loadMapper(conf, ReviewMapper.class); @@ -131,7 +148,7 @@ public class MyBatis implements BatchComponent, ServerComponent { private InputStream getPathToMapper(Class mapperClass) { InputStream input = getClass().getResourceAsStream( - "/" + StringUtils.replace(mapperClass.getName(), ".", "/") + "-" + database.getDialect().getId() + ".xml"); + "/" + StringUtils.replace(mapperClass.getName(), ".", "/") + "-" + database.getDialect().getId() + ".xml"); if (input == null) { input = getClass().getResourceAsStream("/" + StringUtils.replace(mapperClass.getName(), ".", "/") + ".xml"); } diff --git a/sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java b/sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java new file mode 100644 index 00000000000..3762d35e529 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java @@ -0,0 +1,53 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.core.properties; + +import java.util.List; + +import org.apache.ibatis.session.SqlSession; +import org.sonar.api.BatchComponent; +import org.sonar.api.ServerComponent; +import org.sonar.core.persistence.MyBatis; + +public class PropertiesDao implements BatchComponent, ServerComponent { + + private MyBatis mybatis; + + public PropertiesDao(MyBatis mybatis) { + this.mybatis = mybatis; + } + + /** + * Returns the logins of users who have flagged as favourite the resource identified by the given id. + * + * @param resourceId the resource id + * @return the list of logins (maybe be empty - obviously) + */ + public List findUserIdsForFavouriteResource(Integer resourceId) { + SqlSession session = mybatis.openSession(); + PropertiesMapper mapper = session.getMapper(PropertiesMapper.class); + try { + return mapper.findUserIdsForFavouriteResource(resourceId); + } finally { + MyBatis.closeQuietly(session); + } + } + +} diff --git a/sonar-core/src/main/java/org/sonar/core/properties/PropertiesMapper.java b/sonar-core/src/main/java/org/sonar/core/properties/PropertiesMapper.java new file mode 100644 index 00000000000..4862efb0382 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/properties/PropertiesMapper.java @@ -0,0 +1,30 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.core.properties; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; + +public interface PropertiesMapper { + + List findUserIdsForFavouriteResource(@Param("resource_id") Integer resourceId); + +} diff --git a/sonar-core/src/main/resources/org/sonar/core/properties/PropertiesMapper.xml b/sonar-core/src/main/resources/org/sonar/core/properties/PropertiesMapper.xml new file mode 100644 index 00000000000..21dc926f564 --- /dev/null +++ b/sonar-core/src/main/resources/org/sonar/core/properties/PropertiesMapper.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java b/sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java new file mode 100644 index 00000000000..590c24404d0 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java @@ -0,0 +1,48 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.core.properties; + +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.core.persistence.DaoTestCase; + +public class PropertiesDaoTest extends DaoTestCase { + + private PropertiesDao dao; + + @Before + public void createDao() throws Exception { + dao = new PropertiesDao(getMyBatis()); + } + + @Test + public void shouldFindUserIdsForFavouriteResource() throws Exception { + setupData("shouldFindUserIdsForFavouriteResource"); + List userIds = dao.findUserIdsForFavouriteResource(2); + assertThat(userIds.size(), is(2)); + assertThat(userIds, hasItems("user3", "user4")); + } +} diff --git a/sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/shouldFindUserIdsForFavouriteResource.xml b/sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/shouldFindUserIdsForFavouriteResource.xml new file mode 100644 index 00000000000..9a5c1a00b6a --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/shouldFindUserIdsForFavouriteResource.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + +