aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java14
-rw-r--r--server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java2
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/notification/NotificationCenter.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/notification/NotificationModule.java48
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/notification/NotificationUpdater.java102
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java162
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWs.java46
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWsAction.java27
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java162
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/notification/ws/package-info.java25
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java18
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/notification/NotificationCenterTest.java36
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/notification/NotificationModuleTest.java35
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/notification/ws/AddActionTest.java223
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/notification/ws/NotificationsWsTest.java56
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/notification/ws/RemoveActionTest.java234
-rw-r--r--sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java4
-rw-r--r--sonar-db/src/test/java/org/sonar/db/DbTester.java7
-rw-r--r--sonar-db/src/test/java/org/sonar/db/notification/NotificationDbTester.java65
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/notification/AddRequest.java86
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/notification/NotificationsWsParameters.java34
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/notification/package-info.java25
22 files changed, 1363 insertions, 54 deletions
diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
index 2b82ec5d194..7e8488e0a49 100644
--- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
+++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
@@ -24,7 +24,6 @@ import java.util.List;
import javax.annotation.CheckForNull;
import org.sonar.api.SonarQubeSide;
import org.sonar.api.SonarQubeVersion;
-import org.sonar.api.config.EmailSettings;
import org.sonar.api.internal.ApiVersion;
import org.sonar.api.internal.SonarRuntimeImpl;
import org.sonar.api.profiles.AnnotationProfileParser;
@@ -90,11 +89,7 @@ import org.sonar.server.issue.workflow.FunctionExecutor;
import org.sonar.server.issue.workflow.IssueWorkflow;
import org.sonar.server.metric.CoreCustomMetrics;
import org.sonar.server.metric.DefaultMetricFinder;
-import org.sonar.server.notification.DefaultNotificationManager;
-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.notification.NotificationModule;
import org.sonar.server.organization.DefaultOrganizationProviderImpl;
import org.sonar.server.permission.GroupPermissionChanger;
import org.sonar.server.permission.PermissionTemplateService;
@@ -354,7 +349,6 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
NewIssuesEmailTemplate.class,
MyNewIssuesEmailTemplate.class,
IssueChangesEmailTemplate.class,
- AlertsEmailTemplate.class,
ChangesOnMyIssueNotificationDispatcher.class,
ChangesOnMyIssueNotificationDispatcher.newMetadata(),
NewIssuesNotificationDispatcher.class,
@@ -364,17 +358,13 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
DoNotFixNotificationDispatcher.class,
DoNotFixNotificationDispatcher.newMetadata(),
NewIssuesNotificationFactory.class, // used by SendIssueNotificationsStep
- EmailNotificationChannel.class,
// technical debt
DebtModelPluginRepository.class,
DebtRulesXMLImporter.class,
// Notifications
- EmailSettings.class,
- NotificationService.class,
- NotificationCenter.class,
- DefaultNotificationManager.class,
+ NotificationModule.class,
// Tests
TestIndexer.class,
diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
index 87e2eb8a969..0a6c45790e8 100644
--- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
+++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
@@ -88,7 +88,7 @@ public class ComputeEngineContainerImplTest {
assertThat(picoContainer.getComponentAdapters())
.hasSize(
CONTAINER_ITSELF
- + 77 // level 4
+ + 83 // level 4
+ 4 // content of CeConfigurationModule
+ 3 // content of CeHttpModule
+ 5 // content of CeQueueModule
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
index 4a16f197e54..ee15c127b01 100644
--- 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
@@ -19,7 +19,7 @@
*/
package org.sonar.server.notification;
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
@@ -69,7 +69,7 @@ public class NotificationCenter {
* If "propertyValue" is null, the verification is done on the existence of such a property (whatever the value).
*/
public List<String> getDispatcherKeysForProperty(String propertyKey, @Nullable String propertyValue) {
- List<String> keys = Lists.newArrayList();
+ ImmutableList.Builder<String> keys = ImmutableList.builder();
for (NotificationDispatcherMetadata metadata : dispatchersMetadata) {
String dispatcherKey = metadata.getDispatcherKey();
String value = metadata.getProperty(propertyKey);
@@ -77,7 +77,7 @@ public class NotificationCenter {
keys.add(dispatcherKey);
}
}
- return keys;
+ return keys.build();
}
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationModule.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationModule.java
new file mode 100644
index 00000000000..7c2cc011a66
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationModule.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.sonar.api.config.EmailSettings;
+import org.sonar.core.platform.Module;
+import org.sonar.server.email.ws.EmailsWsModule;
+import org.sonar.server.notification.email.AlertsEmailTemplate;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+import org.sonar.server.notification.ws.AddAction;
+import org.sonar.server.notification.ws.NotificationsWs;
+
+public class NotificationModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ EmailSettings.class,
+ NotificationService.class,
+ NotificationCenter.class,
+ NotificationUpdater.class,
+ DefaultNotificationManager.class,
+ EmailsWsModule.class,
+ NotificationDaemon.class,
+ AlertsEmailTemplate.class,
+ EmailNotificationChannel.class,
+ // WS
+ NotificationsWs.class,
+ AddAction.class);
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationUpdater.java
new file mode 100644
index 00000000000..417cadaf65b
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationUpdater.java
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.db.property.PropertyQuery;
+import org.sonar.server.user.UserSession;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public class NotificationUpdater {
+ private static final String PROP_NOTIFICATION_PREFIX = "notification";
+ private static final String PROP_NOTIFICATION_VALUE = "true";
+
+ private final UserSession userSession;
+ private final DbClient dbClient;
+
+ public NotificationUpdater(UserSession userSession, DbClient dbClient) {
+ this.userSession = userSession;
+ this.dbClient = dbClient;
+ }
+
+ /**
+ * Add a notification to the current authenticated user.
+ * Does nothing if the user is not authenticated.
+ */
+ public void add(DbSession dbSession, String channel, String dispatcher, @Nullable ComponentDto project) {
+ if (!userSession.isLoggedIn()) {
+ return;
+ }
+
+ String key = String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel);
+ Long projectId = project == null ? null : project.getId();
+
+ List<PropertyDto> existingNotification = dbClient.propertiesDao().selectByQuery(
+ PropertyQuery.builder()
+ .setKey(key)
+ .setComponentId(projectId)
+ .setUserId(userSession.getUserId())
+ .build(),
+ dbSession);
+ checkArgument(existingNotification.isEmpty()
+ || !PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification already added");
+
+ dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto()
+ .setKey(key)
+ .setUserId(Long.valueOf(userSession.getUserId()))
+ .setValue(PROP_NOTIFICATION_VALUE)
+ .setResourceId(projectId));
+ }
+
+ /**
+ * Remove a notification to the current authenticated user.
+ * Does nothing if the user is not authenticated.
+ */
+ public void remove(DbSession dbSession, String channel, String dispatcher, @Nullable ComponentDto project) {
+ if (!userSession.isLoggedIn()) {
+ return;
+ }
+
+ String key = String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel);
+ Long projectId = project == null ? null : project.getId();
+
+ List<PropertyDto> existingNotification = dbClient.propertiesDao().selectByQuery(
+ PropertyQuery.builder()
+ .setKey(key)
+ .setComponentId(projectId)
+ .setUserId(userSession.getUserId())
+ .build(),
+ dbSession);
+ checkArgument(!existingNotification.isEmpty() && PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification doesn't exist");
+
+ dbClient.propertiesDao().delete(dbSession, new PropertyDto()
+ .setKey(key)
+ .setUserId(Long.valueOf(userSession.getUserId()))
+ .setValue(PROP_NOTIFICATION_VALUE)
+ .setResourceId(projectId));
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java
new file mode 100644
index 00000000000..bef211f4f58
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java
@@ -0,0 +1,162 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ws;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher;
+import org.sonar.server.notification.NotificationCenter;
+import org.sonar.server.notification.NotificationUpdater;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.ws.KeyExamples;
+import org.sonarqube.ws.client.notification.AddRequest;
+
+import static java.util.Optional.empty;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
+import static org.sonar.server.ws.WsUtils.checkRequest;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.ACTION_ADD;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_NOTIFICATION;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_PROJECT;
+
+public class AddAction implements NotificationsWsAction {
+ private final NotificationCenter notificationCenter;
+ private final NotificationUpdater notificationUpdater;
+ private final DbClient dbClient;
+ private final ComponentFinder componentFinder;
+ private final UserSession userSession;
+ private final List<String> globalDispatchers;
+ private final List<String> projectDispatchers;
+
+ public AddAction(NotificationCenter notificationCenter, NotificationUpdater notificationUpdater, DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) {
+ this.notificationCenter = notificationCenter;
+ this.notificationUpdater = notificationUpdater;
+ this.dbClient = dbClient;
+ this.componentFinder = componentFinder;
+ this.userSession = userSession;
+ this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true");
+ this.projectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true");
+ }
+
+ @Override
+ public void define(WebService.NewController context) {
+ WebService.NewAction action = context.createAction(ACTION_ADD)
+ .setDescription("Add a notification for the authenticated user.<br>" +
+ "Requires authentication. If a project is provided, requires the 'Browse' permission on the specified project.")
+ .setSince("6.3")
+ .setPost(true)
+ .setHandler(this);
+
+ action.createParam(PARAM_PROJECT)
+ .setDescription("Project key")
+ .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
+
+ List<NotificationChannel> channels = notificationCenter.getChannels();
+ action.createParam(PARAM_CHANNEL)
+ .setDescription("Channel through which the notification is sent. For example, notifications can be sent by email.")
+ .setPossibleValues(channels)
+ .setDefaultValue(EmailNotificationChannel.class.getSimpleName());
+
+ action.createParam(PARAM_NOTIFICATION)
+ .setDescription("Notification. Possible values are for:" +
+ "<ul>" +
+ " <li>Overall notifications: %s</li>" +
+ " <li>Per project notifications: %s</li>" +
+ "</ul>",
+ globalDispatchers.stream().sorted().collect(Collectors.joining(", ")),
+ projectDispatchers.stream().sorted().collect(Collectors.joining(", ")))
+ .setRequired(true)
+ .setExampleValue(MyNewIssuesNotificationDispatcher.KEY);
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ Stream.of(request)
+ .map(toWsRequest())
+ .peek(checkPermissions())
+ .forEach(add());
+
+ response.noContent();
+ }
+
+ private Consumer<AddRequest> add() {
+ return request -> {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ Optional<ComponentDto> project = searchProject(dbSession, request);
+ notificationUpdater.add(dbSession, request.getChannel(), request.getNotification(), project.orElse(null));
+ dbSession.commit();
+ }
+ };
+ }
+
+ private Optional<ComponentDto> searchProject(DbSession dbSession, AddRequest request) {
+ Optional<ComponentDto> project = request.getProject() == null ? empty() : Optional.of(componentFinder.getByKey(dbSession, request.getProject()));
+ project.ifPresent(p -> checkRequest(Qualifiers.PROJECT.equals(p.qualifier()) && Scopes.PROJECT.equals(p.scope()),
+ "Component '%s' must be a project", request.getProject()));
+ return project;
+ }
+
+ private Consumer<AddRequest> checkPermissions() {
+ return request -> userSession.checkLoggedIn();
+ }
+
+ private Function<Request, AddRequest> toWsRequest() {
+ return request -> {
+ AddRequest.Builder requestBuilder = AddRequest.builder()
+ .setNotification(request.mandatoryParam(PARAM_NOTIFICATION))
+ .setChannel(request.mandatoryParam(PARAM_CHANNEL));
+ String project = request.param(PARAM_PROJECT);
+ setNullable(project, requestBuilder::setProject);
+ AddRequest wsRequest = requestBuilder.build();
+
+ if (wsRequest.getProject() == null) {
+ checkRequest(globalDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s",
+ PARAM_NOTIFICATION,
+ wsRequest.getNotification(),
+ globalDispatchers);
+ } else {
+ checkRequest(projectDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s",
+ PARAM_NOTIFICATION,
+ wsRequest.getNotification(),
+ projectDispatchers);
+ }
+
+ return wsRequest;
+ };
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWs.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWs.java
new file mode 100644
index 00000000000..eef7c3bfb29
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWs.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ws;
+
+import org.sonar.api.server.ws.WebService;
+
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.CONTROLLER;
+
+public class NotificationsWs implements WebService {
+ private final NotificationsWsAction[] actions;
+
+ public NotificationsWs(NotificationsWsAction[] actions) {
+ this.actions = actions;
+ }
+
+ @Override
+ public void define(Context context) {
+ NewController controller = context.createController(CONTROLLER)
+ .setDescription("Manage notifications of the authenticated user")
+ .setSince("6.3");
+
+ for (NotificationsWsAction action : actions) {
+ action.define(controller);
+ }
+
+ controller.done();
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWsAction.java
new file mode 100644
index 00000000000..eb82a25c162
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWsAction.java
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ws;
+
+import org.sonar.server.ws.WsAction;
+
+public interface NotificationsWsAction extends WsAction {
+ // marker interface
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java
new file mode 100644
index 00000000000..0424f2df9be
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java
@@ -0,0 +1,162 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ws;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher;
+import org.sonar.server.notification.NotificationCenter;
+import org.sonar.server.notification.NotificationUpdater;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.ws.KeyExamples;
+import org.sonarqube.ws.client.notification.AddRequest;
+
+import static java.util.Optional.empty;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
+import static org.sonar.server.ws.WsUtils.checkRequest;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.ACTION_ADD;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_NOTIFICATION;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_PROJECT;
+
+public class RemoveAction implements NotificationsWsAction {
+ private final NotificationCenter notificationCenter;
+ private final NotificationUpdater notificationUpdater;
+ private final DbClient dbClient;
+ private final ComponentFinder componentFinder;
+ private final UserSession userSession;
+ private final List<String> globalDispatchers;
+ private final List<String> projectDispatchers;
+
+ public RemoveAction(NotificationCenter notificationCenter, NotificationUpdater notificationUpdater, DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) {
+ this.notificationCenter = notificationCenter;
+ this.notificationUpdater = notificationUpdater;
+ this.dbClient = dbClient;
+ this.componentFinder = componentFinder;
+ this.userSession = userSession;
+ this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true");
+ this.projectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true");
+ }
+
+ @Override
+ public void define(WebService.NewController context) {
+ WebService.NewAction action = context.createAction(ACTION_ADD)
+ .setDescription("Remove a notification for the authenticated user.<br>" +
+ "Requires authentication. If a project is provided, requires the 'Browse' permission on the specified project.")
+ .setSince("6.3")
+ .setPost(true)
+ .setHandler(this);
+
+ action.createParam(PARAM_PROJECT)
+ .setDescription("Project key")
+ .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
+
+ List<NotificationChannel> channels = notificationCenter.getChannels();
+ action.createParam(PARAM_CHANNEL)
+ .setDescription("Channel through which the notification is sent. For example, notifications can be sent by email.")
+ .setPossibleValues(channels)
+ .setDefaultValue(EmailNotificationChannel.class.getSimpleName());
+
+ action.createParam(PARAM_NOTIFICATION)
+ .setDescription("Notification. Possible values are for:" +
+ "<ul>" +
+ " <li>Overall notifications: %s</li>" +
+ " <li>Per project notifications: %s</li>" +
+ "</ul>",
+ globalDispatchers.stream().sorted().collect(Collectors.joining(", ")),
+ projectDispatchers.stream().sorted().collect(Collectors.joining(", ")))
+ .setRequired(true)
+ .setExampleValue(MyNewIssuesNotificationDispatcher.KEY);
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ Stream.of(request)
+ .map(toWsRequest())
+ .peek(checkPermissions())
+ .forEach(remove());
+
+ response.noContent();
+ }
+
+ private Consumer<AddRequest> remove() {
+ return request -> {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ Optional<ComponentDto> project = searchProject(dbSession, request);
+ notificationUpdater.remove(dbSession, request.getChannel(), request.getNotification(), project.orElse(null));
+ dbSession.commit();
+ }
+ };
+ }
+
+ private Optional<ComponentDto> searchProject(DbSession dbSession, AddRequest request) {
+ Optional<ComponentDto> project = request.getProject() == null ? empty() : Optional.of(componentFinder.getByKey(dbSession, request.getProject()));
+ project.ifPresent(p -> checkRequest(Qualifiers.PROJECT.equals(p.qualifier()) && Scopes.PROJECT.equals(p.scope()),
+ "Component '%s' must be a project", request.getProject()));
+ return project;
+ }
+
+ private Consumer<AddRequest> checkPermissions() {
+ return request -> userSession.checkLoggedIn();
+ }
+
+ private Function<Request, AddRequest> toWsRequest() {
+ return request -> {
+ AddRequest.Builder requestBuilder = AddRequest.builder()
+ .setNotification(request.mandatoryParam(PARAM_NOTIFICATION))
+ .setChannel(request.mandatoryParam(PARAM_CHANNEL));
+ String project = request.param(PARAM_PROJECT);
+ setNullable(project, requestBuilder::setProject);
+ AddRequest wsRequest = requestBuilder.build();
+
+ if (wsRequest.getProject() == null) {
+ checkRequest(globalDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s",
+ PARAM_NOTIFICATION,
+ wsRequest.getNotification(),
+ globalDispatchers);
+ } else {
+ checkRequest(projectDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s",
+ PARAM_NOTIFICATION,
+ wsRequest.getNotification(),
+ projectDispatchers);
+ }
+
+ return wsRequest;
+ };
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/package-info.java
new file mode 100644
index 00000000000..e1a0ac3109f
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ws;
+
+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 9d0ae80c37d..c2dfc70e9d1 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
@@ -20,7 +20,6 @@
package org.sonar.server.platform.platformlevel;
import java.util.List;
-import org.sonar.api.config.EmailSettings;
import org.sonar.api.profiles.AnnotationProfileParser;
import org.sonar.api.profiles.XMLProfileParser;
import org.sonar.api.profiles.XMLProfileSerializer;
@@ -52,7 +51,6 @@ import org.sonar.server.debt.DebtRulesXMLImporter;
import org.sonar.server.duplication.ws.DuplicationsJsonWriter;
import org.sonar.server.duplication.ws.DuplicationsParser;
import org.sonar.server.duplication.ws.DuplicationsWs;
-import org.sonar.server.email.ws.EmailsWsModule;
import org.sonar.server.es.IndexCreator;
import org.sonar.server.es.IndexDefinitions;
import org.sonar.server.event.NewAlerts;
@@ -85,12 +83,7 @@ import org.sonar.server.measure.ws.TimeMachineWs;
import org.sonar.server.metric.CoreCustomMetrics;
import org.sonar.server.metric.DefaultMetricFinder;
import org.sonar.server.metric.ws.MetricsWsModule;
-import org.sonar.server.notification.DefaultNotificationManager;
-import org.sonar.server.notification.NotificationCenter;
-import org.sonar.server.notification.NotificationDaemon;
-import org.sonar.server.notification.NotificationService;
-import org.sonar.server.notification.email.AlertsEmailTemplate;
-import org.sonar.server.notification.email.EmailNotificationChannel;
+import org.sonar.server.notification.NotificationModule;
import org.sonar.server.organization.ws.OrganizationsWsModule;
import org.sonar.server.permission.GroupPermissionChanger;
import org.sonar.server.permission.PermissionTemplateService;
@@ -396,8 +389,6 @@ public class PlatformLevel4 extends PlatformLevel {
DoNotFixNotificationDispatcher.class,
DoNotFixNotificationDispatcher.newMetadata(),
NewIssuesNotificationFactory.class,
- EmailNotificationChannel.class,
- AlertsEmailTemplate.class,
// issues actions
AssignAction.class,
@@ -437,12 +428,7 @@ public class PlatformLevel4 extends PlatformLevel {
RubyTextService.class,
// Notifications
- EmailSettings.class,
- NotificationService.class,
- NotificationCenter.class,
- DefaultNotificationManager.class,
- EmailsWsModule.class,
- NotificationDaemon.class,
+ NotificationModule.class,
// Tests
TestsWs.class,
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
index 0440867c6bb..cd4f3c3ab74 100644
--- 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
@@ -21,31 +21,25 @@ 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;
+import static org.mockito.Mockito.mock;
public class NotificationCenterTest {
- @Mock
- private NotificationChannel emailChannel;
+ private NotificationChannel emailChannel = mock(NotificationChannel.class);
+ private NotificationChannel gtalkChannel = mock(NotificationChannel.class);
- @Mock
- private NotificationChannel gtalkChannel;
-
- private NotificationCenter notificationCenter;
+ private NotificationCenter underTest;
@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(
+ underTest = new NotificationCenter(
new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3},
new NotificationChannel[] {emailChannel, gtalkChannel}
);
@@ -53,30 +47,30 @@ public class NotificationCenterTest {
@Test
public void shouldReturnChannels() {
- assertThat(notificationCenter.getChannels()).containsOnly(emailChannel, gtalkChannel);
+ assertThat(underTest.getChannels()).containsOnly(emailChannel, gtalkChannel);
}
@Test
public void shouldReturnDispatcherKeysForSpecificPropertyValue() {
- assertThat(notificationCenter.getDispatcherKeysForProperty("global", "true")).containsOnly("Dispatcher1", "Dispatcher2");
+ assertThat(underTest.getDispatcherKeysForProperty("global", "true")).containsOnly("Dispatcher1", "Dispatcher2");
}
@Test
public void shouldReturnDispatcherKeysForExistenceOfProperty() {
- assertThat(notificationCenter.getDispatcherKeysForProperty("on-project", null)).containsOnly("Dispatcher1", "Dispatcher3");
+ assertThat(underTest.getDispatcherKeysForProperty("on-project", null)).containsOnly("Dispatcher1", "Dispatcher3");
}
@Test
public void testDefaultConstructors() {
- notificationCenter = new NotificationCenter(new NotificationChannel[] {emailChannel});
- assertThat(notificationCenter.getChannels()).hasSize(1);
+ underTest = new NotificationCenter(new NotificationChannel[] {emailChannel});
+ assertThat(underTest.getChannels()).hasSize(1);
- notificationCenter = new NotificationCenter();
- assertThat(notificationCenter.getChannels()).hasSize(0);
+ underTest = new NotificationCenter();
+ assertThat(underTest.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);
+ underTest = new NotificationCenter(new NotificationDispatcherMetadata[] {NotificationDispatcherMetadata.create("Dispatcher1").setProperty("global", "true")});
+ assertThat(underTest.getChannels()).hasSize(0);
+ assertThat(underTest.getDispatcherKeysForProperty("global", null)).hasSize(1);
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationModuleTest.java
new file mode 100644
index 00000000000..f879ed4542c
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationModuleTest.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NotificationModuleTest {
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new NotificationModule().configure(container);
+ assertThat(container.size()).isEqualTo(11 + 2);
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/ws/AddActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/AddActionTest.java
new file mode 100644
index 00000000000..f62f552b95f
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/AddActionTest.java
@@ -0,0 +1,223 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ws;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.notification.NotificationCenter;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.NotificationUpdater;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.client.notification.AddRequest;
+
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.db.component.ComponentTesting.newView;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_NOTIFICATION;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_PROJECT;
+
+public class AddActionTest {
+ private static final String NOTIF_MY_NEW_ISSUES = "Dispatcher1";
+ private static final String NOTIF_NEW_ISSUES = "Dispatcher2";
+ private static final String NOTIF_NEW_QUALITY_GATE_STATUS = "Dispatcher3";
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone().login().setUserId(123);
+ @Rule
+ public DbTester db = DbTester.create();
+ private DbClient dbClient = db.getDbClient();
+ private DbSession dbSession = db.getSession();
+
+ private NotificationChannel emailChannel = new FakeNotificationChannel("EmailChannel");
+ private NotificationChannel twitterChannel = new FakeNotificationChannel("TwitterChannel");
+ // default channel, based on class simple name
+ private NotificationChannel defaultChannel = new FakeNotificationChannel("EmailNotificationChannel");
+
+ private NotificationCenter notificationCenter;
+ private AddAction underTest;
+ private WsActionTester ws;
+
+ private AddRequest.Builder request = AddRequest.builder()
+ .setNotification(NOTIF_MY_NEW_ISSUES);
+
+ @Before
+ public void setUp() {
+ NotificationDispatcherMetadata metadata1 = NotificationDispatcherMetadata.create(NOTIF_MY_NEW_ISSUES)
+ .setProperty(GLOBAL_NOTIFICATION, "true")
+ .setProperty(PER_PROJECT_NOTIFICATION, "true");
+ NotificationDispatcherMetadata metadata2 = NotificationDispatcherMetadata.create(NOTIF_NEW_ISSUES)
+ .setProperty(GLOBAL_NOTIFICATION, "true");
+ NotificationDispatcherMetadata metadata3 = NotificationDispatcherMetadata.create(NOTIF_NEW_QUALITY_GATE_STATUS)
+ .setProperty(GLOBAL_NOTIFICATION, "true")
+ .setProperty(PER_PROJECT_NOTIFICATION, "true");
+
+ notificationCenter = new NotificationCenter(
+ new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3},
+ new NotificationChannel[] {emailChannel, twitterChannel, defaultChannel});
+ underTest = new AddAction(notificationCenter, new NotificationUpdater(userSession, dbClient), dbClient, new ComponentFinder(dbClient), userSession);
+ ws = new WsActionTester(underTest);
+ }
+
+ @Test
+ public void add_to_email_channel_by_default() {
+ call(request);
+
+ db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), null);
+ }
+
+ @Test
+ public void add_to_a_specific_channel() {
+ call(request.setNotification(NOTIF_NEW_QUALITY_GATE_STATUS).setChannel(twitterChannel.getKey()));
+
+ db.notifications().assertExists(twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, userSession.getUserId(), null);
+ }
+
+ @Test
+ public void add_a_project_notification() {
+ ComponentDto project = db.components().insertProject();
+
+ call(request.setProject(project.getKey()));
+
+ db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), project);
+ }
+
+ @Test
+ public void http_no_content() {
+ TestResponse result = call(request);
+
+ assertThat(result.getStatus()).isEqualTo(HTTP_NO_CONTENT);
+ }
+
+ @Test
+ public void fail_when_notification_already_exists() {
+ call(request);
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Notification already added");
+
+ call(request);
+ }
+
+ @Test
+ public void fail_when_unknown_channel() {
+ expectedException.expect(IllegalArgumentException.class);
+
+ call(request.setChannel("Channel42"));
+ }
+
+ @Test
+ public void fail_when_unknown_global_dispatcher() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher2, Dispatcher3]");
+
+ call(request.setNotification("Dispatcher42"));
+ }
+
+ @Test
+ public void fail_when_unknown_project_dispatcher() {
+ ComponentDto project = db.components().insertProject();
+
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher3]");
+
+ call(request.setNotification("Dispatcher42").setProject(project.key()));
+ }
+
+ @Test
+ public void fail_when_no_dispatcher() {
+ expectedException.expect(IllegalArgumentException.class);
+
+ ws.newRequest().execute();
+ }
+
+ @Test
+ public void fail_when_project_is_unknown() {
+ expectedException.expect(NotFoundException.class);
+
+ call(request.setProject("Project-42"));
+ }
+
+ @Test
+ public void fail_when_component_is_not_a_project() {
+ db.components().insertViewAndSnapshot(newView().setKey("VIEW_1"));
+
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Component 'VIEW_1' must be a project");
+
+ call(request.setProject("VIEW_1"));
+ }
+
+ @Test
+ public void fail_when_not_authenticated() {
+ userSession.anonymous();
+
+ expectedException.expect(UnauthorizedException.class);
+
+ call(request);
+ }
+
+ private TestResponse call(AddRequest.Builder wsRequestBuilder) {
+ AddRequest wsRequest = wsRequestBuilder.build();
+ TestRequest request = ws.newRequest();
+ request.setParam(PARAM_NOTIFICATION, wsRequest.getNotification());
+ setNullable(wsRequest.getChannel(), channel -> request.setParam(PARAM_CHANNEL, channel));
+ setNullable(wsRequest.getProject(), project -> request.setParam(PARAM_PROJECT, project));
+ return request.execute();
+ }
+
+ private static class FakeNotificationChannel extends NotificationChannel {
+ private final String key;
+
+ private FakeNotificationChannel(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public String getKey() {
+ return this.key;
+ }
+
+ @Override
+ public void deliver(Notification notification, String username) {
+ // do nothing
+ }
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/ws/NotificationsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/NotificationsWsTest.java
new file mode 100644
index 00000000000..a4634657b98
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/NotificationsWsTest.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ws;
+
+import org.junit.Test;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.server.ws.WebService.Controller;
+import org.sonar.server.ws.WsTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NotificationsWsTest {
+ private NotificationsWsAction action = new FakeNotificationAction();
+ private NotificationsWsAction[] actions = {action};
+ private WsTester ws = new WsTester(new NotificationsWs(actions));
+
+ private Controller underTest = ws.controller("api/notifications");
+
+ @Test
+ public void definition() {
+ assertThat(underTest.path()).isEqualTo("api/notifications");
+ }
+
+ private static class FakeNotificationAction implements NotificationsWsAction {
+ @Override
+ public void define(WebService.NewController context) {
+ context.createAction("fake")
+ .setHandler(this);
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ // do nothing
+ }
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/ws/RemoveActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/RemoveActionTest.java
new file mode 100644
index 00000000000..fd371d3e77b
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/RemoveActionTest.java
@@ -0,0 +1,234 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ws;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.notification.NotificationCenter;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.NotificationUpdater;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.client.notification.AddRequest;
+
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.db.component.ComponentTesting.newView;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_NOTIFICATION;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_PROJECT;
+
+public class RemoveActionTest {
+ private static final String NOTIF_MY_NEW_ISSUES = "Dispatcher1";
+ private static final String NOTIF_NEW_ISSUES = "Dispatcher2";
+ private static final String NOTIF_NEW_QUALITY_GATE_STATUS = "Dispatcher3";
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone().login().setUserId(123);
+ @Rule
+ public DbTester db = DbTester.create();
+ private DbClient dbClient = db.getDbClient();
+ private DbSession dbSession = db.getSession();
+
+ private NotificationChannel emailChannel = new FakeNotificationChannel("EmailChannel");
+ private NotificationChannel twitterChannel = new FakeNotificationChannel("TwitterChannel");
+ // default channel, based on class simple name
+ private NotificationChannel defaultChannel = new FakeNotificationChannel("EmailNotificationChannel");
+
+ private NotificationCenter notificationCenter;
+ private NotificationUpdater notificationUpdater;
+ private RemoveAction underTest;
+
+ private WsActionTester ws;
+ private AddRequest.Builder request = AddRequest.builder()
+ .setNotification(NOTIF_MY_NEW_ISSUES);
+
+ @Before
+ public void setUp() {
+ NotificationDispatcherMetadata metadata1 = NotificationDispatcherMetadata.create(NOTIF_MY_NEW_ISSUES)
+ .setProperty(GLOBAL_NOTIFICATION, "true")
+ .setProperty(PER_PROJECT_NOTIFICATION, "true");
+ NotificationDispatcherMetadata metadata2 = NotificationDispatcherMetadata.create(NOTIF_NEW_ISSUES)
+ .setProperty(GLOBAL_NOTIFICATION, "true");
+ NotificationDispatcherMetadata metadata3 = NotificationDispatcherMetadata.create(NOTIF_NEW_QUALITY_GATE_STATUS)
+ .setProperty(GLOBAL_NOTIFICATION, "true")
+ .setProperty(PER_PROJECT_NOTIFICATION, "true");
+
+ notificationCenter = new NotificationCenter(
+ new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3},
+ new NotificationChannel[] {emailChannel, twitterChannel, defaultChannel});
+ notificationUpdater = new NotificationUpdater(userSession, dbClient);
+ underTest = new RemoveAction(notificationCenter, notificationUpdater, dbClient, new ComponentFinder(dbClient), userSession);
+ ws = new WsActionTester(underTest);
+ }
+
+ @Test
+ public void remove_to_email_channel_by_default() {
+ notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+ dbSession.commit();
+
+ call(request);
+
+ db.notifications().assertDoesNotExist(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), null);
+ }
+
+ @Test
+ public void remove_from_a_specific_channel() {
+ notificationUpdater.add(dbSession, twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, null);
+ dbSession.commit();
+
+ call(request.setNotification(NOTIF_NEW_QUALITY_GATE_STATUS).setChannel(twitterChannel.getKey()));
+
+ db.notifications().assertDoesNotExist(twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, userSession.getUserId(), null);
+ }
+
+ @Test
+ public void remove_a_project_notification() {
+ ComponentDto project = db.components().insertProject();
+ notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, project);
+ dbSession.commit();
+
+ call(request.setProject(project.getKey()));
+
+ db.notifications().assertDoesNotExist(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), project);
+ }
+
+ @Test
+ public void http_no_content() {
+ notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+ dbSession.commit();
+
+ TestResponse result = call(request);
+
+ assertThat(result.getStatus()).isEqualTo(HTTP_NO_CONTENT);
+ }
+
+ @Test
+ public void fail_when_notification_does_not_exist() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Notification doesn't exist");
+
+ call(request);
+ }
+
+ @Test
+ public void fail_when_unknown_channel() {
+ expectedException.expect(IllegalArgumentException.class);
+
+ call(request.setChannel("Channel42"));
+ }
+
+ @Test
+ public void fail_when_unknown_global_dispatcher() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher2, Dispatcher3]");
+
+ call(request.setNotification("Dispatcher42"));
+ }
+
+ @Test
+ public void fail_when_unknown_project_dispatcher() {
+ ComponentDto project = db.components().insertProject();
+
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher3]");
+
+ call(request.setNotification("Dispatcher42").setProject(project.key()));
+ }
+
+ @Test
+ public void fail_when_no_dispatcher() {
+ expectedException.expect(IllegalArgumentException.class);
+
+ ws.newRequest().execute();
+ }
+
+ @Test
+ public void fail_when_project_is_unknown() {
+ expectedException.expect(NotFoundException.class);
+
+ call(request.setProject("Project-42"));
+ }
+
+ @Test
+ public void fail_when_component_is_not_a_project() {
+ db.components().insertViewAndSnapshot(newView().setKey("VIEW_1"));
+
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Component 'VIEW_1' must be a project");
+
+ call(request.setProject("VIEW_1"));
+ }
+
+ @Test
+ public void fail_when_not_authenticated() {
+ userSession.anonymous();
+
+ expectedException.expect(UnauthorizedException.class);
+
+ call(request);
+ }
+
+ private TestResponse call(AddRequest.Builder wsRequestBuilder) {
+ AddRequest wsRequest = wsRequestBuilder.build();
+ TestRequest request = ws.newRequest();
+ request.setParam(PARAM_NOTIFICATION, wsRequest.getNotification());
+ setNullable(wsRequest.getChannel(), channel -> request.setParam(PARAM_CHANNEL, channel));
+ setNullable(wsRequest.getProject(), project -> request.setParam(PARAM_PROJECT, project));
+ return request.execute();
+ }
+
+ private static class FakeNotificationChannel extends NotificationChannel {
+ private final String key;
+
+ private FakeNotificationChannel(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public String getKey() {
+ return this.key;
+ }
+
+ @Override
+ public void deliver(Notification notification, String username) {
+ // do nothing
+ }
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java b/sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java
index 34a8bb5cfe1..45befce078f 100644
--- a/sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java
+++ b/sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java
@@ -19,6 +19,8 @@
*/
package org.sonar.db.property;
+import javax.annotation.Nullable;
+
public class PropertyQuery {
private final String key;
@@ -57,7 +59,7 @@ public class PropertyQuery {
return this;
}
- public Builder setComponentId(Long componentId) {
+ public Builder setComponentId(@Nullable Long componentId) {
this.componentId = componentId;
return this;
}
diff --git a/sonar-db/src/test/java/org/sonar/db/DbTester.java b/sonar-db/src/test/java/org/sonar/db/DbTester.java
index 48fbd8494c3..970e8756171 100644
--- a/sonar-db/src/test/java/org/sonar/db/DbTester.java
+++ b/sonar-db/src/test/java/org/sonar/db/DbTester.java
@@ -67,6 +67,7 @@ import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.event.EventDbTester;
import org.sonar.db.favorite.FavoriteDbTester;
import org.sonar.db.issue.IssueDbTester;
+import org.sonar.db.notification.NotificationDbTester;
import org.sonar.db.organization.OrganizationDbTester;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.organization.OrganizationTesting;
@@ -110,6 +111,7 @@ public class DbTester extends ExternalResource {
private final QualityGateDbTester qualityGateDbTester;
private final IssueDbTester issueDbTester;
private final RuleDbTester ruleDbTester;
+ private final NotificationDbTester notificationDbTester;
private final RootFlagAssertions rootFlagAssertions;
private DbTester(System2 system2, @Nullable String schemaPath) {
@@ -125,6 +127,7 @@ public class DbTester extends ExternalResource {
this.qualityGateDbTester = new QualityGateDbTester(this);
this.issueDbTester = new IssueDbTester(this);
this.ruleDbTester = new RuleDbTester(this);
+ this.notificationDbTester = new NotificationDbTester(this);
this.rootFlagAssertions = new RootFlagAssertions(this);
}
@@ -225,6 +228,10 @@ public class DbTester extends ExternalResource {
return ruleDbTester;
}
+ public NotificationDbTester notifications() {
+ return notificationDbTester;
+ }
+
@Override
protected void after() {
if (session != null) {
diff --git a/sonar-db/src/test/java/org/sonar/db/notification/NotificationDbTester.java b/sonar-db/src/test/java/org/sonar/db/notification/NotificationDbTester.java
new file mode 100644
index 00000000000..4ad7893ee73
--- /dev/null
+++ b/sonar-db/src/test/java/org/sonar/db/notification/NotificationDbTester.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.db.notification;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.db.property.PropertyQuery;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NotificationDbTester {
+ private static final String PROP_NOTIFICATION_PREFIX = "notification";
+
+ private final DbTester db;
+ private final DbClient dbClient;
+ private final DbSession dbSession;
+
+ public NotificationDbTester(DbTester db) {
+ this.db = db;
+ this.dbClient = db.getDbClient();
+ this.dbSession = db.getSession();
+ }
+
+ public void assertExists(String channel, String dispatcher, long userId, @Nullable ComponentDto component) {
+ List<PropertyDto> result = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder()
+ .setKey(String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel))
+ .setComponentId(component == null ? null : component.getId())
+ .setUserId((int) userId)
+ .build(), dbSession);
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).getValue()).isEqualTo("true");
+ }
+
+ public void assertDoesNotExist(String channel, String dispatcher, long userId, @Nullable ComponentDto component) {
+ List<PropertyDto> result = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder()
+ .setKey(String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel))
+ .setComponentId(component == null ? null : component.getId())
+ .setUserId((int) userId)
+ .build(), dbSession);
+ assertThat(result).isEmpty();
+ }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/AddRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/AddRequest.java
new file mode 100644
index 00000000000..c0e97ea9fd7
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/AddRequest.java
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.sonarqube.ws.client.notification;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import static java.util.Objects.requireNonNull;
+
+public class AddRequest {
+ private final String notification;
+ private final String channel;
+ private final String project;
+
+ private AddRequest(Builder builder) {
+ this.channel = builder.channel;
+ this.notification = builder.notification;
+ this.project = builder.project;
+ }
+
+ public String getNotification() {
+ return notification;
+ }
+
+ @CheckForNull
+ public String getChannel() {
+ return channel;
+ }
+
+ @CheckForNull
+ public String getProject() {
+ return project;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String notification;
+ private String channel;
+ private String project;
+
+ private Builder() {
+ // enforce factory method
+ }
+
+ public Builder setNotification(String notification) {
+ this.notification = notification;
+ return this;
+ }
+
+ public Builder setChannel(@Nullable String channel) {
+ this.channel = channel;
+ return this;
+ }
+
+ public Builder setProject(@Nullable String project) {
+ this.project = project;
+ return this;
+ }
+
+ public AddRequest build() {
+ requireNonNull(notification, "Notification is required");
+ return new AddRequest(this);
+ }
+ }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/NotificationsWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/NotificationsWsParameters.java
new file mode 100644
index 00000000000..a065b831ac9
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/NotificationsWsParameters.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.sonarqube.ws.client.notification;
+
+public class NotificationsWsParameters {
+ public static final String CONTROLLER = "api/notifications";
+ public static final String ACTION_ADD = "add";
+
+ public static final String PARAM_PROJECT = "project";
+ public static final String PARAM_CHANNEL = "channel";
+ public static final String PARAM_NOTIFICATION = "notification";
+
+ private NotificationsWsParameters() {
+ // prevent instantiation
+ }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/package-info.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/package-info.java
new file mode 100644
index 00000000000..fdfcd790e5e
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.sonarqube.ws.client.notification;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+