Skip to content

Commit

Permalink
[3019] Restore support for the project events subscription
Browse files Browse the repository at this point in the history
Bug: #3019
Signed-off-by: Stéphane Bégaudeau <stephane.begaudeau@obeo.fr>
  • Loading branch information
sbegaudeau committed Feb 23, 2024
1 parent 5940967 commit fbe2108
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 1 deletion.
@@ -0,0 +1,59 @@
/*******************************************************************************
* Copyright (c) 2024 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.application.project.controllers;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Objects;
import java.util.function.Supplier;

import org.eclipse.sirius.components.annotations.spring.graphql.SubscriptionDataFetcher;
import org.eclipse.sirius.components.core.api.IPayload;
import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates;
import org.eclipse.sirius.components.graphql.api.IExceptionWrapper;
import org.eclipse.sirius.web.application.project.dto.ProjectEventInput;
import org.eclipse.sirius.web.application.project.services.api.IProjectSubscriptions;
import org.reactivestreams.Publisher;

import graphql.schema.DataFetchingEnvironment;
import reactor.core.publisher.Flux;

/**
* Data fetcher for the field Subscription#projectEvent.
*
* @author sbegaudeau
*/
@SubscriptionDataFetcher(type = "Subscription", field = "projectEvent")
public class SubscriptionProjectEventDataFetcher implements IDataFetcherWithFieldCoordinates<Publisher<IPayload>> {

private final ObjectMapper objectMapper;

private final IExceptionWrapper exceptionWrapper;

private final IProjectSubscriptions projectSubscriptions;

public SubscriptionProjectEventDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IProjectSubscriptions projectSubscriptions) {
this.objectMapper = Objects.requireNonNull(objectMapper);
this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper);
this.projectSubscriptions = Objects.requireNonNull(projectSubscriptions);
}

@Override
public Publisher<IPayload> get(DataFetchingEnvironment environment) throws Exception {
Object argument = environment.getArgument("input");
var input = this.objectMapper.convertValue(argument, ProjectEventInput.class);

Supplier<Flux<IPayload>> supplier = () -> this.projectSubscriptions.findProjectSubscriptionById(input.projectId()).orElse(Flux.empty());
return this.exceptionWrapper.wrapFlux(supplier, input);
}
}
@@ -0,0 +1,27 @@
/*******************************************************************************
* Copyright (c) 2023, 2024 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.application.project.dto;

import java.util.UUID;

import org.eclipse.sirius.components.core.api.IInput;

import jakarta.validation.constraints.NotNull;

/**
* The input of the project event subscription.
*
* @author arichard
*/
public record ProjectEventInput(@NotNull UUID id, @NotNull UUID projectId) implements IInput {
}
@@ -0,0 +1,27 @@
/*******************************************************************************
* Copyright (c) 2023, 2024 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.application.project.dto;

import java.util.UUID;

import org.eclipse.sirius.components.core.api.IPayload;

import jakarta.validation.constraints.NotNull;

/**
* Payload used to indicate that project's name has been updated.
*
* @author arichard
*/
public record ProjectRenamedEventPayload(@NotNull UUID id, @NotNull UUID projectId, @NotNull String newName) implements IPayload {
}
@@ -0,0 +1,49 @@
/*******************************************************************************
* Copyright (c) 2024 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.application.project.listeners;

import java.util.Objects;
import java.util.UUID;

import org.eclipse.sirius.web.application.project.dto.ProjectRenamedEventPayload;
import org.eclipse.sirius.web.application.project.services.api.IProjectSubscriptions;
import org.eclipse.sirius.web.domain.boundedcontexts.project.events.ProjectDeletedEvent;
import org.eclipse.sirius.web.domain.boundedcontexts.project.events.ProjectNameUpdatedEvent;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;

/**
* Used to listen to project events in order to provide events for the project subscriptions.
*
* @author sbegaudeau
*/
@Service
public class ProjectSubscriptionListener {

private final IProjectSubscriptions projectSubscriptions;

public ProjectSubscriptionListener(IProjectSubscriptions projectSubscriptions) {
this.projectSubscriptions = Objects.requireNonNull(projectSubscriptions);
}

@TransactionalEventListener
public void onProjectNameUpdatedEvent(ProjectNameUpdatedEvent event) {
var payload = new ProjectRenamedEventPayload(UUID.randomUUID(), event.project().getId(), event.project().getName());
this.projectSubscriptions.emit(event.project().getId(), payload);
}

@TransactionalEventListener
public void onProjectDeletedEvent(ProjectDeletedEvent event) {
this.projectSubscriptions.dispose(event.project().getId());
}
}
@@ -0,0 +1,66 @@
/*******************************************************************************
* Copyright (c) 2024 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.application.project.services;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

import org.eclipse.sirius.components.core.api.IPayload;
import org.eclipse.sirius.web.application.project.services.api.IProjectSubscriptions;
import org.eclipse.sirius.web.domain.boundedcontexts.project.services.api.IProjectSearchService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;

/**
* Used to publish and subscribe to project events.
*
* @author sbegaudeau
*/
@Service
public class ProjectSubscriptions implements IProjectSubscriptions {

private final IProjectSearchService projectSearchService;

private final Map<UUID, Sinks.Many<IPayload>> projectIdsToSink = new HashMap<>();

public ProjectSubscriptions(IProjectSearchService projectSearchService) {
this.projectSearchService = Objects.requireNonNull(projectSearchService);
}

@Override
@Transactional(readOnly = true)
public Optional<Flux<IPayload>> findProjectSubscriptionById(UUID projectId) {
if (this.projectSearchService.existsById(projectId)) {
var many = this.projectIdsToSink.computeIfAbsent(projectId, id -> Sinks.many().multicast().directBestEffort());
return Optional.of(many.asFlux());
}
return Optional.empty();
}

@Override
public void emit(UUID projectId, IPayload payload) {
Optional.ofNullable(this.projectIdsToSink.get(projectId)).ifPresent(many -> many.tryEmitNext(payload));
}

@Override
public void dispose(UUID projectId) {
Optional.ofNullable(this.projectIdsToSink.get(projectId)).ifPresent(Sinks.Many::tryEmitComplete);
this.projectIdsToSink.remove(projectId);
}
}
@@ -0,0 +1,33 @@
/*******************************************************************************
* Copyright (c) 2024, 2024 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.application.project.services.api;

import java.util.Optional;
import java.util.UUID;

import org.eclipse.sirius.components.core.api.IPayload;

import reactor.core.publisher.Flux;

/**
* Used to publish or subscribe to project events.
*
* @author sbegaudeau
*/
public interface IProjectSubscriptions {
Optional<Flux<IPayload>> findProjectSubscriptionById(UUID projectId);

void emit(UUID projectId, IPayload payload);

void dispose(UUID projectId);
}
Expand Up @@ -84,4 +84,21 @@ union DeleteProjectPayload = ErrorPayload | DeleteProjectSuccessPayload

type DeleteProjectSuccessPayload {
id: ID!
}

extend type Subscription {
projectEvent(input: ProjectEventInput!): ProjectEventPayload!
}

input ProjectEventInput {
id: ID!
projectId: ID!
}

union ProjectEventPayload = ErrorPayload | ProjectRenamedEventPayload

type ProjectRenamedEventPayload {
id: ID!
projectId: ID!
newName: String!
}
@@ -0,0 +1,64 @@
/*******************************************************************************
* Copyright (c) 2022, 2024 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.starter;

import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

import org.eclipse.sirius.components.core.api.IInput;
import org.eclipse.sirius.components.core.api.IPayload;
import org.eclipse.sirius.components.graphql.api.IExceptionWrapper;

import graphql.relay.Connection;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
* Used to encapsulate calls to methods which can throw exceptions.
*
* @author sbegaudeau
*/
public class ExceptionWrapper implements IExceptionWrapper {

@Override
public Flux<IPayload> wrapFlux(Supplier<Flux<IPayload>> supplier, IInput input) {
return supplier.get();
}

@Override
public IPayload wrap(Supplier<IPayload> supplier, IInput input) {
return supplier.get();
}

@Override
public <T> List<T> wrapList(Supplier<List<T>> supplier) {
return supplier.get();
}

@Override
public <T> Optional<T> wrapOptional(Supplier<Optional<T>> supplier) {
return supplier.get();
}

@Override
public <T> Connection<T> wrapConnection(Supplier<Connection<T>> supplier) {
return supplier.get();
}

@Override
public Mono<IPayload> wrapMono(Supplier<Mono<IPayload>> supplier, IInput input) {
return supplier.get();
}

}
Expand Up @@ -17,6 +17,7 @@
import org.eclipse.sirius.components.collaborative.api.ISubscriptionManagerFactory;
import org.eclipse.sirius.components.collaborative.editingcontext.api.IEditingContextEventProcessorExecutorServiceProvider;
import org.eclipse.sirius.components.collaborative.representations.SubscriptionManager;
import org.eclipse.sirius.components.graphql.api.IExceptionWrapper;
import org.eclipse.sirius.components.graphql.ws.api.IGraphQLWebSocketHandlerListener;
import org.eclipse.sirius.components.web.concurrent.DelegatingRequestContextExecutorService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
Expand Down Expand Up @@ -66,6 +67,12 @@ public ISubscriptionManagerFactory subscriptionManagerFactory() {
return SubscriptionManager::new;
}

@Bean
@ConditionalOnMissingBean
public IExceptionWrapper exceptionWrapper() {
return new ExceptionWrapper();
}

@Bean
@ConditionalOnMissingBean(IGraphQLWebSocketHandlerListener.class)
public IGraphQLWebSocketHandlerListener graphQLWebSocketHandlerListener() {
Expand Down
5 changes: 5 additions & 0 deletions packages/sirius-web/backend/sirius-web/pom.xml
Expand Up @@ -54,6 +54,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
Expand Down
Expand Up @@ -22,4 +22,6 @@ spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

spring.liquibase.change-log=classpath:db/db.changelog-master.xml
spring.liquibase.change-log=classpath:db/db.changelog-master.xml

sirius.components.cors.allowedOriginPatterns=*

0 comments on commit fbe2108

Please sign in to comment.