]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8557 Create WS api/notifications/list
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Thu, 29 Dec 2016 11:22:05 +0000 (12:22 +0100)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Thu, 29 Dec 2016 16:20:59 +0000 (17:20 +0100)
17 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-server/src/main/java/org/sonar/server/notification/NotificationModule.java
server/sonar-server/src/main/java/org/sonar/server/notification/NotificationUpdater.java
server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java
server/sonar-server/src/main/java/org/sonar/server/notification/ws/ListAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java
server/sonar-server/src/main/resources/org/sonar/server/notification/ws/list-example.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/notification/NotificationModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/notification/ws/AddActionTest.java
server/sonar-server/src/test/java/org/sonar/server/notification/ws/ListActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/notification/ws/RemoveActionTest.java
sonar-db/src/test/java/org/sonar/db/notification/NotificationDbTester.java
sonar-ws/src/main/java/org/sonarqube/ws/client/notification/AddRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/notification/NotificationsWsParameters.java
sonar-ws/src/main/java/org/sonarqube/ws/client/notification/RemoveRequest.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-notifications.proto [new file with mode: 0644]

index 7e8488e0a49e8a150cf5948deeb0107e9f67c483..34bf314e848aa82e8ac685a002b3beae214d580c 100644 (file)
@@ -24,6 +24,7 @@ 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;
@@ -89,7 +90,11 @@ 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.NotificationModule;
+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.organization.DefaultOrganizationProviderImpl;
 import org.sonar.server.permission.GroupPermissionChanger;
 import org.sonar.server.permission.PermissionTemplateService;
@@ -364,7 +369,12 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
       DebtRulesXMLImporter.class,
 
       // Notifications
-      NotificationModule.class,
+      AlertsEmailTemplate.class,
+      EmailSettings.class,
+      NotificationService.class,
+      NotificationCenter.class,
+      DefaultNotificationManager.class,
+      EmailNotificationChannel.class,
 
       // Tests
       TestIndexer.class,
index 0a6c45790e84a1db621ee049cdff20f438454110..87e2eb8a96930baa8d9ca7f742072ce379310c0d 100644 (file)
@@ -88,7 +88,7 @@ public class ComputeEngineContainerImplTest {
     assertThat(picoContainer.getComponentAdapters())
       .hasSize(
         CONTAINER_ITSELF
-          + 83 // level 4
+          + 77 // level 4
           + 4 // content of CeConfigurationModule
           + 3 // content of CeHttpModule
           + 5 // content of CeQueueModule
index 7c2cc011a663023b4dfdc4239440a3afc78cb97f..204e1a7a5eddb84d93bc9e259c29953785ff32f2 100644 (file)
@@ -26,7 +26,9 @@ 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.ListAction;
 import org.sonar.server.notification.ws.NotificationsWs;
+import org.sonar.server.notification.ws.RemoveAction;
 
 public class NotificationModule extends Module {
   @Override
@@ -43,6 +45,8 @@ public class NotificationModule extends Module {
       EmailNotificationChannel.class,
       // WS
       NotificationsWs.class,
-      AddAction.class);
+      AddAction.class,
+      RemoveAction.class,
+      ListAction.class);
   }
 }
index 417cadaf65bbfbf536f406f42e325599fdd38886..bb67f56a2fb1381f3b8e6692668349fd90a9bebf 100644 (file)
@@ -21,7 +21,9 @@
 package org.sonar.server.notification;
 
 import java.util.List;
+import java.util.function.Predicate;
 import javax.annotation.Nullable;
+import org.sonar.core.util.stream.Collectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
@@ -61,7 +63,9 @@ public class NotificationUpdater {
         .setComponentId(projectId)
         .setUserId(userSession.getUserId())
         .build(),
-      dbSession);
+      dbSession).stream()
+      .filter(notificationScope(project))
+      .collect(Collectors.toList());
     checkArgument(existingNotification.isEmpty()
       || !PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification already added");
 
@@ -90,7 +94,9 @@ public class NotificationUpdater {
         .setComponentId(projectId)
         .setUserId(userSession.getUserId())
         .build(),
-      dbSession);
+      dbSession).stream()
+      .filter(notificationScope(project))
+      .collect(Collectors.toList());
     checkArgument(!existingNotification.isEmpty() && PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification doesn't exist");
 
     dbClient.propertiesDao().delete(dbSession, new PropertyDto()
@@ -99,4 +105,8 @@ public class NotificationUpdater {
       .setValue(PROP_NOTIFICATION_VALUE)
       .setResourceId(projectId));
   }
+
+  private static Predicate<PropertyDto> notificationScope(@Nullable ComponentDto project) {
+    return prop -> project == null ? (prop.getResourceId() == null) : (prop.getResourceId() != null);
+  }
 }
index bef211f4f588e5fe81b05cdbfb0a0304c025c359..9613721b5d6ebdec4c7e80310595727675940f49 100644 (file)
@@ -24,7 +24,6 @@ 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;
@@ -46,13 +45,14 @@ 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.core.util.stream.Collectors.toList;
 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;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_TYPE;
 
 public class AddAction implements NotificationsWsAction {
   private final NotificationCenter notificationCenter;
@@ -69,8 +69,8 @@ public class AddAction implements NotificationsWsAction {
     this.dbClient = dbClient;
     this.componentFinder = componentFinder;
     this.userSession = userSession;
-    this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true");
-    this.projectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true");
+    this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true").stream().sorted().collect(toList());
+    this.projectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true").stream().sorted().collect(toList());
   }
 
   @Override
@@ -92,14 +92,14 @@ public class AddAction implements NotificationsWsAction {
       .setPossibleValues(channels)
       .setDefaultValue(EmailNotificationChannel.class.getSimpleName());
 
-    action.createParam(PARAM_NOTIFICATION)
-      .setDescription("Notification. Possible values are for:" +
+    action.createParam(PARAM_TYPE)
+      .setDescription("Notification type. Possible values are for:" +
         "<ul>" +
-        "  <li>Overall notifications: %s</li>" +
+        "  <li>Global notifications: %s</li>" +
         "  <li>Per project notifications: %s</li>" +
         "</ul>",
-        globalDispatchers.stream().sorted().collect(Collectors.joining(", ")),
-        projectDispatchers.stream().sorted().collect(Collectors.joining(", ")))
+        String.join(", ", globalDispatchers),
+        String.join(", ", projectDispatchers))
       .setRequired(true)
       .setExampleValue(MyNewIssuesNotificationDispatcher.KEY);
   }
@@ -118,7 +118,7 @@ public class AddAction implements NotificationsWsAction {
     return request -> {
       try (DbSession dbSession = dbClient.openSession(false)) {
         Optional<ComponentDto> project = searchProject(dbSession, request);
-        notificationUpdater.add(dbSession, request.getChannel(), request.getNotification(), project.orElse(null));
+        notificationUpdater.add(dbSession, request.getChannel(), request.getType(), project.orElse(null));
         dbSession.commit();
       }
     };
@@ -138,21 +138,21 @@ public class AddAction implements NotificationsWsAction {
   private Function<Request, AddRequest> toWsRequest() {
     return request -> {
       AddRequest.Builder requestBuilder = AddRequest.builder()
-        .setNotification(request.mandatoryParam(PARAM_NOTIFICATION))
+        .setType(request.mandatoryParam(PARAM_TYPE))
         .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(),
+        checkRequest(globalDispatchers.contains(wsRequest.getType()), "Value of parameter '%s' (%s) must be one of: %s",
+          PARAM_TYPE,
+          wsRequest.getType(),
           globalDispatchers);
       } else {
-        checkRequest(projectDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s",
-          PARAM_NOTIFICATION,
-          wsRequest.getNotification(),
+        checkRequest(projectDispatchers.contains(wsRequest.getType()), "Value of parameter '%s' (%s) must be one of: %s",
+          PARAM_TYPE,
+          wsRequest.getType(),
           projectDispatchers);
       }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/ListAction.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/ListAction.java
new file mode 100644 (file)
index 0000000..4093234
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * 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 com.google.common.base.Splitter;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import java.util.stream.Stream;
+import org.sonar.api.notifications.NotificationChannel;
+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.web.UserRole;
+import org.sonar.core.util.stream.Collectors;
+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.notification.NotificationCenter;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Notifications.ListResponse;
+import org.sonarqube.ws.Notifications.Notification;
+
+import static java.util.Comparator.comparing;
+import static java.util.Comparator.naturalOrder;
+import static java.util.Comparator.nullsFirst;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.core.util.stream.Collectors.toList;
+import static org.sonar.core.util.stream.Collectors.toOneElement;
+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.writeProtobuf;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.ACTION_LIST;
+
+public class ListAction implements NotificationsWsAction {
+  private static final Splitter PROPERTY_KEY_SPLITTER = Splitter.on(".");
+
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final List<String> globalDispatchers;
+  private final List<String> perProjectDispatchers;
+  private final List<String> channels;
+
+  public ListAction(NotificationCenter notificationCenter, DbClient dbClient, UserSession userSession) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+    this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true").stream().sorted().collect(Collectors.toList());
+    this.perProjectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true").stream().sorted().collect(Collectors.toList());
+    this.channels = notificationCenter.getChannels().stream().map(NotificationChannel::getKey).sorted().collect(Collectors.toList());
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    context.createAction(ACTION_LIST)
+      .setDescription("List notifications of the authenticated user.<br>" +
+        "Requires authentication.")
+      .setSince("6.3")
+      .setResponseExample(getClass().getResource("list-example.json"))
+      .setHandler(this);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    ListResponse listResponse = Stream.of(request)
+      .peek(checkPermissions())
+      .map(search())
+      .collect(toOneElement());
+
+    writeProtobuf(listResponse, request, response);
+  }
+
+  private Function<Request, ListResponse> search() {
+    return request -> {
+      try (DbSession dbSession = dbClient.openSession(false)) {
+        return Stream
+          .of(ListResponse.newBuilder())
+          .map(r -> r.addAllChannels(channels))
+          .map(r -> r.addAllGlobalTypes(globalDispatchers))
+          .map(r -> r.addAllPerProjectTypes(perProjectDispatchers))
+          .map(addNotifications(dbSession))
+          .map(ListResponse.Builder::build)
+          .collect(toOneElement());
+      }
+    };
+  }
+
+  private UnaryOperator<ListResponse.Builder> addNotifications(DbSession dbSession) {
+    return response -> {
+      List<PropertyDto> properties = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder().setUserId(userSession.getUserId()).build(), dbSession);
+      Map<Long, ComponentDto> componentsById = searchProjects(dbSession, properties);
+
+      Predicate<PropertyDto> isNotification = prop -> prop.getKey().startsWith("notification.");
+      Predicate<PropertyDto> isComponentInDb = prop -> prop.getResourceId() == null || componentsById.containsKey(prop.getResourceId());
+
+      Notification.Builder notification = Notification.newBuilder();
+
+      properties.stream()
+        .filter(isNotification)
+        .filter(channelAndDispatcherAuthorized())
+        .filter(isComponentInDb)
+        .map(toWsNotification(notification, componentsById))
+        .sorted(comparing(Notification::getProject, nullsFirst(naturalOrder()))
+          .thenComparing(comparing(Notification::getChannel))
+          .thenComparing(comparing(Notification::getType)))
+        .forEach(response::addNotifications);
+
+      return response;
+    };
+  }
+
+  private Predicate<PropertyDto> channelAndDispatcherAuthorized() {
+    return prop -> {
+      List<String> key = PROPERTY_KEY_SPLITTER.splitToList(prop.getKey());
+      return key.size() == 3
+        && channels.contains(key.get(2))
+        && isDispatcherAuthorized(prop, key.get(1));
+    };
+  }
+
+  private boolean isDispatcherAuthorized(PropertyDto prop, String dispatcher) {
+    return (prop.getResourceId() != null && perProjectDispatchers.contains(dispatcher)) || globalDispatchers.contains(dispatcher);
+  }
+
+  private Map<Long, ComponentDto> searchProjects(DbSession dbSession, List<PropertyDto> properties) {
+    Collection<String> authorizedComponentUuids = dbClient.authorizationDao().selectAuthorizedRootProjectsUuids(dbSession, userSession.getUserId(), UserRole.USER);
+    return dbClient.componentDao().selectByIds(dbSession,
+      properties.stream()
+        .filter(prop -> prop.getResourceId() != null)
+        .map(PropertyDto::getResourceId)
+        .distinct()
+        .collect(toList()))
+      .stream()
+      .filter(c -> authorizedComponentUuids.contains(c.uuid()))
+      .collect(Collectors.uniqueIndex(ComponentDto::getId));
+  }
+
+  private static Function<PropertyDto, Notification> toWsNotification(Notification.Builder notification, Map<Long, ComponentDto> projectsById) {
+    return property -> {
+      notification.clear();
+      List<String> propertyKey = Splitter.on(".").splitToList(property.getKey());
+      notification.setType(propertyKey.get(1));
+      notification.setChannel(propertyKey.get(2));
+      setNullable(property.getResourceId(), componentId -> {
+        ComponentDto project = projectsById.get(componentId);
+        notification.setProject(project.getKey());
+        notification.setProjectName(project.name());
+        return notification;
+      });
+
+      return notification.build();
+    };
+  }
+
+  private Consumer<Request> checkPermissions() {
+    return request -> userSession.checkLoggedIn();
+  }
+}
index 0424f2df9be02727060f13756670bd27c4bd4d56..cf680d95ad25847118c3dfe803594353b6418cf4 100644 (file)
@@ -42,17 +42,17 @@ 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 org.sonarqube.ws.client.notification.RemoveRequest;
 
 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.ACTION_REMOVE;
 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;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_TYPE;
 
 public class RemoveAction implements NotificationsWsAction {
   private final NotificationCenter notificationCenter;
@@ -75,7 +75,7 @@ public class RemoveAction implements NotificationsWsAction {
 
   @Override
   public void define(WebService.NewController context) {
-    WebService.NewAction action = context.createAction(ACTION_ADD)
+    WebService.NewAction action = context.createAction(ACTION_REMOVE)
       .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")
@@ -92,10 +92,10 @@ public class RemoveAction implements NotificationsWsAction {
       .setPossibleValues(channels)
       .setDefaultValue(EmailNotificationChannel.class.getSimpleName());
 
-    action.createParam(PARAM_NOTIFICATION)
-      .setDescription("Notification. Possible values are for:" +
+    action.createParam(PARAM_TYPE)
+      .setDescription("Notification type. Possible values are for:" +
         "<ul>" +
-        "  <li>Overall notifications: %s</li>" +
+        "  <li>Global notifications: %s</li>" +
         "  <li>Per project notifications: %s</li>" +
         "</ul>",
         globalDispatchers.stream().sorted().collect(Collectors.joining(", ")),
@@ -114,45 +114,45 @@ public class RemoveAction implements NotificationsWsAction {
     response.noContent();
   }
 
-  private Consumer<AddRequest> remove() {
+  private Consumer<RemoveRequest> 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));
+        notificationUpdater.remove(dbSession, request.getChannel(), request.getType(), project.orElse(null));
         dbSession.commit();
       }
     };
   }
 
-  private Optional<ComponentDto> searchProject(DbSession dbSession, AddRequest request) {
+  private Optional<ComponentDto> searchProject(DbSession dbSession, RemoveRequest 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() {
+  private Consumer<RemoveRequest> checkPermissions() {
     return request -> userSession.checkLoggedIn();
   }
 
-  private Function<Request, AddRequest> toWsRequest() {
+  private Function<Request, RemoveRequest> toWsRequest() {
     return request -> {
-      AddRequest.Builder requestBuilder = AddRequest.builder()
-        .setNotification(request.mandatoryParam(PARAM_NOTIFICATION))
+      RemoveRequest.Builder requestBuilder = RemoveRequest.builder()
+        .setType(request.mandatoryParam(PARAM_TYPE))
         .setChannel(request.mandatoryParam(PARAM_CHANNEL));
       String project = request.param(PARAM_PROJECT);
       setNullable(project, requestBuilder::setProject);
-      AddRequest wsRequest = requestBuilder.build();
+      RemoveRequest 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(),
+        checkRequest(globalDispatchers.contains(wsRequest.getType()), "Value of parameter '%s' (%s) must be one of: %s",
+          PARAM_TYPE,
+          wsRequest.getType(),
           globalDispatchers);
       } else {
-        checkRequest(projectDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s",
-          PARAM_NOTIFICATION,
-          wsRequest.getNotification(),
+        checkRequest(projectDispatchers.contains(wsRequest.getType()), "Value of parameter '%s' (%s) must be one of: %s",
+          PARAM_TYPE,
+          wsRequest.getType(),
           projectDispatchers);
       }
 
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/notification/ws/list-example.json b/server/sonar-server/src/main/resources/org/sonar/server/notification/ws/list-example.json
new file mode 100644 (file)
index 0000000..e794aae
--- /dev/null
@@ -0,0 +1,47 @@
+{
+  "notifications": [
+    {
+      "channel": "EmailChannel",
+      "type": "MyNewIssues"
+    },
+    {
+      "channel": "EmailChannel",
+      "type": "NewIssues"
+    },
+    {
+      "channel": "TwitterChannel",
+      "type": "MyNewIssues"
+    },
+    {
+      "channel": "EmailChannel",
+      "type": "MyNewIssues",
+      "project": "my_project",
+      "projectName": "My Project"
+    },
+    {
+      "channel": "EmailChannel",
+      "type": "NewQualityGateStatus",
+      "project": "my_project",
+      "projectName": "My Project"
+    },
+    {
+      "channel": "TwitterChannel",
+      "type": "MyNewIssues",
+      "project": "my_project",
+      "projectName": "My Project"
+    }
+  ],
+  "channels": [
+    "EmailChannel",
+    "TwitterChannel"
+  ],
+  "globalTypes": [
+    "MyNewIssues",
+    "NewIssues",
+    "NewQualityGateStatus"
+  ],
+  "perProjectTypes": [
+    "MyNewIssues",
+    "NewQualityGateStatus"
+  ]
+}
index f879ed4542c8428e77e18de85ac4329daf9792f3..f81c2c5a0de21d1bd681f4eca4c49420df3b1041 100644 (file)
@@ -30,6 +30,6 @@ public class NotificationModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new NotificationModule().configure(container);
-    assertThat(container.size()).isEqualTo(11 + 2);
+    assertThat(container.size()).isEqualTo(13 + 2);
   }
 }
index f62f552b95fbe13bae8069ad31910cfedaf37ae1..b9c35d460758d9f007d475ff9423d3ffb4f9c004 100644 (file)
@@ -27,7 +27,6 @@ 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;
@@ -50,8 +49,8 @@ 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;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_TYPE;
 
 public class AddActionTest {
   private static final String NOTIF_MY_NEW_ISSUES = "Dispatcher1";
@@ -64,7 +63,6 @@ public class AddActionTest {
   @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");
@@ -76,7 +74,7 @@ public class AddActionTest {
   private WsActionTester ws;
 
   private AddRequest.Builder request = AddRequest.builder()
-    .setNotification(NOTIF_MY_NEW_ISSUES);
+    .setType(NOTIF_MY_NEW_ISSUES);
 
   @Before
   public void setUp() {
@@ -105,7 +103,7 @@ public class AddActionTest {
 
   @Test
   public void add_to_a_specific_channel() {
-    call(request.setNotification(NOTIF_NEW_QUALITY_GATE_STATUS).setChannel(twitterChannel.getKey()));
+    call(request.setType(NOTIF_NEW_QUALITY_GATE_STATUS).setChannel(twitterChannel.getKey()));
 
     db.notifications().assertExists(twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, userSession.getUserId(), null);
   }
@@ -119,6 +117,28 @@ public class AddActionTest {
     db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), project);
   }
 
+  @Test
+  public void add_a_global_notification_when_a_project_one_exists() {
+    ComponentDto project = db.components().insertProject();
+    call(request.setProject(project.getKey()));
+
+    call(request.setProject(null));
+
+    db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), project);
+    db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), null);
+  }
+
+  @Test
+  public void add_a_project_notification_when_a_global_one_exists() {
+    ComponentDto project = db.components().insertProject();
+    call(request);
+
+    call(request.setProject(project.getKey()));
+
+    db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), project);
+    db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), null);
+  }
+
   @Test
   public void http_no_content() {
     TestResponse result = call(request);
@@ -146,9 +166,9 @@ public class AddActionTest {
   @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]");
+    expectedException.expectMessage("Value of parameter 'type' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher2, Dispatcher3]");
 
-    call(request.setNotification("Dispatcher42"));
+    call(request.setType("Dispatcher42"));
   }
 
   @Test
@@ -156,9 +176,9 @@ public class AddActionTest {
     ComponentDto project = db.components().insertProject();
 
     expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher3]");
+    expectedException.expectMessage("Value of parameter 'type' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher3]");
 
-    call(request.setNotification("Dispatcher42").setProject(project.key()));
+    call(request.setType("Dispatcher42").setProject(project.key()));
   }
 
   @Test
@@ -197,7 +217,7 @@ public class AddActionTest {
   private TestResponse call(AddRequest.Builder wsRequestBuilder) {
     AddRequest wsRequest = wsRequestBuilder.build();
     TestRequest request = ws.newRequest();
-    request.setParam(PARAM_NOTIFICATION, wsRequest.getNotification());
+    request.setParam(PARAM_TYPE, wsRequest.getType());
     setNullable(wsRequest.getChannel(), channel -> request.setParam(PARAM_CHANNEL, channel));
     setNullable(wsRequest.getProject(), project -> request.setParam(PARAM_PROJECT, project));
     return request.execute();
diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/ws/ListActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/ListActionTest.java
new file mode 100644 (file)
index 0000000..a676f8f
--- /dev/null
@@ -0,0 +1,255 @@
+/*
+ * 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 com.google.common.base.Throwables;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.web.UserRole;
+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.permission.UserPermissionDto;
+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.WsActionTester;
+import org.sonarqube.ws.Notifications.ListResponse;
+import org.sonarqube.ws.Notifications.Notification;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
+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.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.test.JsonAssert.assertJson;
+import static org.sonarqube.ws.MediaTypes.PROTOBUF;
+
+public class ListActionTest {
+  private static final String NOTIF_MY_NEW_ISSUES = "MyNewIssues";
+  private static final String NOTIF_NEW_ISSUES = "NewIssues";
+  private static final String NOTIF_NEW_QUALITY_GATE_STATUS = "NewQualityGateStatus";
+  @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");
+
+  private NotificationUpdater notificationUpdater;
+
+  private WsActionTester ws;
+
+  @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 notificationCenter = new NotificationCenter(
+      new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3},
+      new NotificationChannel[] {emailChannel, twitterChannel});
+    notificationUpdater = new NotificationUpdater(userSession, dbClient);
+    ListAction underTest = new ListAction(notificationCenter, dbClient, userSession);
+    ws = new WsActionTester(underTest);
+  }
+
+  @Test
+  public void channels() {
+    ListResponse result = call();
+
+    assertThat(result.getChannelsList()).containsExactly(emailChannel.getKey(), twitterChannel.getKey());
+  }
+
+  @Test
+  public void overall_dispatchers() {
+    ListResponse result = call();
+
+    assertThat(result.getGlobalTypesList()).containsExactly(NOTIF_MY_NEW_ISSUES, NOTIF_NEW_ISSUES, NOTIF_NEW_QUALITY_GATE_STATUS);
+  }
+
+  @Test
+  public void per_project_dispatchers() {
+    ListResponse result = call();
+
+    assertThat(result.getPerProjectTypesList()).containsExactly(NOTIF_MY_NEW_ISSUES, NOTIF_NEW_QUALITY_GATE_STATUS);
+  }
+
+  @Test
+  public void filter_unauthorized_projects() {
+    ComponentDto project = addComponent(newProjectDto().setKey("K1"));
+    ComponentDto anotherProject = db.components().insertProject();
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, project);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, anotherProject);
+    dbSession.commit();
+
+    ListResponse result = call();
+
+    assertThat(result.getNotificationsList()).extracting(Notification::getProject).containsOnly("K1");
+  }
+
+  @Test
+  public void filter_channels() {
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+    notificationUpdater.add(dbSession, "Unknown Channel", NOTIF_MY_NEW_ISSUES, null);
+    dbSession.commit();
+
+    ListResponse result = call();
+
+    assertThat(result.getNotificationsList()).extracting(Notification::getChannel).containsOnly(emailChannel.getKey());
+  }
+
+  @Test
+  public void filter_overall_dispatchers() {
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), "Unknown Notification", null);
+    dbSession.commit();
+
+    ListResponse result = call();
+
+    assertThat(result.getNotificationsList()).extracting(Notification::getType).containsOnly(NOTIF_MY_NEW_ISSUES);
+  }
+
+  @Test
+  public void filter_per_project_dispatchers() {
+    ComponentDto project = addComponent(newProjectDto().setKey("K1"));
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, project);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), "Unknown Notification", project);
+    dbSession.commit();
+
+    ListResponse result = call();
+
+    assertThat(result.getNotificationsList()).extracting(Notification::getType).containsOnly(NOTIF_MY_NEW_ISSUES);
+  }
+
+  @Test
+  public void order_with_global_then_by_channel_and_dispatcher() {
+    ComponentDto project = addComponent(newProjectDto().setKey("K1"));
+    notificationUpdater.add(dbSession, twitterChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_NEW_ISSUES, null);
+    notificationUpdater.add(dbSession, twitterChannel.getKey(), NOTIF_MY_NEW_ISSUES, project);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, project);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, project);
+    dbSession.commit();
+
+    ListResponse result = call();
+
+    assertThat(result.getNotificationsList()).extracting(Notification::getChannel, Notification::getType, Notification::getProject)
+      .containsExactly(
+        tuple(emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, ""),
+        tuple(emailChannel.getKey(), NOTIF_NEW_ISSUES, ""),
+        tuple(twitterChannel.getKey(), NOTIF_MY_NEW_ISSUES, ""),
+        tuple(emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, "K1"),
+        tuple(emailChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, "K1"),
+        tuple(twitterChannel.getKey(), NOTIF_MY_NEW_ISSUES, "K1"));
+  }
+
+  @Test
+  public void json_example() {
+    ComponentDto project = addComponent(newProjectDto().setKey(KEY_PROJECT_EXAMPLE_001).setName("My Project"));
+    notificationUpdater.add(dbSession, twitterChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_NEW_ISSUES, null);
+    notificationUpdater.add(dbSession, twitterChannel.getKey(), NOTIF_MY_NEW_ISSUES, project);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_MY_NEW_ISSUES, project);
+    notificationUpdater.add(dbSession, emailChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, project);
+    dbSession.commit();
+
+    String result = ws.newRequest().execute().getInput();
+
+    assertJson(ws.getDef().responseExampleAsString()).withStrictArrayOrder().isSimilarTo(result);
+  }
+
+  @Test
+  public void definition() {
+    WebService.Action definition = ws.getDef();
+
+    assertThat(definition.key()).isEqualTo("list");
+    assertThat(definition.isPost()).isFalse();
+    assertThat(definition.params()).isEmpty();
+    assertThat(definition.responseExampleAsString()).isNotEmpty();
+  }
+
+  @Test
+  public void fail_when_not_authenticated() {
+    userSession.anonymous();
+
+    expectedException.expect(UnauthorizedException.class);
+
+    call();
+  }
+
+  private ListResponse call() {
+    try {
+      return ListResponse.parseFrom(ws.newRequest()
+        .setMediaType(PROTOBUF)
+        .execute().getInputStream());
+    } catch (IOException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+
+  private ComponentDto addComponent(ComponentDto component) {
+    db.components().insertComponent(component);
+    dbClient.userPermissionDao().insert(dbSession, new UserPermissionDto("O1", UserRole.USER, userSession.getUserId(), component.getId()));
+    db.commit();
+
+    return component;
+  }
+
+  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(org.sonar.api.notifications.Notification notification, String username) {
+      // do nothing
+    }
+  }
+}
index fd371d3e77b6dd3d1ee6d1eeed518be11bc48c70..cc4e297d4760aa4c5c4c6d929fcb7c2e57d2576b 100644 (file)
@@ -50,8 +50,8 @@ 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;
+import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_TYPE;
 
 public class RemoveActionTest {
   private static final String NOTIF_MY_NEW_ISSUES = "Dispatcher1";
@@ -77,7 +77,7 @@ public class RemoveActionTest {
 
   private WsActionTester ws;
   private AddRequest.Builder request = AddRequest.builder()
-    .setNotification(NOTIF_MY_NEW_ISSUES);
+    .setType(NOTIF_MY_NEW_ISSUES);
 
   @Before
   public void setUp() {
@@ -113,7 +113,7 @@ public class RemoveActionTest {
     notificationUpdater.add(dbSession, twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, null);
     dbSession.commit();
 
-    call(request.setNotification(NOTIF_NEW_QUALITY_GATE_STATUS).setChannel(twitterChannel.getKey()));
+    call(request.setType(NOTIF_NEW_QUALITY_GATE_STATUS).setChannel(twitterChannel.getKey()));
 
     db.notifications().assertDoesNotExist(twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, userSession.getUserId(), null);
   }
@@ -129,6 +129,30 @@ public class RemoveActionTest {
     db.notifications().assertDoesNotExist(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), project);
   }
 
+  @Test
+  public void fail_when_remove_a_global_notification_when_a_project_one_exists() {
+    ComponentDto project = db.components().insertProject();
+    notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, project);
+    dbSession.commit();
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Notification doesn't exist");
+
+    call(request);
+  }
+
+  @Test
+  public void fail_when_remove_a_project_notification_when_a_global_one_exists() {
+    ComponentDto project = db.components().insertProject();
+    notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
+    dbSession.commit();
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Notification doesn't exist");
+
+    call(request.setProject(project.getKey()));
+  }
+
   @Test
   public void http_no_content() {
     notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, null);
@@ -157,9 +181,9 @@ public class RemoveActionTest {
   @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]");
+    expectedException.expectMessage("Value of parameter 'type' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher2, Dispatcher3]");
 
-    call(request.setNotification("Dispatcher42"));
+    call(request.setType("Dispatcher42"));
   }
 
   @Test
@@ -167,9 +191,9 @@ public class RemoveActionTest {
     ComponentDto project = db.components().insertProject();
 
     expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher3]");
+    expectedException.expectMessage("Value of parameter 'type' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher3]");
 
-    call(request.setNotification("Dispatcher42").setProject(project.key()));
+    call(request.setType("Dispatcher42").setProject(project.key()));
   }
 
   @Test
@@ -208,7 +232,7 @@ public class RemoveActionTest {
   private TestResponse call(AddRequest.Builder wsRequestBuilder) {
     AddRequest wsRequest = wsRequestBuilder.build();
     TestRequest request = ws.newRequest();
-    request.setParam(PARAM_NOTIFICATION, wsRequest.getNotification());
+    request.setParam(PARAM_TYPE, wsRequest.getType());
     setNullable(wsRequest.getChannel(), channel -> request.setParam(PARAM_CHANNEL, channel));
     setNullable(wsRequest.getProject(), project -> request.setParam(PARAM_PROJECT, project));
     return request.execute();
index 4ad7893ee734fd9465a735d4345326890a5c0409..49e7a39d35c9d7a6997bb3c7bbd26c07c37a66a1 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.db.notification;
 
 import java.util.List;
 import javax.annotation.Nullable;
+import org.sonar.core.util.stream.Collectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
@@ -49,7 +50,9 @@ public class NotificationDbTester {
       .setKey(String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel))
       .setComponentId(component == null ? null : component.getId())
       .setUserId((int) userId)
-      .build(), dbSession);
+      .build(), dbSession).stream()
+      .filter(prop -> component == null ? prop.getResourceId() == null : prop.getResourceId() != null)
+      .collect(Collectors.toList());
     assertThat(result).hasSize(1);
     assertThat(result.get(0).getValue()).isEqualTo("true");
   }
index c0e97ea9fd759a1f24982054af317c509f9ae861..4903c457d0361309cfb776dd9c5f467916bd0c1d 100644 (file)
@@ -26,18 +26,18 @@ import javax.annotation.Nullable;
 import static java.util.Objects.requireNonNull;
 
 public class AddRequest {
-  private final String notification;
+  private final String type;
   private final String channel;
   private final String project;
 
   private AddRequest(Builder builder) {
     this.channel = builder.channel;
-    this.notification = builder.notification;
+    this.type = builder.type;
     this.project = builder.project;
   }
 
-  public String getNotification() {
-    return notification;
+  public String getType() {
+    return type;
   }
 
   @CheckForNull
@@ -55,7 +55,7 @@ public class AddRequest {
   }
 
   public static class Builder {
-    private String notification;
+    private String type;
     private String channel;
     private String project;
 
@@ -63,8 +63,8 @@ public class AddRequest {
       // enforce factory method
     }
 
-    public Builder setNotification(String notification) {
-      this.notification = notification;
+    public Builder setType(String type) {
+      this.type = type;
       return this;
     }
 
@@ -79,7 +79,7 @@ public class AddRequest {
     }
 
     public AddRequest build() {
-      requireNonNull(notification, "Notification is required");
+      requireNonNull(type, "Notification is required");
       return new AddRequest(this);
     }
   }
index a065b831ac94ee308c30a832df812bb2ffe249f4..a15f23b6eca0ddac16a22bef2e5c765bfff6e61b 100644 (file)
@@ -23,10 +23,12 @@ 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 ACTION_REMOVE = "remove";
+  public static final String ACTION_LIST = "list";
 
   public static final String PARAM_PROJECT = "project";
   public static final String PARAM_CHANNEL = "channel";
-  public static final String PARAM_NOTIFICATION = "notification";
+  public static final String PARAM_TYPE = "type";
 
   private NotificationsWsParameters() {
     // prevent instantiation
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/RemoveRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/RemoveRequest.java
new file mode 100644 (file)
index 0000000..95cac44
--- /dev/null
@@ -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 RemoveRequest {
+  private final String type;
+  private final String channel;
+  private final String project;
+
+  private RemoveRequest(Builder builder) {
+    this.channel = builder.channel;
+    this.type = builder.type;
+    this.project = builder.project;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  @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 type;
+    private String channel;
+    private String project;
+
+    private Builder() {
+      // enforce factory method
+    }
+
+    public Builder setType(String type) {
+      this.type = type;
+      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 RemoveRequest build() {
+      requireNonNull(type, "Notification is required");
+      return new RemoveRequest(this);
+    }
+  }
+}
diff --git a/sonar-ws/src/main/protobuf/ws-notifications.proto b/sonar-ws/src/main/protobuf/ws-notifications.proto
new file mode 100644 (file)
index 0000000..aada112
--- /dev/null
@@ -0,0 +1,42 @@
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2016 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.
+
+syntax = "proto2";
+
+package sonarqube.ws.notification;
+
+import "ws-commons.proto";
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "Notifications";
+option optimize_for = SPEED;
+
+// WS api/notifications/list
+message ListResponse {
+  repeated Notification notifications = 1;
+  repeated string channels = 2;
+  repeated string globalTypes = 3;
+  repeated string perProjectTypes = 4;
+}
+
+message Notification {
+  optional string channel = 1;
+  optional string type = 2;
+  optional string project = 3;
+  optional string projectName = 4;
+}