Skip to content

Commit 0760bf3

Browse files
sven1103KochTobi
andauthored
Introduce first service API concept and reference implementation (#1031)
Introduces a service API that shall replace the service injections in UI components. This PR comes with a first service API interface and a reference implementation for updating a project design. This PR also introduces reactor, a library for reactive, non-blocking execution of tasks. --------- Co-authored-by: KochTobi <[email protected]> Co-authored-by: Sven F. <[email protected]> Co-authored-by: Tobias Koch <[email protected]>
1 parent a3124f0 commit 0760bf3

File tree

9 files changed

+383
-14
lines changed

9 files changed

+383
-14
lines changed

project-management/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
<artifactId>spring-boot-starter-test</artifactId>
3131
<scope>test</scope>
3232
</dependency>
33+
<!-- https://mvnrepository.com/artifact/io.projectreactor/reactor-core -->
34+
<dependency>
35+
<groupId>io.projectreactor</groupId>
36+
<artifactId>reactor-core</artifactId>
37+
</dependency>
3338
<dependency>
3439
<groupId>jakarta.persistence</groupId>
3540
<artifactId>jakarta.persistence-api</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package life.qbic.projectmanagement.application.api;
2+
3+
import reactor.core.publisher.Mono;
4+
5+
/**
6+
* Service API layer the user interface code shall interact with in the application.
7+
* <p>
8+
* The API uses a straight-forward request-response pattern and promotes a reactive service
9+
* interaction.
10+
* <p>
11+
* The interface definition also contains the request and response object records and their body
12+
* interfaces.
13+
* <p>
14+
* Implementing classes must ensure to throw the proper exceptions expected by the client based on
15+
* the service methods exposed in this interface.
16+
*
17+
* @since 1.9.0
18+
*/
19+
public interface AsyncProjectService {
20+
21+
/**
22+
* Submits a project update request and returns a reactive {@link Mono<ProjectUpdateResponse>}
23+
* object immediately.
24+
* <p>
25+
* The method is non-blocking.
26+
* <p>
27+
* The implementing class must ensure to be able to process all implementing classes of the
28+
* {@link UpdateRequestBody} interface contained in the request.
29+
* <p>
30+
* The implementing class must also ensure to only return responses with classes implementing the
31+
* {@link UpdateResponseBody} interface.
32+
*
33+
* @param request the request to update a project
34+
* @return a {@link Mono<ProjectUpdateResponse>} object publishing an
35+
* {@link ProjectUpdateResponse} on success.
36+
* @throws UnknownRequestException if an unknown request has been used in the service call
37+
* @throws RequestFailedException if the request was not successfully executed
38+
* @throws AccessDeniedException if the user has insufficient rights
39+
* @since 1.9.0
40+
*/
41+
Mono<ProjectUpdateResponse> update(
42+
ProjectUpdateRequest request)
43+
throws UnknownRequestException, RequestFailedException, AccessDeniedException;
44+
45+
/**
46+
* Container of an update request for a service call and part of the
47+
* {@link ProjectUpdateRequest}.
48+
*
49+
* @since 1.9.0
50+
*/
51+
sealed interface UpdateRequestBody permits ProjectDesign {
52+
53+
}
54+
55+
/**
56+
* Container of an update response from a service call and part of the
57+
* {@link ProjectUpdateResponse}.
58+
*
59+
* @since 1.9.0
60+
*/
61+
sealed interface UpdateResponseBody permits ProjectDesign {
62+
63+
}
64+
65+
/**
66+
* Container for passing information in an {@link UpdateRequestBody} or
67+
* {@link UpdateResponseBody}.
68+
*
69+
* @param title the title of the project
70+
* @param objective the objective of the project
71+
* @since 1.9.0
72+
*/
73+
record ProjectDesign(String title, String objective) implements UpdateRequestBody,
74+
UpdateResponseBody {
75+
76+
}
77+
78+
/**
79+
* A service request to update project information.
80+
*
81+
* @param projectId the project's id
82+
* @param requestBody the information to be updated.
83+
* @since 1.9.0
84+
*/
85+
record ProjectUpdateRequest(String projectId, UpdateRequestBody requestBody) {
86+
87+
}
88+
89+
/**
90+
* A service response from an update project information request.
91+
*
92+
* @param projectId the project's id
93+
* @param responseBody the information that was updated.
94+
* @since 1.9.0
95+
*/
96+
record ProjectUpdateResponse(String projectId, UpdateResponseBody responseBody) {
97+
98+
}
99+
100+
/**
101+
* Exception to indicate that the service did not recognise the request.
102+
*
103+
* @since 1.9.0
104+
*/
105+
class UnknownRequestException extends RuntimeException {
106+
107+
public UnknownRequestException(String message) {
108+
super(message);
109+
}
110+
}
111+
112+
/**
113+
* Exception to indicate that the service tried to execute the request, but it failed.
114+
*
115+
* @since 1.9.0
116+
*/
117+
class RequestFailedException extends RuntimeException {
118+
119+
public RequestFailedException(String message) {
120+
super(message);
121+
}
122+
123+
public RequestFailedException(String message, Throwable cause) {
124+
super(message, cause);
125+
}
126+
}
127+
128+
/**
129+
* Exception to indicate that the service tried to execute the request, but the user had
130+
* insufficient rights and thus the request failed.
131+
*
132+
* @since 1.9.0
133+
*/
134+
class AccessDeniedException extends RuntimeException {
135+
136+
public AccessDeniedException(String message) {
137+
super(message);
138+
}
139+
140+
public AccessDeniedException(String message, Throwable cause) {
141+
super(message, cause);
142+
}
143+
}
144+
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package life.qbic.projectmanagement.application.api;
2+
3+
import java.util.Objects;
4+
import java.util.function.Supplier;
5+
import life.qbic.projectmanagement.application.ProjectInformationService;
6+
import life.qbic.projectmanagement.domain.model.project.ProjectId;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.lang.NonNull;
9+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
10+
import org.springframework.security.core.context.SecurityContext;
11+
import org.springframework.security.core.context.SecurityContextHolder;
12+
import org.springframework.stereotype.Service;
13+
import reactor.core.publisher.Mono;
14+
import reactor.core.scheduler.Scheduler;
15+
16+
/**
17+
* <b><class short description - 1 Line!></b>
18+
*
19+
* <p><More detailed description - When to use, what it solves, etc.></p>
20+
*
21+
* @since <version tag>
22+
*/
23+
@Service
24+
public class AsyncProjectServiceImpl implements AsyncProjectService {
25+
26+
private final ProjectInformationService projectService;
27+
private final Scheduler scheduler;
28+
29+
public AsyncProjectServiceImpl(@Autowired ProjectInformationService projectService,
30+
@Autowired Scheduler scheduler) {
31+
this.projectService = Objects.requireNonNull(projectService);
32+
this.scheduler = Objects.requireNonNull(scheduler);
33+
}
34+
35+
@Override
36+
public Mono<ProjectUpdateResponse> update(@NonNull ProjectUpdateRequest request)
37+
throws UnknownRequestException, RequestFailedException, AccessDeniedException {
38+
var projectId = request.projectId();
39+
switch (request.requestBody()) {
40+
case ProjectDesign design:
41+
return
42+
withSecurityContext(SecurityContextHolder.getContext(),
43+
() -> updateProjectDesign(projectId, design)).subscribeOn(scheduler);
44+
default:
45+
return Mono.error(new UnknownRequestException("Invalid request body"));
46+
}
47+
}
48+
49+
/*
50+
Configures and writes the provided security context for a supplier of type Mono<ProjectUpdateResponse>. Without
51+
the context written to the reactive stream, services that have access control methods will fail.
52+
*/
53+
private Mono<ProjectUpdateResponse> withSecurityContext(SecurityContext sctx,
54+
Supplier<Mono<ProjectUpdateResponse>> supplier) {
55+
var rcontext = ReactiveSecurityContextHolder.withSecurityContext(Mono.just(sctx));
56+
return ReactiveSecurityContextHolder.getContext().flatMap(securityContext1 -> {
57+
SecurityContextHolder.setContext(securityContext1);
58+
return supplier.get();
59+
}).contextWrite(rcontext);
60+
}
61+
62+
private Mono<ProjectUpdateResponse> updateProjectDesign(String projectId, ProjectDesign design) {
63+
return Mono.create(sink -> {
64+
try {
65+
var id = ProjectId.parse(projectId);
66+
projectService.updateTitle(id, design.title());
67+
projectService.updateObjective(id, design.objective());
68+
sink.success(new ProjectUpdateResponse(projectId, design));
69+
} catch (IllegalArgumentException e) {
70+
sink.error(new RequestFailedException("Invalid project id: " + projectId));
71+
} catch (org.springframework.security.access.AccessDeniedException e) {
72+
sink.error(new AccessDeniedException("Access denied"));
73+
} catch (RuntimeException e) {
74+
sink.error(new RequestFailedException("Update project design failed", e));
75+
}
76+
});
77+
}
78+
}
79+
80+
-700 KB
Binary file not shown.

user-interface/src/main/java/life/qbic/datamanager/AppConfig.java

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import org.springframework.context.annotation.Bean;
7575
import org.springframework.context.annotation.ComponentScan;
7676
import org.springframework.context.annotation.Configuration;
77+
import reactor.core.scheduler.Scheduler;
7778

7879
/**
7980
* <b>App bean configuration class</b>
@@ -91,6 +92,11 @@ public class AppConfig {
9192
9293
Section starts below
9394
*/
95+
@Bean
96+
public Scheduler reactiveScheduler() {
97+
return VirtualThreadScheduler.getScheduler();
98+
}
99+
94100

95101
@Bean
96102
public IdentityService userRegistrationService(

user-interface/src/main/java/life/qbic/datamanager/AsyncConfig.java

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.scheduling.annotation.AsyncConfigurer;
99
import org.springframework.scheduling.annotation.EnableAsync;
1010
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
11+
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
1112
import org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor;
1213

1314
@Configuration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package life.qbic.datamanager;
2+
3+
import java.util.concurrent.Executor;
4+
import java.util.concurrent.Executors;
5+
import org.springframework.context.annotation.Bean;
6+
import reactor.core.scheduler.Scheduler;
7+
import reactor.core.scheduler.Schedulers;
8+
9+
/**
10+
* <b><class short description - 1 Line!></b>
11+
*
12+
* <p><More detailed description - When to use, what it solves, etc.></p>
13+
*
14+
* @since <version tag>
15+
*/
16+
public class VirtualThreadScheduler {
17+
18+
private static final Executor executor = Executors.newVirtualThreadPerTaskExecutor();
19+
private static final Scheduler scheduler = Schedulers.fromExecutor(executor);
20+
21+
public static Scheduler getScheduler() {
22+
return scheduler;
23+
}
24+
25+
@Bean
26+
public static Scheduler scheduler() {
27+
return scheduler;
28+
}
29+
30+
31+
32+
}

0 commit comments

Comments
 (0)