]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2747 Send email when new violations appear on favourite project
authorFabrice Bellingard <bellingard@gmail.com>
Fri, 3 Feb 2012 16:09:26 +0000 (17:09 +0100)
committerFabrice Bellingard <bellingard@gmail.com>
Fri, 3 Feb 2012 16:19:07 +0000 (17:19 +0100)
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...)

18 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewViolationsDecorator.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewViolationsDecoratorTest.java
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsEmailTemplate.java [new file with mode: 0644]
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsOnMyFavouriteProject.java [new file with mode: 0644]
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/reviews/ChangesInReviewAssignedToMeOrCreatedByMe.java
plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsOnMyFavouriteProjectTest.java [new file with mode: 0644]
plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/newviolations/NewViolationsTemplateTest.java [new file with mode: 0644]
plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-batch/src/main/java/org/sonar/batch/components/TimeMachineConfiguration.java
sonar-batch/src/test/java/org/sonar/batch/components/TimeMachineConfigurationTest.java
sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java
sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java
sonar-core/src/main/java/org/sonar/core/properties/PropertiesDao.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/properties/PropertiesMapper.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/core/properties/PropertiesMapper.xml [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/properties/PropertiesDaoTest.java [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/properties/PropertiesDaoTest/shouldFindUserIdsForFavouriteResource.xml [new file with mode: 0644]

index 64005d358bdbfa52939ee18bd88a9b609b2f374a..48b97f0a60e19f3810de1c7991a722b766bec7ab 100644 (file)
@@ -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();
index 8dc10282cb3c1ef3d87113398a82cc459627c3e5..d38f59e30ebea3d600dcfcfed6a2f2ac248c02a9 100644 (file)
  */
 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<Violation> createViolations() {
     List<Violation> violations = Lists.newLinkedList();
     violations.add(Violation.create(rule1, resource).setSeverity(RulePriority.CRITICAL).setCreatedAt(rightNow));
index 85ad80b6fc382e72946f2f82af29f95050d28060..9c8965c2366fc4adaf475a57e6ce6165259905de 100644 (file)
@@ -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 (file)
index 0000000..d0a874f
--- /dev/null
@@ -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 (file)
index 0000000..4f2d637
--- /dev/null
@@ -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<String> userLogins = propertiesDao.findUserIdsForFavouriteResource(projectId);
+      for (String userLogin : userLogins) {
+        context.addUser(userLogin);
+      }
+    }
+  }
+
+}
index 725636edb75ae6fb8bc852a27647394d7936832d..047b6269d1ca7cc0e4bd1fbace2b0a11d228f996 100644 (file)
@@ -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 (file)
index 0000000..ed2de22
--- /dev/null
@@ -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 (file)
index 0000000..ed065d1
--- /dev/null
@@ -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());
+  }
+
+  /**
+   * <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 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"));
+  }
+
+}
index 77fbccad0432cc3313a7782121f771119b41e17e..1de8b496d0ec3d0a694114214822c9d73cf521b6 100644 (file)
@@ -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
 
 
 #------------------------------------------------------------------------------
index 749e3b36de8b4469ec7a2e84bceb146739637692..59a07f5eaa8ed440c0185bb3883ad44dd8210a08 100644 (file)
  */
 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<PastSnapshot> 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;
+  }
 }
index ab1c621cdde9f09a606ff7a6930177bb6854ec06..7d4be9f7802eac4ab5bcc1838536e50ec57a09c0 100644 (file)
  */
 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));
+  }
+
 }
index a6d536ca07f79a60a70cd0be015c7a5c15884ee4..a750228cc37d671beb5c581f2ccf2ab7ba634b57 100644 (file)
@@ -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,
index b42b27d54d507d0b7678c74fc19c9bd984ff3f2d..3629a399df32ea7d5ba886f7fe4a93fe7eba47c5 100644 (file)
  */
 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 (file)
index 0000000..3762d35
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..4862efb
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..21dc926
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.core.properties.PropertiesMapper">
+
+  <select id="findUserIdsForFavouriteResource" parameterType="map" resultType="String">
+    SELECT U.login
+    FROM properties P, users U
+    WHERE P.prop_key = 'favourite' AND P.resource_id = #{resource_id} AND P.user_id = U.id
+  </select>
+
+</mapper>
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 (file)
index 0000000..590c244
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..9a5c1a0
--- /dev/null
@@ -0,0 +1,43 @@
+<dataset>
+
+  <properties
+    id="1"
+    prop_key="sonar.core.id"
+    text_value="2.14"
+    resource_id="[null]"
+    user_id="[null]"/>
+
+  <properties
+    id="2"
+    prop_key="favourite"
+    text_value=""
+    resource_id="1"
+    user_id="2"/>
+
+  <properties
+    id="3"
+    prop_key="favourite"
+    text_value=""
+    resource_id="2"
+    user_id="3"/>
+
+  <properties
+    id="4"
+    prop_key="favourite"
+    text_value=""
+    resource_id="2"
+    user_id="4"/>
+    
+  <users
+    id="2"
+    login="user2"/>
+    
+  <users
+    id="3"
+    login="user3"/>
+    
+  <users
+    id="4"
+    login="user4"/>
+
+</dataset>