import org.sonar.core.DryRunIncompatible;
import org.sonar.core.issue.IssueNotifications;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
/**
* @since 3.6
*/
private void sendNotifications(Project project) {
int newIssues = 0;
IssueChangeContext context = IssueChangeContext.createScan(project.getAnalysisDate());
+ Map<DefaultIssue, Rule> shouldSentNotification = new LinkedHashMap<DefaultIssue, Rule>();
for (DefaultIssue issue : issueCache.all()) {
if (issue.isNew() && issue.resolution() == null) {
newIssues++;
Rule rule = ruleFinder.findByKey(issue.ruleKey());
// TODO warning - rules with status REMOVED are currently ignored, but should not
if (rule != null) {
- notifications.sendChanges(issue, context, rule, project, null);
+ shouldSentNotification.put(issue, rule);
}
}
}
+ if (!shouldSentNotification.isEmpty()) {
+ notifications.sendChanges(shouldSentNotification, context, project, null);
+ }
if (newIssues > 0) {
notifications.sendNewIssues(project, newIssues);
}
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.core.issue.IssueNotifications;
import java.util.Arrays;
-
-import static org.mockito.Mockito.*;
+import java.util.Map;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class SendIssueNotificationsPostJobTest {
when(issueCache.all()).thenReturn(Arrays.asList(
new DefaultIssue().setNew(true),
new DefaultIssue().setNew(false)
- ));
+ ));
SendIssueNotificationsPostJob job = new SendIssueNotificationsPostJob(issueCache, notifications, ruleFinder);
job.executeOn(project, sensorContext);
when(project.getAnalysisDate()).thenReturn(DateUtils.parseDate("2013-05-18"));
when(issueCache.all()).thenReturn(Arrays.asList(
new DefaultIssue().setNew(false)
- ));
+ ));
SendIssueNotificationsPostJob job = new SendIssueNotificationsPostJob(issueCache, notifications, ruleFinder);
job.executeOn(project, sensorContext);
SendIssueNotificationsPostJob job = new SendIssueNotificationsPostJob(issueCache, notifications, ruleFinder);
job.executeOn(project, sensorContext);
- verify(notifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(rule), any(Component.class), (Component)isNull());
+ verify(notifications).sendChanges(argThat(matchMapOf(issue, rule)), any(IssueChangeContext.class), any(Component.class), (Component) isNull());
}
@Test
SendIssueNotificationsPostJob job = new SendIssueNotificationsPostJob(issueCache, notifications, ruleFinder);
job.executeOn(project, sensorContext);
- verify(notifications, never()).sendChanges(eq(issue), eq(changeContext), any(Rule.class), any(Component.class), any(Component.class));
+ verify(notifications, never()).sendChanges(argThat(matchMapOf(issue, null)), eq(changeContext), any(Component.class), any(Component.class));
}
@Test
SendIssueNotificationsPostJob job = new SendIssueNotificationsPostJob(issueCache, notifications, ruleFinder);
job.executeOn(project, sensorContext);
- verify(notifications, never()).sendChanges(eq(issue), eq(changeContext), any(Rule.class), any(Component.class), any(Component.class));
+ verify(notifications, never()).sendChanges(argThat(matchMapOf(issue, null)), eq(changeContext), any(Component.class), any(Component.class));
+ }
+
+ private static IsMapOfIssueAndRule matchMapOf(DefaultIssue issue, Rule rule) {
+ return new IsMapOfIssueAndRule(issue, rule);
+ }
+
+ static class IsMapOfIssueAndRule extends ArgumentMatcher<Map<DefaultIssue, Rule>> {
+ private DefaultIssue issue;
+ private Rule rule;
+
+ public IsMapOfIssueAndRule(DefaultIssue issue, Rule rule) {
+ this.issue = issue;
+ this.rule = rule;
+ }
+
+ public boolean matches(Object arg) {
+ Map map = (Map) arg;
+ return map.size() == 1 && map.get(issue) != null && map.get(issue).equals(rule);
+ }
}
}
*/
package org.sonar.core.issue;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import org.sonar.api.BatchComponent;
import org.sonar.api.ServerComponent;
import org.sonar.api.component.Component;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+
+import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Map.Entry;
/**
* Send notifications related to issues.
}
@CheckForNull
- public Notification sendChanges(DefaultIssue issue, IssueChangeContext context, IssueQueryResult queryResult) {
- return sendChanges(issue, context, queryResult.rule(issue), queryResult.project(issue), queryResult.component(issue));
+ public List<Notification> sendChanges(DefaultIssue issue, IssueChangeContext context, IssueQueryResult queryResult) {
+ Map<DefaultIssue, Rule> issues = Maps.newHashMap();
+ issues.put(issue, queryResult.rule(issue));
+ return sendChanges(issues, context, queryResult.project(issue), queryResult.component(issue));
}
@CheckForNull
- public Notification sendChanges(DefaultIssue issue, IssueChangeContext context, Rule rule, Component project, @Nullable Component component) {
- Notification notification = createChangeNotification(issue, context, rule, project, component, null);
- if (notification != null) {
- notificationsManager.scheduleForSending(notification);
+ public List<Notification> sendChanges(Map<DefaultIssue, Rule> issues, IssueChangeContext context, Component project, @Nullable Component component) {
+ List<Notification> notifications = Lists.newArrayList();
+ for (Entry<DefaultIssue, Rule> entry : issues.entrySet()) {
+ Notification notification = createChangeNotification(entry.getKey(), context, entry.getValue(), project, component, null);
+ if (notification != null) {
+ notifications.add(notification);
+ }
}
- return notification;
+ notificationsManager.scheduleForSending(notifications);
+ return notifications;
}
@CheckForNull
@CheckForNull
private Notification createChangeNotification(DefaultIssue issue, IssueChangeContext context, Rule rule, Component project,
- @Nullable Component component, @Nullable String comment) {
+ @Nullable Component component, @Nullable String comment) {
Notification notification = null;
if (comment != null || issue.mustSendNotifications()) {
FieldDiffs currentChange = issue.currentChange();
package org.sonar.core.notification;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import org.sonar.api.notifications.Notification;
*/
public void scheduleForSending(Notification notification) {
NotificationQueueDto dto = NotificationQueueDto.toNotificationQueueDto(notification);
- notificationQueueDao.insert(dto);
+ notificationQueueDao.insert(Arrays.asList(dto));
+ }
+
+ public void scheduleForSending(List<Notification> notification) {
+ notificationQueueDao.insert(Lists.transform(notification, new Function<Notification, NotificationQueueDto>() {
+ @Override
+ public NotificationQueueDto apply(Notification notification) {
+ return NotificationQueueDto.toNotificationQueueDto(notification);
+ }
+ }));
}
/**
this.mybatis = mybatis;
}
- public void insert(NotificationQueueDto dto) {
- SqlSession session = mybatis.openSession();
+ public void insert(List<NotificationQueueDto> dtos) {
+ SqlSession session = mybatis.openBatchSession();
try {
- session.getMapper(NotificationQueueMapper.class).insert(dto);
+ for (NotificationQueueDto dto : dtos) {
+ session.getMapper(NotificationQueueMapper.class).insert(dto);
+ }
session.commit();
} finally {
MyBatis.closeQuietly(session);
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
-import java.util.Date;
/**
* @since 4.0
public class NotificationQueueDto {
private Long id;
- private Date createdAt;
private byte[] data;
public Long getId() {
return this;
}
- public Date getCreatedAt() {
- return createdAt;
- }
-
- public NotificationQueueDto setCreatedAt(Date createdAt) {
- this.createdAt = createdAt;
- return this;
- }
-
public byte[] getData() {
return data;
}
return this;
}
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- NotificationQueueDto actionPlanDto = (NotificationQueueDto) o;
- return !(id != null ? !id.equals(actionPlanDto.id) : actionPlanDto.id != null);
- }
-
- @Override
- public int hashCode() {
- return id != null ? id.hashCode() : 0;
- }
-
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
return new NotificationQueueDto().setData(byteArrayOutputStream.toByteArray());
} catch (IOException e) {
- throw new SonarException(e);
+ throw new SonarException("Unable to write notification", e);
} finally {
IOUtils.closeQuietly(byteArrayOutputStream);
return (Notification) result;
} catch (IOException e) {
- throw new SonarException(e);
+ throw new SonarException("Unable to read notification", e);
} catch (ClassNotFoundException e) {
- throw new SonarException(e);
+ throw new SonarException("Unable to read notification", e);
} finally {
IOUtils.closeQuietly(byteArrayInputStream);
if (null != mappedStatement) {
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
if (keyGenerator instanceof Jdbc3KeyGenerator) {
- throw new IllegalStateException("Batch updates cannot use generated keys");
+ throw new IllegalStateException("Batch inserts cannot use generated keys");
}
}
}
<mapper namespace="org.sonar.core.notification.db.NotificationQueueMapper">
- <insert id="insert" parameterType="NotificationQueue" keyColumn="id" useGeneratedKeys="true" keyProperty="id">
+ <insert id="insert" parameterType="NotificationQueue" useGeneratedKeys="false">
INSERT INTO notifications (created_at, data)
VALUES (current_timestamp, #{data})
</insert>
</delete>
<select id="findOldest" parameterType="int" resultType="NotificationQueue">
- select id, created_at, data
+ select id, data
from notifications
order by created_at asc
limit #{count}
<!-- SQL Server -->
<select id="findOldest" parameterType="int" resultType="NotificationQueue" databaseId="mssql">
- select top (#{count}) id, created_at, data
+ select top (#{count}) id, data
from notifications
order by created_at asc
</select>
<!-- Oracle -->
<select id="findOldest" parameterType="int" resultType="NotificationQueue" databaseId="oracle">
select * from (select
- id, created_at, data
+ id, data
from notifications
order by created_at asc
)
import java.util.Date;
import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
@RunWith(MockitoJUnitRunner.class)
public class IssueNotificationsTest {
.setSendNotifications(true)
.setComponentKey("struts:Action")
.setProjectKey("struts");
- DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue>asList(issue));
- queryResult.addProjects(Arrays.<Component>asList(new Project("struts")));
+ DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue> asList(issue));
+ queryResult.addProjects(Arrays.<Component> asList(new Project("struts")));
- Notification notification = issueNotifications.sendChanges(issue, context, queryResult);
+ Notification notification = issueNotifications.sendChanges(issue, context, queryResult).get(0);
assertThat(notification.getFieldValue("message")).isEqualTo("the message");
assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE");
assertThat(notification.getFieldValue("new.status")).isEqualTo("RESOLVED");
assertThat(notification.getFieldValue("old.assignee")).isEqualTo("simon");
assertThat(notification.getFieldValue("new.assignee")).isNull();
- Mockito.verify(manager).scheduleForSending(notification);
+ Mockito.verify(manager).scheduleForSending(eq(Arrays.asList(notification)));
}
@Test
.setAssignee("freddy")
.setComponentKey("struts:Action")
.setProjectKey("struts");
- DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue>asList(issue));
- queryResult.addProjects(Arrays.<Component>asList(new Project("struts")));
+ DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue> asList(issue));
+ queryResult.addProjects(Arrays.<Component> asList(new Project("struts")));
Notification notification = issueNotifications.sendChanges(issue, context, queryResult, "I don't know how to fix it?");
.setSendNotifications(true)
.setComponentKey("struts:Action")
.setProjectKey("struts");
- DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue>asList(issue));
- queryResult.addProjects(Arrays.<Component>asList(new Project("struts")));
- queryResult.addComponents(Arrays.<Component>asList(new ResourceComponent(new File("struts:Action").setEffectiveKey("struts:Action"))));
+ DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue> asList(issue));
+ queryResult.addProjects(Arrays.<Component> asList(new Project("struts")));
+ queryResult.addComponents(Arrays.<Component> asList(new ResourceComponent(new File("struts:Action").setEffectiveKey("struts:Action"))));
- Notification notification = issueNotifications.sendChanges(issue, context, queryResult);
+ Notification notification = issueNotifications.sendChanges(issue, context, queryResult).get(0);
assertThat(notification.getFieldValue("message")).isEqualTo("the message");
assertThat(notification.getFieldValue("key")).isEqualTo("ABCDE");
assertThat(notification.getFieldValue("componentName")).isEqualTo("struts:Action");
assertThat(notification.getFieldValue("old.resolution")).isNull();
assertThat(notification.getFieldValue("new.resolution")).isEqualTo("FIXED");
- Mockito.verify(manager).scheduleForSending(notification);
+ Mockito.verify(manager).scheduleForSending(eq(Arrays.asList(notification)));
}
@Test
.setKey("ABCDE")
.setComponentKey("struts:Action")
.setProjectKey("struts");
- DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue>asList(issue));
- queryResult.addProjects(Arrays.<Component>asList(new Project("struts")));
+ DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue> asList(issue));
+ queryResult.addProjects(Arrays.<Component> asList(new Project("struts")));
Notification notification = issueNotifications.sendChanges(issue, context, queryResult, null);
Notification notification = new Notification("test");
manager.scheduleForSending(notification);
- verify(notificationQueueDao, only()).insert(any(NotificationQueueDto.class));
+ verify(notificationQueueDao, only()).insert(any(List.class));
}
@Test
public void should_insert_new_notification_queue() {
NotificationQueueDto notificationQueueDto = NotificationQueueDto.toNotificationQueueDto(new Notification("email"));
- dao.insert(notificationQueueDto);
+ dao.insert(Arrays.asList(notificationQueueDto));
checkTables("should_insert_new_notification_queue", new String[] {"id", "created_at"}, "notifications");
assertThat(dao.findOldest(1).get(0).toNotification().getType()).isEqualTo("email");
import javax.annotation.Nullable;
+import java.util.List;
+
/**
* <p>
* The notification manager receives notifications and is in charge of storing them so that they are processed by the notification service.
* </p>
* <p>
- * Pico provides an instance of this class, and plugins just need to create notifications and pass them to this manager with
+ * Pico provides an instance of this class, and plugins just need to create notifications and pass them to this manager with
* the {@link NotificationManager#scheduleForSending(Notification)} method.
* </p>
- *
+ *
* @since 2.10
*/
public interface NotificationManager extends ServerComponent, BatchComponent {
/**
* Receives a notification and stores it so that it is processed by the notification service.
- *
+ *
* @param notification the notification.
*/
void scheduleForSending(Notification notification);
+ /**
+ * Receives notifications and stores them so that they are processed by the notification service.
+ *
+ * @param notifications the notifications.
+ * @since 4.0
+ */
+ void scheduleForSending(List<Notification> notifications);
+
/**
* <p>
- * Returns the list of users who subscribed to the given dispatcher, along with the notification channels (email, twitter, ...) that they choose
+ * Returns the list of users who subscribed to the given dispatcher, along with the notification channels (email, twitter, ...) that they choose
* for this dispatcher.
* </p>
* <p>
* The resource ID can be null in case of notifications that have nothing to do with a specific project (like system notifications).
* </p>
- *
+ *
* @param dispatcher the dispatcher for which this list of users is requested
* @param resourceId the optional resource which is concerned by this request
* @return the list of user login along with the subscribed channels