From: Simon Brandhof Date: Mon, 8 Jun 2015 13:22:34 +0000 (+0200) Subject: SONAR-6567 move notifications from sonar-core to sonar-server X-Git-Tag: 5.2-RC1~1574 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=abbf32571232db81a5343db17a933a9ce6923b44;p=sonarqube.git SONAR-6567 move notifications from sonar-core to sonar-server --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java index cbc929306ea..5129e8a4959 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java @@ -34,13 +34,13 @@ import org.sonar.server.issue.notification.MyNewIssuesNotification; import org.sonar.server.issue.notification.NewIssuesNotification; import org.sonar.server.issue.notification.NewIssuesNotificationFactory; import org.sonar.server.issue.notification.NewIssuesStatistics; -import org.sonar.server.notifications.NotificationService; +import org.sonar.server.notification.NotificationService; import org.sonar.server.util.CloseableIterator; /** * Reads issues from disk cache and send related notifications. For performance reasons, * the standard notification DB queue is not used as a temporary storage. Notifications - * are directly processed by {@link org.sonar.server.notifications.NotificationService}. + * are directly processed by {@link NotificationService}. */ public class SendIssueNotificationsStep implements ComputationStep { /** diff --git a/server/sonar-server/src/main/java/org/sonar/server/event/NewAlerts.java b/server/sonar-server/src/main/java/org/sonar/server/event/NewAlerts.java index cf4c219e848..6ec224ec1f6 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/event/NewAlerts.java +++ b/server/sonar-server/src/main/java/org/sonar/server/event/NewAlerts.java @@ -24,9 +24,9 @@ import org.sonar.api.notifications.*; import java.util.Collection; import java.util.Map; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationDispatcherMetadata; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; /** * This dispatcher means: "notify me each new alert event". diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java index 29f84cafcca..53c4539b16f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java @@ -44,7 +44,7 @@ import org.sonar.api.utils.log.Loggers; import org.sonar.core.component.ComponentDto; import org.sonar.core.issue.db.IssueDto; import org.sonar.core.issue.db.IssueStorage; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationManager; import org.sonar.core.persistence.DbSession; import org.sonar.core.persistence.MyBatis; import org.sonar.server.db.DbClient; diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java index b407ec4b635..0f342ce585a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java @@ -26,7 +26,7 @@ import org.sonar.api.issue.ActionPlan; import org.sonar.api.issue.Issue; import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.issue.internal.IssueChangeContext; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationManager; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.rules.Rule; diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcher.java index b38eb2f855b..5aa290eb83b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcher.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcher.java @@ -25,9 +25,9 @@ import org.sonar.api.notifications.*; import javax.annotation.Nullable; import java.util.Collection; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationDispatcherMetadata; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; /** * This dispatcher means: "notify me when a change is done on an issue that is assigned to me or reported by me". diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/DoNotFixNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/DoNotFixNotificationDispatcher.java index e4f0a5f81d9..1ee261ea214 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/DoNotFixNotificationDispatcher.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/DoNotFixNotificationDispatcher.java @@ -27,9 +27,9 @@ import org.sonar.api.notifications.*; import java.util.Collection; import java.util.Map; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationDispatcherMetadata; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; /** * This dispatcher means: "notify me when an issue is resolved as false positive or won't fix". diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcher.java index 3810279554f..c4be5a62776 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcher.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcher.java @@ -24,9 +24,9 @@ import com.google.common.collect.Multimap; import org.sonar.api.notifications.*; import java.util.Collection; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationDispatcherMetadata; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; /** * This dispatcher means: "notify me when new issues are introduced during project analysis" diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcher.java index e346b1af744..6c28a8aadf9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcher.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcher.java @@ -24,9 +24,9 @@ import org.sonar.api.notifications.*; import java.util.Collection; import java.util.Map; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationDispatcherMetadata; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; /** * This dispatcher means: "notify me when new issues are introduced during project analysis" diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/DefaultNotificationManager.java b/server/sonar-server/src/main/java/org/sonar/server/notification/DefaultNotificationManager.java new file mode 100644 index 00000000000..82706d1001d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/DefaultNotificationManager.java @@ -0,0 +1,177 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.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 java.io.IOException; +import java.io.InvalidClassException; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.api.utils.SonarException; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.notification.db.NotificationQueueDao; +import org.sonar.core.notification.db.NotificationQueueDto; +import org.sonar.core.properties.PropertiesDao; + +public class DefaultNotificationManager implements NotificationManager { + + private static final Logger LOG = Loggers.get(DefaultNotificationManager.class); + + private static final String UNABLE_TO_READ_NOTIFICATION = "Unable to read notification"; + + private NotificationChannel[] notificationChannels; + private NotificationQueueDao notificationQueueDao; + private PropertiesDao propertiesDao; + + private boolean alreadyLoggedDeserializationIssue = false; + + /** + * Default constructor used by Pico + */ + public DefaultNotificationManager(NotificationChannel[] channels, NotificationQueueDao notificationQueueDao, PropertiesDao propertiesDao) { + this.notificationChannels = channels; + this.notificationQueueDao = notificationQueueDao; + this.propertiesDao = propertiesDao; + } + + /** + * Constructor if no notification channel + */ + public DefaultNotificationManager(NotificationQueueDao notificationQueueDao, PropertiesDao propertiesDao) { + this(new NotificationChannel[0], notificationQueueDao, propertiesDao); + } + + /** + * {@inheritDoc} + */ + @Override + public void scheduleForSending(Notification notification) { + NotificationQueueDto dto = NotificationQueueDto.toNotificationQueueDto(notification); + notificationQueueDao.insert(Arrays.asList(dto)); + } + + @Override + public void scheduleForSending(List notification) { + notificationQueueDao.insert(Lists.transform(notification, new Function() { + @Override + public NotificationQueueDto apply(Notification notification) { + return NotificationQueueDto.toNotificationQueueDto(notification); + } + })); + } + + /** + * Give the notification queue so that it can be processed + */ + public Notification getFromQueue() { + int batchSize = 1; + List notificationDtos = notificationQueueDao.findOldest(batchSize); + if (notificationDtos.isEmpty()) { + return null; + } + notificationQueueDao.delete(notificationDtos); + + return convertToNotification(notificationDtos); + } + + private Notification convertToNotification(List notifications) { + try { + // If batchSize is increased then we should return a list instead of a single element + return notifications.get(0).toNotification(); + } catch (InvalidClassException e) { + // SONAR-4739 + if (!alreadyLoggedDeserializationIssue) { + logDeserializationIssue(); + alreadyLoggedDeserializationIssue = true; + } + return null; + } catch (IOException e) { + throw new SonarException(UNABLE_TO_READ_NOTIFICATION, e); + + } catch (ClassNotFoundException e) { + throw new SonarException(UNABLE_TO_READ_NOTIFICATION, e); + } + } + + @VisibleForTesting + void logDeserializationIssue() { + LOG.warn("It is impossible to send pending notifications which existed prior to the upgrade of SonarQube. They will be ignored."); + } + + public long count() { + return notificationQueueDao.count(); + } + + /** + * {@inheritDoc} + */ + @Override + public Multimap findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, @Nullable Integer resourceId) { + String dispatcherKey = dispatcher.getKey(); + + SetMultimap recipients = HashMultimap.create(); + for (NotificationChannel channel : notificationChannels) { + String channelKey = channel.getKey(); + + // Find users subscribed globally to the dispatcher (i.e. not on a specific project) + addUsersToRecipientListForChannel(propertiesDao.findUsersForNotification(dispatcherKey, channelKey, null), recipients, channel); + + if (resourceId != null) { + // Find users subscribed to the dispatcher specifically for the resource + addUsersToRecipientListForChannel(propertiesDao.findUsersForNotification(dispatcherKey, channelKey, resourceId.longValue()), recipients, channel); + } + } + + return recipients; + } + + @Override + public Multimap findNotificationSubscribers(NotificationDispatcher dispatcher, @Nullable String componentKey) { + String dispatcherKey = dispatcher.getKey(); + + SetMultimap recipients = HashMultimap.create(); + for (NotificationChannel channel : notificationChannels) { + addUsersToRecipientListForChannel(propertiesDao.findNotificationSubscribers(dispatcherKey, channel.getKey(), componentKey), recipients, channel); + } + + return recipients; + } + + @VisibleForTesting + protected List getChannels() { + return Arrays.asList(notificationChannels); + } + + private static void addUsersToRecipientListForChannel(List users, SetMultimap recipients, NotificationChannel channel) { + for (String username : users) { + recipients.put(username, channel); + } + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationCenter.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationCenter.java new file mode 100644 index 00000000000..b739a60edcd --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationCenter.java @@ -0,0 +1,99 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.collect.Lists; +import org.sonar.api.server.ServerSide; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import javax.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; + +/** + * @since 3.5 + */ +@ServerSide +public class NotificationCenter { + + private static final Logger LOG = Loggers.get(NotificationCenter.class); + + private final NotificationDispatcherMetadata[] dispatchersMetadata; + private final NotificationChannel[] channels; + + /** + * Constructor for {@link NotificationCenter} + */ + public NotificationCenter(NotificationDispatcherMetadata[] metadata, NotificationChannel[] channels) { + this.dispatchersMetadata = metadata; + this.channels = channels; + } + + /** + * Default constructor when no channels. + */ + public NotificationCenter(NotificationDispatcherMetadata[] metadata) { + this(metadata, new NotificationChannel[0]); + LOG.warn("There is no notification channel - no notification will be delivered!"); + } + + /** + * Default constructor when no dispatcher metadata. + */ + public NotificationCenter(NotificationChannel[] channels) { + this(new NotificationDispatcherMetadata[0], channels); + } + + /** + * Default constructor. + */ + public NotificationCenter() { + this(new NotificationDispatcherMetadata[0], new NotificationChannel[0]); + LOG.warn("There is no notification channel - no notification will be delivered!"); + } + + /** + * Returns all the available channels. + */ + public List getChannels() { + return Arrays.asList(channels); + } + + /** + * Returns all the available dispatchers which metadata matches the given property and its value. + *
+ * If "propertyValue" is null, the verification is done on the existence of such a property (whatever the value). + */ + public List getDispatcherKeysForProperty(String propertyKey, @Nullable String propertyValue) { + List keys = Lists.newArrayList(); + for (NotificationDispatcherMetadata metadata : dispatchersMetadata) { + String dispatcherKey = metadata.getDispatcherKey(); + String value = metadata.getProperty(propertyKey); + if (value != null && (propertyValue == null || value.equals(propertyValue))) { + keys.add(dispatcherKey); + } + } + return keys; + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDispatcher.java new file mode 100644 index 00000000000..3c99cbe0003 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDispatcher.java @@ -0,0 +1,130 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.apache.commons.lang.StringUtils; +import org.sonar.api.ExtensionPoint; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.api.server.ServerSide; + +/** + *

+ * Plugins should extend this class to provide logic to determine which users are interested in receiving notifications, + * along with which delivery channels they selected. + *

+ * For example: + *
    + *
  • notify me by email when someone comments an issue reported by me
  • + *
  • notify me by twitter when someone comments an issue assigned to me
  • + *
  • notify me by Jabber when someone mentions me in an issue comment
  • + *
  • send me by SMS when there are system notifications (like password reset, account creation, ...)
  • + *
+ * + * @since 2.10 + */ +@ServerSide +@ExtensionPoint +public abstract class NotificationDispatcher { + + private final String notificationType; + + /** + * Additional information related to the notification, which will be used + * to know who should receive the notification. + */ + public interface Context { + /** + * This method is not used any longer. Calling it will result in an {@link UnsupportedOperationException}. + * + * @deprecated Use {@link #addUser(String, NotificationChannel)} instead. + */ + @Deprecated + void addUser(String userLogin); + + /** + * Adds a user that will be notified through the given notification channel. + * + * @param userLogin the user login + * @param notificationChannel the notification channel to use for this user + */ + void addUser(String userLogin, NotificationChannel notificationChannel); + } + + /** + * Creates a new dispatcher for notifications of the given type. + * + * @param notificationType the type of notifications handled by this dispatcher + */ + public NotificationDispatcher(String notificationType) { + this.notificationType = notificationType; + } + + /** + * Creates a new generic dispatcher, used for any kind of notification. + *

+ * Should be avoided and replaced by the other constructor - as it is easier to understand that a + * dispatcher listens for a specific type of notification. + */ + public NotificationDispatcher() { + this(""); + } + + /** + * The unique key of this dispatcher. By default it's the class name without the package prefix. + *

+ * The related label in l10n bundles is 'notification.dispatcher.', for example 'notification.dispatcher.NewFalsePositive'. + */ + public String getKey() { + return getClass().getSimpleName(); + } + + /** + * @since 5.1 + */ + public String getType() { + return notificationType; + } + + /** + *

+ * Performs the dispatch. + *

+ */ + public final void performDispatch(Notification notification, Context context) { + if (StringUtils.equals(notification.getType(), notificationType) || StringUtils.equals("", notificationType)) { + dispatch(notification, context); + } + } + + /** + *

+ * Implements the logic that defines which users will receive the notification. + *

+ * The purpose of this method is to populate the context object with users, based on the type of notification and the content of the notification. + */ + public abstract void dispatch(Notification notification, Context context); + + @Override + public String toString() { + return getKey(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDispatcherMetadata.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDispatcherMetadata.java new file mode 100644 index 00000000000..de80ec7a2a7 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDispatcherMetadata.java @@ -0,0 +1,102 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.collect.Maps; +import java.util.Map; +import org.sonar.api.server.ServerSide; + +/** + *

+ * Notification dispatchers (see {@link NotificationDispatcher}) can define their own metadata class in order + * to tell more about them. + *

+ * Instances of these classes must be declared in {@link org.sonar.api.SonarPlugin#getExtensions()}. + * + * @since 3.5 + */ +@ServerSide +public final class NotificationDispatcherMetadata { + + public static final String GLOBAL_NOTIFICATION = "globalNotification"; + public static final String PER_PROJECT_NOTIFICATION = "perProjectNotification"; + + private String dispatcherKey; + private Map properties; + + private NotificationDispatcherMetadata(String dispatcherKey) { + this.dispatcherKey = dispatcherKey; + this.properties = Maps.newHashMap(); + } + + /** + * Creates a new metadata instance for the given dispatcher. + *

+ * By default the key is the class name without package. It can be changed by overriding + * {@link NotificationDispatcher#getKey()}. + */ + public static NotificationDispatcherMetadata create(String dispatcherKey) { + return new NotificationDispatcherMetadata(dispatcherKey); + } + + /** + * Sets a property on this metadata object. + */ + public NotificationDispatcherMetadata setProperty(String key, String value) { + properties.put(key, value); + return this; + } + + /** + * Gives the property for the given key. + */ + public String getProperty(String key) { + return properties.get(key); + } + + /** + * Returns the unique key of the dispatcher. + */ + public String getDispatcherKey() { + return dispatcherKey; + } + + @Override + public String toString() { + return dispatcherKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NotificationDispatcherMetadata that = (NotificationDispatcherMetadata) o; + return dispatcherKey.equals(that.dispatcherKey); + } + + @Override + public int hashCode() { + return dispatcherKey.hashCode(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationManager.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationManager.java new file mode 100644 index 00000000000..d499a8fcc5c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationManager.java @@ -0,0 +1,70 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.collect.Multimap; +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; + +/** + *

+ * The notification manager receives notifications and is in charge of storing them so that they are processed by the notification service. + *

+ *

+ * 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. + *

+ */ +public interface NotificationManager { + + /** + * 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 3.7.1 + */ + void scheduleForSending(List notifications); + + /** + *

+ * Returns the list of users who subscribed to the given dispatcher, along with the notification channels (email, twitter, ...) that they choose + * for this dispatcher. + *

+ *

+ * The resource ID can be null in case of notifications that have nothing to do with a specific project (like system notifications). + *

+ * + * @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 + */ + Multimap findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, @Nullable Integer resourceId); + + Multimap findNotificationSubscribers(NotificationDispatcher dispatcher, @Nullable String componentKey); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationService.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationService.java new file mode 100644 index 00000000000..b3951e8ee02 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationService.java @@ -0,0 +1,230 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.SetMultimap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.picocontainer.Startable; +import org.sonar.api.Properties; +import org.sonar.api.Property; +import org.sonar.api.server.ServerSide; +import org.sonar.api.config.Settings; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.jpa.session.DatabaseSessionFactory; +import org.sonar.server.db.DbClient; + +/** + * @since 2.10 + */ +@Properties({ + @Property( + key = NotificationService.PROPERTY_DELAY, + defaultValue = "60", + name = "Delay of notifications, in seconds", + project = false, + global = false), + @Property( + key = NotificationService.PROPERTY_DELAY_BEFORE_REPORTING_STATUS, + defaultValue = "600", + name = "Delay before reporting notification status, in seconds", + project = false, + global = false) +}) +@ServerSide +public class NotificationService implements Startable { + + private static final Logger LOG = Loggers.get(NotificationService.class); + + public static final String PROPERTY_DELAY = "sonar.notifications.delay"; + public static final String PROPERTY_DELAY_BEFORE_REPORTING_STATUS = "sonar.notifications.runningDelayBeforeReportingStatus"; + + private final long delayInSeconds; + private final long delayBeforeReportingStatusInSeconds; + private final DatabaseSessionFactory databaseSessionFactory; + private final DefaultNotificationManager manager; + private final List dispatchers; + private final DbClient dbClient; + + private ScheduledExecutorService executorService; + private boolean stopping = false; + + /** + * Constructor for {@link NotificationService} + */ + public NotificationService(Settings settings, DefaultNotificationManager manager, DbClient dbClient, + DatabaseSessionFactory databaseSessionFactory, NotificationDispatcher[] dispatchers) { + this.databaseSessionFactory = databaseSessionFactory; + this.delayInSeconds = settings.getLong(PROPERTY_DELAY); + this.delayBeforeReportingStatusInSeconds = settings.getLong(PROPERTY_DELAY_BEFORE_REPORTING_STATUS); + this.manager = manager; + this.dbClient = dbClient; + this.dispatchers = ImmutableList.copyOf(dispatchers); + } + + /** + * Default constructor when no dispatchers. + */ + public NotificationService(Settings settings, DefaultNotificationManager manager, DbClient dbClient, + DatabaseSessionFactory databaseSessionFactory) { + this(settings, manager, dbClient, databaseSessionFactory, new NotificationDispatcher[0]); + } + + @Override + public void start() { + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + try { + processQueue(); + } catch (Exception e) { + LOG.error("Error in NotificationService", e); + } finally { + // Free Hibernate session + // See https://jira.sonarsource.com/browse/SONAR-6566 + databaseSessionFactory.clear(); + } + } + }, 0, delayInSeconds, TimeUnit.SECONDS); + LOG.info("Notification service started (delay {} sec.)", delayInSeconds); + } + + @Override + public void stop() { + try { + stopping = true; + executorService.shutdown(); + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOG.error("Error during stop of notification service", e); + } + LOG.info("Notification service stopped"); + } + + @VisibleForTesting + synchronized void processQueue() { + long start = now(); + long lastLog = start; + long notifSentCount = 0; + + Notification notifToSend = manager.getFromQueue(); + while (notifToSend != null) { + deliver(notifToSend); + notifSentCount++; + if (stopping) { + break; + } + long now = now(); + if (now - lastLog > delayBeforeReportingStatusInSeconds * 1000) { + long remainingNotifCount = manager.count(); + lastLog = now; + long spentTimeInMinutes = (now - start) / (60 * 1000); + log(notifSentCount, remainingNotifCount, spentTimeInMinutes); + } + notifToSend = manager.getFromQueue(); + } + } + + @VisibleForTesting + void log(long notifSentCount, long remainingNotifCount, long spentTimeInMinutes) { + LOG.info("{} notifications sent during the past {} minutes and {} still waiting to be sent", + new Object[] {notifSentCount, spentTimeInMinutes, remainingNotifCount}); + } + + @VisibleForTesting + long now() { + return System.currentTimeMillis(); + } + + public void deliver(Notification notification) { + final SetMultimap recipients = HashMultimap.create(); + for (NotificationDispatcher dispatcher : dispatchers) { + NotificationDispatcher.Context context = new NotificationDispatcher.Context() { + @Override + public void addUser(String username) { + // This method is not used anymore + } + + @Override + public void addUser(String userLogin, NotificationChannel notificationChannel) { + if (userLogin != null) { + recipients.put(userLogin, notificationChannel); + } + } + }; + try { + dispatcher.performDispatch(notification, context); + } catch (Exception e) { + // catch all exceptions in order to dispatch using other dispatchers + LOG.warn("Unable to dispatch notification " + notification + " using " + dispatcher, e); + } + } + dispatch(notification, recipients); + } + + private void dispatch(Notification notification, SetMultimap recipients) { + for (Map.Entry> entry : recipients.asMap().entrySet()) { + String username = entry.getKey(); + Collection userChannels = entry.getValue(); + LOG.debug("For user {} via {}", username, userChannels); + for (NotificationChannel channel : userChannels) { + try { + channel.deliver(notification, username); + } catch (Exception e) { + // catch all exceptions in order to deliver via other channels + LOG.warn("Unable to deliver notification " + notification + " for user " + username + " via " + channel, e); + } + } + } + } + + @VisibleForTesting + protected List getDispatchers() { + return dispatchers; + } + + /** + * Returns true if at least one user is subscribed to at least one notifications with given types. + * Subscription can be globally or on the specific project. + */ + public boolean hasProjectSubscribersForTypes(String projectUuid, Set notificationTypes) { + Collection dispatcherKeys = new ArrayList<>(); + for (NotificationDispatcher dispatcher : dispatchers) { + if (notificationTypes.contains(dispatcher.getType())) { + dispatcherKeys.add(dispatcher.getKey()); + } + } + + return dbClient.propertiesDao().hasProjectNotificationSubscribersForDispatchers(projectUuid, dispatcherKeys); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/email/AlertsEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/notification/email/AlertsEmailTemplate.java new file mode 100644 index 00000000000..77a53501929 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/email/AlertsEmailTemplate.java @@ -0,0 +1,107 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification.email; + +import org.apache.commons.lang.StringUtils; +import org.sonar.api.config.EmailSettings; +import org.sonar.api.measures.Metric; +import org.sonar.api.notifications.Notification; +import org.sonar.plugins.emailnotifications.api.EmailMessage; +import org.sonar.plugins.emailnotifications.api.EmailTemplate; + +/** + * Creates email message for notification "alerts". + * + * @since 3.5 + */ +public class AlertsEmailTemplate extends EmailTemplate { + + private EmailSettings configuration; + + public AlertsEmailTemplate(EmailSettings configuration) { + this.configuration = configuration; + } + + @Override + public EmailMessage format(Notification notification) { + if (!"alerts".equals(notification.getType())) { + return null; + } + + // Retrieve useful values + String projectId = notification.getFieldValue("projectId"); + String projectKey = notification.getFieldValue("projectKey"); + String projectName = notification.getFieldValue("projectName"); + String alertName = notification.getFieldValue("alertName"); + String alertText = notification.getFieldValue("alertText"); + String alertLevel = notification.getFieldValue("alertLevel"); + boolean isNewAlert = Boolean.parseBoolean(notification.getFieldValue("isNewAlert")); + + // Generate text + String subject = generateSubject(projectName, alertLevel, isNewAlert); + String messageBody = generateMessageBody(projectName, projectKey, alertName, alertText, isNewAlert); + + // And finally return the email that will be sent + return new EmailMessage() + .setMessageId("alerts/" + projectId) + .setSubject(subject) + .setMessage(messageBody); + } + + private static String generateSubject(String projectName, String alertLevel, boolean isNewAlert) { + StringBuilder subjectBuilder = new StringBuilder(); + if (Metric.Level.OK.toString().equals(alertLevel)) { + subjectBuilder.append("\"").append(projectName).append("\" is back to green"); + } else if (isNewAlert) { + subjectBuilder.append("New quality gate threshold reached on \"").append(projectName).append("\""); + } else { + subjectBuilder.append("Quality gate status changed on \"").append(projectName).append("\""); + } + return subjectBuilder.toString(); + } + + private String generateMessageBody(String projectName, String projectKey, String alertName, String alertText, boolean isNewAlert) { + StringBuilder messageBody = new StringBuilder(); + messageBody.append("Project: ").append(projectName).append("\n"); + messageBody.append("Quality gate status: ").append(alertName).append("\n\n"); + + String[] alerts = StringUtils.split(alertText, ","); + if (alerts.length > 0) { + if (isNewAlert) { + messageBody.append("New quality gate threshold"); + } else { + messageBody.append("Quality gate threshold"); + } + if (alerts.length == 1) { + messageBody.append(": ").append(alerts[0].trim()).append("\n"); + } else { + messageBody.append("s:\n"); + for (String alert : alerts) { + messageBody.append(" - ").append(alert.trim()).append("\n"); + } + } + } + + messageBody.append("\n").append("See it in SonarQube: ").append(configuration.getServerBaseURL()).append("/dashboard/index/").append(projectKey); + + return messageBody.toString(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java b/server/sonar-server/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java new file mode 100644 index 00000000000..de7dc0f81ea --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java @@ -0,0 +1,225 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification.email; + +import java.net.MalformedURLException; +import java.net.URL; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.SimpleEmail; +import org.sonar.api.config.EmailSettings; +import org.sonar.api.database.model.User; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.api.security.UserFinder; +import org.sonar.api.utils.SonarException; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.plugins.emailnotifications.api.EmailMessage; +import org.sonar.plugins.emailnotifications.api.EmailTemplate; + +/** + * References: + * + * + * @since 2.10 + */ +public class EmailNotificationChannel extends NotificationChannel { + + private static final Logger LOG = Loggers.get(EmailNotificationChannel.class); + + /** + * @see org.apache.commons.mail.Email#setSocketConnectionTimeout(int) + * @see org.apache.commons.mail.Email#setSocketTimeout(int) + */ + private static final int SOCKET_TIMEOUT = 30000; + + /** + * Email Header Field: "List-ID". + * Value of this field should contain mailing list identifier as specified in RFC 2919. + */ + private static final String LIST_ID_HEADER = "List-ID"; + + /** + * Email Header Field: "List-Archive". + * Value of this field should contain URL of mailing list archive as specified in RFC 2369. + */ + private static final String LIST_ARCHIVE_HEADER = "List-Archive"; + + /** + * Email Header Field: "In-Reply-To". + * Value of this field should contain related message identifier as specified in RFC 2822. + */ + private static final String IN_REPLY_TO_HEADER = "In-Reply-To"; + + /** + * Email Header Field: "References". + * Value of this field should contain related message identifier as specified in RFC 2822 + */ + private static final String REFERENCES_HEADER = "References"; + + private static final String FROM_NAME_DEFAULT = "SonarQube"; + private static final String SUBJECT_DEFAULT = "Notification"; + + private EmailSettings configuration; + private EmailTemplate[] templates; + private UserFinder userFinder; + + public EmailNotificationChannel(EmailSettings configuration, EmailTemplate[] templates, UserFinder userFinder) { + this.configuration = configuration; + this.templates = templates; + this.userFinder = userFinder; + } + + @Override + public void deliver(Notification notification, String username) { + User user = userFinder.findByLogin(username); + if (StringUtils.isBlank(user.getEmail())) { + LOG.debug("Email not defined for user: " + username); + return; + } + EmailMessage emailMessage = format(notification); + if (emailMessage != null) { + emailMessage.setTo(user.getEmail()); + deliver(emailMessage); + } + } + + private EmailMessage format(Notification notification) { + for (EmailTemplate template : templates) { + EmailMessage email = template.format(notification); + if (email != null) { + return email; + } + } + LOG.warn("Email template not found for notification: {}", notification); + return null; + } + + /** + * Visibility has been relaxed for tests. + */ + void deliver(EmailMessage emailMessage) { + if (StringUtils.isBlank(configuration.getSmtpHost())) { + LOG.debug("SMTP host was not configured - email will not be sent"); + return; + } + try { + send(emailMessage); + } catch (EmailException e) { + LOG.error("Unable to send email", e); + } + } + + private void send(EmailMessage emailMessage) throws EmailException { + // Trick to correctly initilize javax.mail library + ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + + try { + LOG.debug("Sending email: {}", emailMessage); + String host = null; + try { + host = new URL(configuration.getServerBaseURL()).getHost(); + } catch (MalformedURLException e) { + // ignore + } + + SimpleEmail email = new SimpleEmail(); + if (StringUtils.isNotBlank(host)) { + /* + * Set headers for proper threading: GMail will not group messages, even if they have same subject, but don't have "In-Reply-To" and + * "References" headers. TODO investigate threading in other clients like KMail, Thunderbird, Outlook + */ + if (StringUtils.isNotEmpty(emailMessage.getMessageId())) { + String messageId = "<" + emailMessage.getMessageId() + "@" + host + ">"; + email.addHeader(IN_REPLY_TO_HEADER, messageId); + email.addHeader(REFERENCES_HEADER, messageId); + } + // Set headers for proper filtering + email.addHeader(LIST_ID_HEADER, "SonarQube "); + email.addHeader(LIST_ARCHIVE_HEADER, configuration.getServerBaseURL()); + } + // Set general information + email.setCharset("UTF-8"); + String from = StringUtils.isBlank(emailMessage.getFrom()) ? FROM_NAME_DEFAULT : (emailMessage.getFrom() + " (SonarQube)"); + email.setFrom(configuration.getFrom(), from); + email.addTo(emailMessage.getTo(), " "); + String subject = StringUtils.defaultIfBlank(StringUtils.trimToEmpty(configuration.getPrefix()) + " ", "") + + StringUtils.defaultString(emailMessage.getSubject(), SUBJECT_DEFAULT); + email.setSubject(subject); + email.setMsg(emailMessage.getMessage()); + // Send + email.setHostName(configuration.getSmtpHost()); + configureSecureConnection(email); + if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) { + email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword()); + } + email.setSocketConnectionTimeout(SOCKET_TIMEOUT); + email.setSocketTimeout(SOCKET_TIMEOUT); + email.send(); + + } finally { + Thread.currentThread().setContextClassLoader(classloader); + } + } + + private void configureSecureConnection(SimpleEmail email) { + if (StringUtils.equalsIgnoreCase(configuration.getSecureConnection(), "ssl")) { + email.setSSLOnConnect(true); + email.setSslSmtpPort(String.valueOf(configuration.getSmtpPort())); + + // this port is not used except in EmailException message, that's why it's set with the same value than SSL port. + // It prevents from getting bad message. + email.setSmtpPort(configuration.getSmtpPort()); + } else if (StringUtils.equalsIgnoreCase(configuration.getSecureConnection(), "starttls")) { + email.setStartTLSEnabled(true); + email.setStartTLSRequired(true); + email.setSmtpPort(configuration.getSmtpPort()); + } else if (StringUtils.isBlank(configuration.getSecureConnection())) { + email.setSmtpPort(configuration.getSmtpPort()); + } else { + throw new SonarException("Unknown type of SMTP secure connection: " + configuration.getSecureConnection()); + } + } + + /** + * Send test email. This method called from Ruby. + * + * @throws EmailException when unable to send + */ + public void sendTestEmail(String toAddress, String subject, String message) throws EmailException { + try { + EmailMessage emailMessage = new EmailMessage(); + emailMessage.setTo(toAddress); + emailMessage.setSubject(subject); + emailMessage.setMessage(message); + send(emailMessage); + } catch (EmailException e) { + LOG.error("Fail to send test email to: " + toAddress, e); + throw e; + } + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/email/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/notification/email/package-info.java new file mode 100644 index 00000000000..f583fa78310 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/email/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.notification.email; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/notification/package-info.java new file mode 100644 index 00000000000..71e0748a18c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +@ParametersAreNonnullByDefault +package org.sonar.server.notification; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationCenter.java b/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationCenter.java deleted file mode 100644 index cf43dca0c31..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationCenter.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications; - -import com.google.common.collect.Lists; -import org.sonar.api.server.ServerSide; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.NotificationDispatcherMetadata; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; - -import javax.annotation.Nullable; - -import java.util.Arrays; -import java.util.List; - -/** - * @since 3.5 - */ -@ServerSide -public class NotificationCenter { - - private static final Logger LOG = Loggers.get(NotificationCenter.class); - - private final NotificationDispatcherMetadata[] dispatchersMetadata; - private final NotificationChannel[] channels; - - /** - * Constructor for {@link NotificationCenter} - */ - public NotificationCenter(NotificationDispatcherMetadata[] metadata, NotificationChannel[] channels) { - this.dispatchersMetadata = metadata; - this.channels = channels; - } - - /** - * Default constructor when no channels. - */ - public NotificationCenter(NotificationDispatcherMetadata[] metadata) { - this(metadata, new NotificationChannel[0]); - LOG.warn("There is no notification channel - no notification will be delivered!"); - } - - /** - * Default constructor when no dispatcher metadata. - */ - public NotificationCenter(NotificationChannel[] channels) { - this(new NotificationDispatcherMetadata[0], channels); - } - - /** - * Default constructor. - */ - public NotificationCenter() { - this(new NotificationDispatcherMetadata[0], new NotificationChannel[0]); - LOG.warn("There is no notification channel - no notification will be delivered!"); - } - - /** - * Returns all the available channels. - */ - public List getChannels() { - return Arrays.asList(channels); - } - - /** - * Returns all the available dispatchers which metadata matches the given property and its value. - *
- * If "propertyValue" is null, the verification is done on the existence of such a property (whatever the value). - */ - public List getDispatcherKeysForProperty(String propertyKey, @Nullable String propertyValue) { - List keys = Lists.newArrayList(); - for (NotificationDispatcherMetadata metadata : dispatchersMetadata) { - String dispatcherKey = metadata.getDispatcherKey(); - String value = metadata.getProperty(propertyKey); - if (value != null && (propertyValue == null || value.equals(propertyValue))) { - keys.add(dispatcherKey); - } - } - return keys; - } - -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationService.java b/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationService.java deleted file mode 100644 index e33af89334d..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/notifications/NotificationService.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.SetMultimap; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import org.picocontainer.Startable; -import org.sonar.api.Properties; -import org.sonar.api.Property; -import org.sonar.api.server.ServerSide; -import org.sonar.api.config.Settings; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; -import org.sonar.core.notification.DefaultNotificationManager; -import org.sonar.jpa.session.DatabaseSessionFactory; -import org.sonar.server.db.DbClient; - -/** - * @since 2.10 - */ -@Properties({ - @Property( - key = NotificationService.PROPERTY_DELAY, - defaultValue = "60", - name = "Delay of notifications, in seconds", - project = false, - global = false), - @Property( - key = NotificationService.PROPERTY_DELAY_BEFORE_REPORTING_STATUS, - defaultValue = "600", - name = "Delay before reporting notification status, in seconds", - project = false, - global = false) -}) -@ServerSide -public class NotificationService implements Startable { - - private static final Logger LOG = Loggers.get(NotificationService.class); - - public static final String PROPERTY_DELAY = "sonar.notifications.delay"; - public static final String PROPERTY_DELAY_BEFORE_REPORTING_STATUS = "sonar.notifications.runningDelayBeforeReportingStatus"; - - private final long delayInSeconds; - private final long delayBeforeReportingStatusInSeconds; - private final DatabaseSessionFactory databaseSessionFactory; - private final DefaultNotificationManager manager; - private final List dispatchers; - private final DbClient dbClient; - - private ScheduledExecutorService executorService; - private boolean stopping = false; - - /** - * Constructor for {@link NotificationService} - */ - public NotificationService(Settings settings, DefaultNotificationManager manager, DbClient dbClient, - DatabaseSessionFactory databaseSessionFactory, NotificationDispatcher[] dispatchers) { - this.databaseSessionFactory = databaseSessionFactory; - this.delayInSeconds = settings.getLong(PROPERTY_DELAY); - this.delayBeforeReportingStatusInSeconds = settings.getLong(PROPERTY_DELAY_BEFORE_REPORTING_STATUS); - this.manager = manager; - this.dbClient = dbClient; - this.dispatchers = ImmutableList.copyOf(dispatchers); - } - - /** - * Default constructor when no dispatchers. - */ - public NotificationService(Settings settings, DefaultNotificationManager manager, DbClient dbClient, - DatabaseSessionFactory databaseSessionFactory) { - this(settings, manager, dbClient, databaseSessionFactory, new NotificationDispatcher[0]); - } - - @Override - public void start() { - executorService = Executors.newSingleThreadScheduledExecutor(); - executorService.scheduleWithFixedDelay(new Runnable() { - @Override - public void run() { - try { - processQueue(); - } catch (Exception e) { - LOG.error("Error in NotificationService", e); - } finally { - // Free Hibernate session - // See https://jira.sonarsource.com/browse/SONAR-6566 - databaseSessionFactory.clear(); - } - } - }, 0, delayInSeconds, TimeUnit.SECONDS); - LOG.info("Notification service started (delay {} sec.)", delayInSeconds); - } - - @Override - public void stop() { - try { - stopping = true; - executorService.shutdown(); - executorService.awaitTermination(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - LOG.error("Error during stop of notification service", e); - } - LOG.info("Notification service stopped"); - } - - @VisibleForTesting - synchronized void processQueue() { - long start = now(); - long lastLog = start; - long notifSentCount = 0; - - Notification notifToSend = manager.getFromQueue(); - while (notifToSend != null) { - deliver(notifToSend); - notifSentCount++; - if (stopping) { - break; - } - long now = now(); - if (now - lastLog > delayBeforeReportingStatusInSeconds * 1000) { - long remainingNotifCount = manager.count(); - lastLog = now; - long spentTimeInMinutes = (now - start) / (60 * 1000); - log(notifSentCount, remainingNotifCount, spentTimeInMinutes); - } - notifToSend = manager.getFromQueue(); - } - } - - @VisibleForTesting - void log(long notifSentCount, long remainingNotifCount, long spentTimeInMinutes) { - LOG.info("{} notifications sent during the past {} minutes and {} still waiting to be sent", - new Object[] {notifSentCount, spentTimeInMinutes, remainingNotifCount}); - } - - @VisibleForTesting - long now() { - return System.currentTimeMillis(); - } - - public void deliver(Notification notification) { - final SetMultimap recipients = HashMultimap.create(); - for (NotificationDispatcher dispatcher : dispatchers) { - NotificationDispatcher.Context context = new NotificationDispatcher.Context() { - @Override - public void addUser(String username) { - // This method is not used anymore - } - - @Override - public void addUser(String userLogin, NotificationChannel notificationChannel) { - if (userLogin != null) { - recipients.put(userLogin, notificationChannel); - } - } - }; - try { - dispatcher.performDispatch(notification, context); - } catch (Exception e) { - // catch all exceptions in order to dispatch using other dispatchers - LOG.warn("Unable to dispatch notification " + notification + " using " + dispatcher, e); - } - } - dispatch(notification, recipients); - } - - private void dispatch(Notification notification, SetMultimap recipients) { - for (Map.Entry> entry : recipients.asMap().entrySet()) { - String username = entry.getKey(); - Collection userChannels = entry.getValue(); - LOG.debug("For user {} via {}", username, userChannels); - for (NotificationChannel channel : userChannels) { - try { - channel.deliver(notification, username); - } catch (Exception e) { - // catch all exceptions in order to deliver via other channels - LOG.warn("Unable to deliver notification " + notification + " for user " + username + " via " + channel, e); - } - } - } - } - - @VisibleForTesting - protected List getDispatchers() { - return dispatchers; - } - - /** - * Returns true if at least one user is subscribed to at least one notifications with given types. - * Subscription can be globally or on the specific project. - */ - public boolean hasProjectSubscribersForTypes(String projectUuid, Set notificationTypes) { - Collection dispatcherKeys = new ArrayList<>(); - for (NotificationDispatcher dispatcher : dispatchers) { - if (notificationTypes.contains(dispatcher.getType())) { - dispatcherKeys.add(dispatcher.getKey()); - } - } - - return dbClient.propertiesDao().hasProjectNotificationSubscribersForDispatchers(projectUuid, dispatcherKeys); - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notifications/email/AlertsEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/notifications/email/AlertsEmailTemplate.java deleted file mode 100644 index 0ea9c3c5544..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/notifications/email/AlertsEmailTemplate.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications.email; - -import org.apache.commons.lang.StringUtils; -import org.sonar.api.config.EmailSettings; -import org.sonar.api.measures.Metric; -import org.sonar.api.notifications.Notification; -import org.sonar.plugins.emailnotifications.api.EmailMessage; -import org.sonar.plugins.emailnotifications.api.EmailTemplate; - -/** - * Creates email message for notification "alerts". - * - * @since 3.5 - */ -public class AlertsEmailTemplate extends EmailTemplate { - - private EmailSettings configuration; - - public AlertsEmailTemplate(EmailSettings configuration) { - this.configuration = configuration; - } - - @Override - public EmailMessage format(Notification notification) { - if (!"alerts".equals(notification.getType())) { - return null; - } - - // Retrieve useful values - String projectId = notification.getFieldValue("projectId"); - String projectKey = notification.getFieldValue("projectKey"); - String projectName = notification.getFieldValue("projectName"); - String alertName = notification.getFieldValue("alertName"); - String alertText = notification.getFieldValue("alertText"); - String alertLevel = notification.getFieldValue("alertLevel"); - boolean isNewAlert = Boolean.parseBoolean(notification.getFieldValue("isNewAlert")); - - // Generate text - String subject = generateSubject(projectName, alertLevel, isNewAlert); - String messageBody = generateMessageBody(projectName, projectKey, alertName, alertText, isNewAlert); - - // And finally return the email that will be sent - return new EmailMessage() - .setMessageId("alerts/" + projectId) - .setSubject(subject) - .setMessage(messageBody); - } - - private static String generateSubject(String projectName, String alertLevel, boolean isNewAlert) { - StringBuilder subjectBuilder = new StringBuilder(); - if (Metric.Level.OK.toString().equals(alertLevel)) { - subjectBuilder.append("\"").append(projectName).append("\" is back to green"); - } else if (isNewAlert) { - subjectBuilder.append("New quality gate threshold reached on \"").append(projectName).append("\""); - } else { - subjectBuilder.append("Quality gate status changed on \"").append(projectName).append("\""); - } - return subjectBuilder.toString(); - } - - private String generateMessageBody(String projectName, String projectKey, String alertName, String alertText, boolean isNewAlert) { - StringBuilder messageBody = new StringBuilder(); - messageBody.append("Project: ").append(projectName).append("\n"); - messageBody.append("Quality gate status: ").append(alertName).append("\n\n"); - - String[] alerts = StringUtils.split(alertText, ","); - if (alerts.length > 0) { - if (isNewAlert) { - messageBody.append("New quality gate threshold"); - } else { - messageBody.append("Quality gate threshold"); - } - if (alerts.length == 1) { - messageBody.append(": ").append(alerts[0].trim()).append("\n"); - } else { - messageBody.append("s:\n"); - for (String alert : alerts) { - messageBody.append(" - ").append(alert.trim()).append("\n"); - } - } - } - - messageBody.append("\n").append("See it in SonarQube: ").append(configuration.getServerBaseURL()).append("/dashboard/index/").append(projectKey); - - return messageBody.toString(); - } - -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notifications/email/EmailNotificationChannel.java b/server/sonar-server/src/main/java/org/sonar/server/notifications/email/EmailNotificationChannel.java deleted file mode 100644 index 3b96270736b..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/notifications/email/EmailNotificationChannel.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications.email; - -import java.net.MalformedURLException; -import java.net.URL; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.mail.EmailException; -import org.apache.commons.mail.SimpleEmail; -import org.sonar.api.config.EmailSettings; -import org.sonar.api.database.model.User; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.api.security.UserFinder; -import org.sonar.api.utils.SonarException; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; -import org.sonar.plugins.emailnotifications.api.EmailMessage; -import org.sonar.plugins.emailnotifications.api.EmailTemplate; - -/** - * References: - * - * - * @since 2.10 - */ -public class EmailNotificationChannel extends NotificationChannel { - - private static final Logger LOG = Loggers.get(EmailNotificationChannel.class); - - /** - * @see org.apache.commons.mail.Email#setSocketConnectionTimeout(int) - * @see org.apache.commons.mail.Email#setSocketTimeout(int) - */ - private static final int SOCKET_TIMEOUT = 30000; - - /** - * Email Header Field: "List-ID". - * Value of this field should contain mailing list identifier as specified in RFC 2919. - */ - private static final String LIST_ID_HEADER = "List-ID"; - - /** - * Email Header Field: "List-Archive". - * Value of this field should contain URL of mailing list archive as specified in RFC 2369. - */ - private static final String LIST_ARCHIVE_HEADER = "List-Archive"; - - /** - * Email Header Field: "In-Reply-To". - * Value of this field should contain related message identifier as specified in RFC 2822. - */ - private static final String IN_REPLY_TO_HEADER = "In-Reply-To"; - - /** - * Email Header Field: "References". - * Value of this field should contain related message identifier as specified in RFC 2822 - */ - private static final String REFERENCES_HEADER = "References"; - - private static final String FROM_NAME_DEFAULT = "SonarQube"; - private static final String SUBJECT_DEFAULT = "Notification"; - - private EmailSettings configuration; - private EmailTemplate[] templates; - private UserFinder userFinder; - - public EmailNotificationChannel(EmailSettings configuration, EmailTemplate[] templates, UserFinder userFinder) { - this.configuration = configuration; - this.templates = templates; - this.userFinder = userFinder; - } - - @Override - public void deliver(Notification notification, String username) { - User user = userFinder.findByLogin(username); - if (StringUtils.isBlank(user.getEmail())) { - LOG.debug("Email not defined for user: " + username); - return; - } - EmailMessage emailMessage = format(notification); - if (emailMessage != null) { - emailMessage.setTo(user.getEmail()); - deliver(emailMessage); - } - } - - private EmailMessage format(Notification notification) { - for (EmailTemplate template : templates) { - EmailMessage email = template.format(notification); - if (email != null) { - return email; - } - } - LOG.warn("Email template not found for notification: {}", notification); - return null; - } - - /** - * Visibility has been relaxed for tests. - */ - void deliver(EmailMessage emailMessage) { - if (StringUtils.isBlank(configuration.getSmtpHost())) { - LOG.debug("SMTP host was not configured - email will not be sent"); - return; - } - try { - send(emailMessage); - } catch (EmailException e) { - LOG.error("Unable to send email", e); - } - } - - private void send(EmailMessage emailMessage) throws EmailException { - // Trick to correctly initilize javax.mail library - ClassLoader classloader = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); - - try { - LOG.debug("Sending email: {}", emailMessage); - String host = null; - try { - host = new URL(configuration.getServerBaseURL()).getHost(); - } catch (MalformedURLException e) { - // ignore - } - - SimpleEmail email = new SimpleEmail(); - if (StringUtils.isNotBlank(host)) { - /* - * Set headers for proper threading: GMail will not group messages, even if they have same subject, but don't have "In-Reply-To" and - * "References" headers. TODO investigate threading in other clients like KMail, Thunderbird, Outlook - */ - if (StringUtils.isNotEmpty(emailMessage.getMessageId())) { - String messageId = "<" + emailMessage.getMessageId() + "@" + host + ">"; - email.addHeader(IN_REPLY_TO_HEADER, messageId); - email.addHeader(REFERENCES_HEADER, messageId); - } - // Set headers for proper filtering - email.addHeader(LIST_ID_HEADER, "SonarQube "); - email.addHeader(LIST_ARCHIVE_HEADER, configuration.getServerBaseURL()); - } - // Set general information - email.setCharset("UTF-8"); - String from = StringUtils.isBlank(emailMessage.getFrom()) ? FROM_NAME_DEFAULT : (emailMessage.getFrom() + " (SonarQube)"); - email.setFrom(configuration.getFrom(), from); - email.addTo(emailMessage.getTo(), " "); - String subject = StringUtils.defaultIfBlank(StringUtils.trimToEmpty(configuration.getPrefix()) + " ", "") - + StringUtils.defaultString(emailMessage.getSubject(), SUBJECT_DEFAULT); - email.setSubject(subject); - email.setMsg(emailMessage.getMessage()); - // Send - email.setHostName(configuration.getSmtpHost()); - configureSecureConnection(email); - if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) { - email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword()); - } - email.setSocketConnectionTimeout(SOCKET_TIMEOUT); - email.setSocketTimeout(SOCKET_TIMEOUT); - email.send(); - - } finally { - Thread.currentThread().setContextClassLoader(classloader); - } - } - - private void configureSecureConnection(SimpleEmail email) { - if (StringUtils.equalsIgnoreCase(configuration.getSecureConnection(), "ssl")) { - email.setSSLOnConnect(true); - email.setSslSmtpPort(String.valueOf(configuration.getSmtpPort())); - - // this port is not used except in EmailException message, that's why it's set with the same value than SSL port. - // It prevents from getting bad message. - email.setSmtpPort(configuration.getSmtpPort()); - } else if (StringUtils.equalsIgnoreCase(configuration.getSecureConnection(), "starttls")) { - email.setStartTLSEnabled(true); - email.setStartTLSRequired(true); - email.setSmtpPort(configuration.getSmtpPort()); - } else if (StringUtils.isBlank(configuration.getSecureConnection())) { - email.setSmtpPort(configuration.getSmtpPort()); - } else { - throw new SonarException("Unknown type of SMTP secure connection: " + configuration.getSecureConnection()); - } - } - - /** - * Send test email. This method called from Ruby. - * - * @throws EmailException when unable to send - */ - public void sendTestEmail(String toAddress, String subject, String message) throws EmailException { - try { - EmailMessage emailMessage = new EmailMessage(); - emailMessage.setTo(toAddress); - emailMessage.setSubject(subject); - emailMessage.setMessage(message); - send(emailMessage); - } catch (EmailException e) { - LOG.error("Fail to send test email to: " + toAddress, e); - throw e; - } - } - -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notifications/email/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/notifications/email/package-info.java deleted file mode 100644 index a81d297b0c7..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/notifications/email/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -@ParametersAreNonnullByDefault -package org.sonar.server.notifications.email; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/notifications/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/notifications/package-info.java deleted file mode 100644 index 34262049a99..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/notifications/package-info.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -@ParametersAreNonnullByDefault -package org.sonar.server.notifications; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index ca0260e5dea..21f69f49d65 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -38,7 +38,7 @@ import org.sonar.core.issue.IssueUpdater; import org.sonar.core.issue.workflow.FunctionExecutor; import org.sonar.core.issue.workflow.IssueWorkflow; import org.sonar.core.metric.DefaultMetricFinder; -import org.sonar.core.notification.DefaultNotificationManager; +import org.sonar.server.notification.DefaultNotificationManager; import org.sonar.core.permission.PermissionFacade; import org.sonar.core.qualitygate.db.ProjectQgateAssociationDao; import org.sonar.core.qualitygate.db.QualityGateConditionDao; @@ -171,10 +171,10 @@ import org.sonar.server.measure.ws.ManualMeasuresWs; import org.sonar.server.measure.ws.TimeMachineWs; import org.sonar.server.metric.CoreCustomMetrics; import org.sonar.server.metric.ws.MetricsWsModule; -import org.sonar.server.notifications.NotificationCenter; -import org.sonar.server.notifications.NotificationService; -import org.sonar.server.notifications.email.AlertsEmailTemplate; -import org.sonar.server.notifications.email.EmailNotificationChannel; +import org.sonar.server.notification.NotificationCenter; +import org.sonar.server.notification.NotificationService; +import org.sonar.server.notification.email.AlertsEmailTemplate; +import org.sonar.server.notification.email.EmailNotificationChannel; import org.sonar.server.permission.InternalPermissionService; import org.sonar.server.permission.InternalPermissionTemplateService; import org.sonar.server.permission.PermissionFinder; diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java index 7fb81786e6c..a66e2c4085e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java @@ -40,7 +40,7 @@ import org.sonar.server.computation.issue.RuleCache; import org.sonar.server.issue.notification.IssueChangeNotification; import org.sonar.server.issue.notification.NewIssuesNotification; import org.sonar.server.issue.notification.NewIssuesNotificationFactory; -import org.sonar.server.notifications.NotificationService; +import org.sonar.server.notification.NotificationService; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; diff --git a/server/sonar-server/src/test/java/org/sonar/server/event/NewAlertsTest.java b/server/sonar-server/src/test/java/org/sonar/server/event/NewAlertsTest.java index 49bb6f7e6f0..d2b2f653eb9 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/event/NewAlertsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/event/NewAlertsTest.java @@ -24,8 +24,8 @@ import com.google.common.collect.Multimap; import org.junit.Test; import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationManager; import static org.mockito.Mockito.*; diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java index fdcefadb1ba..a11fd69465c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationDispatcherTest.java @@ -27,9 +27,9 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.sonar.api.notifications.*; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationDispatcherMetadata; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/DoNotFixNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/DoNotFixNotificationDispatcherTest.java index 4f42809dfb7..91061b4cb7e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/DoNotFixNotificationDispatcherTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/DoNotFixNotificationDispatcherTest.java @@ -25,9 +25,9 @@ import org.junit.Test; import org.sonar.api.issue.Issue; import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationDispatcherMetadata; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcherTest.java index 9173ef7227c..b697cdfe159 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcherTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcherTest.java @@ -26,8 +26,8 @@ import org.junit.Before; import org.junit.Test; import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationManager; import static org.mockito.Mockito.*; diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java index 656d3bb4231..ec975b52966 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java @@ -25,8 +25,8 @@ import org.junit.Before; import org.junit.Test; import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.NotificationManager; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationManager; import static org.mockito.Mockito.*; diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/DefaultNotificationManagerTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/DefaultNotificationManagerTest.java new file mode 100644 index 00000000000..e534646911c --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/DefaultNotificationManagerTest.java @@ -0,0 +1,180 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.core.notification.db.NotificationQueueDao; +import org.sonar.core.notification.db.NotificationQueueDto; +import org.sonar.core.properties.PropertiesDao; +import org.sonar.jpa.test.AbstractDbUnitTestCase; + +import java.io.InvalidClassException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DefaultNotificationManagerTest extends AbstractDbUnitTestCase { + + private DefaultNotificationManager manager; + + @Mock + private PropertiesDao propertiesDao; + + @Mock + private NotificationDispatcher dispatcher; + + @Mock + private NotificationChannel emailChannel; + + @Mock + private NotificationChannel twitterChannel; + + @Mock + private NotificationQueueDao notificationQueueDao; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(dispatcher.getKey()).thenReturn("NewViolations"); + when(emailChannel.getKey()).thenReturn("Email"); + when(twitterChannel.getKey()).thenReturn("Twitter"); + + manager = new DefaultNotificationManager(new NotificationChannel[] {emailChannel, twitterChannel}, notificationQueueDao, propertiesDao); + } + + @Test + public void shouldProvideChannelList() { + assertThat(manager.getChannels()).containsOnly(emailChannel, twitterChannel); + + manager = new DefaultNotificationManager(notificationQueueDao, propertiesDao); + assertThat(manager.getChannels()).hasSize(0); + } + + @Test + public void shouldPersist() { + Notification notification = new Notification("test"); + manager.scheduleForSending(notification); + + verify(notificationQueueDao, only()).insert(any(List.class)); + } + + @Test + public void shouldGetFromQueueAndDelete() { + Notification notification = new Notification("test"); + NotificationQueueDto dto = NotificationQueueDto.toNotificationQueueDto(notification); + List dtos = Arrays.asList(dto); + when(notificationQueueDao.findOldest(1)).thenReturn(dtos); + + assertThat(manager.getFromQueue()).isNotNull(); + + InOrder inOrder = inOrder(notificationQueueDao); + inOrder.verify(notificationQueueDao).findOldest(1); + inOrder.verify(notificationQueueDao).delete(dtos); + } + + // SONAR-4739 + @Test + public void shouldNotFailWhenUnableToDeserialize() throws Exception { + NotificationQueueDto dto1 = mock(NotificationQueueDto.class); + when(dto1.toNotification()).thenThrow(new InvalidClassException("Pouet")); + List dtos = Arrays.asList(dto1); + when(notificationQueueDao.findOldest(1)).thenReturn(dtos); + + manager = spy(manager); + assertThat(manager.getFromQueue()).isNull(); + assertThat(manager.getFromQueue()).isNull(); + + verify(manager, times(1)).logDeserializationIssue(); + } + + @Test + public void shouldFindNoRecipient() { + assertThat(manager.findSubscribedRecipientsForDispatcher(dispatcher, 45).asMap().entrySet()).hasSize(0); + } + + @Test + public void shouldFindSubscribedRecipientForGivenResource() { + when(propertiesDao.findUsersForNotification("NewViolations", "Email", 45L)).thenReturn(Lists.newArrayList("user1", "user2")); + when(propertiesDao.findUsersForNotification("NewViolations", "Email", null)).thenReturn(Lists.newArrayList("user1", "user3")); + when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", 56L)).thenReturn(Lists.newArrayList("user2")); + when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", null)).thenReturn(Lists.newArrayList("user3")); + when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", null)).thenReturn(Lists.newArrayList("user4")); + + Multimap multiMap = manager.findSubscribedRecipientsForDispatcher(dispatcher, 45); + assertThat(multiMap.entries()).hasSize(4); + + Map> map = multiMap.asMap(); + assertThat(map.get("user1")).containsOnly(emailChannel); + assertThat(map.get("user2")).containsOnly(emailChannel); + assertThat(map.get("user3")).containsOnly(emailChannel, twitterChannel); + assertThat(map.get("user4")).isNull(); + } + + @Test + public void shouldFindSubscribedRecipientForNoResource() { + when(propertiesDao.findUsersForNotification("NewViolations", "Email", 45L)).thenReturn(Lists.newArrayList("user1", "user2")); + when(propertiesDao.findUsersForNotification("NewViolations", "Email", null)).thenReturn(Lists.newArrayList("user1", "user3")); + when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", 56L)).thenReturn(Lists.newArrayList("user2")); + when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", null)).thenReturn(Lists.newArrayList("user3")); + when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", null)).thenReturn(Lists.newArrayList("user4")); + + Multimap multiMap = manager.findSubscribedRecipientsForDispatcher(dispatcher, (Integer) null); + assertThat(multiMap.entries()).hasSize(3); + + Map> map = multiMap.asMap(); + assertThat(map.get("user1")).containsOnly(emailChannel); + assertThat(map.get("user3")).containsOnly(emailChannel, twitterChannel); + assertThat(map.get("user2")).isNull(); + assertThat(map.get("user4")).isNull(); + } + + @Test + public void findNotificationSubscribers() { + when(propertiesDao.findNotificationSubscribers("NewViolations", "Email", "struts")).thenReturn(Lists.newArrayList("user1", "user2")); + when(propertiesDao.findNotificationSubscribers("NewViolations", "Twitter", "struts")).thenReturn(Lists.newArrayList("user2")); + + Multimap multiMap = manager.findNotificationSubscribers(dispatcher, "struts"); + assertThat(multiMap.entries()).hasSize(3); + + Map> map = multiMap.asMap(); + assertThat(map.get("user1")).containsOnly(emailChannel); + assertThat(map.get("user2")).containsOnly(emailChannel, twitterChannel); + assertThat(map.get("other")).isNull(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationCenterTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationCenterTest.java new file mode 100644 index 00000000000..e4d7b28fca5 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationCenterTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sonar.api.notifications.NotificationChannel; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationCenterTest { + + @Mock + private NotificationChannel emailChannel; + + @Mock + private NotificationChannel gtalkChannel; + + private NotificationCenter notificationCenter; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + + NotificationDispatcherMetadata metadata1 = NotificationDispatcherMetadata.create("Dispatcher1").setProperty("global", "true").setProperty("on-project", "true"); + NotificationDispatcherMetadata metadata2 = NotificationDispatcherMetadata.create("Dispatcher2").setProperty("global", "true"); + NotificationDispatcherMetadata metadata3 = NotificationDispatcherMetadata.create("Dispatcher3").setProperty("global", "FOO").setProperty("on-project", "BAR"); + + notificationCenter = new NotificationCenter( + new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3}, + new NotificationChannel[] {emailChannel, gtalkChannel} + ); + } + + @Test + public void shouldReturnChannels() { + assertThat(notificationCenter.getChannels()).containsOnly(emailChannel, gtalkChannel); + } + + @Test + public void shouldReturnDispatcherKeysForSpecificPropertyValue() { + assertThat(notificationCenter.getDispatcherKeysForProperty("global", "true")).containsOnly("Dispatcher1", "Dispatcher2"); + } + + @Test + public void shouldReturnDispatcherKeysForExistenceOfProperty() { + assertThat(notificationCenter.getDispatcherKeysForProperty("on-project", null)).containsOnly("Dispatcher1", "Dispatcher3"); + } + + @Test + public void testDefaultConstructors() { + notificationCenter = new NotificationCenter(new NotificationChannel[] {emailChannel}); + assertThat(notificationCenter.getChannels()).hasSize(1); + + notificationCenter = new NotificationCenter(); + assertThat(notificationCenter.getChannels()).hasSize(0); + + notificationCenter = new NotificationCenter(new NotificationDispatcherMetadata[] {NotificationDispatcherMetadata.create("Dispatcher1").setProperty("global", "true")}); + assertThat(notificationCenter.getChannels()).hasSize(0); + assertThat(notificationCenter.getDispatcherKeysForProperty("global", null)).hasSize(1); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationChannelTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationChannelTest.java new file mode 100644 index 00000000000..e0a0c823bcf --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationChannelTest.java @@ -0,0 +1,44 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; + +public class NotificationChannelTest { + + @Test + public void defaultMethods() { + NotificationChannel channel = new FakeNotificationChannel(); + assertThat(channel.getKey(), is("FakeNotificationChannel")); + assertThat(channel.toString(), is("FakeNotificationChannel")); + } + + class FakeNotificationChannel extends NotificationChannel { + @Override + public void deliver(Notification notification, String username) { + } + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDispatcherMetadataTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDispatcherMetadataTest.java new file mode 100644 index 00000000000..7874ab50f45 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDispatcherMetadataTest.java @@ -0,0 +1,46 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationDispatcherMetadataTest { + + private NotificationDispatcherMetadata metadata; + + @Before + public void init() { + metadata = NotificationDispatcherMetadata.create("NewViolations").setProperty("global", "true"); + } + + @Test + public void shouldReturnDispatcherKey() { + assertThat(metadata.getDispatcherKey()).isEqualTo("NewViolations"); + } + + @Test + public void shouldReturnProperty() { + assertThat(metadata.getProperty("global")).isEqualTo("true"); + assertThat(metadata.getProperty("per-project")).isNull(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDispatcherTest.java new file mode 100644 index 00000000000..e6ec1a7d450 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDispatcherTest.java @@ -0,0 +1,101 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class NotificationDispatcherTest { + + @Mock + private NotificationChannel channel; + + @Mock + private Notification notification; + + @Mock + private NotificationDispatcher.Context context; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + when(notification.getType()).thenReturn("event1"); + } + + @Test + public void defaultMethods() { + NotificationDispatcher dispatcher = new FakeGenericNotificationDispatcher(); + assertThat(dispatcher.getKey(), is("FakeGenericNotificationDispatcher")); + assertThat(dispatcher.toString(), is("FakeGenericNotificationDispatcher")); + } + + @Test + public void shouldAlwaysRunDispatchForGenericDispatcher() { + NotificationDispatcher dispatcher = new FakeGenericNotificationDispatcher(); + dispatcher.performDispatch(notification, context); + + verify(context, times(1)).addUser("user1", channel); + } + + @Test + public void shouldNotAlwaysRunDispatchForSpecificDispatcher() { + NotificationDispatcher dispatcher = new FakeSpecificNotificationDispatcher(); + + // a "event1" notif is sent + dispatcher.performDispatch(notification, context); + verify(context, never()).addUser("user1", channel); + + // now, a "specific-event" notif is sent + when(notification.getType()).thenReturn("specific-event"); + dispatcher.performDispatch(notification, context); + verify(context, times(1)).addUser("user1", channel); + } + + class FakeGenericNotificationDispatcher extends NotificationDispatcher { + @Override + public void dispatch(Notification notification, Context context) { + context.addUser("user1", channel); + } + } + + class FakeSpecificNotificationDispatcher extends NotificationDispatcher { + + public FakeSpecificNotificationDispatcher() { + super("specific-event"); + } + + @Override + public void dispatch(Notification notification, Context context) { + context.addUser("user1", channel); + } + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationServiceTest.java new file mode 100644 index 00000000000..515c4279864 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationServiceTest.java @@ -0,0 +1,254 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.collect.Sets; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.api.config.Settings; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.core.properties.PropertiesDao; +import org.sonar.jpa.session.DatabaseSessionFactory; +import org.sonar.server.db.DbClient; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +public class NotificationServiceTest { + private static String CREATOR_SIMON = "simon"; + private static String CREATOR_EVGENY = "evgeny"; + private static String ASSIGNEE_SIMON = "simon"; + + DefaultNotificationManager manager = mock(DefaultNotificationManager.class); + Notification notification = mock(Notification.class); + NotificationChannel emailChannel = mock(NotificationChannel.class); + NotificationChannel gtalkChannel = mock(NotificationChannel.class); + NotificationDispatcher commentOnIssueAssignedToMe = mock(NotificationDispatcher.class); + NotificationDispatcher commentOnIssueCreatedByMe = mock(NotificationDispatcher.class); + NotificationDispatcher qualityGateChange = mock(NotificationDispatcher.class); + DbClient dbClient = mock(DbClient.class); + + private NotificationService service; + + private void setUpMocks() { + when(emailChannel.getKey()).thenReturn("email"); + when(gtalkChannel.getKey()).thenReturn("gtalk"); + when(commentOnIssueAssignedToMe.getKey()).thenReturn("CommentOnIssueAssignedToMe"); + when(commentOnIssueAssignedToMe.getType()).thenReturn("issue-changes"); + when(commentOnIssueCreatedByMe.getKey()).thenReturn("CommentOnIssueCreatedByMe"); + when(commentOnIssueCreatedByMe.getType()).thenReturn("issue-changes"); + when(qualityGateChange.getKey()).thenReturn("QGateChange"); + when(qualityGateChange.getType()).thenReturn("qgate-changes"); + when(manager.getFromQueue()).thenReturn(notification).thenReturn(null); + + Settings settings = new Settings().setProperty("sonar.notifications.delay", 1L); + + service = new NotificationService(settings, manager, + dbClient, mock(DatabaseSessionFactory.class), + new NotificationDispatcher[] {commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange}); + } + + /** + * Given: + * Simon wants to receive notifications by email on comments for reviews assigned to him or created by him. + * + * When: + * Freddy adds comment to review created by Simon and assigned to Simon. + * + * Then: + * Only one notification should be delivered to Simon by Email. + */ + @Test + public void scenario1() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + service.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + service.stop(); + + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + /** + * Given: + * Evgeny wants to receive notification by GTalk on comments for reviews created by him. + * Simon wants to receive notification by Email on comments for reviews assigned to him. + * + * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + * + * Then: + * Two notifications should be delivered - one to Simon by Email and another to Evgeny by GTalk. + */ + @Test + public void scenario2() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_EVGENY, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + service.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + verify(gtalkChannel, timeout(2000)).deliver(notification, CREATOR_EVGENY); + service.stop(); + + verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + /** + * Given: + * Simon wants to receive notifications by Email and GTLak on comments for reviews assigned to him. + * + * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + * + * Then: + * Two notifications should be delivered to Simon - one by Email and another by GTalk. + */ + @Test + public void scenario3() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, new NotificationChannel[] {emailChannel, gtalkChannel})) + .when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + service.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + verify(gtalkChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + service.stop(); + + verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); + verify(gtalkChannel, never()).deliver(notification, CREATOR_EVGENY); + } + + /** + * Given: + * Nobody wants to receive notifications. + * + * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + * + * Then: + * No notifications. + */ + @Test + public void scenario4() { + setUpMocks(); + + service.start(); + service.stop(); + + verify(emailChannel, never()).deliver(any(Notification.class), anyString()); + verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); + } + + // SONAR-4548 + @Test + public void shouldNotStopWhenException() { + setUpMocks(); + when(manager.getFromQueue()).thenThrow(new RuntimeException("Unexpected exception")).thenReturn(notification).thenReturn(null); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + service.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + service.stop(); + + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + @Test + public void shouldNotAddNullAsUser() { + setUpMocks(); + doAnswer(addUser(null, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + service.start(); + service.stop(); + + verify(emailChannel, never()).deliver(any(Notification.class), anyString()); + verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); + } + + @Test + public void getDispatchers() { + setUpMocks(); + + assertThat(service.getDispatchers()).containsOnly(commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange); + } + + @Test + public void getDispatchers_empty() { + Settings settings = new Settings().setProperty("sonar.notifications.delay", 1L); + + service = new NotificationService(settings, manager, dbClient, mock(DatabaseSessionFactory.class)); + assertThat(service.getDispatchers()).hasSize(0); + } + + @Test + public void shouldLogEvery10Minutes() { + setUpMocks(); + // Emulate 2 notifications in DB + when(manager.getFromQueue()).thenReturn(notification).thenReturn(notification).thenReturn(null); + when(manager.count()).thenReturn(1L).thenReturn(0L); + service = spy(service); + // Emulate processing of each notification take 10 min to have a log each time + when(service.now()).thenReturn(0L).thenReturn(10 * 60 * 1000 + 1L).thenReturn(20 * 60 * 1000 + 2L); + service.start(); + verify(service, timeout(200)).log(1, 1, 10); + verify(service, timeout(200)).log(2, 0, 20); + service.stop(); + } + + @Test + public void hasProjectSubscribersForType() { + setUpMocks(); + + PropertiesDao dao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(dao); + + // no subscribers + when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(false); + assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isFalse(); + + // has subscribers on one dispatcher (among the two) + when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(true); + assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isTrue(); + } + + private static Answer addUser(final String user, final NotificationChannel channel) { + return addUser(user, new NotificationChannel[] {channel}); + } + + private static Answer addUser(final String user, final NotificationChannel[] channels) { + return new Answer() { + public Object answer(InvocationOnMock invocation) { + for (NotificationChannel channel : channels) { + ((NotificationDispatcher.Context) invocation.getArguments()[1]).addUser(user, channel); + } + return null; + } + }; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationTest.java new file mode 100644 index 00000000000..d90b000212c --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationTest.java @@ -0,0 +1,77 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.notifications.Notification; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationTest { + + private Notification notification; + + @Before + public void init() { + notification = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "42"); + } + + @Test + public void shouldReturnType() { + assertThat(notification.getType()).isEqualTo("alerts"); + } + + @Test + public void shouldReturnDefaultMessage() { + assertThat(notification.getDefaultMessage()).isEqualTo("There are new alerts"); + } + + @Test + public void shouldReturnToStringIfDefaultMessageNotSet() { + notification = new Notification("alerts").setFieldValue("alertCount", "42"); + System.out.println(notification); + assertThat(notification.getDefaultMessage()).contains("type='alerts'"); + assertThat(notification.getDefaultMessage()).contains("fields={alertCount=42}"); + } + + @Test + public void shouldReturnField() { + assertThat(notification.getFieldValue("alertCount")).isEqualTo("42"); + assertThat(notification.getFieldValue("fake")).isNull(); + + // default message is stored as field as well + assertThat(notification.getFieldValue("default_message")).isEqualTo("There are new alerts"); + } + + @Test + public void shouldEqual() { + assertThat(notification.equals("")).isFalse(); + assertThat(notification.equals(null)).isFalse(); + assertThat(notification.equals(notification)).isTrue(); + + Notification otherNotif = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "42"); + assertThat(otherNotif).isEqualTo(notification); + + otherNotif = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "15000"); + assertThat(otherNotif).isNotEqualTo(notification); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/email/AlertsEmailTemplateTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/email/AlertsEmailTemplateTest.java new file mode 100644 index 00000000000..3d81a495628 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/email/AlertsEmailTemplateTest.java @@ -0,0 +1,131 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification.email; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.config.EmailSettings; +import org.sonar.api.notifications.Notification; +import org.sonar.plugins.emailnotifications.api.EmailMessage; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AlertsEmailTemplateTest { + + private AlertsEmailTemplate template; + + @Before + public void setUp() { + EmailSettings configuration = mock(EmailSettings.class); + when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org"); + template = new AlertsEmailTemplate(configuration); + } + + @Test + public void shouldNotFormatIfNotCorrectNotification() { + Notification notification = new Notification("other-notif"); + EmailMessage message = template.format(notification); + assertThat(message, nullValue()); + } + + @Test + public void shouldFormatAlertWithSeveralMessages() { + Notification notification = createNotification("Orange (was Red)", "violations > 4, coverage < 75%", "WARN", "false"); + + EmailMessage message = template.format(notification); + assertThat(message.getMessageId(), is("alerts/45")); + assertThat(message.getSubject(), is("Quality gate status changed on \"Foo\"")); + assertThat(message.getMessage(), is("" + + "Project: Foo\n" + + "Quality gate status: Orange (was Red)\n" + + "\n" + + "Quality gate thresholds:\n" + + " - violations > 4\n" + + " - coverage < 75%\n" + + "\n" + + "See it in SonarQube: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo")); + } + + @Test + public void shouldFormatNewAlertWithSeveralMessages() { + Notification notification = createNotification("Orange (was Red)", "violations > 4, coverage < 75%", "WARN", "true"); + + EmailMessage message = template.format(notification); + assertThat(message.getMessageId(), is("alerts/45")); + assertThat(message.getSubject(), is("New quality gate threshold reached on \"Foo\"")); + assertThat(message.getMessage(), is("" + + "Project: Foo\n" + + "Quality gate status: Orange (was Red)\n" + + "\n" + + "New quality gate thresholds:\n" + + " - violations > 4\n" + + " - coverage < 75%\n" + + "\n" + + "See it in SonarQube: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo")); + } + + @Test + public void shouldFormatNewAlertWithOneMessage() { + Notification notification = createNotification("Orange (was Red)", "violations > 4", "WARN", "true"); + + EmailMessage message = template.format(notification); + assertThat(message.getMessageId(), is("alerts/45")); + assertThat(message.getSubject(), is("New quality gate threshold reached on \"Foo\"")); + assertThat(message.getMessage(), is("" + + "Project: Foo\n" + + "Quality gate status: Orange (was Red)\n" + + "\n" + + "New quality gate threshold: violations > 4\n" + + "\n" + + "See it in SonarQube: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo")); + } + + @Test + public void shouldFormatBackToGreenMessage() { + Notification notification = createNotification("Green (was Red)", "", "OK", "false"); + + EmailMessage message = template.format(notification); + assertThat(message.getMessageId(), is("alerts/45")); + assertThat(message.getSubject(), is("\"Foo\" is back to green")); + assertThat(message.getMessage(), is("" + + "Project: Foo\n" + + "Quality gate status: Green (was Red)\n" + + "\n" + + "\n" + + "See it in SonarQube: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo")); + } + + private Notification createNotification(String alertName, String alertText, String alertLevel, String isNewAlert) { + Notification notification = new Notification("alerts") + .setFieldValue("projectName", "Foo") + .setFieldValue("projectKey", "org.sonar.foo:foo") + .setFieldValue("projectId", "45") + .setFieldValue("alertName", alertName) + .setFieldValue("alertText", alertText) + .setFieldValue("alertLevel", alertLevel) + .setFieldValue("isNewAlert", isNewAlert); + return notification; + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java new file mode 100644 index 00000000000..e7cbcce0d57 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java @@ -0,0 +1,212 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification.email; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.List; +import javax.mail.internet.MimeMessage; +import org.apache.commons.mail.EmailException; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.EmailSettings; +import org.sonar.plugins.emailnotifications.api.EmailMessage; +import org.subethamail.wiser.Wiser; +import org.subethamail.wiser.WiserMessage; + +import static junit.framework.Assert.fail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class EmailNotificationChannelTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private int port; + private Wiser server; + private EmailSettings configuration; + private EmailNotificationChannel channel; + + private static int getNextAvailablePort() { + try { + ServerSocket socket = new ServerSocket(0); + int unusedPort = socket.getLocalPort(); + socket.close(); + return unusedPort; + } catch (IOException e) { + throw new RuntimeException("Error getting an available port from system", e); + } + } + + @Before + public void setUp() { + port = getNextAvailablePort(); + server = new Wiser(); + server.setPort(port); + server.start(); + + configuration = mock(EmailSettings.class); + channel = new EmailNotificationChannel(configuration, null, null); + } + + @After + public void tearDown() { + server.stop(); + } + + @Test + public void shouldSendTestEmail() throws Exception { + configure(); + channel.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); + + List messages = server.getMessages(); + assertThat(messages).hasSize(1); + + MimeMessage email = messages.get(0).getMimeMessage(); + assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); + assertThat(email.getHeader("From", ",")).isEqualTo("SonarQube "); + assertThat(email.getHeader("To", null)).isEqualTo(""); + assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Test Message from SonarQube"); + assertThat((String) email.getContent()).startsWith("This is a test message from SonarQube."); + } + + @Test + public void shouldThrowAnExceptionWhenUnableToSendTestEmail() { + configure(); + server.stop(); + + try { + channel.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); + fail(); + } catch (EmailException e) { + // expected + } + } + + @Test + public void shouldNotSendEmailWhenHostnameNotConfigured() { + EmailMessage emailMessage = new EmailMessage() + .setTo("user@nowhere") + .setSubject("Foo") + .setMessage("Bar"); + channel.deliver(emailMessage); + assertThat(server.getMessages()).isEmpty(); + } + + @Test + public void shouldSendThreadedEmail() throws Exception { + configure(); + EmailMessage emailMessage = new EmailMessage() + .setMessageId("reviews/view/1") + .setFrom("Full Username") + .setTo("user@nowhere") + .setSubject("Review #3") + .setMessage("I'll take care of this violation."); + channel.deliver(emailMessage); + + List messages = server.getMessages(); + assertThat(messages).hasSize(1); + + MimeMessage email = messages.get(0).getMimeMessage(); + + assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); + + assertThat(email.getHeader("In-Reply-To", null)).isEqualTo(""); + assertThat(email.getHeader("References", null)).isEqualTo(""); + + assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube "); + assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org"); + + assertThat(email.getHeader("From", ",")).isEqualTo("\"Full Username (SonarQube)\" "); + assertThat(email.getHeader("To", null)).isEqualTo(""); + assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Review #3"); + assertThat((String) email.getContent()).startsWith("I'll take care of this violation."); + } + + @Test + public void shouldSendNonThreadedEmail() throws Exception { + configure(); + EmailMessage emailMessage = new EmailMessage() + .setTo("user@nowhere") + .setSubject("Foo") + .setMessage("Bar"); + channel.deliver(emailMessage); + + List messages = server.getMessages(); + assertThat(messages).hasSize(1); + + MimeMessage email = messages.get(0).getMimeMessage(); + + assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); + + assertThat(email.getHeader("In-Reply-To", null)).isNull(); + assertThat(email.getHeader("References", null)).isNull(); + + assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube "); + assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org"); + + assertThat(email.getHeader("From", null)).isEqualTo("SonarQube "); + assertThat(email.getHeader("To", null)).isEqualTo(""); + assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Foo"); + assertThat((String) email.getContent()).startsWith("Bar"); + } + + @Test + public void shouldNotThrowAnExceptionWhenUnableToSendEmail() { + configure(); + server.stop(); + + EmailMessage emailMessage = new EmailMessage() + .setTo("user@nowhere") + .setSubject("Foo") + .setMessage("Bar"); + channel.deliver(emailMessage); + } + + @Test + public void shouldSendTestEmailWithSTARTTLS() { + server.getServer().setEnableTLS(true); + server.getServer().setRequireTLS(true); + configure(); + when(configuration.getSecureConnection()).thenReturn("STARTTLS"); + + try { + channel.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); + fail("An SSL exception was expected a a proof that STARTTLS is enabled"); + } catch (EmailException e) { + // We don't have a SSL certificate so we are expecting a SSL error + assertThat(e.getCause().getMessage()).isEqualTo("Could not convert socket to TLS"); + } + } + + private void configure() { + when(configuration.getSmtpHost()).thenReturn("localhost"); + when(configuration.getSmtpPort()).thenReturn(port); + when(configuration.getFrom()).thenReturn("server@nowhere"); + when(configuration.getPrefix()).thenReturn("[SONARQUBE]"); + when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org"); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationCenterTest.java b/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationCenterTest.java deleted file mode 100644 index ecd6ef52117..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationCenterTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.NotificationDispatcherMetadata; - -import static org.assertj.core.api.Assertions.assertThat; - -public class NotificationCenterTest { - - @Mock - private NotificationChannel emailChannel; - - @Mock - private NotificationChannel gtalkChannel; - - private NotificationCenter notificationCenter; - - @Before - public void init() { - MockitoAnnotations.initMocks(this); - - NotificationDispatcherMetadata metadata1 = NotificationDispatcherMetadata.create("Dispatcher1").setProperty("global", "true").setProperty("on-project", "true"); - NotificationDispatcherMetadata metadata2 = NotificationDispatcherMetadata.create("Dispatcher2").setProperty("global", "true"); - NotificationDispatcherMetadata metadata3 = NotificationDispatcherMetadata.create("Dispatcher3").setProperty("global", "FOO").setProperty("on-project", "BAR"); - - notificationCenter = new NotificationCenter( - new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3}, - new NotificationChannel[] {emailChannel, gtalkChannel} - ); - } - - @Test - public void shouldReturnChannels() { - assertThat(notificationCenter.getChannels()).containsOnly(emailChannel, gtalkChannel); - } - - @Test - public void shouldReturnDispatcherKeysForSpecificPropertyValue() { - assertThat(notificationCenter.getDispatcherKeysForProperty("global", "true")).containsOnly("Dispatcher1", "Dispatcher2"); - } - - @Test - public void shouldReturnDispatcherKeysForExistenceOfProperty() { - assertThat(notificationCenter.getDispatcherKeysForProperty("on-project", null)).containsOnly("Dispatcher1", "Dispatcher3"); - } - - @Test - public void testDefaultConstructors() { - notificationCenter = new NotificationCenter(new NotificationChannel[] {emailChannel}); - assertThat(notificationCenter.getChannels()).hasSize(1); - - notificationCenter = new NotificationCenter(); - assertThat(notificationCenter.getChannels()).hasSize(0); - - notificationCenter = new NotificationCenter(new NotificationDispatcherMetadata[] {NotificationDispatcherMetadata.create("Dispatcher1").setProperty("global", "true")}); - assertThat(notificationCenter.getChannels()).hasSize(0); - assertThat(notificationCenter.getDispatcherKeysForProperty("global", null)).hasSize(1); - } - -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationServiceTest.java deleted file mode 100644 index c818dee6eac..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationServiceTest.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications; - -import com.google.common.collect.Sets; -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.sonar.api.config.Settings; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.NotificationDispatcher; -import org.sonar.core.notification.DefaultNotificationManager; -import org.sonar.core.properties.PropertiesDao; -import org.sonar.jpa.session.DatabaseSessionFactory; -import org.sonar.server.db.DbClient; - -import java.util.Arrays; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -public class NotificationServiceTest { - private static String CREATOR_SIMON = "simon"; - private static String CREATOR_EVGENY = "evgeny"; - private static String ASSIGNEE_SIMON = "simon"; - - DefaultNotificationManager manager = mock(DefaultNotificationManager.class); - Notification notification = mock(Notification.class); - NotificationChannel emailChannel = mock(NotificationChannel.class); - NotificationChannel gtalkChannel = mock(NotificationChannel.class); - NotificationDispatcher commentOnIssueAssignedToMe = mock(NotificationDispatcher.class); - NotificationDispatcher commentOnIssueCreatedByMe = mock(NotificationDispatcher.class); - NotificationDispatcher qualityGateChange = mock(NotificationDispatcher.class); - DbClient dbClient = mock(DbClient.class); - - private NotificationService service; - - private void setUpMocks() { - when(emailChannel.getKey()).thenReturn("email"); - when(gtalkChannel.getKey()).thenReturn("gtalk"); - when(commentOnIssueAssignedToMe.getKey()).thenReturn("CommentOnIssueAssignedToMe"); - when(commentOnIssueAssignedToMe.getType()).thenReturn("issue-changes"); - when(commentOnIssueCreatedByMe.getKey()).thenReturn("CommentOnIssueCreatedByMe"); - when(commentOnIssueCreatedByMe.getType()).thenReturn("issue-changes"); - when(qualityGateChange.getKey()).thenReturn("QGateChange"); - when(qualityGateChange.getType()).thenReturn("qgate-changes"); - when(manager.getFromQueue()).thenReturn(notification).thenReturn(null); - - Settings settings = new Settings().setProperty("sonar.notifications.delay", 1L); - - service = new NotificationService(settings, manager, - dbClient, mock(DatabaseSessionFactory.class), - new NotificationDispatcher[] {commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange}); - } - - /** - * Given: - * Simon wants to receive notifications by email on comments for reviews assigned to him or created by him. - * - * When: - * Freddy adds comment to review created by Simon and assigned to Simon. - * - * Then: - * Only one notification should be delivered to Simon by Email. - */ - @Test - public void scenario1() { - setUpMocks(); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - service.stop(); - - verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); - } - - /** - * Given: - * Evgeny wants to receive notification by GTalk on comments for reviews created by him. - * Simon wants to receive notification by Email on comments for reviews assigned to him. - * - * When: - * Freddy adds comment to review created by Evgeny and assigned to Simon. - * - * Then: - * Two notifications should be delivered - one to Simon by Email and another to Evgeny by GTalk. - */ - @Test - public void scenario2() { - setUpMocks(); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_EVGENY, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - verify(gtalkChannel, timeout(2000)).deliver(notification, CREATOR_EVGENY); - service.stop(); - - verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); - verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); - } - - /** - * Given: - * Simon wants to receive notifications by Email and GTLak on comments for reviews assigned to him. - * - * When: - * Freddy adds comment to review created by Evgeny and assigned to Simon. - * - * Then: - * Two notifications should be delivered to Simon - one by Email and another by GTalk. - */ - @Test - public void scenario3() { - setUpMocks(); - doAnswer(addUser(ASSIGNEE_SIMON, new NotificationChannel[] {emailChannel, gtalkChannel})) - .when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - verify(gtalkChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - service.stop(); - - verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); - verify(gtalkChannel, never()).deliver(notification, CREATOR_EVGENY); - } - - /** - * Given: - * Nobody wants to receive notifications. - * - * When: - * Freddy adds comment to review created by Evgeny and assigned to Simon. - * - * Then: - * No notifications. - */ - @Test - public void scenario4() { - setUpMocks(); - - service.start(); - service.stop(); - - verify(emailChannel, never()).deliver(any(Notification.class), anyString()); - verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); - } - - // SONAR-4548 - @Test - public void shouldNotStopWhenException() { - setUpMocks(); - when(manager.getFromQueue()).thenThrow(new RuntimeException("Unexpected exception")).thenReturn(notification).thenReturn(null); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - service.stop(); - - verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); - } - - @Test - public void shouldNotAddNullAsUser() { - setUpMocks(); - doAnswer(addUser(null, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - service.stop(); - - verify(emailChannel, never()).deliver(any(Notification.class), anyString()); - verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); - } - - @Test - public void getDispatchers() { - setUpMocks(); - - assertThat(service.getDispatchers()).containsOnly(commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange); - } - - @Test - public void getDispatchers_empty() { - Settings settings = new Settings().setProperty("sonar.notifications.delay", 1L); - - service = new NotificationService(settings, manager, dbClient, mock(DatabaseSessionFactory.class)); - assertThat(service.getDispatchers()).hasSize(0); - } - - @Test - public void shouldLogEvery10Minutes() { - setUpMocks(); - // Emulate 2 notifications in DB - when(manager.getFromQueue()).thenReturn(notification).thenReturn(notification).thenReturn(null); - when(manager.count()).thenReturn(1L).thenReturn(0L); - service = spy(service); - // Emulate processing of each notification take 10 min to have a log each time - when(service.now()).thenReturn(0L).thenReturn(10 * 60 * 1000 + 1L).thenReturn(20 * 60 * 1000 + 2L); - service.start(); - verify(service, timeout(200)).log(1, 1, 10); - verify(service, timeout(200)).log(2, 0, 20); - service.stop(); - } - - @Test - public void hasProjectSubscribersForType() { - setUpMocks(); - - PropertiesDao dao = mock(PropertiesDao.class); - when(dbClient.propertiesDao()).thenReturn(dao); - - // no subscribers - when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(false); - assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isFalse(); - - // has subscribers on one dispatcher (among the two) - when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(true); - assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isTrue(); - } - - private static Answer addUser(final String user, final NotificationChannel channel) { - return addUser(user, new NotificationChannel[] {channel}); - } - - private static Answer addUser(final String user, final NotificationChannel[] channels) { - return new Answer() { - public Object answer(InvocationOnMock invocation) { - for (NotificationChannel channel : channels) { - ((NotificationDispatcher.Context) invocation.getArguments()[1]).addUser(user, channel); - } - return null; - } - }; - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationTest.java b/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationTest.java deleted file mode 100644 index 2c7c9194f9c..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/notifications/NotificationTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications; - -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.notifications.Notification; - -import static org.assertj.core.api.Assertions.assertThat; - -public class NotificationTest { - - private Notification notification; - - @Before - public void init() { - notification = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "42"); - } - - @Test - public void shouldReturnType() { - assertThat(notification.getType()).isEqualTo("alerts"); - } - - @Test - public void shouldReturnDefaultMessage() { - assertThat(notification.getDefaultMessage()).isEqualTo("There are new alerts"); - } - - @Test - public void shouldReturnToStringIfDefaultMessageNotSet() { - notification = new Notification("alerts").setFieldValue("alertCount", "42"); - System.out.println(notification); - assertThat(notification.getDefaultMessage()).contains("type='alerts'"); - assertThat(notification.getDefaultMessage()).contains("fields={alertCount=42}"); - } - - @Test - public void shouldReturnField() { - assertThat(notification.getFieldValue("alertCount")).isEqualTo("42"); - assertThat(notification.getFieldValue("fake")).isNull(); - - // default message is stored as field as well - assertThat(notification.getFieldValue("default_message")).isEqualTo("There are new alerts"); - } - - @Test - public void shouldEqual() { - assertThat(notification.equals("")).isFalse(); - assertThat(notification.equals(null)).isFalse(); - assertThat(notification.equals(notification)).isTrue(); - - Notification otherNotif = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "42"); - assertThat(otherNotif).isEqualTo(notification); - - otherNotif = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "15000"); - assertThat(otherNotif).isNotEqualTo(notification); - } - -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notifications/email/AlertsEmailTemplateTest.java b/server/sonar-server/src/test/java/org/sonar/server/notifications/email/AlertsEmailTemplateTest.java deleted file mode 100644 index 3834df8b9e8..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/notifications/email/AlertsEmailTemplateTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications.email; - -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.config.EmailSettings; -import org.sonar.api.notifications.Notification; -import org.sonar.plugins.emailnotifications.api.EmailMessage; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class AlertsEmailTemplateTest { - - private AlertsEmailTemplate template; - - @Before - public void setUp() { - EmailSettings configuration = mock(EmailSettings.class); - when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org"); - template = new AlertsEmailTemplate(configuration); - } - - @Test - public void shouldNotFormatIfNotCorrectNotification() { - Notification notification = new Notification("other-notif"); - EmailMessage message = template.format(notification); - assertThat(message, nullValue()); - } - - @Test - public void shouldFormatAlertWithSeveralMessages() { - Notification notification = createNotification("Orange (was Red)", "violations > 4, coverage < 75%", "WARN", "false"); - - EmailMessage message = template.format(notification); - assertThat(message.getMessageId(), is("alerts/45")); - assertThat(message.getSubject(), is("Quality gate status changed on \"Foo\"")); - assertThat(message.getMessage(), is("" + - "Project: Foo\n" + - "Quality gate status: Orange (was Red)\n" + - "\n" + - "Quality gate thresholds:\n" + - " - violations > 4\n" + - " - coverage < 75%\n" + - "\n" + - "See it in SonarQube: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo")); - } - - @Test - public void shouldFormatNewAlertWithSeveralMessages() { - Notification notification = createNotification("Orange (was Red)", "violations > 4, coverage < 75%", "WARN", "true"); - - EmailMessage message = template.format(notification); - assertThat(message.getMessageId(), is("alerts/45")); - assertThat(message.getSubject(), is("New quality gate threshold reached on \"Foo\"")); - assertThat(message.getMessage(), is("" + - "Project: Foo\n" + - "Quality gate status: Orange (was Red)\n" + - "\n" + - "New quality gate thresholds:\n" + - " - violations > 4\n" + - " - coverage < 75%\n" + - "\n" + - "See it in SonarQube: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo")); - } - - @Test - public void shouldFormatNewAlertWithOneMessage() { - Notification notification = createNotification("Orange (was Red)", "violations > 4", "WARN", "true"); - - EmailMessage message = template.format(notification); - assertThat(message.getMessageId(), is("alerts/45")); - assertThat(message.getSubject(), is("New quality gate threshold reached on \"Foo\"")); - assertThat(message.getMessage(), is("" + - "Project: Foo\n" + - "Quality gate status: Orange (was Red)\n" + - "\n" + - "New quality gate threshold: violations > 4\n" + - "\n" + - "See it in SonarQube: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo")); - } - - @Test - public void shouldFormatBackToGreenMessage() { - Notification notification = createNotification("Green (was Red)", "", "OK", "false"); - - EmailMessage message = template.format(notification); - assertThat(message.getMessageId(), is("alerts/45")); - assertThat(message.getSubject(), is("\"Foo\" is back to green")); - assertThat(message.getMessage(), is("" + - "Project: Foo\n" + - "Quality gate status: Green (was Red)\n" + - "\n" + - "\n" + - "See it in SonarQube: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo")); - } - - private Notification createNotification(String alertName, String alertText, String alertLevel, String isNewAlert) { - Notification notification = new Notification("alerts") - .setFieldValue("projectName", "Foo") - .setFieldValue("projectKey", "org.sonar.foo:foo") - .setFieldValue("projectId", "45") - .setFieldValue("alertName", alertName) - .setFieldValue("alertText", alertText) - .setFieldValue("alertLevel", alertLevel) - .setFieldValue("isNewAlert", isNewAlert); - return notification; - } - -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notifications/email/EmailNotificationChannelTest.java b/server/sonar-server/src/test/java/org/sonar/server/notifications/email/EmailNotificationChannelTest.java deleted file mode 100644 index 3c5e94587af..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/notifications/email/EmailNotificationChannelTest.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notifications.email; - -import java.io.IOException; -import java.net.ServerSocket; -import java.util.List; -import javax.mail.internet.MimeMessage; -import org.apache.commons.mail.EmailException; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.api.config.EmailSettings; -import org.sonar.plugins.emailnotifications.api.EmailMessage; -import org.subethamail.wiser.Wiser; -import org.subethamail.wiser.WiserMessage; - -import static junit.framework.Assert.fail; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class EmailNotificationChannelTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private int port; - private Wiser server; - private EmailSettings configuration; - private EmailNotificationChannel channel; - - private static int getNextAvailablePort() { - try { - ServerSocket socket = new ServerSocket(0); - int unusedPort = socket.getLocalPort(); - socket.close(); - return unusedPort; - } catch (IOException e) { - throw new RuntimeException("Error getting an available port from system", e); - } - } - - @Before - public void setUp() { - port = getNextAvailablePort(); - server = new Wiser(); - server.setPort(port); - server.start(); - - configuration = mock(EmailSettings.class); - channel = new EmailNotificationChannel(configuration, null, null); - } - - @After - public void tearDown() { - server.stop(); - } - - @Test - public void shouldSendTestEmail() throws Exception { - configure(); - channel.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); - - List messages = server.getMessages(); - assertThat(messages).hasSize(1); - - MimeMessage email = messages.get(0).getMimeMessage(); - assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); - assertThat(email.getHeader("From", ",")).isEqualTo("SonarQube "); - assertThat(email.getHeader("To", null)).isEqualTo(""); - assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Test Message from SonarQube"); - assertThat((String) email.getContent()).startsWith("This is a test message from SonarQube."); - } - - @Test - public void shouldThrowAnExceptionWhenUnableToSendTestEmail() { - configure(); - server.stop(); - - try { - channel.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); - fail(); - } catch (EmailException e) { - // expected - } - } - - @Test - public void shouldNotSendEmailWhenHostnameNotConfigured() { - EmailMessage emailMessage = new EmailMessage() - .setTo("user@nowhere") - .setSubject("Foo") - .setMessage("Bar"); - channel.deliver(emailMessage); - assertThat(server.getMessages()).isEmpty(); - } - - @Test - public void shouldSendThreadedEmail() throws Exception { - configure(); - EmailMessage emailMessage = new EmailMessage() - .setMessageId("reviews/view/1") - .setFrom("Full Username") - .setTo("user@nowhere") - .setSubject("Review #3") - .setMessage("I'll take care of this violation."); - channel.deliver(emailMessage); - - List messages = server.getMessages(); - assertThat(messages).hasSize(1); - - MimeMessage email = messages.get(0).getMimeMessage(); - - assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); - - assertThat(email.getHeader("In-Reply-To", null)).isEqualTo(""); - assertThat(email.getHeader("References", null)).isEqualTo(""); - - assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube "); - assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org"); - - assertThat(email.getHeader("From", ",")).isEqualTo("\"Full Username (SonarQube)\" "); - assertThat(email.getHeader("To", null)).isEqualTo(""); - assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Review #3"); - assertThat((String) email.getContent()).startsWith("I'll take care of this violation."); - } - - @Test - public void shouldSendNonThreadedEmail() throws Exception { - configure(); - EmailMessage emailMessage = new EmailMessage() - .setTo("user@nowhere") - .setSubject("Foo") - .setMessage("Bar"); - channel.deliver(emailMessage); - - List messages = server.getMessages(); - assertThat(messages).hasSize(1); - - MimeMessage email = messages.get(0).getMimeMessage(); - - assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); - - assertThat(email.getHeader("In-Reply-To", null)).isNull(); - assertThat(email.getHeader("References", null)).isNull(); - - assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube "); - assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org"); - - assertThat(email.getHeader("From", null)).isEqualTo("SonarQube "); - assertThat(email.getHeader("To", null)).isEqualTo(""); - assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Foo"); - assertThat((String) email.getContent()).startsWith("Bar"); - } - - @Test - public void shouldNotThrowAnExceptionWhenUnableToSendEmail() { - configure(); - server.stop(); - - EmailMessage emailMessage = new EmailMessage() - .setTo("user@nowhere") - .setSubject("Foo") - .setMessage("Bar"); - channel.deliver(emailMessage); - } - - @Test - public void shouldSendTestEmailWithSTARTTLS() { - server.getServer().setEnableTLS(true); - server.getServer().setRequireTLS(true); - configure(); - when(configuration.getSecureConnection()).thenReturn("STARTTLS"); - - try { - channel.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); - fail("An SSL exception was expected a a proof that STARTTLS is enabled"); - } catch (EmailException e) { - // We don't have a SSL certificate so we are expecting a SSL error - assertThat(e.getCause().getMessage()).isEqualTo("Could not convert socket to TLS"); - } - } - - private void configure() { - when(configuration.getSmtpHost()).thenReturn("localhost"); - when(configuration.getSmtpPort()).thenReturn(port); - when(configuration.getFrom()).thenReturn("server@nowhere"); - when(configuration.getPrefix()).thenReturn("[SONARQUBE]"); - when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org"); - } - -} diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java index ac15c254939..1773cd1e7a6 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java @@ -69,7 +69,6 @@ import org.sonar.batch.scm.ScmSensor; import org.sonar.batch.source.CodeColorizerSensor; import org.sonar.batch.source.LinesSensor; import org.sonar.core.config.CorePropertyDefinitions; -import org.sonar.core.notification.DefaultNotificationManager; import org.sonar.core.resource.DefaultResourceTypes; public class BatchComponents { @@ -108,8 +107,6 @@ public class BatchComponents { SqaleRatingDecorator.class, SqaleRatingSettings.class, - DefaultNotificationManager.class, - // Quality Gate QualityGateVerifier.class, diff --git a/sonar-core/src/main/java/org/sonar/core/notification/DefaultNotificationManager.java b/sonar-core/src/main/java/org/sonar/core/notification/DefaultNotificationManager.java deleted file mode 100644 index 6857590edda..00000000000 --- a/sonar-core/src/main/java/org/sonar/core/notification/DefaultNotificationManager.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.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 java.io.IOException; -import java.io.InvalidClassException; -import java.util.Arrays; -import java.util.List; -import javax.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.batch.RequiresDB; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.api.utils.SonarException; -import org.sonar.core.notification.db.NotificationQueueDao; -import org.sonar.core.notification.db.NotificationQueueDto; -import org.sonar.core.properties.PropertiesDao; - -/** - * @since 2.10 - */ -@RequiresDB -public class DefaultNotificationManager implements NotificationManager { - - private static final Logger LOG = LoggerFactory.getLogger(DefaultNotificationManager.class); - - private static final String UNABLE_TO_READ_NOTIFICATION = "Unable to read notification"; - - private NotificationChannel[] notificationChannels; - private NotificationQueueDao notificationQueueDao; - private PropertiesDao propertiesDao; - - private boolean alreadyLoggedDeserializationIssue = false; - - /** - * Default constructor used by Pico - */ - public DefaultNotificationManager(NotificationChannel[] channels, NotificationQueueDao notificationQueueDao, PropertiesDao propertiesDao) { - this.notificationChannels = channels; - this.notificationQueueDao = notificationQueueDao; - this.propertiesDao = propertiesDao; - } - - /** - * Constructor if no notification channel - */ - public DefaultNotificationManager(NotificationQueueDao notificationQueueDao, PropertiesDao propertiesDao) { - this(new NotificationChannel[0], notificationQueueDao, propertiesDao); - } - - /** - * {@inheritDoc} - */ - @Override - public void scheduleForSending(Notification notification) { - NotificationQueueDto dto = NotificationQueueDto.toNotificationQueueDto(notification); - notificationQueueDao.insert(Arrays.asList(dto)); - } - - @Override - public void scheduleForSending(List notification) { - notificationQueueDao.insert(Lists.transform(notification, new Function() { - @Override - public NotificationQueueDto apply(Notification notification) { - return NotificationQueueDto.toNotificationQueueDto(notification); - } - })); - } - - /** - * Give the notification queue so that it can be processed - */ - public Notification getFromQueue() { - int batchSize = 1; - List notificationDtos = notificationQueueDao.findOldest(batchSize); - if (notificationDtos.isEmpty()) { - return null; - } - notificationQueueDao.delete(notificationDtos); - - return convertToNotification(notificationDtos); - } - - private Notification convertToNotification(List notifications) { - try { - // If batchSize is increased then we should return a list instead of a single element - return notifications.get(0).toNotification(); - } catch (InvalidClassException e) { - // SONAR-4739 - if (!alreadyLoggedDeserializationIssue) { - logDeserializationIssue(); - alreadyLoggedDeserializationIssue = true; - } - return null; - } catch (IOException e) { - throw new SonarException(UNABLE_TO_READ_NOTIFICATION, e); - - } catch (ClassNotFoundException e) { - throw new SonarException(UNABLE_TO_READ_NOTIFICATION, e); - } - } - - @VisibleForTesting - void logDeserializationIssue() { - LOG.warn("It is impossible to send pending notifications which existed prior to the upgrade of SonarQube. They will be ignored."); - } - - public long count() { - return notificationQueueDao.count(); - } - - /** - * {@inheritDoc} - */ - @Override - public Multimap findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, @Nullable Integer resourceId) { - String dispatcherKey = dispatcher.getKey(); - - SetMultimap recipients = HashMultimap.create(); - for (NotificationChannel channel : notificationChannels) { - String channelKey = channel.getKey(); - - // Find users subscribed globally to the dispatcher (i.e. not on a specific project) - addUsersToRecipientListForChannel(propertiesDao.findUsersForNotification(dispatcherKey, channelKey, null), recipients, channel); - - if (resourceId != null) { - // Find users subscribed to the dispatcher specifically for the resource - addUsersToRecipientListForChannel(propertiesDao.findUsersForNotification(dispatcherKey, channelKey, resourceId.longValue()), recipients, channel); - } - } - - return recipients; - } - - @Override - public Multimap findNotificationSubscribers(NotificationDispatcher dispatcher, @Nullable String componentKey) { - String dispatcherKey = dispatcher.getKey(); - - SetMultimap recipients = HashMultimap.create(); - for (NotificationChannel channel : notificationChannels) { - addUsersToRecipientListForChannel(propertiesDao.findNotificationSubscribers(dispatcherKey, channel.getKey(), componentKey), recipients, channel); - } - - return recipients; - } - - @VisibleForTesting - protected List getChannels() { - return Arrays.asList(notificationChannels); - } - - private static void addUsersToRecipientListForChannel(List users, SetMultimap recipients, NotificationChannel channel) { - for (String username : users) { - recipients.put(username, channel); - } - } - -} diff --git a/sonar-core/src/main/java/org/sonar/core/notification/NotificationDispatcher.java b/sonar-core/src/main/java/org/sonar/core/notification/NotificationDispatcher.java deleted file mode 100644 index 33a127af77f..00000000000 --- a/sonar-core/src/main/java/org/sonar/core/notification/NotificationDispatcher.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.core.notification; - -import org.apache.commons.lang.StringUtils; -import org.sonar.api.ExtensionPoint; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.api.server.ServerSide; - -/** - *

- * Plugins should extend this class to provide logic to determine which users are interested in receiving notifications, - * along with which delivery channels they selected. - *

- * For example: - *
    - *
  • notify me by email when someone comments an issue reported by me
  • - *
  • notify me by twitter when someone comments an issue assigned to me
  • - *
  • notify me by Jabber when someone mentions me in an issue comment
  • - *
  • send me by SMS when there are system notifications (like password reset, account creation, ...)
  • - *
- * - * @since 2.10 - */ -@ServerSide -@ExtensionPoint -public abstract class NotificationDispatcher { - - private final String notificationType; - - /** - * Additional information related to the notification, which will be used - * to know who should receive the notification. - */ - public interface Context { - /** - * This method is not used any longer. Calling it will result in an {@link UnsupportedOperationException}. - * - * @deprecated Use {@link #addUser(String, NotificationChannel)} instead. - */ - @Deprecated - void addUser(String userLogin); - - /** - * Adds a user that will be notified through the given notification channel. - * - * @param userLogin the user login - * @param notificationChannel the notification channel to use for this user - */ - void addUser(String userLogin, NotificationChannel notificationChannel); - } - - /** - * Creates a new dispatcher for notifications of the given type. - * - * @param notificationType the type of notifications handled by this dispatcher - */ - public NotificationDispatcher(String notificationType) { - this.notificationType = notificationType; - } - - /** - * Creates a new generic dispatcher, used for any kind of notification. - *

- * Should be avoided and replaced by the other constructor - as it is easier to understand that a - * dispatcher listens for a specific type of notification. - */ - public NotificationDispatcher() { - this(""); - } - - /** - * The unique key of this dispatcher. By default it's the class name without the package prefix. - *

- * The related label in l10n bundles is 'notification.dispatcher.', for example 'notification.dispatcher.NewFalsePositive'. - */ - public String getKey() { - return getClass().getSimpleName(); - } - - /** - * @since 5.1 - */ - public String getType() { - return notificationType; - } - - /** - *

- * Performs the dispatch. - *

- */ - public final void performDispatch(Notification notification, Context context) { - if (StringUtils.equals(notification.getType(), notificationType) || StringUtils.equals("", notificationType)) { - dispatch(notification, context); - } - } - - /** - *

- * Implements the logic that defines which users will receive the notification. - *

- * The purpose of this method is to populate the context object with users, based on the type of notification and the content of the notification. - */ - public abstract void dispatch(Notification notification, Context context); - - @Override - public String toString() { - return getKey(); - } - -} diff --git a/sonar-core/src/main/java/org/sonar/core/notification/NotificationDispatcherMetadata.java b/sonar-core/src/main/java/org/sonar/core/notification/NotificationDispatcherMetadata.java deleted file mode 100644 index 2eb04a59c21..00000000000 --- a/sonar-core/src/main/java/org/sonar/core/notification/NotificationDispatcherMetadata.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.core.notification; - -import com.google.common.collect.Maps; -import java.util.Map; -import org.sonar.api.server.ServerSide; - -/** - *

- * Notification dispatchers (see {@link NotificationDispatcher}) can define their own metadata class in order - * to tell more about them. - *

- * Instances of these classes must be declared in {@link org.sonar.api.SonarPlugin#getExtensions()}. - * - * @since 3.5 - */ -@ServerSide -public final class NotificationDispatcherMetadata { - - public static final String GLOBAL_NOTIFICATION = "globalNotification"; - public static final String PER_PROJECT_NOTIFICATION = "perProjectNotification"; - - private String dispatcherKey; - private Map properties; - - private NotificationDispatcherMetadata(String dispatcherKey) { - this.dispatcherKey = dispatcherKey; - this.properties = Maps.newHashMap(); - } - - /** - * Creates a new metadata instance for the given dispatcher. - *

- * By default the key is the class name without package. It can be changed by overriding - * {@link NotificationDispatcher#getKey()}. - */ - public static NotificationDispatcherMetadata create(String dispatcherKey) { - return new NotificationDispatcherMetadata(dispatcherKey); - } - - /** - * Sets a property on this metadata object. - */ - public NotificationDispatcherMetadata setProperty(String key, String value) { - properties.put(key, value); - return this; - } - - /** - * Gives the property for the given key. - */ - public String getProperty(String key) { - return properties.get(key); - } - - /** - * Returns the unique key of the dispatcher. - */ - public String getDispatcherKey() { - return dispatcherKey; - } - - @Override - public String toString() { - return dispatcherKey; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NotificationDispatcherMetadata that = (NotificationDispatcherMetadata) o; - return dispatcherKey.equals(that.dispatcherKey); - } - - @Override - public int hashCode() { - return dispatcherKey.hashCode(); - } -} diff --git a/sonar-core/src/main/java/org/sonar/core/notification/NotificationManager.java b/sonar-core/src/main/java/org/sonar/core/notification/NotificationManager.java deleted file mode 100644 index f9b5e0a8489..00000000000 --- a/sonar-core/src/main/java/org/sonar/core/notification/NotificationManager.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.core.notification; - -import com.google.common.collect.Multimap; -import org.sonar.api.batch.BatchSide; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.api.server.ServerSide; -import org.sonar.api.batch.InstantiationStrategy; - -import javax.annotation.Nullable; - -import java.util.List; - -/** - *

- * The notification manager receives notifications and is in charge of storing them so that they are processed by the notification service. - *

- *

- * 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. - *

- * - * @since 2.10 - */ -@ServerSide -@BatchSide -@InstantiationStrategy(InstantiationStrategy.PER_BATCH) -public interface NotificationManager { - - /** - * 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 3.7.1 - */ - void scheduleForSending(List notifications); - - /** - *

- * Returns the list of users who subscribed to the given dispatcher, along with the notification channels (email, twitter, ...) that they choose - * for this dispatcher. - *

- *

- * The resource ID can be null in case of notifications that have nothing to do with a specific project (like system notifications). - *

- * - * @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 - */ - Multimap findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, @Nullable Integer resourceId); - - Multimap findNotificationSubscribers(NotificationDispatcher dispatcher, @Nullable String componentKey); -} diff --git a/sonar-core/src/test/java/org/sonar/core/notification/DefaultNotificationManagerTest.java b/sonar-core/src/test/java/org/sonar/core/notification/DefaultNotificationManagerTest.java deleted file mode 100644 index c5e60e1afa5..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/notification/DefaultNotificationManagerTest.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.core.notification; - -import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; -import org.junit.Before; -import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.core.notification.db.NotificationQueueDao; -import org.sonar.core.notification.db.NotificationQueueDto; -import org.sonar.core.properties.PropertiesDao; -import org.sonar.jpa.test.AbstractDbUnitTestCase; - -import java.io.InvalidClassException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.only; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class DefaultNotificationManagerTest extends AbstractDbUnitTestCase { - - private DefaultNotificationManager manager; - - @Mock - private PropertiesDao propertiesDao; - - @Mock - private NotificationDispatcher dispatcher; - - @Mock - private NotificationChannel emailChannel; - - @Mock - private NotificationChannel twitterChannel; - - @Mock - private NotificationQueueDao notificationQueueDao; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(dispatcher.getKey()).thenReturn("NewViolations"); - when(emailChannel.getKey()).thenReturn("Email"); - when(twitterChannel.getKey()).thenReturn("Twitter"); - - manager = new DefaultNotificationManager(new NotificationChannel[] {emailChannel, twitterChannel}, notificationQueueDao, propertiesDao); - } - - @Test - public void shouldProvideChannelList() { - assertThat(manager.getChannels()).containsOnly(emailChannel, twitterChannel); - - manager = new DefaultNotificationManager(notificationQueueDao, propertiesDao); - assertThat(manager.getChannels()).hasSize(0); - } - - @Test - public void shouldPersist() { - Notification notification = new Notification("test"); - manager.scheduleForSending(notification); - - verify(notificationQueueDao, only()).insert(any(List.class)); - } - - @Test - public void shouldGetFromQueueAndDelete() { - Notification notification = new Notification("test"); - NotificationQueueDto dto = NotificationQueueDto.toNotificationQueueDto(notification); - List dtos = Arrays.asList(dto); - when(notificationQueueDao.findOldest(1)).thenReturn(dtos); - - assertThat(manager.getFromQueue()).isNotNull(); - - InOrder inOrder = inOrder(notificationQueueDao); - inOrder.verify(notificationQueueDao).findOldest(1); - inOrder.verify(notificationQueueDao).delete(dtos); - } - - // SONAR-4739 - @Test - public void shouldNotFailWhenUnableToDeserialize() throws Exception { - NotificationQueueDto dto1 = mock(NotificationQueueDto.class); - when(dto1.toNotification()).thenThrow(new InvalidClassException("Pouet")); - List dtos = Arrays.asList(dto1); - when(notificationQueueDao.findOldest(1)).thenReturn(dtos); - - manager = spy(manager); - assertThat(manager.getFromQueue()).isNull(); - assertThat(manager.getFromQueue()).isNull(); - - verify(manager, times(1)).logDeserializationIssue(); - } - - @Test - public void shouldFindNoRecipient() { - assertThat(manager.findSubscribedRecipientsForDispatcher(dispatcher, 45).asMap().entrySet()).hasSize(0); - } - - @Test - public void shouldFindSubscribedRecipientForGivenResource() { - when(propertiesDao.findUsersForNotification("NewViolations", "Email", 45L)).thenReturn(Lists.newArrayList("user1", "user2")); - when(propertiesDao.findUsersForNotification("NewViolations", "Email", null)).thenReturn(Lists.newArrayList("user1", "user3")); - when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", 56L)).thenReturn(Lists.newArrayList("user2")); - when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", null)).thenReturn(Lists.newArrayList("user3")); - when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", null)).thenReturn(Lists.newArrayList("user4")); - - Multimap multiMap = manager.findSubscribedRecipientsForDispatcher(dispatcher, 45); - assertThat(multiMap.entries()).hasSize(4); - - Map> map = multiMap.asMap(); - assertThat(map.get("user1")).containsOnly(emailChannel); - assertThat(map.get("user2")).containsOnly(emailChannel); - assertThat(map.get("user3")).containsOnly(emailChannel, twitterChannel); - assertThat(map.get("user4")).isNull(); - } - - @Test - public void shouldFindSubscribedRecipientForNoResource() { - when(propertiesDao.findUsersForNotification("NewViolations", "Email", 45L)).thenReturn(Lists.newArrayList("user1", "user2")); - when(propertiesDao.findUsersForNotification("NewViolations", "Email", null)).thenReturn(Lists.newArrayList("user1", "user3")); - when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", 56L)).thenReturn(Lists.newArrayList("user2")); - when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", null)).thenReturn(Lists.newArrayList("user3")); - when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", null)).thenReturn(Lists.newArrayList("user4")); - - Multimap multiMap = manager.findSubscribedRecipientsForDispatcher(dispatcher, (Integer) null); - assertThat(multiMap.entries()).hasSize(3); - - Map> map = multiMap.asMap(); - assertThat(map.get("user1")).containsOnly(emailChannel); - assertThat(map.get("user3")).containsOnly(emailChannel, twitterChannel); - assertThat(map.get("user2")).isNull(); - assertThat(map.get("user4")).isNull(); - } - - @Test - public void findNotificationSubscribers() { - when(propertiesDao.findNotificationSubscribers("NewViolations", "Email", "struts")).thenReturn(Lists.newArrayList("user1", "user2")); - when(propertiesDao.findNotificationSubscribers("NewViolations", "Twitter", "struts")).thenReturn(Lists.newArrayList("user2")); - - Multimap multiMap = manager.findNotificationSubscribers(dispatcher, "struts"); - assertThat(multiMap.entries()).hasSize(3); - - Map> map = multiMap.asMap(); - assertThat(map.get("user1")).containsOnly(emailChannel); - assertThat(map.get("user2")).containsOnly(emailChannel, twitterChannel); - assertThat(map.get("other")).isNull(); - } -} diff --git a/sonar-core/src/test/java/org/sonar/core/notification/NotificationChannelTest.java b/sonar-core/src/test/java/org/sonar/core/notification/NotificationChannelTest.java deleted file mode 100644 index dcd80b4c08d..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/notification/NotificationChannelTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.core.notification; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -import org.junit.Test; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; - -public class NotificationChannelTest { - - @Test - public void defaultMethods() { - NotificationChannel channel = new FakeNotificationChannel(); - assertThat(channel.getKey(), is("FakeNotificationChannel")); - assertThat(channel.toString(), is("FakeNotificationChannel")); - } - - class FakeNotificationChannel extends NotificationChannel { - @Override - public void deliver(Notification notification, String username) { - } - } - -} diff --git a/sonar-core/src/test/java/org/sonar/core/notification/NotificationDispatcherMetadataTest.java b/sonar-core/src/test/java/org/sonar/core/notification/NotificationDispatcherMetadataTest.java deleted file mode 100644 index c19e5453be6..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/notification/NotificationDispatcherMetadataTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.core.notification; - -import org.junit.Before; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class NotificationDispatcherMetadataTest { - - private NotificationDispatcherMetadata metadata; - - @Before - public void init() { - metadata = NotificationDispatcherMetadata.create("NewViolations").setProperty("global", "true"); - } - - @Test - public void shouldReturnDispatcherKey() { - assertThat(metadata.getDispatcherKey()).isEqualTo("NewViolations"); - } - - @Test - public void shouldReturnProperty() { - assertThat(metadata.getProperty("global")).isEqualTo("true"); - assertThat(metadata.getProperty("per-project")).isNull(); - } -} diff --git a/sonar-core/src/test/java/org/sonar/core/notification/NotificationDispatcherTest.java b/sonar-core/src/test/java/org/sonar/core/notification/NotificationDispatcherTest.java deleted file mode 100644 index 89296125536..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/notification/NotificationDispatcherTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.core.notification; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class NotificationDispatcherTest { - - @Mock - private NotificationChannel channel; - - @Mock - private Notification notification; - - @Mock - private NotificationDispatcher.Context context; - - @Before - public void init() { - MockitoAnnotations.initMocks(this); - when(notification.getType()).thenReturn("event1"); - } - - @Test - public void defaultMethods() { - NotificationDispatcher dispatcher = new FakeGenericNotificationDispatcher(); - assertThat(dispatcher.getKey(), is("FakeGenericNotificationDispatcher")); - assertThat(dispatcher.toString(), is("FakeGenericNotificationDispatcher")); - } - - @Test - public void shouldAlwaysRunDispatchForGenericDispatcher() { - NotificationDispatcher dispatcher = new FakeGenericNotificationDispatcher(); - dispatcher.performDispatch(notification, context); - - verify(context, times(1)).addUser("user1", channel); - } - - @Test - public void shouldNotAlwaysRunDispatchForSpecificDispatcher() { - NotificationDispatcher dispatcher = new FakeSpecificNotificationDispatcher(); - - // a "event1" notif is sent - dispatcher.performDispatch(notification, context); - verify(context, never()).addUser("user1", channel); - - // now, a "specific-event" notif is sent - when(notification.getType()).thenReturn("specific-event"); - dispatcher.performDispatch(notification, context); - verify(context, times(1)).addUser("user1", channel); - } - - class FakeGenericNotificationDispatcher extends NotificationDispatcher { - @Override - public void dispatch(Notification notification, Context context) { - context.addUser("user1", channel); - } - } - - class FakeSpecificNotificationDispatcher extends NotificationDispatcher { - - public FakeSpecificNotificationDispatcher() { - super("specific-event"); - } - - @Override - public void dispatch(Notification notification, Context context) { - context.addUser("user1", channel); - } - } - -}