Skip to content

Commit 7cb7648

Browse files
authored
Merge branch 'development' into feature/api-for-variables
2 parents e9dfaeb + 9810423 commit 7cb7648

File tree

10 files changed

+249
-62
lines changed

10 files changed

+249
-62
lines changed

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

+15-27
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import java.util.Optional;
99
import java.util.Set;
1010
import java.util.UUID;
11+
import life.qbic.application.commons.SortOrder;
1112
import life.qbic.projectmanagement.application.batch.SampleUpdateRequest.SampleInformation;
1213
import life.qbic.projectmanagement.application.confounding.ConfoundingVariableService.ConfoundingVariableInformation;
13-
import life.qbic.projectmanagement.application.sample.SampleIdCodeEntry;
1414
import life.qbic.projectmanagement.application.sample.SamplePreview;
1515
import life.qbic.projectmanagement.domain.model.sample.Sample;
1616
import life.qbic.projectmanagement.domain.model.sample.SampleRegistrationRequest;
@@ -143,24 +143,6 @@ Mono<ProjectCreationResponse> create(ProjectCreationRequest request)
143143
Flux<ByteBuffer> roCrateSummary(String projectId)
144144
throws RequestFailedException, AccessDeniedException;
145145

146-
/**
147-
* Requests {@link SamplePreview} for a given experiment.
148-
* <p>
149-
* <b>Exceptions</b>
150-
* <p>
151-
* Exceptions are wrapped as {@link Mono#error(Throwable)} and are one of the types described in
152-
* the throw section below.
153-
*
154-
* @param projectId the project ID for the project to get the samples for
155-
* @param experimentId the experiment ID for which the sample preview shall be retrieved
156-
* @return a reactive stream of {@link SamplePreview} objects of the experiment. Exceptions are
157-
* provided as {@link Mono#error(Throwable)}.
158-
* @throws RequestFailedException if the request could not be executed
159-
* @since 1.10.0
160-
*/
161-
Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId)
162-
throws RequestFailedException;
163-
164146
/**
165147
* Requests {@link SamplePreview} for a given experiment with pagination support.
166148
* <p>
@@ -174,12 +156,15 @@ Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId)
174156
* @param offset the offset from 0 of all available previews the returned previews should
175157
* start
176158
* @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
177161
* @return a reactive stream of {@link SamplePreview} objects in the experiment. Exceptions are
178162
* provided as {@link Mono#error(Throwable)}.
163+
* @throws RequestFailedException if the request could not be executed
179164
* @since 1.10.0
180165
*/
181166
Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId, int offset,
182-
int limit);
167+
int limit, List<SortOrder> sortOrders, String filter);
183168

184169
/**
185170
* Requests all {@link Sample} for a given experiment.
@@ -217,22 +202,25 @@ Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId, int
217202
Flux<Sample> getSamplesForBatch(String projectId, String batchId) throws RequestFailedException;
218203

219204
/**
220-
* 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+
*
221209
* <p>
222210
* <b>Exceptions</b>
223211
* <p>
224212
* Exceptions are wrapped as {@link Mono#error(Throwable)} and are one of the types described in
225213
* the throw section below.
226214
*
227-
* @param projectId the project ID for the project to get the samples for
228-
* @param sampleCode the sample code (e.g. Q2TEST001AE) for the project
229-
* @return a reactive container of {@link SampleIdCodeEntry} for the sample code. Exceptions are
230-
* 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)}.
231220
* @throws RequestFailedException in case the request cannot be executed
232221
* @since 1.10.0
233222
*/
234-
Mono<SampleIdCodeEntry> findSampleId(String projectId, String sampleCode)
235-
throws RequestFailedException;
223+
Mono<Sample> findSample(String projectId, String sampleId);
236224

237225
/**
238226
* Container of an update request for a service call and part of the

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

+67-12
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22

33
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.applySecurityContext;
44
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.writeSecurityContext;
5+
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.applySecurityContextMany;
6+
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.writeSecurityContextMany;
57

68
import java.nio.ByteBuffer;
9+
import java.util.List;
710
import java.util.Objects;
11+
import life.qbic.application.commons.SortOrder;
812
import life.qbic.logging.api.Logger;
913
import life.qbic.logging.service.LoggerFactory;
1014
import life.qbic.projectmanagement.application.ProjectInformationService;
1115
import life.qbic.projectmanagement.application.sample.SampleIdCodeEntry;
16+
import life.qbic.projectmanagement.application.sample.SampleInformationService;
1217
import life.qbic.projectmanagement.application.sample.SamplePreview;
18+
import life.qbic.projectmanagement.domain.model.experiment.ExperimentId;
1319
import life.qbic.projectmanagement.domain.model.project.ProjectId;
1420
import life.qbic.projectmanagement.domain.model.sample.Sample;
21+
import life.qbic.projectmanagement.domain.model.sample.SampleId;
1522
import org.springframework.beans.factory.annotation.Autowired;
1623
import org.springframework.lang.NonNull;
1724
import org.springframework.security.core.context.SecurityContext;
@@ -33,13 +40,17 @@
3340
@Service
3441
public class AsyncProjectServiceImpl implements AsyncProjectService {
3542

43+
public static final String ACCESS_DENIED = "Access denied";
3644
private static final Logger log = LoggerFactory.logger(AsyncProjectServiceImpl.class);
3745
private final ProjectInformationService projectService;
3846
private final Scheduler scheduler;
47+
private final SampleInformationService sampleInfoService;
3948

4049
public AsyncProjectServiceImpl(@Autowired ProjectInformationService projectService,
50+
@Autowired SampleInformationService sampleInfoService,
4151
@Autowired Scheduler scheduler) {
4252
this.projectService = Objects.requireNonNull(projectService);
53+
this.sampleInfoService = Objects.requireNonNull(sampleInfoService);
4354
this.scheduler = Objects.requireNonNull(scheduler);
4455
}
4556

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

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

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

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

97141
@Override
@@ -101,9 +145,20 @@ public Flux<Sample> getSamplesForBatch(String projectId, String batchId)
101145
}
102146

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

109164
@Override
@@ -153,7 +208,7 @@ private Mono<ProjectUpdateResponse> updateProjectDesign(String projectId, Projec
153208
} catch (IllegalArgumentException e) {
154209
sink.error(new RequestFailedException("Invalid project id: " + projectId));
155210
} catch (org.springframework.security.access.AccessDeniedException e) {
156-
sink.error(new AccessDeniedException("Access denied"));
211+
sink.error(new AccessDeniedException(ACCESS_DENIED));
157212
} catch (RuntimeException e) {
158213
sink.error(new RequestFailedException("Update project design failed", e));
159214
}

Diff for: 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
}

Diff for: project-management/src/main/java/life/qbic/projectmanagement/application/sample/SampleInformationService.java

+71-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
import life.qbic.logging.service.LoggerFactory;
1212
import life.qbic.projectmanagement.domain.model.batch.BatchId;
1313
import life.qbic.projectmanagement.domain.model.experiment.ExperimentId;
14+
import life.qbic.projectmanagement.domain.model.project.ProjectId;
1415
import life.qbic.projectmanagement.domain.model.sample.Sample;
1516
import life.qbic.projectmanagement.domain.model.sample.SampleCode;
1617
import life.qbic.projectmanagement.domain.model.sample.SampleId;
1718
import life.qbic.projectmanagement.domain.repository.SampleRepository;
1819
import org.springframework.beans.factory.annotation.Autowired;
20+
import org.springframework.security.access.prepost.PreAuthorize;
1921
import org.springframework.stereotype.Service;
2022

2123
/**
@@ -44,12 +46,23 @@ public SampleInformationService(@Autowired SamplePreviewLookup samplePreviewLook
4446
* @param experimentId {@link ExperimentId}s of the experiment for which it should be determined
4547
* if it has samples registered
4648
* @return true if experiments has samples, false if not
49+
* @deprecated Use {@link SampleInformationService#hasSamples(ProjectId, String)} instead.
4750
*/
4851
public boolean hasSamples(ExperimentId experimentId) {
4952
Objects.requireNonNull(experimentId, "experiment id must not be null");
5053
return sampleRepository.countSamplesWithExperimentId(experimentId) != 0;
5154
}
5255

56+
@PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'READ')")
57+
public boolean hasSamples(ProjectId projectId, String experimentId) {
58+
return sampleRepository.countSamplesWithExperimentId(ExperimentId.parse(experimentId)) != 0;
59+
}
60+
61+
/**
62+
* @deprecated Use
63+
* {@link SampleInformationService#retrieveSamplesForExperiment(ProjectId, String)} instead.
64+
*/
65+
@Deprecated(since = "1.10.0", forRemoval = true)
5366
public Result<Collection<Sample>, ResponseCode> retrieveSamplesForExperiment(
5467
ExperimentId experimentId) {
5568
Objects.requireNonNull(experimentId, "experiment id must not be null");
@@ -61,14 +74,34 @@ public Result<Collection<Sample>, ResponseCode> retrieveSamplesForExperiment(
6174
}
6275
}
6376

64-
public List<SamplePreview> retrieveSamplePreviewsForExperiment(ExperimentId experimentId) {
65-
return samplePreviewLookup.queryByExperimentId(experimentId);
77+
@PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'READ')")
78+
public Collection<Sample> retrieveSamplesForExperiment(ProjectId projectId, String experimentId) {
79+
return sampleRepository.findSamplesByExperimentId(ExperimentId.parse(experimentId));
6680
}
6781

82+
/**
83+
* @deprecated Use {@link #retrieveSamplesByIds(ProjectId, Collection)} instead.
84+
*/
85+
@Deprecated(since = "1.10.0", forRemoval = true)
6886
public List<Sample> retrieveSamplesByIds(Collection<SampleId> sampleIds) {
6987
return sampleRepository.findSamplesBySampleId(sampleIds.stream().toList());
7088
}
7189

90+
@PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'READ')")
91+
public List<Sample> retrieveSamplesByIds(ProjectId projectId, Collection<SampleId> sampleIds) {
92+
return sampleRepository.findSamplesBySampleId(sampleIds.stream().toList());
93+
}
94+
95+
@PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'READ')")
96+
public List<Sample> retrieveSampleForBatch(ProjectId projectId, String batchId) {
97+
return sampleRepository.findSamplesByBatchId(BatchId.parse(batchId));
98+
}
99+
100+
/**
101+
* @deprecated Use {@link SampleInformationService#retrieveSampleForBatch(ProjectId, String)}
102+
* instead.
103+
*/
104+
@Deprecated(since = "1.10.0", forRemoval = true)
72105
public List<Sample> retrieveSamplesForBatch(BatchId batchId) {
73106
Objects.requireNonNull(batchId, "batch id must not be null");
74107
return sampleRepository.findSamplesByBatchId(batchId);
@@ -82,7 +115,9 @@ public List<Sample> retrieveSamplesForBatch(BatchId batchId) {
82115
* @param sortOrders the sort orders to apply
83116
* @return the results in the provided range
84117
* @since 1.0.0
118+
* @deprecated Use {@link #queryPreview(ProjectId, ExperimentId, int, int, List, String)} instead.
85119
*/
120+
@Deprecated(since = "1.10.0", forRemoval = true)
86121
public List<SamplePreview> queryPreview(ExperimentId experimentId, int offset, int limit,
87122
List<SortOrder> sortOrders, String filter) {
88123
// returned by JPA -> UnmodifiableRandomAccessList
@@ -94,6 +129,40 @@ public List<SamplePreview> queryPreview(ExperimentId experimentId, int offset, i
94129
return new ArrayList<>(previewList);
95130
}
96131

132+
/**
133+
* Queries {@link SamplePreview}s with a provided offset and limit that supports pagination.
134+
* Applies the Spring Security context as well.
135+
*
136+
* @param projectId the project ID that contains the information (required to apply the security
137+
* context)
138+
* @param offset the offset for the search result to start
139+
* @param limit the maximum number of results that should be returned
140+
* @param sortOrders the sort orders to apply
141+
* @return the results in the provided range
142+
* @since 1.10.0
143+
*/
144+
@PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'READ')")
145+
public List<SamplePreview> queryPreview(ProjectId projectId, ExperimentId experimentId,
146+
int offset, int limit,
147+
List<SortOrder> sortOrders, String filter) {
148+
// returned by JPA -> UnmodifiableRandomAccessList
149+
List<SamplePreview> previewList = samplePreviewLookup.queryByExperimentId(experimentId,
150+
offset,
151+
limit,
152+
sortOrders, filter);
153+
// the list must be modifiable for spring security to filter it
154+
return new ArrayList<>(previewList);
155+
}
156+
157+
@PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'READ')")
158+
public Optional<Sample> findSample(ProjectId projectId, SampleId sampleId) {
159+
return sampleRepository.findSample(sampleId);
160+
}
161+
162+
/**
163+
* @deprecated Use {@link #findSample(ProjectId, SampleId)} instead.
164+
*/
165+
@Deprecated(since = "1.10.0", forRemoval = true)
97166
public Optional<Sample> findSample(SampleId sampleId) {
98167
return sampleRepository.findSample(sampleId);
99168
}

0 commit comments

Comments
 (0)