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;
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;
DebtRulesXMLImporter.class,
// Notifications
- NotificationModule.class,
+ AlertsEmailTemplate.class,
+ EmailSettings.class,
+ NotificationService.class,
+ NotificationCenter.class,
+ DefaultNotificationManager.class,
+ EmailNotificationChannel.class,
// Tests
TestIndexer.class,
assertThat(picoContainer.getComponentAdapters())
.hasSize(
CONTAINER_ITSELF
- + 83 // level 4
+ + 77 // level 4
+ 4 // content of CeConfigurationModule
+ 3 // content of CeHttpModule
+ 5 // content of CeQueueModule
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
EmailNotificationChannel.class,
// WS
NotificationsWs.class,
- AddAction.class);
+ AddAction.class,
+ RemoveAction.class,
+ ListAction.class);
}
}
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;
.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");
.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()
.setValue(PROP_NOTIFICATION_VALUE)
.setResourceId(projectId));
}
+
+ private static Predicate<PropertyDto> notificationScope(@Nullable ComponentDto project) {
+ return prop -> project == null ? (prop.getResourceId() == null) : (prop.getResourceId() != null);
+ }
}
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 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;
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
.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);
}
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();
}
};
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);
}
--- /dev/null
+/*
+ * 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();
+ }
+}
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;
@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")
.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(", ")),
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);
}
--- /dev/null
+{
+ "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"
+ ]
+}
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);
}
}
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 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";
@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 WsActionTester ws;
private AddRequest.Builder request = AddRequest.builder()
- .setNotification(NOTIF_MY_NEW_ISSUES);
+ .setType(NOTIF_MY_NEW_ISSUES);
@Before
public void setUp() {
@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);
}
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);
@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
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
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();
--- /dev/null
+/*
+ * 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
+ }
+ }
+}
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";
private WsActionTester ws;
private AddRequest.Builder request = AddRequest.builder()
- .setNotification(NOTIF_MY_NEW_ISSUES);
+ .setType(NOTIF_MY_NEW_ISSUES);
@Before
public void setUp() {
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);
}
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);
@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
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
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();
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;
.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");
}
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
}
public static class Builder {
- private String notification;
+ private String type;
private String channel;
private String project;
// enforce factory method
}
- public Builder setNotification(String notification) {
- this.notification = notification;
+ public Builder setType(String type) {
+ this.type = type;
return this;
}
}
public AddRequest build() {
- requireNonNull(notification, "Notification is required");
+ requireNonNull(type, "Notification is required");
return new AddRequest(this);
}
}
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
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+// 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;
+}