Skip to content

Commit 9810423

Browse files
authored
Implement API for sample management (#1063)
Provides the async service implementation for the sample management related requests.
1 parent b38fa11 commit 9810423

File tree

10 files changed

+254
-66
lines changed

10 files changed

+254
-66
lines changed

project-management/src/main/java/life/qbic/projectmanagement/application/api/AsyncProjectService.java

+17-28
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package life.qbic.projectmanagement.application.api;
22

3+
import static java.util.Objects.nonNull;
4+
35
import java.nio.ByteBuffer;
46
import java.util.Collection;
57
import java.util.List;
6-
import static java.util.Objects.nonNull;
78
import java.util.Optional;
89
import java.util.Set;
910
import java.util.UUID;
11+
import life.qbic.application.commons.SortOrder;
1012
import life.qbic.projectmanagement.application.batch.SampleUpdateRequest.SampleInformation;
1113
import life.qbic.projectmanagement.application.confounding.ConfoundingVariableService.ConfoundingVariableInformation;
12-
import life.qbic.projectmanagement.application.sample.SampleIdCodeEntry;
1314
import life.qbic.projectmanagement.application.sample.SamplePreview;
1415
import life.qbic.projectmanagement.domain.model.sample.Sample;
1516
import life.qbic.projectmanagement.domain.model.sample.SampleRegistrationRequest;
@@ -142,24 +143,6 @@ Mono<ProjectCreationResponse> create(ProjectCreationRequest request)
142143
Flux<ByteBuffer> roCrateSummary(String projectId)
143144
throws RequestFailedException, AccessDeniedException;
144145

145-
/**
146-
* Requests {@link SamplePreview} for a given experiment.
147-
* <p>
148-
* <b>Exceptions</b>
149-
* <p>
150-
* Exceptions are wrapped as {@link Mono#error(Throwable)} and are one of the types described in
151-
* the throw section below.
152-
*
153-
* @param projectId the project ID for the project to get the samples for
154-
* @param experimentId the experiment ID for which the sample preview shall be retrieved
155-
* @return a reactive stream of {@link SamplePreview} objects of the experiment. Exceptions are
156-
* provided as {@link Mono#error(Throwable)}.
157-
* @throws RequestFailedException if the request could not be executed
158-
* @since 1.10.0
159-
*/
160-
Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId)
161-
throws RequestFailedException;
162-
163146
/**
164147
* Requests {@link SamplePreview} for a given experiment with pagination support.
165148
* <p>
@@ -173,12 +156,15 @@ Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId)
173156
* @param offset the offset from 0 of all available previews the returned previews should
174157
* start
175158
* @param limit the maximum number of previews that should be returned
159+
* @param sortOrders the sort orders to apply
160+
* @param filter the filter to apply
176161
* @return a reactive stream of {@link SamplePreview} objects in the experiment. Exceptions are
177162
* provided as {@link Mono#error(Throwable)}.
163+
* @throws RequestFailedException if the request could not be executed
178164
* @since 1.10.0
179165
*/
180166
Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId, int offset,
181-
int limit);
167+
int limit, List<SortOrder> sortOrders, String filter);
182168

183169
/**
184170
* Requests all {@link Sample} for a given experiment.
@@ -216,22 +202,25 @@ Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId, int
216202
Flux<Sample> getSamplesForBatch(String projectId, String batchId) throws RequestFailedException;
217203

218204
/**
219-
* Find the sample ID for a given sample code
205+
* Finds the sample for a given sample ID.
206+
* <p>
207+
* In case no matching sample is found, a {@link Mono#empty()} is returned.
208+
*
220209
* <p>
221210
* <b>Exceptions</b>
222211
* <p>
223212
* Exceptions are wrapped as {@link Mono#error(Throwable)} and are one of the types described in
224213
* the throw section below.
225214
*
226-
* @param projectId the project ID for the project to get the samples for
227-
* @param sampleCode the sample code (e.g. Q2TEST001AE) for the project
228-
* @return a reactive container of {@link SampleIdCodeEntry} for the sample code. Exceptions are
229-
* provided as {@link Mono#error(Throwable)}.
215+
* @param projectId the project id to which the sample belongs to
216+
* @param sampleId the sample id of the sample to find
217+
* @return a reactive container of {@link Sample} for the sample matching the sample id. For no
218+
* matches a {@link Mono#empty()} is returned. Exceptions are * provided as
219+
* {@link Mono#error(Throwable)}.
230220
* @throws RequestFailedException in case the request cannot be executed
231221
* @since 1.10.0
232222
*/
233-
Mono<SampleIdCodeEntry> findSampleId(String projectId, String sampleCode)
234-
throws RequestFailedException;
223+
Mono<Sample> findSample(String projectId, String sampleId);
235224

236225
/**
237226
* Container of an update request for a service call and part of the

project-management/src/main/java/life/qbic/projectmanagement/application/api/AsyncProjectServiceImpl.java

+70-15
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package life.qbic.projectmanagement.application.api;
22

3+
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.applySecurityContext;
4+
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.applySecurityContextMany;
5+
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.writeSecurityContext;
6+
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.writeSecurityContextMany;
7+
38
import java.nio.ByteBuffer;
9+
import java.util.List;
410
import java.util.Objects;
11+
import life.qbic.application.commons.SortOrder;
512
import life.qbic.logging.api.Logger;
613
import life.qbic.logging.service.LoggerFactory;
714
import life.qbic.projectmanagement.application.ProjectInformationService;
8-
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.applySecurityContext;
9-
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.writeSecurityContext;
10-
import life.qbic.projectmanagement.application.sample.SampleIdCodeEntry;
15+
import life.qbic.projectmanagement.application.sample.SampleInformationService;
1116
import life.qbic.projectmanagement.application.sample.SamplePreview;
17+
import life.qbic.projectmanagement.domain.model.experiment.ExperimentId;
1218
import life.qbic.projectmanagement.domain.model.project.ProjectId;
1319
import life.qbic.projectmanagement.domain.model.sample.Sample;
20+
import life.qbic.projectmanagement.domain.model.sample.SampleId;
1421
import org.springframework.beans.factory.annotation.Autowired;
1522
import org.springframework.lang.NonNull;
1623
import org.springframework.security.core.context.SecurityContext;
@@ -32,13 +39,17 @@
3239
@Service
3340
public class AsyncProjectServiceImpl implements AsyncProjectService {
3441

42+
public static final String ACCESS_DENIED = "Access denied";
3543
private static final Logger log = LoggerFactory.logger(AsyncProjectServiceImpl.class);
3644
private final ProjectInformationService projectService;
3745
private final Scheduler scheduler;
46+
private final SampleInformationService sampleInfoService;
3847

3948
public AsyncProjectServiceImpl(@Autowired ProjectInformationService projectService,
49+
@Autowired SampleInformationService sampleInfoService,
4050
@Autowired Scheduler scheduler) {
4151
this.projectService = Objects.requireNonNull(projectService);
52+
this.sampleInfoService = Objects.requireNonNull(sampleInfoService);
4253
this.scheduler = Objects.requireNonNull(scheduler);
4354
}
4455

@@ -75,22 +86,55 @@ public Flux<ByteBuffer> roCrateSummary(String projectId) {
7586
throw new RuntimeException("not implemented");
7687
}
7788

78-
@Override
79-
public Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId)
80-
throws RequestFailedException {
81-
throw new RuntimeException("not implemented");
82-
}
8389

8490
@Override
8591
public Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId, int offset,
86-
int limit) {
87-
throw new RuntimeException("not implemented");
92+
int limit, List<SortOrder> sortOrders, String filter) {
93+
SecurityContext securityContext = SecurityContextHolder.getContext();
94+
return applySecurityContextMany(Flux.defer(() ->
95+
fetchSamplePreviews(projectId, experimentId, offset, limit, sortOrders, filter)))
96+
.subscribeOn(scheduler)
97+
.transform(original -> writeSecurityContextMany(original, securityContext))
98+
.retryWhen(defaultRetryStrategy());
99+
}
100+
101+
private Flux<SamplePreview> fetchSamplePreviews(String projectId, String experimentId, int offset,
102+
int limit, List<SortOrder> sortOrders, String filter) {
103+
try {
104+
return Flux.fromIterable(
105+
sampleInfoService.queryPreview(ProjectId.parse(projectId),
106+
ExperimentId.parse(experimentId), offset, limit,
107+
sortOrders, filter));
108+
} catch (Exception e) {
109+
log.error("Error getting sample previews", e);
110+
return Flux.error(new RequestFailedException("Error getting sample previews"));
111+
}
88112
}
89113

90114
@Override
91115
public Flux<Sample> getSamples(String projectId, String experimentId)
92116
throws RequestFailedException {
93-
throw new RuntimeException("not implemented");
117+
SecurityContext securityContext = SecurityContextHolder.getContext();
118+
return applySecurityContextMany(Flux.defer(() -> fetchSamples(projectId, experimentId)))
119+
.subscribeOn(scheduler)
120+
.transform(original -> writeSecurityContextMany(original, securityContext))
121+
.retryWhen(defaultRetryStrategy());
122+
}
123+
124+
// disclaimer: no security context, no scheduler applied
125+
private Flux<Sample> fetchSamples(String projectId, String experimentId) {
126+
try {
127+
return Flux.fromIterable(
128+
sampleInfoService.retrieveSamplesForExperiment(ProjectId.parse(projectId),
129+
experimentId));
130+
} catch (org.springframework.security.access.AccessDeniedException e) {
131+
log.error("Error getting samples", e);
132+
return Flux.error(new AccessDeniedException(ACCESS_DENIED));
133+
} catch (Exception e) {
134+
log.error("Unexpected exception getting samples", e);
135+
return Flux.error(
136+
new RequestFailedException("Error getting samples for experiment " + experimentId));
137+
}
94138
}
95139

96140
@Override
@@ -100,9 +144,20 @@ public Flux<Sample> getSamplesForBatch(String projectId, String batchId)
100144
}
101145

102146
@Override
103-
public Mono<SampleIdCodeEntry> findSampleId(String projectId, String sampleCode)
104-
throws RequestFailedException {
105-
throw new RuntimeException("not implemented");
147+
public Mono<Sample> findSample(String projectId, String sampleId) {
148+
return Mono.defer(() -> {
149+
try {
150+
return Mono.justOrEmpty(
151+
sampleInfoService.findSample(ProjectId.parse(projectId), SampleId.parse(sampleId)));
152+
} catch (org.springframework.security.access.AccessDeniedException e) {
153+
log.error(ACCESS_DENIED, e);
154+
return Mono.error(new AccessDeniedException(ACCESS_DENIED));
155+
} catch (Exception e) {
156+
log.error("Error getting sample for sample " + sampleId, e);
157+
return Mono.error(
158+
new RequestFailedException("Error getting sample for sample " + sampleId));
159+
}
160+
}).subscribeOn(scheduler);
106161
}
107162

108163
@Override
@@ -165,7 +220,7 @@ private Mono<ProjectUpdateResponse> updateProjectDesign(String projectId, Projec
165220
} catch (IllegalArgumentException e) {
166221
sink.error(new RequestFailedException("Invalid project id: " + projectId));
167222
} catch (org.springframework.security.access.AccessDeniedException e) {
168-
sink.error(new AccessDeniedException("Access denied"));
223+
sink.error(new AccessDeniedException(ACCESS_DENIED));
169224
} catch (RuntimeException e) {
170225
sink.error(new RequestFailedException("Update project design failed", e));
171226
}

project-management/src/main/java/life/qbic/projectmanagement/application/authorization/ReactiveSecurityContextUtils.java

+32
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
44
import org.springframework.security.core.context.SecurityContext;
55
import org.springframework.security.core.context.SecurityContextHolder;
6+
import reactor.core.publisher.Flux;
67
import reactor.core.publisher.Mono;
78
import reactor.util.context.Context;
89

@@ -45,4 +46,35 @@ public static <T> Mono<T> applySecurityContext(Mono<T> original) {
4546
});
4647
}
4748

49+
/**
50+
* Same as {@link #applySecurityContext(Mono)} but applies to {@link Flux}.
51+
*
52+
* @param original the original reactive stream
53+
* @param <T> the type of the flux
54+
* @return the reactive stream for which the security context has been set explicitly
55+
* @since 1.10.0
56+
*/
57+
public static <T> Flux<T> applySecurityContextMany(Flux<T> original) {
58+
return ReactiveSecurityContextHolder.getContext().flatMapMany(securityContext -> {
59+
SecurityContextHolder.setContext(securityContext);
60+
return original;
61+
});
62+
}
63+
64+
/**
65+
* Same as {@link #writeSecurityContext(Mono, SecurityContext)} but applies to {@link Flux}.
66+
*
67+
* @param original the original reactive stream
68+
* @param securityContext the security context to write into the context of the flux
69+
* @param <T> the type of the flux
70+
* @return the reactive stream for which the {@link ReactiveSecurityContextHolder} has been
71+
* configured with the provided {@link SecurityContext}.
72+
* @since 1.10.0
73+
*/
74+
public static <T> Flux<T> writeSecurityContextMany(Flux<T> original,
75+
SecurityContext securityContext) {
76+
return original.contextWrite(
77+
ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
78+
}
79+
4880
}

0 commit comments

Comments
 (0)