Skip to content

Implement API for sample management #1063

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

Merged
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
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package life.qbic.projectmanagement.application.api;

import static java.util.Objects.nonNull;

import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import static java.util.Objects.nonNull;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import life.qbic.application.commons.SortOrder;
import life.qbic.projectmanagement.application.batch.SampleUpdateRequest.SampleInformation;
import life.qbic.projectmanagement.application.confounding.ConfoundingVariableService.ConfoundingVariableInformation;
import life.qbic.projectmanagement.application.sample.SampleIdCodeEntry;
import life.qbic.projectmanagement.application.sample.SamplePreview;
import life.qbic.projectmanagement.domain.model.sample.Sample;
import life.qbic.projectmanagement.domain.model.sample.SampleRegistrationRequest;
Expand Down Expand Up @@ -142,24 +143,6 @@ Mono<ProjectCreationResponse> create(ProjectCreationRequest request)
Flux<ByteBuffer> roCrateSummary(String projectId)
throws RequestFailedException, AccessDeniedException;

/**
* Requests {@link SamplePreview} for a given experiment.
* <p>
* <b>Exceptions</b>
* <p>
* Exceptions are wrapped as {@link Mono#error(Throwable)} and are one of the types described in
* the throw section below.
*
* @param projectId the project ID for the project to get the samples for
* @param experimentId the experiment ID for which the sample preview shall be retrieved
* @return a reactive stream of {@link SamplePreview} objects of the experiment. Exceptions are
* provided as {@link Mono#error(Throwable)}.
* @throws RequestFailedException if the request could not be executed
* @since 1.10.0
*/
Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId)
throws RequestFailedException;

/**
* Requests {@link SamplePreview} for a given experiment with pagination support.
* <p>
Expand All @@ -173,12 +156,15 @@ Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId)
* @param offset the offset from 0 of all available previews the returned previews should
* start
* @param limit the maximum number of previews that should be returned
* @param sortOrders the sort orders to apply
* @param filter the filter to apply
* @return a reactive stream of {@link SamplePreview} objects in the experiment. Exceptions are
* provided as {@link Mono#error(Throwable)}.
* @throws RequestFailedException if the request could not be executed
* @since 1.10.0
*/
Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId, int offset,
int limit);
int limit, List<SortOrder> sortOrders, String filter);

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

/**
* Find the sample ID for a given sample code
* Finds the sample for a given sample ID.
* <p>
* In case no matching sample is found, a {@link Mono#empty()} is returned.
*
* <p>
* <b>Exceptions</b>
* <p>
* Exceptions are wrapped as {@link Mono#error(Throwable)} and are one of the types described in
* the throw section below.
*
* @param projectId the project ID for the project to get the samples for
* @param sampleCode the sample code (e.g. Q2TEST001AE) for the project
* @return a reactive container of {@link SampleIdCodeEntry} for the sample code. Exceptions are
* provided as {@link Mono#error(Throwable)}.
* @param projectId the project id to which the sample belongs to
* @param sampleId the sample id of the sample to find
* @return a reactive container of {@link Sample} for the sample matching the sample id. For no
* matches a {@link Mono#empty()} is returned. Exceptions are * provided as
* {@link Mono#error(Throwable)}.
* @throws RequestFailedException in case the request cannot be executed
* @since 1.10.0
*/
Mono<SampleIdCodeEntry> findSampleId(String projectId, String sampleCode)
throws RequestFailedException;
Mono<Sample> findSample(String projectId, String sampleId);

/**
* Container of an update request for a service call and part of the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package life.qbic.projectmanagement.application.api;

import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.applySecurityContext;
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.applySecurityContextMany;
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.writeSecurityContext;
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.writeSecurityContextMany;

import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
import life.qbic.application.commons.SortOrder;
import life.qbic.logging.api.Logger;
import life.qbic.logging.service.LoggerFactory;
import life.qbic.projectmanagement.application.ProjectInformationService;
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.applySecurityContext;
import static life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils.writeSecurityContext;
import life.qbic.projectmanagement.application.sample.SampleIdCodeEntry;
import life.qbic.projectmanagement.application.sample.SampleInformationService;
import life.qbic.projectmanagement.application.sample.SamplePreview;
import life.qbic.projectmanagement.domain.model.experiment.ExperimentId;
import life.qbic.projectmanagement.domain.model.project.ProjectId;
import life.qbic.projectmanagement.domain.model.sample.Sample;
import life.qbic.projectmanagement.domain.model.sample.SampleId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.SecurityContext;
Expand All @@ -32,13 +39,17 @@
@Service
public class AsyncProjectServiceImpl implements AsyncProjectService {

public static final String ACCESS_DENIED = "Access denied";
private static final Logger log = LoggerFactory.logger(AsyncProjectServiceImpl.class);
private final ProjectInformationService projectService;
private final Scheduler scheduler;
private final SampleInformationService sampleInfoService;

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

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

@Override
public Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId)
throws RequestFailedException {
throw new RuntimeException("not implemented");
}

@Override
public Flux<SamplePreview> getSamplePreviews(String projectId, String experimentId, int offset,
int limit) {
throw new RuntimeException("not implemented");
int limit, List<SortOrder> sortOrders, String filter) {
SecurityContext securityContext = SecurityContextHolder.getContext();
return applySecurityContextMany(Flux.defer(() ->
fetchSamplePreviews(projectId, experimentId, offset, limit, sortOrders, filter)))
.subscribeOn(scheduler)
.transform(original -> writeSecurityContextMany(original, securityContext))
.retryWhen(defaultRetryStrategy());
}

private Flux<SamplePreview> fetchSamplePreviews(String projectId, String experimentId, int offset,
int limit, List<SortOrder> sortOrders, String filter) {
try {
return Flux.fromIterable(
sampleInfoService.queryPreview(ProjectId.parse(projectId),
ExperimentId.parse(experimentId), offset, limit,
sortOrders, filter));
} catch (Exception e) {
log.error("Error getting sample previews", e);
return Flux.error(new RequestFailedException("Error getting sample previews"));
}
}

@Override
public Flux<Sample> getSamples(String projectId, String experimentId)
throws RequestFailedException {
throw new RuntimeException("not implemented");
SecurityContext securityContext = SecurityContextHolder.getContext();
return applySecurityContextMany(Flux.defer(() -> fetchSamples(projectId, experimentId)))
.subscribeOn(scheduler)
.transform(original -> writeSecurityContextMany(original, securityContext))
.retryWhen(defaultRetryStrategy());
}

// disclaimer: no security context, no scheduler applied
private Flux<Sample> fetchSamples(String projectId, String experimentId) {
try {
return Flux.fromIterable(
sampleInfoService.retrieveSamplesForExperiment(ProjectId.parse(projectId),
experimentId));
} catch (org.springframework.security.access.AccessDeniedException e) {
log.error("Error getting samples", e);
return Flux.error(new AccessDeniedException(ACCESS_DENIED));
} catch (Exception e) {
log.error("Unexpected exception getting samples", e);
return Flux.error(
new RequestFailedException("Error getting samples for experiment " + experimentId));
}
}

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

@Override
public Mono<SampleIdCodeEntry> findSampleId(String projectId, String sampleCode)
throws RequestFailedException {
throw new RuntimeException("not implemented");
public Mono<Sample> findSample(String projectId, String sampleId) {
return Mono.defer(() -> {
try {
return Mono.justOrEmpty(
sampleInfoService.findSample(ProjectId.parse(projectId), SampleId.parse(sampleId)));
} catch (org.springframework.security.access.AccessDeniedException e) {
log.error(ACCESS_DENIED, e);
return Mono.error(new AccessDeniedException(ACCESS_DENIED));
} catch (Exception e) {
log.error("Error getting sample for sample " + sampleId, e);
return Mono.error(
new RequestFailedException("Error getting sample for sample " + sampleId));
}
}).subscribeOn(scheduler);
}

@Override
Expand Down Expand Up @@ -165,7 +220,7 @@ private Mono<ProjectUpdateResponse> updateProjectDesign(String projectId, Projec
} catch (IllegalArgumentException e) {
sink.error(new RequestFailedException("Invalid project id: " + projectId));
} catch (org.springframework.security.access.AccessDeniedException e) {
sink.error(new AccessDeniedException("Access denied"));
sink.error(new AccessDeniedException(ACCESS_DENIED));
} catch (RuntimeException e) {
sink.error(new RequestFailedException("Update project design failed", e));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

Expand Down Expand Up @@ -45,4 +46,35 @@ public static <T> Mono<T> applySecurityContext(Mono<T> original) {
});
}

/**
* Same as {@link #applySecurityContext(Mono)} but applies to {@link Flux}.
*
* @param original the original reactive stream
* @param <T> the type of the flux
* @return the reactive stream for which the security context has been set explicitly
* @since 1.10.0
*/
public static <T> Flux<T> applySecurityContextMany(Flux<T> original) {
return ReactiveSecurityContextHolder.getContext().flatMapMany(securityContext -> {
SecurityContextHolder.setContext(securityContext);
return original;
});
}

/**
* Same as {@link #writeSecurityContext(Mono, SecurityContext)} but applies to {@link Flux}.
*
* @param original the original reactive stream
* @param securityContext the security context to write into the context of the flux
* @param <T> the type of the flux
* @return the reactive stream for which the {@link ReactiveSecurityContextHolder} has been
* configured with the provided {@link SecurityContext}.
* @since 1.10.0
*/
public static <T> Flux<T> writeSecurityContextMany(Flux<T> original,
SecurityContext securityContext) {
return original.contextWrite(
ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
}

}
Loading