Skip to content

Commit 75f266d

Browse files
authored
Implement experiment query API (#1125)
Implements the reactive API for querying experimental information. Wires the async service call in the UI and renders information about species, specimen and analyte dynamically.
1 parent c5fb021 commit 75f266d

File tree

5 files changed

+167
-71
lines changed

5 files changed

+167
-71
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,18 @@ record OntologyTerm(String label, Curie oboId, URI id) {
814814
*/
815815
record Curie(String idSpace, String localId) {
816816

817+
public static Curie parse(String value) {
818+
requireNonNull(value);
819+
if (value.contains(":")) {
820+
var parts = value.split(":");
821+
return new Curie(parts[0], parts[1]);
822+
}
823+
throw new IllegalArgumentException("Invalid Curie: " + value);
824+
}
825+
826+
public String toString() {
827+
return idSpace + ":" + localId;
828+
}
817829
}
818830

819831
/**

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

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66

77
import java.io.IOException;
88
import java.io.InputStream;
9+
import java.net.URI;
910
import java.nio.ByteBuffer;
1011
import java.util.ArrayList;
1112
import java.util.HashSet;
13+
import java.util.Collection;
1214
import java.util.List;
1315
import java.util.Objects;
16+
import java.util.Set;
1417
import java.util.concurrent.Callable;
1518
import java.util.function.Function;
1619
import java.util.function.UnaryOperator;
20+
import java.util.stream.Collectors;
1721
import life.qbic.application.commons.SortOrder;
1822
import life.qbic.logging.api.Logger;
1923
import life.qbic.logging.service.LoggerFactory;
@@ -27,6 +31,7 @@
2731
import life.qbic.projectmanagement.application.api.template.TemplateService;
2832
import life.qbic.projectmanagement.application.authorization.ReactiveSecurityContextUtils;
2933
import life.qbic.projectmanagement.application.experiment.ExperimentInformationService;
34+
import life.qbic.projectmanagement.application.experiment.ExperimentInformationService;
3035
import life.qbic.projectmanagement.application.experiment.ExperimentInformationService.ExperimentalVariableAddition;
3136
import life.qbic.projectmanagement.application.measurement.validation.MeasurementValidationService;
3237
import life.qbic.projectmanagement.application.sample.SampleInformationService;
@@ -99,6 +104,17 @@ private static Retry defaultRetryStrategy() {
99104
.doBeforeRetry(retrySignal -> log.warn("Operation failed (" + retrySignal + ")"));
100105
}
101106

107+
private static Set<OntologyTerm> convertToApi(
108+
Collection<life.qbic.projectmanagement.domain.model.OntologyTerm> terms) {
109+
return terms.stream().map(AsyncProjectServiceImpl::convertToApi).collect(Collectors.toSet());
110+
}
111+
112+
private static OntologyTerm convertToApi(
113+
life.qbic.projectmanagement.domain.model.OntologyTerm term) {
114+
return new OntologyTerm(term.getLabel(), Curie.parse(term.oboId().toString()),
115+
URI.create(term.getOntologyIri()));
116+
}
117+
102118
@Override
103119
public Mono<ProjectUpdateResponse> update(@NonNull ProjectUpdateRequest request)
104120
throws UnknownRequestException, RequestFailedException, AccessDeniedException {
@@ -168,7 +184,20 @@ public Flux<ByteBuffer> roCrateSummary(String projectId) {
168184

169185
@Override
170186
public Flux<ExperimentDescription> getExperiments(String projectId) {
171-
throw new RuntimeException("Not yet implemented");
187+
var securityContext = SecurityContextHolder.getContext();
188+
return applySecurityContextMany(Flux.fromIterable(
189+
() -> experimentInformationService.findAllForProject(ProjectId.parse(projectId)).iterator())
190+
.map(e -> e.experimentId())
191+
.flatMap(id -> {
192+
var analytes = experimentInformationService.getAnalytesOfExperiment(id);
193+
var specimen = experimentInformationService.getSpecimensOfExperiment(id);
194+
var species = experimentInformationService.getSpeciesOfExperiment(id);
195+
var experimentName = experimentInformationService.find(projectId, id)
196+
.map(Experiment::getName).orElse("not available");
197+
return Mono.just(new ExperimentDescription(experimentName, convertToApi(species),
198+
convertToApi(specimen), convertToApi(analytes)));
199+
})).subscribeOn(VirtualThreadScheduler.getScheduler())
200+
.contextWrite(reactiveSecurity(securityContext));
172201
}
173202

174203
// Requires the SecurityContext to work
@@ -315,7 +344,8 @@ public Mono<DigitalObject> sampleRegistrationTemplate(String projectId, String e
315344
}
316345

317346
@Override
318-
public Mono<DigitalObject> sampleUpdateTemplate(String projectId, String experimentId, String batchId,
347+
public Mono<DigitalObject> sampleUpdateTemplate(String projectId, String experimentId,
348+
String batchId,
319349
MimeType mimeType) {
320350
SecurityContext securityContext = SecurityContextHolder.getContext();
321351
return applySecurityContext(Mono.fromCallable(
@@ -542,7 +572,8 @@ private Mono<ProjectDeletionResponse> delete(String projectId, String requestId,
542572
return Mono.error(new AccessDeniedException(ACCESS_DENIED));
543573
} catch (Exception e) {
544574
log.error("Unexpected exception deleting funding information", e);
545-
return Mono.error(new RequestFailedException("Unexpected exception deleting funding information"));
575+
return Mono.error(
576+
new RequestFailedException("Unexpected exception deleting funding information"));
546577
}
547578
});
548579
}
@@ -605,20 +636,20 @@ private Mono<ExperimentUpdateResponse> addExperimentalVariables(
605636
private Mono<ProjectUpdateResponse> update(ProjectId projectId, String requestId,
606637
ProjectDesign design) {
607638
return
608-
Mono.<ProjectUpdateResponse>create(sink -> {
609-
try {
610-
projectService.updateTitle(projectId, design.title());
611-
projectService.updateObjective(projectId, design.objective());
612-
sink.success(new ProjectUpdateResponse(projectId.value(), design, requestId));
613-
} catch (IllegalArgumentException e) {
614-
sink.error(new RequestFailedException("Invalid project id: " + projectId));
615-
} catch (org.springframework.security.access.AccessDeniedException e) {
616-
sink.error(new AccessDeniedException(ACCESS_DENIED));
617-
} catch (RuntimeException e) {
618-
sink.error(new RequestFailedException("Update project design failed", e));
619-
}
620-
}
621-
);
639+
Mono.<ProjectUpdateResponse>create(sink -> {
640+
try {
641+
projectService.updateTitle(projectId, design.title());
642+
projectService.updateObjective(projectId, design.objective());
643+
sink.success(new ProjectUpdateResponse(projectId.value(), design, requestId));
644+
} catch (IllegalArgumentException e) {
645+
sink.error(new RequestFailedException("Invalid project id: " + projectId));
646+
} catch (org.springframework.security.access.AccessDeniedException e) {
647+
sink.error(new AccessDeniedException(ACCESS_DENIED));
648+
} catch (RuntimeException e) {
649+
sink.error(new RequestFailedException("Update project design failed", e));
650+
}
651+
}
652+
);
622653
}
623654

624655
}

project-management/src/main/java/life/qbic/projectmanagement/domain/model/OntologyTerm.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class OntologyTerm implements Serializable {
3434
private String description;
3535
@JsonProperty("classIri")
3636
private String classIri;
37+
@JsonProperty("oboId")
38+
private OboId realOboId;
3739

3840
public OntologyTerm() {
3941
}
@@ -106,10 +108,21 @@ public void setDescription(String description) {
106108
this.description = description;
107109
}
108110

111+
@Deprecated(since = "1.10.0", forRemoval = true)
109112
public String getOboId() {
110113
return oboId;
111114
}
112115

116+
public OboId oboId() {
117+
// Legacy support for CURIE specification violation
118+
return Objects.requireNonNullElseGet(realOboId, () -> OboId.parse(oboId, "_"));
119+
}
120+
121+
public void setOboId(OboId oboId) {
122+
this.realOboId = Objects.requireNonNull(oboId);
123+
}
124+
125+
@Deprecated(since = "1.10.0", forRemoval = true)
113126
public void setOboId(String oboId) {
114127
this.oboId = oboId;
115128
}
@@ -162,4 +175,26 @@ public String toString() {
162175
.add("classIri='" + classIri + "'")
163176
.toString();
164177
}
178+
179+
public record OboId(String idSpace, String localId) {
180+
static OboId parse(String id) {
181+
if (id == null || id.isEmpty()) {
182+
throw new IllegalArgumentException("OboId cannot be null or empty");
183+
}
184+
String[] parts = id.split(":");
185+
return new OboId(parts[0], parts[1]);
186+
}
187+
188+
static OboId parse(String id, String separator) {
189+
if (id == null || id.isEmpty()) {
190+
throw new IllegalArgumentException("OboId cannot be null or empty");
191+
}
192+
String[] parts = id.split(separator);
193+
return new OboId(parts[0], parts[1]);
194+
}
195+
196+
public String toString() {
197+
return idSpace + ":" + localId;
198+
}
199+
}
165200
}

project-management/src/test/groovy/life/qbic/projectmanagement/application/api/AsyncProjectServiceImplTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import life.qbic.projectmanagement.application.api.AsyncProjectService.ProjectDesign;
2424
import life.qbic.projectmanagement.application.api.AsyncProjectService.ProjectUpdateRequest;
2525
import life.qbic.projectmanagement.application.api.fair.DigitalObjectFactory;
26+
import life.qbic.projectmanagement.application.experiment.ExperimentInformationService;
27+
import life.qbic.projectmanagement.application.measurement.validation.MeasurementValidationService;
2628
import life.qbic.projectmanagement.application.api.template.TemplateService;
2729
import life.qbic.projectmanagement.application.experiment.ExperimentInformationService;
2830
import life.qbic.projectmanagement.application.measurement.validation.MeasurementValidationService;
@@ -49,6 +51,7 @@ class AsyncProjectServiceImplTest {
4951
MeasurementValidationService measurementValidationService = mock(
5052
MeasurementValidationService.class);
5153
TemplateService templateService = mock(TemplateService.class);
54+
ExperimentInformationService experimentInformationService = mock(ExperimentInformationService.class);
5255
ExperimentInformationService experimentInformationServiceMock = mock(
5356
ExperimentInformationService.class);
5457

0 commit comments

Comments
 (0)