Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce first service API concept and reference implementation #1031

Merged
merged 19 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions project-management/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.projectreactor/reactor-core -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package life.qbic.projectmanagement.application.api;

import reactor.core.publisher.Mono;

/**
* Service API layer the user interface code shall interact with in the application.
* <p>
* The API uses a straight-forward request-response pattern and promotes a reactive service
* interaction.
* <p>
* The interface definition also contains the request and response object records and their body
* interfaces.
* <p>
* Implementing classes must ensure to throw the proper exceptions expected by the client based on
* the service methods exposed in this interface.
*
* @since 1.9.0
*/
public interface AsyncProjectService {

/**
* Submits a project update request and returns a reactive {@link Mono<ProjectUpdateResponse>}
* object immediately.
* <p>
* The method is non-blocking.
* <p>
* The implementing class must ensure to be able to process all implementing classes of the
* {@link UpdateRequestBody} interface contained in the request.
* <p>
* The implementing class must also ensure to only return responses with classes implementing the
* {@link UpdateResponseBody} interface.
*
* @param request the request to update a project
* @return a {@link Mono<ProjectUpdateResponse>} object publishing an
* {@link ProjectUpdateResponse} on success.
* @throws UnknownRequestException if an unknown request has been used in the service call
* @throws RequestFailedException if the request was not successfully executed
* @throws AccessDeniedException if the user has insufficient rights
* @since 1.9.0
*/
Mono<ProjectUpdateResponse> update(
ProjectUpdateRequest request)
throws UnknownRequestException, RequestFailedException, AccessDeniedException;

/**
* Container of an update request for a service call and part of the
* {@link ProjectUpdateRequest}.
*
* @since 1.9.0
*/
sealed interface UpdateRequestBody permits ProjectDesign {

}

/**
* Container of an update response from a service call and part of the
* {@link ProjectUpdateResponse}.
*
* @since 1.9.0
*/
sealed interface UpdateResponseBody permits ProjectDesign {

}

/**
* Container for passing information in an {@link UpdateRequestBody} or
* {@link UpdateResponseBody}.
*
* @param title the title of the project
* @param objective the objective of the project
* @since 1.9.0
*/
record ProjectDesign(String title, String objective) implements UpdateRequestBody,
UpdateResponseBody {

}

/**
* A service request to update project information.
*
* @param projectId the project's id
* @param requestBody the information to be updated.
* @since 1.9.0
*/
record ProjectUpdateRequest(String projectId, UpdateRequestBody requestBody) {

}

/**
* A service response from an update project information request.
*
* @param projectId the project's id
* @param responseBody the information that was updated.
* @since 1.9.0
*/
record ProjectUpdateResponse(String projectId, UpdateResponseBody responseBody) {

}

/**
* Exception to indicate that the service did not recognise the request.
*
* @since 1.9.0
*/
class UnknownRequestException extends RuntimeException {

public UnknownRequestException(String message) {
super(message);
}
}

/**
* Exception to indicate that the service tried to execute the request, but it failed.
*
* @since 1.9.0
*/
class RequestFailedException extends RuntimeException {

public RequestFailedException(String message) {
super(message);
}

public RequestFailedException(String message, Throwable cause) {
super(message, cause);
}
}

/**
* Exception to indicate that the service tried to execute the request, but the user had
* insufficient rights and thus the request failed.
*
* @since 1.9.0
*/
class AccessDeniedException extends RuntimeException {

public AccessDeniedException(String message) {
super(message);
}

public AccessDeniedException(String message, Throwable cause) {
super(message, cause);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package life.qbic.projectmanagement.application.api;

import java.util.Objects;
import java.util.function.Supplier;
import life.qbic.projectmanagement.application.ProjectInformationService;
import life.qbic.projectmanagement.domain.model.project.ProjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;

/**
* <b><class short description - 1 Line!></b>
*
* <p><More detailed description - When to use, what it solves, etc.></p>
*
* @since <version tag>
*/
@Service
public class AsyncProjectServiceImpl implements AsyncProjectService {

private final ProjectInformationService projectService;
private final Scheduler scheduler;

public AsyncProjectServiceImpl(@Autowired ProjectInformationService projectService,
@Autowired Scheduler scheduler) {
this.projectService = Objects.requireNonNull(projectService);
this.scheduler = Objects.requireNonNull(scheduler);
}

@Override
public Mono<ProjectUpdateResponse> update(@NonNull ProjectUpdateRequest request)
throws UnknownRequestException, RequestFailedException, AccessDeniedException {
var projectId = request.projectId();
switch (request.requestBody()) {
case ProjectDesign design:
return
withSecurityContext(SecurityContextHolder.getContext(),
() -> updateProjectDesign(projectId, design)).subscribeOn(scheduler);
default:
return Mono.error(new UnknownRequestException("Invalid request body"));
}
}

/*
Configures and writes the provided security context for a supplier of type Mono<ProjectUpdateResponse>. Without
the context written to the reactive stream, services that have access control methods will fail.
*/
private Mono<ProjectUpdateResponse> withSecurityContext(SecurityContext sctx,
Supplier<Mono<ProjectUpdateResponse>> supplier) {
var rcontext = ReactiveSecurityContextHolder.withSecurityContext(Mono.just(sctx));
return ReactiveSecurityContextHolder.getContext().flatMap(securityContext1 -> {
SecurityContextHolder.setContext(securityContext1);
return supplier.get();
}).contextWrite(rcontext);
}

private Mono<ProjectUpdateResponse> updateProjectDesign(String projectId, ProjectDesign design) {
return Mono.create(sink -> {
try {
var id = ProjectId.parse(projectId);
projectService.updateTitle(id, design.title());
projectService.updateObjective(id, design.objective());
sink.success(new ProjectUpdateResponse(projectId, design));
} catch (IllegalArgumentException e) {
sink.error(new RequestFailedException("Invalid project id: " + projectId));
} catch (org.springframework.security.access.AccessDeniedException e) {
sink.error(new AccessDeniedException("Access denied"));
} catch (RuntimeException e) {
sink.error(new RequestFailedException("Update project design failed", e));
}
});
}
}


Binary file modified user-interface/src/main/bundles/prod.bundle
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import reactor.core.scheduler.Scheduler;

/**
* <b>App bean configuration class</b>
Expand All @@ -91,6 +92,11 @@ public class AppConfig {

Section starts below
*/
@Bean
public Scheduler reactiveScheduler() {
return VirtualThreadScheduler.getScheduler();
}


@Bean
public IdentityService userRegistrationService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor;

@Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package life.qbic.datamanager;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.springframework.context.annotation.Bean;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
* <b><class short description - 1 Line!></b>
*
* <p><More detailed description - When to use, what it solves, etc.></p>
*
* @since <version tag>
*/
public class VirtualThreadScheduler {

private static final Executor executor = Executors.newVirtualThreadPerTaskExecutor();
private static final Scheduler scheduler = Schedulers.fromExecutor(executor);

public static Scheduler getScheduler() {
return scheduler;
}

@Bean
public static Scheduler scheduler() {
return scheduler;
}



}
Loading
Loading