Skip to content

Commit

Permalink
SONAR-8557 Create WS api/notifications/list
Browse files Browse the repository at this point in the history
  • Loading branch information
teryk committed Dec 29, 2016
1 parent 3c0c955 commit f5ab57a
Show file tree
Hide file tree
Showing 17 changed files with 757 additions and 73 deletions.
Expand Up @@ -24,6 +24,7 @@
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import org.sonar.api.SonarQubeSide; import org.sonar.api.SonarQubeSide;
import org.sonar.api.SonarQubeVersion; import org.sonar.api.SonarQubeVersion;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.internal.ApiVersion; import org.sonar.api.internal.ApiVersion;
import org.sonar.api.internal.SonarRuntimeImpl; import org.sonar.api.internal.SonarRuntimeImpl;
import org.sonar.api.profiles.AnnotationProfileParser; import org.sonar.api.profiles.AnnotationProfileParser;
Expand Down Expand Up @@ -89,7 +90,11 @@
import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.issue.workflow.IssueWorkflow;
import org.sonar.server.metric.CoreCustomMetrics; import org.sonar.server.metric.CoreCustomMetrics;
import org.sonar.server.metric.DefaultMetricFinder; 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.organization.DefaultOrganizationProviderImpl;
import org.sonar.server.permission.GroupPermissionChanger; import org.sonar.server.permission.GroupPermissionChanger;
import org.sonar.server.permission.PermissionTemplateService; import org.sonar.server.permission.PermissionTemplateService;
Expand Down Expand Up @@ -364,7 +369,12 @@ private static Object[] level4Components() {
DebtRulesXMLImporter.class, DebtRulesXMLImporter.class,


// Notifications // Notifications
NotificationModule.class, AlertsEmailTemplate.class,
EmailSettings.class,
NotificationService.class,
NotificationCenter.class,
DefaultNotificationManager.class,
EmailNotificationChannel.class,


// Tests // Tests
TestIndexer.class, TestIndexer.class,
Expand Down
Expand Up @@ -88,7 +88,7 @@ public void real_start() throws IOException {
assertThat(picoContainer.getComponentAdapters()) assertThat(picoContainer.getComponentAdapters())
.hasSize( .hasSize(
CONTAINER_ITSELF CONTAINER_ITSELF
+ 83 // level 4 + 77 // level 4
+ 4 // content of CeConfigurationModule + 4 // content of CeConfigurationModule
+ 3 // content of CeHttpModule + 3 // content of CeHttpModule
+ 5 // content of CeQueueModule + 5 // content of CeQueueModule
Expand Down
Expand Up @@ -26,7 +26,9 @@
import org.sonar.server.notification.email.AlertsEmailTemplate; import org.sonar.server.notification.email.AlertsEmailTemplate;
import org.sonar.server.notification.email.EmailNotificationChannel; import org.sonar.server.notification.email.EmailNotificationChannel;
import org.sonar.server.notification.ws.AddAction; 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.NotificationsWs;
import org.sonar.server.notification.ws.RemoveAction;


public class NotificationModule extends Module { public class NotificationModule extends Module {
@Override @Override
Expand All @@ -43,6 +45,8 @@ protected void configureModule() {
EmailNotificationChannel.class, EmailNotificationChannel.class,
// WS // WS
NotificationsWs.class, NotificationsWs.class,
AddAction.class); AddAction.class,
RemoveAction.class,
ListAction.class);
} }
} }
Expand Up @@ -21,7 +21,9 @@
package org.sonar.server.notification; package org.sonar.server.notification;


import java.util.List; import java.util.List;
import java.util.function.Predicate;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.sonar.core.util.stream.Collectors;
import org.sonar.db.DbClient; import org.sonar.db.DbClient;
import org.sonar.db.DbSession; import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentDto;
Expand Down Expand Up @@ -61,7 +63,9 @@ public void add(DbSession dbSession, String channel, String dispatcher, @Nullabl
.setComponentId(projectId) .setComponentId(projectId)
.setUserId(userSession.getUserId()) .setUserId(userSession.getUserId())
.build(), .build(),
dbSession); dbSession).stream()
.filter(notificationScope(project))
.collect(Collectors.toList());
checkArgument(existingNotification.isEmpty() checkArgument(existingNotification.isEmpty()
|| !PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification already added"); || !PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification already added");


Expand Down Expand Up @@ -90,7 +94,9 @@ public void remove(DbSession dbSession, String channel, String dispatcher, @Null
.setComponentId(projectId) .setComponentId(projectId)
.setUserId(userSession.getUserId()) .setUserId(userSession.getUserId())
.build(), .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"); checkArgument(!existingNotification.isEmpty() && PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification doesn't exist");


dbClient.propertiesDao().delete(dbSession, new PropertyDto() dbClient.propertiesDao().delete(dbSession, new PropertyDto()
Expand All @@ -99,4 +105,8 @@ public void remove(DbSession dbSession, String channel, String dispatcher, @Null
.setValue(PROP_NOTIFICATION_VALUE) .setValue(PROP_NOTIFICATION_VALUE)
.setResourceId(projectId)); .setResourceId(projectId));
} }

private static Predicate<PropertyDto> notificationScope(@Nullable ComponentDto project) {
return prop -> project == null ? (prop.getResourceId() == null) : (prop.getResourceId() != null);
}
} }
Expand Up @@ -24,7 +24,6 @@
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.sonar.api.notifications.NotificationChannel; import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Qualifiers;
Expand All @@ -46,13 +45,14 @@


import static java.util.Optional.empty; import static java.util.Optional.empty;
import static org.sonar.core.util.Protobuf.setNullable; 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.GLOBAL_NOTIFICATION;
import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION; import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
import static org.sonar.server.ws.WsUtils.checkRequest; 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_ADD;
import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL; 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_PROJECT;
import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_TYPE;


public class AddAction implements NotificationsWsAction { public class AddAction implements NotificationsWsAction {
private final NotificationCenter notificationCenter; private final NotificationCenter notificationCenter;
Expand All @@ -69,8 +69,8 @@ public AddAction(NotificationCenter notificationCenter, NotificationUpdater noti
this.dbClient = dbClient; this.dbClient = dbClient;
this.componentFinder = componentFinder; this.componentFinder = componentFinder;
this.userSession = userSession; this.userSession = userSession;
this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true"); this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true").stream().sorted().collect(toList());
this.projectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true"); this.projectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true").stream().sorted().collect(toList());
} }


@Override @Override
Expand All @@ -92,14 +92,14 @@ public void define(WebService.NewController context) {
.setPossibleValues(channels) .setPossibleValues(channels)
.setDefaultValue(EmailNotificationChannel.class.getSimpleName()); .setDefaultValue(EmailNotificationChannel.class.getSimpleName());


action.createParam(PARAM_NOTIFICATION) action.createParam(PARAM_TYPE)
.setDescription("Notification. Possible values are for:" + .setDescription("Notification type. Possible values are for:" +
"<ul>" + "<ul>" +
" <li>Overall notifications: %s</li>" + " <li>Global notifications: %s</li>" +
" <li>Per project notifications: %s</li>" + " <li>Per project notifications: %s</li>" +
"</ul>", "</ul>",
globalDispatchers.stream().sorted().collect(Collectors.joining(", ")), String.join(", ", globalDispatchers),
projectDispatchers.stream().sorted().collect(Collectors.joining(", "))) String.join(", ", projectDispatchers))
.setRequired(true) .setRequired(true)
.setExampleValue(MyNewIssuesNotificationDispatcher.KEY); .setExampleValue(MyNewIssuesNotificationDispatcher.KEY);
} }
Expand All @@ -118,7 +118,7 @@ private Consumer<AddRequest> add() {
return request -> { return request -> {
try (DbSession dbSession = dbClient.openSession(false)) { try (DbSession dbSession = dbClient.openSession(false)) {
Optional<ComponentDto> project = searchProject(dbSession, request); 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(); dbSession.commit();
} }
}; };
Expand All @@ -138,21 +138,21 @@ private Consumer<AddRequest> checkPermissions() {
private Function<Request, AddRequest> toWsRequest() { private Function<Request, AddRequest> toWsRequest() {
return request -> { return request -> {
AddRequest.Builder requestBuilder = AddRequest.builder() AddRequest.Builder requestBuilder = AddRequest.builder()
.setNotification(request.mandatoryParam(PARAM_NOTIFICATION)) .setType(request.mandatoryParam(PARAM_TYPE))
.setChannel(request.mandatoryParam(PARAM_CHANNEL)); .setChannel(request.mandatoryParam(PARAM_CHANNEL));
String project = request.param(PARAM_PROJECT); String project = request.param(PARAM_PROJECT);
setNullable(project, requestBuilder::setProject); setNullable(project, requestBuilder::setProject);
AddRequest wsRequest = requestBuilder.build(); AddRequest wsRequest = requestBuilder.build();


if (wsRequest.getProject() == null) { if (wsRequest.getProject() == null) {
checkRequest(globalDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s", checkRequest(globalDispatchers.contains(wsRequest.getType()), "Value of parameter '%s' (%s) must be one of: %s",
PARAM_NOTIFICATION, PARAM_TYPE,
wsRequest.getNotification(), wsRequest.getType(),
globalDispatchers); globalDispatchers);
} else { } else {
checkRequest(projectDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s", checkRequest(projectDispatchers.contains(wsRequest.getType()), "Value of parameter '%s' (%s) must be one of: %s",
PARAM_NOTIFICATION, PARAM_TYPE,
wsRequest.getNotification(), wsRequest.getType(),
projectDispatchers); projectDispatchers);
} }


Expand Down
@@ -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();
}
}

0 comments on commit f5ab57a

Please sign in to comment.