Skip to content

Commit a20e184

Browse files
committed
Move Validation before the Pipeline
1 parent 93e527d commit a20e184

File tree

12 files changed

+227
-101
lines changed

12 files changed

+227
-101
lines changed

src/main/java/de/medizininformatikinitiative/torch/management/OperationOutcomeCreator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public static OperationOutcome createOperationOutcome(String jobId, Throwable th
1111
OperationOutcome.OperationOutcomeIssueComponent issueComponent = new OperationOutcome.OperationOutcomeIssueComponent();
1212
issueComponent.setSeverity(OperationOutcome.IssueSeverity.FATAL);
1313
issueComponent.setCode(createIssueType(throwable));
14-
issueComponent.setDiagnostics(throwable.getClass().getSimpleName() + ": " + throwable.getMessage());
14+
issueComponent.setDiagnostics(throwable.getMessage());
1515
operationOutcome.addIssue(issueComponent);
1616
return operationOutcome;
1717
}

src/main/java/de/medizininformatikinitiative/torch/rest/FhirController.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package de.medizininformatikinitiative.torch.rest;
22

33
import ca.uhn.fhir.context.FhirContext;
4+
import de.medizininformatikinitiative.torch.exceptions.ValidationException;
5+
import de.medizininformatikinitiative.torch.service.CrtdlValidatorService;
46
import de.medizininformatikinitiative.torch.service.ExtractDataService;
57
import de.medizininformatikinitiative.torch.util.ResultFileManager;
68
import org.hl7.fhir.r4.model.OperationOutcome;
@@ -26,9 +28,7 @@
2628

2729
import static de.medizininformatikinitiative.torch.management.OperationOutcomeCreator.createOperationOutcome;
2830
import static java.util.Objects.requireNonNull;
29-
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
30-
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
31-
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
31+
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
3232
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
3333
import static org.springframework.web.reactive.function.server.ServerResponse.accepted;
3434
import static org.springframework.web.reactive.function.server.ServerResponse.notFound;
@@ -45,15 +45,17 @@ public class FhirController {
4545
private final ExtractDataParametersParser extractDataParametersParser;
4646
private final ExtractDataService extractDataService;
4747
private final String baseUrl;
48+
private final CrtdlValidatorService validator;
4849

4950
@Autowired
5051
public FhirController(FhirContext fhirContext, ResultFileManager resultFileManager,
51-
ExtractDataParametersParser parser, ExtractDataService extractDataService, @Value("${torch.base.url}") String baseUrl) {
52+
ExtractDataParametersParser parser, ExtractDataService extractDataService, @Value("${torch.base.url}") String baseUrl, CrtdlValidatorService validator) {
5253
this.fhirContext = requireNonNull(fhirContext);
5354
this.resultFileManager = requireNonNull(resultFileManager);
5455
this.extractDataParametersParser = requireNonNull(parser);
5556
this.extractDataService = requireNonNull(extractDataService);
5657
this.baseUrl = baseUrl;
58+
this.validator = requireNonNull(validator);
5759
}
5860

5961
private Mono<ServerResponse> getGlobalStatus(ServerRequest serverRequest) {
@@ -90,10 +92,13 @@ public Mono<ServerResponse> handleExtractData(ServerRequest request) {
9092
.switchIfEmpty(Mono.error(new IllegalArgumentException("Empty request body")))
9193
.map(extractDataParametersParser::parseParameters)
9294
.flatMap(parameters -> {
93-
Mono<Void> jobMono = extractDataService
94-
.startJob(parameters.crtdl(), parameters.patientIds(), jobId);
95-
96-
// Launch it asynchronously
95+
Mono<Void> jobMono;
96+
try {
97+
jobMono = extractDataService
98+
.startJob(validator.validateAndAnnotate(parameters.crtdl()), parameters.patientIds(), jobId);
99+
} catch (ValidationException e) {
100+
return Mono.error(new IllegalArgumentException(e.getMessage()));
101+
}
97102
jobMono.subscribe();
98103
return ServerResponse.accepted()
99104
.header("Content-Location",

src/main/java/de/medizininformatikinitiative/torch/service/CrtdlValidatorService.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,7 @@
1616
import org.slf4j.Logger;
1717
import org.slf4j.LoggerFactory;
1818

19-
import java.util.ArrayList;
20-
import java.util.HashSet;
21-
import java.util.List;
22-
import java.util.Objects;
23-
import java.util.Optional;
24-
import java.util.Set;
19+
import java.util.*;
2520
import java.util.function.Predicate;
2621

2722
public class CrtdlValidatorService {
@@ -45,7 +40,7 @@ public CrtdlValidatorService(StructureDefinitionHandler profileHandler, Standard
4540
* @param crtdl the Crtdl to be validated.
4641
* @return the validated Crtdl or an error signal with ValidationException if a profile is unknown.
4742
*/
48-
public AnnotatedCrtdl validate(Crtdl crtdl) throws ValidationException {
43+
public AnnotatedCrtdl validateAndAnnotate(Crtdl crtdl) throws ValidationException {
4944
List<AnnotatedAttributeGroup> annotatedAttributeGroups = new ArrayList<>();
5045
Set<String> linkedGroups = new HashSet<>();
5146
Set<String> successfullyAnnotatedGroups = new HashSet<>();

src/main/java/de/medizininformatikinitiative/torch/service/ExtractDataService.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package de.medizininformatikinitiative.torch.service;
22

33
import de.medizininformatikinitiative.torch.exceptions.ValidationException;
4-
import de.medizininformatikinitiative.torch.model.crtdl.Crtdl;
4+
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedCrtdl;
55
import de.medizininformatikinitiative.torch.util.ResultFileManager;
66
import org.hl7.fhir.r4.model.OperationOutcome;
77
import org.springframework.http.HttpStatus;
@@ -18,24 +18,19 @@
1818
public class ExtractDataService {
1919

2020
private final ResultFileManager resultFileManager;
21-
private final CrtdlValidatorService validatorService;
2221
private final CrtdlProcessingService processingService;
2322

2423
public ExtractDataService(ResultFileManager resultFileManager,
25-
CrtdlValidatorService validatorService,
2624
CrtdlProcessingService processingService) {
2725
this.resultFileManager = requireNonNull(resultFileManager);
28-
this.validatorService = requireNonNull(validatorService);
2926
this.processingService = requireNonNull(processingService);
3027
}
3128

32-
public Mono<Void> startJob(Crtdl crtdl, List<String> patientIds, String jobId) {
29+
public Mono<Void> startJob(AnnotatedCrtdl crtdl, List<String> patientIds, String jobId) {
3330
resultFileManager.setStatus(jobId, HttpStatus.ACCEPTED);
3431

3532
return resultFileManager.initJobDir(jobId)
36-
.then(Mono.fromCallable(() -> validatorService.validate(crtdl))
37-
.subscribeOn(Schedulers.boundedElastic()))
38-
.flatMap(validated -> processingService.process(validated, jobId, patientIds))
33+
.then(processingService.process(crtdl, jobId, patientIds))
3934
.doOnSuccess(v -> resultFileManager.setStatus(jobId, HttpStatus.OK))
4035
.doOnError(e -> handleJobError(jobId, e))
4136
.onErrorResume(e -> Mono.empty());

src/main/java/de/medizininformatikinitiative/torch/util/ResultFileManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public Map<String, HttpStatus> getJobStatusMap() {
7777
return jobStatusMap;
7878
}
7979

80-
public void loadExistingResults() {
80+
private void loadExistingResults() {
8181
try (Stream<Path> jobDirs = Files.list(resultsDirPath)) {
8282
jobDirs.filter(Files::isDirectory)
8383
.forEach(jobDir -> {

src/test/java/de/medizininformatikinitiative/torch/FhirControllerIT.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ void testMustHave() throws IOException, ValidationException {
148148
FileInputStream fis = new FileInputStream(RESOURCE_PATH_PREFIX + "CRTDL/CRTDL_observation_must_have.json");
149149
Crtdl crtdl = objectMapper.readValue(fis, Crtdl.class);
150150
PatientBatchWithConsent patients = PatientBatchWithConsent.fromBatch(PatientBatch.of("3"));
151-
AnnotatedCrtdl annotatedCrtdl = validatorService.validate(crtdl);
151+
AnnotatedCrtdl annotatedCrtdl = validatorService.validateAndAnnotate(crtdl);
152152
Mono<PatientBatchWithConsent> collectedResourcesMono = transformer.directLoadPatientCompartment(annotatedCrtdl.dataExtraction().attributeGroups(), patients);
153153
PatientBatchWithConsent result = collectedResourcesMono.block(); // Blocking to get the result
154154
assertThat(result).isNotNull();
@@ -162,7 +162,7 @@ void testCoreMustHave() throws IOException, ValidationException {
162162

163163
FileInputStream fis = new FileInputStream(RESOURCE_PATH_PREFIX + "CRTDL/CRTDL_medication_must_have.json");
164164
Crtdl crtdl = objectMapper.readValue(fis, Crtdl.class);
165-
AnnotatedCrtdl annotatedCrtdl = validatorService.validate(crtdl);
165+
AnnotatedCrtdl annotatedCrtdl = validatorService.validateAndAnnotate(crtdl);
166166
Mono<ResourceBundle> collectedResourcesMono = transformer.processCoreAttributeGroups(annotatedCrtdl.dataExtraction().attributeGroups(), new ResourceBundle());
167167

168168
// Verify that the Mono fails with the expected exception
@@ -173,7 +173,7 @@ void testCoreMustHave() throws IOException, ValidationException {
173173
fis.close();
174174
}
175175

176-
void testExecutor(String filePath, String url, HttpHeaders headers, int expectedFinalCode) {
176+
void testExecutor(String filePath, String url, HttpHeaders headers) {
177177
TestRestTemplate restTemplate = new TestRestTemplate();
178178
try {
179179
String fileContent = Files.readString(Paths.get(filePath), StandardCharsets.UTF_8);
@@ -186,7 +186,7 @@ void testExecutor(String filePath, String url, HttpHeaders headers, int expected
186186
assertThat(durationSecondsSince(start)).isLessThan(1);
187187
List<String> locations = response.getHeaders().get("Content-Location");
188188
assertThat(locations).hasSize(1);
189-
pollStatusEndpoint(restTemplate, headers, locations.getFirst(), expectedFinalCode);
189+
pollStatusEndpoint(restTemplate, headers, locations.getFirst(), 200);
190190
clearDirectory(locations.getFirst().substring(locations.getFirst().lastIndexOf('/')));
191191
} catch (HttpStatusCodeException e) {
192192
logger.error("HTTP Status code error: {}", e.getStatusCode(), e);
@@ -274,7 +274,7 @@ class Endpoint {
274274
void validObservation(String parametersFile) {
275275
HttpHeaders headers = new HttpHeaders();
276276
headers.add("content-type", "application/fhir+json");
277-
testExecutor(parametersFile, "http://localhost:" + port + "/fhir/$extract-data", headers, 200);
277+
testExecutor(parametersFile, "http://localhost:" + port + "/fhir/$extract-data", headers);
278278
}
279279

280280
@Test
@@ -291,12 +291,17 @@ void emptyRequestBodyReturnsBadRequest() {
291291
assertThat(response.getBody()).contains("Empty request body");
292292
}
293293

294-
@ParameterizedTest
295-
@ValueSource(strings = {"src/test/resources/CRTDL_Parameters/Parameters_invalid_CRTDL.json"})
296-
void invalidCRTDLReturnsValidationException(String parametersFile) {
294+
@Test
295+
void invalidCRTDLReturnsBadRequest() throws IOException {
296+
TestRestTemplate restTemplate = new TestRestTemplate();
297297
HttpHeaders headers = new HttpHeaders();
298298
headers.add("content-type", "application/fhir+json");
299-
testExecutor(parametersFile, "http://localhost:" + port + "/fhir/$extract-data", headers, 400);
299+
String fileContent = Files.readString(Paths.get("src/test/resources/CRTDL_Parameters/Parameters_invalid_CRTDL.json"), StandardCharsets.UTF_8);
300+
HttpEntity<String> entity = new HttpEntity<>(fileContent, headers);
301+
long start = System.nanoTime();
302+
ResponseEntity<String> response = restTemplate.exchange("http://localhost:" + port + "/fhir/$extract-data", HttpMethod.POST, entity, String.class);
303+
assertThat(response.getStatusCode().value()).isEqualTo(400);
304+
assertThat(durationSecondsSince(start)).isLessThan(1);
300305
}
301306

302307
@Test

src/test/java/de/medizininformatikinitiative/torch/rest/FhirControllerTest.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package de.medizininformatikinitiative.torch.rest;
22

33
import ca.uhn.fhir.context.FhirContext;
4+
import de.medizininformatikinitiative.torch.exceptions.ValidationException;
45
import de.medizininformatikinitiative.torch.model.crtdl.ExtractDataParameters;
6+
import de.medizininformatikinitiative.torch.service.CrtdlValidatorService;
57
import de.medizininformatikinitiative.torch.service.ExtractDataService;
68
import de.medizininformatikinitiative.torch.util.CrtdlFactory;
79
import de.medizininformatikinitiative.torch.util.ResultFileManager;
@@ -24,15 +26,13 @@
2426
import java.util.Map;
2527

2628
import static org.assertj.core.api.Assertions.assertThat;
27-
import static org.mockito.ArgumentMatchers.any;
28-
import static org.mockito.ArgumentMatchers.anyString;
29-
import static org.mockito.ArgumentMatchers.eq;
29+
import static org.mockito.ArgumentMatchers.*;
3030
import static org.mockito.Mockito.when;
3131

3232
@ExtendWith(MockitoExtension.class)
3333
class FhirControllerTest {
3434

35-
private final static String BASE_URL = "http://base-url";
35+
private static final String BASE_URL = "http://base-url";
3636

3737
@Mock
3838
ResultFileManager resultFileManager;
@@ -43,12 +43,15 @@ class FhirControllerTest {
4343
@Mock
4444
ExtractDataService extractDataService;
4545

46+
@Mock
47+
CrtdlValidatorService validator;
48+
4649
WebTestClient client;
4750

4851
@BeforeEach
4952
void setup() {
5053
FhirContext fhirContext = FhirContext.forR4();
51-
FhirController fhirController = new FhirController(fhirContext, resultFileManager, extractDataParametersParser, extractDataService, BASE_URL);
54+
FhirController fhirController = new FhirController(fhirContext, resultFileManager, extractDataParametersParser, extractDataService, BASE_URL, validator);
5255
client = WebTestClient.bindToRouterFunction(fhirController.queryRouter()).build();
5356
}
5457

@@ -180,4 +183,21 @@ void blankRequestBodyTriggersBadRequest() {
180183
.jsonPath("$.resourceType").isEqualTo("OperationOutcome");
181184
}
182185
}
186+
187+
@Nested
188+
class Validator {
189+
@Test
190+
void invalidCrtdlTriggersBadRequest() throws ValidationException {
191+
ExtractDataParameters params = new ExtractDataParameters(CrtdlFactory.empty(), Collections.emptyList());
192+
when(extractDataParametersParser.parseParameters(any())).thenReturn(params);
193+
when(validator.validateAndAnnotate(any())).thenThrow(new ValidationException("Invalid CRTDL"));
194+
195+
var response = client.post().uri("/fhir/$extract-data").contentType(MediaType.APPLICATION_JSON).bodyValue("{}").exchange();
196+
197+
response.expectStatus().isBadRequest().expectHeader().contentType("application/fhir+json")
198+
.expectBody()
199+
.jsonPath("$.resourceType").isEqualTo("OperationOutcome")
200+
.jsonPath("$.issue[0].diagnostics").isEqualTo("Invalid CRTDL");
201+
}
202+
}
183203
}

src/test/java/de/medizininformatikinitiative/torch/service/CrtdlProcessingServiceIT.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@
88
import de.medizininformatikinitiative.torch.model.management.PatientBatch;
99
import de.medizininformatikinitiative.torch.setup.IntegrationTestSetup;
1010
import de.medizininformatikinitiative.torch.util.ResultFileManager;
11-
import org.junit.jupiter.api.AfterAll;
12-
import org.junit.jupiter.api.Assertions;
13-
import org.junit.jupiter.api.BeforeAll;
14-
import org.junit.jupiter.api.Nested;
15-
import org.junit.jupiter.api.Test;
16-
import org.junit.jupiter.api.TestInstance;
11+
import org.junit.jupiter.api.*;
1712
import org.slf4j.Logger;
1813
import org.slf4j.LoggerFactory;
1914
import org.springframework.beans.factory.annotation.Autowired;
@@ -47,9 +42,6 @@ class CrtdlProcessingServiceIT {
4742

4843
private static final Logger logger = LoggerFactory.getLogger(CrtdlProcessingServiceIT.class);
4944

50-
// Create an instance of BaseTestSetup
51-
private static IntegrationTestSetup INTEGRATION_TEST_SETUP;
52-
5345

5446
@Autowired
5547
CrtdlProcessingService service;
@@ -69,18 +61,19 @@ class CrtdlProcessingServiceIT {
6961

7062
@BeforeAll
7163
void init() throws IOException, ValidationException {
72-
INTEGRATION_TEST_SETUP = new IntegrationTestSetup();
64+
// Create an instance of BaseTestSetup
65+
IntegrationTestSetup integrationTestSetup = new IntegrationTestSetup();
7366
FileInputStream fis = new FileInputStream("src/test/resources/CRTDL/CRTDL_observation_all_fields_withoutReference.json");
74-
crtdlAllObservations = validator.validate(INTEGRATION_TEST_SETUP.objectMapper().readValue(fis, Crtdl.class));
67+
crtdlAllObservations = validator.validateAndAnnotate(integrationTestSetup.objectMapper().readValue(fis, Crtdl.class));
7568
fis.close();
7669
fis = new FileInputStream("src/test/resources/CRTDL/CRTDL_observation_not_contained.json");
77-
crtdlNoPatients = validator.validate(INTEGRATION_TEST_SETUP.objectMapper().readValue(fis, Crtdl.class));
70+
crtdlNoPatients = validator.validateAndAnnotate(integrationTestSetup.objectMapper().readValue(fis, Crtdl.class));
7871
fis.close();
7972
fis = new FileInputStream("src/test/resources/CRTDL/CRTDL_observation_linked_encounter.json");
80-
crtdlObservationLinked = validator.validate(INTEGRATION_TEST_SETUP.objectMapper().readValue(fis, Crtdl.class));
73+
crtdlObservationLinked = validator.validateAndAnnotate(integrationTestSetup.objectMapper().readValue(fis, Crtdl.class));
8174
fis.close();
8275
fis = new FileInputStream("src/test/resources/CRTDL/CRTDL_MedicationAdministraion_linked_encounter_linked_medication.json");
83-
crtdlObservationMedicationLinked = validator.validate(INTEGRATION_TEST_SETUP.objectMapper().readValue(fis, Crtdl.class));
76+
crtdlObservationMedicationLinked = validator.validateAndAnnotate(integrationTestSetup.objectMapper().readValue(fis, Crtdl.class));
8477
fis.close();
8578

8679
webClient.post()

0 commit comments

Comments
 (0)