From fbc7f204c3bf1e0faaf35b80f0fd261aec878656 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Tue, 5 Dec 2023 16:16:09 +0100 Subject: [PATCH] Use a Transaction Bundle to Create Measure and Library Resource Closes: #84 --- .../docker-compose.yml | 4 +- .../service/StoreFeasibilityResources.java | 59 ++++++++---- .../process/feasibility/Assertions.java | 19 ++++ .../process/feasibility/BaseAssert.java | 20 +++++ .../process/feasibility/BundleAssert.java | 20 +++++ .../BundleEntryRequestComponentAssert.java | 30 +++++++ .../FlareWebserviceClientImplBaseIT.java | 2 +- .../client/store/StoreClientIT.java | 2 +- .../StoreFeasibilityResourcesTest.java | 90 ++++++++++++++----- 9 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/Assertions.java create mode 100644 mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BaseAssert.java create mode 100644 mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BundleAssert.java create mode 100644 mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BundleEntryRequestComponentAssert.java diff --git a/mii-process-feasibility-docker-test-setup/docker-compose.yml b/mii-process-feasibility-docker-test-setup/docker-compose.yml index 8d6de1d..88f35a7 100755 --- a/mii-process-feasibility-docker-test-setup/docker-compose.yml +++ b/mii-process-feasibility-docker-test-setup/docker-compose.yml @@ -551,7 +551,7 @@ services: # ---- DIC-2 - FHIR Data Store ---------------------------------------------- dic-2-store: - image: samply/blaze:0.22.3 + image: samply/blaze:0.24 restart: on-failure healthcheck: test: [ "CMD", "curl", "http://localhost:8080/health" ] @@ -739,7 +739,7 @@ services: # ---- DIC-3 - FHIR Data Store ---------------------------------------------- dic-3-store: - image: samply/blaze:0.22.3 + image: samply/blaze:0.24 restart: on-failure healthcheck: test: [ "CMD", "curl", "http://localhost:8080/health" ] diff --git a/mii-process-feasibility/src/main/java/de/medizininformatik_initiative/process/feasibility/service/StoreFeasibilityResources.java b/mii-process-feasibility/src/main/java/de/medizininformatik_initiative/process/feasibility/service/StoreFeasibilityResources.java index c583e14..b736de9 100755 --- a/mii-process-feasibility/src/main/java/de/medizininformatik_initiative/process/feasibility/service/StoreFeasibilityResources.java +++ b/mii-process-feasibility/src/main/java/de/medizininformatik_initiative/process/feasibility/service/StoreFeasibilityResources.java @@ -1,27 +1,32 @@ package de.medizininformatik_initiative.process.feasibility.service; -import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.api.IGenericClient; -import de.medizininformatik_initiative.process.feasibility.variables.ConstantsFeasibility; import dev.dsf.bpe.v1.ProcessPluginApi; import dev.dsf.bpe.v1.activity.AbstractServiceDelegate; import dev.dsf.bpe.v1.variables.Variables; import org.camunda.bpm.engine.delegate.DelegateExecution; -import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.Measure; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; +import java.util.ArrayList; import java.util.Objects; +import java.util.regex.Pattern; -import static de.medizininformatik_initiative.process.feasibility.variables.ConstantsFeasibility.VARIABLE_LIBRARY; -import static de.medizininformatik_initiative.process.feasibility.variables.ConstantsFeasibility.VARIABLE_MEASURE; +import static de.medizininformatik_initiative.process.feasibility.variables.ConstantsFeasibility.*; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION; +import static org.hl7.fhir.r4.model.Bundle.HTTPVerb.POST; public class StoreFeasibilityResources extends AbstractServiceDelegate implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(StoreFeasibilityResources.class); + private static final Pattern MEASURE_URL_PATTERN = Pattern.compile("(.+)/Measure/(.+)"); + private static final Pattern LIBRARY_URL_PATTERN = Pattern.compile("urn:uuid:(.+)"); private final IGenericClient storeClient; private final FeasibilityResourceCleaner cleaner; @@ -49,21 +54,43 @@ protected void doExecute(DelegateExecution execution, Variables variables) { cleaner.cleanLibrary(library); cleaner.cleanMeasure(measure); - var libraryRes = storeLibraryResource(library); - var measureRes = storeMeasureResource(measure, libraryRes.getId()); + fixCanonical(measure, library); - variables.setString(ConstantsFeasibility.VARIABLE_MEASURE_ID, measureRes.getId().getIdPart()); + var transactionResponse = storeResources(measure, library); + + variables.setString(VARIABLE_MEASURE_ID, extractMeasureId(transactionResponse)); + } + + private void fixCanonical(Measure measure, Library library) { + var measureUrlMatcher = MEASURE_URL_PATTERN.matcher(measure.getUrl()); + var libraryUrlMatcher = LIBRARY_URL_PATTERN.matcher(library.getUrl()); + if (measureUrlMatcher.find() && libraryUrlMatcher.find()) { + var base = measureUrlMatcher.group(1); + var measureId = measureUrlMatcher.group(2); + var libraryId = libraryUrlMatcher.group(1); + var libraryUrl = base + "/Library/" + libraryId; + measure.setLibrary(new ArrayList<>()); + measure.addLibrary(libraryUrl); + library.setUrl(libraryUrl); + library.setName(libraryId); + library.setVersion("1.0.0"); + var data = new String(library.getContent().get(0).getData(), UTF_8); + var rest = data.split("\n", 2)[1]; + var newData = "library \"%s\" version '1.0.0'\n".formatted(libraryId) + rest; + library.getContent().get(0).setData(newData.getBytes(UTF_8)); + } } - private MethodOutcome storeLibraryResource(Library library) { - logger.info("Store Library `{}`", library.getId()); - return storeClient.create().resource(library).execute(); + private Bundle storeResources(Measure measure, Library library) { + logger.info("Store Measure `{}` and Library `{}`", measure.getId(), library.getUrl()); + + Bundle bundle = new Bundle().setType(TRANSACTION); + bundle.addEntry().setResource(measure).getRequest().setMethod(POST).setUrl("Measure"); + bundle.addEntry().setResource(library).getRequest().setMethod(POST).setUrl("Library"); + return storeClient.transaction().withBundle(bundle).execute(); } - private MethodOutcome storeMeasureResource(Measure measure, IIdType libraryId) { - logger.info("Store Measure `{}`", measure.getId()); - measure.getLibrary().clear(); - measure.addLibrary("Library/" + libraryId.getIdPart()); - return storeClient.create().resource(measure).execute(); + private String extractMeasureId(Bundle transactionResponse) { + return new IdType(transactionResponse.getEntryFirstRep().getResponse().getLocation()).getIdPart(); } } diff --git a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/Assertions.java b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/Assertions.java new file mode 100644 index 0000000..44d1796 --- /dev/null +++ b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/Assertions.java @@ -0,0 +1,19 @@ +package de.medizininformatik_initiative.process.feasibility; + +import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.Bundle; + +public interface Assertions { + + static BaseAssert assertThat(Base actual) { + return new BaseAssert(actual); + } + + static BundleAssert assertThat(Bundle actual) { + return new BundleAssert(actual); + } + + static BundleEntryRequestComponentAssert assertThat(Bundle.BundleEntryRequestComponent actual) { + return new BundleEntryRequestComponentAssert(actual); + } +} diff --git a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BaseAssert.java b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BaseAssert.java new file mode 100644 index 0000000..58f0c84 --- /dev/null +++ b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BaseAssert.java @@ -0,0 +1,20 @@ +package de.medizininformatik_initiative.process.feasibility; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Condition; +import org.hl7.fhir.r4.model.Base; + +public class BaseAssert extends AbstractAssert { + + protected BaseAssert(Base actual) { + super(actual, BaseAssert.class); + } + + private static Condition deepEqualTo(Base expected) { + return new Condition<>(actual -> actual.equalsDeep(expected), "deep equal to " + expected); + } + + public BaseAssert isDeepEqualTo(Base expected) { + return is(deepEqualTo(expected)); + } +} diff --git a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BundleAssert.java b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BundleAssert.java new file mode 100644 index 0000000..67f80ab --- /dev/null +++ b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BundleAssert.java @@ -0,0 +1,20 @@ +package de.medizininformatik_initiative.process.feasibility; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Condition; +import org.hl7.fhir.r4.model.Bundle; + +public class BundleAssert extends AbstractAssert { + + protected BundleAssert(Bundle actual) { + super(actual, BundleAssert.class); + } + + public BundleAssert hasType(String type) { + return has(type(type)); + } + + private static Condition type(String type) { + return new Condition<>(bundle -> bundle.getType().toCode().equals(type), "of type " + type); + } +} diff --git a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BundleEntryRequestComponentAssert.java b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BundleEntryRequestComponentAssert.java new file mode 100644 index 0000000..3761aea --- /dev/null +++ b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/BundleEntryRequestComponentAssert.java @@ -0,0 +1,30 @@ +package de.medizininformatik_initiative.process.feasibility; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Condition; +import org.hl7.fhir.r4.model.Bundle; + + +public class BundleEntryRequestComponentAssert extends AbstractAssert { + + protected BundleEntryRequestComponentAssert(Bundle.BundleEntryRequestComponent actual) { + super(actual, BundleEntryRequestComponentAssert.class); + } + + public BundleEntryRequestComponentAssert hasMethod(Bundle.HTTPVerb method) { + return has(method(method)); + } + + private static Condition method(Bundle.HTTPVerb method) { + return new Condition<>(bundle -> bundle.getMethod() == method, "of method " + method); + } + + public BundleEntryRequestComponentAssert hasUrl(String url) { + return has(url(url)); + } + + private static Condition url(String url) { + return new Condition<>(bundle -> bundle.getUrl().equals(url), "of URL " + url); + } +} diff --git a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/client/flare/FlareWebserviceClientImplBaseIT.java b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/client/flare/FlareWebserviceClientImplBaseIT.java index 9030fff..2246277 100644 --- a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/client/flare/FlareWebserviceClientImplBaseIT.java +++ b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/client/flare/FlareWebserviceClientImplBaseIT.java @@ -12,7 +12,7 @@ public abstract class FlareWebserviceClientImplBaseIT { protected static final Network DEFAULT_CONTAINER_NETWORK = Network.newNetwork(); - public static GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.23.0")) + public static GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.24")) .withExposedPorts(8080) .withNetwork(DEFAULT_CONTAINER_NETWORK) .withNetworkAliases("fhir-server") diff --git a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/client/store/StoreClientIT.java b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/client/store/StoreClientIT.java index 258c764..057aff3 100644 --- a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/client/store/StoreClientIT.java +++ b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/client/store/StoreClientIT.java @@ -53,7 +53,7 @@ public class StoreClientIT { private static final Network DEFAULT_CONTAINER_NETWORK = Network.newNetwork(); @Container - public GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.23.0")) + public GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.24")) .withExposedPorts(8080) .withNetwork(DEFAULT_CONTAINER_NETWORK) .withNetworkAliases("fhir-server") diff --git a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/service/StoreFeasibilityResourcesTest.java b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/service/StoreFeasibilityResourcesTest.java index 704cb62..c05487c 100644 --- a/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/service/StoreFeasibilityResourcesTest.java +++ b/mii-process-feasibility/src/test/java/de/medizininformatik_initiative/process/feasibility/service/StoreFeasibilityResourcesTest.java @@ -1,22 +1,21 @@ package de.medizininformatik_initiative.process.feasibility.service; -import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.api.IGenericClient; import dev.dsf.bpe.v1.variables.Variables; import org.camunda.bpm.engine.delegate.DelegateExecution; -import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.Measure; -import org.hl7.fhir.r4.model.Resource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; -import org.mockito.InjectMocks; -import org.mockito.Mock; +import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; +import static de.medizininformatik_initiative.process.feasibility.Assertions.assertThat; import static de.medizininformatik_initiative.process.feasibility.variables.ConstantsFeasibility.*; -import static org.mockito.ArgumentMatchers.any; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hl7.fhir.r4.model.Bundle.HTTPVerb.POST; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,6 +31,9 @@ public class StoreFeasibilityResourcesTest { @Mock private FeasibilityResourceCleaner cleaner; + @Captor + private ArgumentCaptor transactionBundleCaptor; + @Mock private DelegateExecution execution; @@ -41,24 +43,72 @@ public class StoreFeasibilityResourcesTest { @InjectMocks private StoreFeasibilityResources service; - @Test - public void testDoExecute() { - var measure = new Measure(); - var library = new Library(); - library.getContentFirstRep().setContentType("text/cql"); - when(variables.getResource(VARIABLE_MEASURE)).thenReturn(measure); - when(variables.getResource(VARIABLE_LIBRARY)).thenReturn(library); + // Creates a Measure like the Feasibility Backend will do. + private static Measure inputMeasure() { + return new Measure() + .setUrl("https://foo.de/Measure/9308b842-2bee-418b-b3bf-cc347541c1c3") + .addLibrary("urn:uuid:7942465b-513a-4812-a078-e72dfea97f43"); + } - var libraryMethodOutcome = new MethodOutcome(new IdType(LIBRARY_ID)); - var measureMethodOutcome = new MethodOutcome(new IdType(MEASURE_ID)); + // Creates a Library like the Feasibility Backend will do. + private static Library inputLibrary() { + var library = new Library() + .setUrl("urn:uuid:7942465b-513a-4812-a078-e72dfea97f43") + .setName("Retrieve"); + library.addContent().setContentType("text/cql").setData(""" + library Retrieve version '1.0.0' + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + """.getBytes(UTF_8)); + return library; + } - when(storeClient.create().resource(any(Resource.class)).execute()) - .thenReturn(libraryMethodOutcome, measureMethodOutcome); + private static Measure outputMeasure() { + return new Measure() + .setUrl("https://foo.de/Measure/9308b842-2bee-418b-b3bf-cc347541c1c3") + .addLibrary("https://foo.de/Library/7942465b-513a-4812-a078-e72dfea97f43"); + } + + private static Library outputLibrary() { + var library = new Library() + .setUrl("https://foo.de/Library/7942465b-513a-4812-a078-e72dfea97f43") + .setName("7942465b-513a-4812-a078-e72dfea97f43") + .setVersion("1.0.0"); + library.addContent().setContentType("text/cql").setData(""" + library "7942465b-513a-4812-a078-e72dfea97f43" version '1.0.0' + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + """.getBytes(UTF_8)); + return library; + } + + @Test + public void testDoExecute() { + var inputMeasure = inputMeasure(); + var inputLibrary = inputLibrary(); + var transactionResponse = new Bundle(); + transactionResponse.addEntry().getResponse().setLocation("Measure/" + MEASURE_ID); + inputLibrary.getContentFirstRep().setContentType("text/cql"); + when(variables.getResource(VARIABLE_MEASURE)).thenReturn(inputMeasure); + when(variables.getResource(VARIABLE_LIBRARY)).thenReturn(inputLibrary); + when(storeClient.transaction().withBundle(transactionBundleCaptor.capture()).execute()).thenReturn(transactionResponse); service.doExecute(execution, variables); - verify(cleaner).cleanLibrary(library); - verify(cleaner).cleanMeasure(measure); + verify(cleaner).cleanLibrary(inputLibrary); + verify(cleaner).cleanMeasure(inputMeasure); + assertThat(transactionBundleCaptor.getValue()).hasType("transaction"); + assertThat(transactionBundleCaptor.getValue().getEntry()).hasSize(2); + assertThat(transactionBundleCaptor.getValue().getEntry().get(0).getResource()) + .isDeepEqualTo(outputMeasure()); + assertThat(transactionBundleCaptor.getValue().getEntry().get(0).getRequest()) + .hasMethod(POST) + .hasUrl("Measure"); + assertThat(transactionBundleCaptor.getValue().getEntry().get(1).getResource()) + .isDeepEqualTo(outputLibrary()); + assertThat(transactionBundleCaptor.getValue().getEntry().get(1).getRequest()) + .hasMethod(POST) + .hasUrl("Library"); verify(variables).setString(VARIABLE_MEASURE_ID, MEASURE_ID); } }