diff --git a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java index f5be403..2b6172f 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -38,6 +38,7 @@ public class GicsConsentService implements ICheckConsent { private final GIcsConfigProperties gIcsConfigProperties; public static final String IS_CONSENTED_ENDPOINT = "/$isConsented"; + public static final String IS_POLICY_STATES_FOR_PERSON_ENDPOINT = "/$currentPolicyStatesForPerson"; private final RetryTemplate retryTemplate; private final RestTemplate restTemplate; private final FhirContext fhirContext; @@ -55,7 +56,7 @@ public class GicsConsentService implements ICheckConsent { log.info("GicsConsentService initialized..."); } - public String getGicsUri() { + public String getGicsUri(String endpoint) { if (url == null) { final String gIcsBaseUri = gIcsConfigProperties.getUri(); if (StringUtils.isBlank(gIcsBaseUri)) { @@ -81,7 +82,7 @@ public class GicsConsentService implements ICheckConsent { return headers; } - public static Parameters getIsConsentedRequestParam(GIcsConfigProperties configProperties, + protected static Parameters getIsConsentedRequestParam(GIcsConfigProperties configProperties, String personIdentifierValue) { var result = new Parameters(); result.addParameter(new ParametersParameterComponent().setName("personIdentifier").setValue( @@ -115,13 +116,13 @@ public class GicsConsentService implements ICheckConsent { return result; } - protected String callGicsApi(Parameters parameter) { + protected String callGicsApi(Parameters parameter, String endpoint) { var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter); HttpEntity requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader); ResponseEntity responseEntity; try { - var url = getGicsUri(); + var url = getGicsUri(endpoint); responseEntity = retryTemplate.execute( ctx -> restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class)); @@ -155,7 +156,8 @@ public class GicsConsentService implements ICheckConsent { var parameter = GicsConsentService.getIsConsentedRequestParam(gIcsConfigProperties, personIdentifierValue); - var consentStatusResponse = callGicsApi(parameter); + var consentStatusResponse = callGicsApi(parameter, + GicsConsentService.IS_CONSENTED_ENDPOINT); return evaluateConsentResponse(consentStatusResponse); } @@ -167,7 +169,8 @@ public class GicsConsentService implements ICheckConsent { var requestParameter = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson( gIcsConfigProperties, personIdentifierValue, requestDate, consentDomain); - var consentDataSerialized = callGicsApi(requestParameter); + var consentDataSerialized = callGicsApi(requestParameter, + GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); if (consentDataSerialized == null) { // error occurred - should not process further! @@ -222,7 +225,7 @@ public class GicsConsentService implements ICheckConsent { ConsentDomain.Modelvorhaben64e, requestDate); } - private static Parameters buildRequestParameterCurrentPolicyStatesForPerson( + protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson( GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate, String targetDomain) { var requestParameter = new Parameters(); diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt index d9eb09f..7499dde 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -87,7 +87,7 @@ data class GIcsConfigProperties( /** * Domain of Modelvorhaben 64e consent resources **/ - val gnomDeConsentDomainName: String = "GenomeDE_MV", + val gnomDeConsentDomainName: String = "GenomDE_MV", /** * Value to expect in case of positiv consent diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt index dcd8469..d5a3bf3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -100,17 +100,21 @@ class AppConfiguration { } @Bean - fun reportService(objectMapper: ObjectMapper): ReportService { - return ReportService(objectMapper) + fun reportService(): ReportService { + return ReportService(getObjectMapper()) + } + + @Bean + fun getObjectMapper () : ObjectMapper{ + return JacksonConfig().objectMapper() } @Bean fun transformationService( - objectMapper: ObjectMapper, configProperties: AppConfigProperties ): TransformationService { logger.info("Apply ${configProperties.transformations.size} transformation rules") - return TransformationService(objectMapper, configProperties.transformations.map { + return TransformationService(getObjectMapper(), configProperties.transformations.map { Transformation.of(it.path) from it.from to it.to }) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt new file mode 100644 index 0000000..43f9f35 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt @@ -0,0 +1,12 @@ +package dev.dnpm.etl.processor.config + + +import com.fasterxml.jackson.databind.module.SimpleModule +import org.hl7.fhir.instance.model.api.IBaseResource + +class FhirResourceModule : SimpleModule() { + init { + addSerializer(IBaseResource::class.java, IBaseResourceSerializer()) + addDeserializer(IBaseResource::class.java, IBaseResourceDeserializer()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/IBaseResourceDeserializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/IBaseResourceDeserializer.kt new file mode 100644 index 0000000..4fe94ca --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/IBaseResourceDeserializer.kt @@ -0,0 +1,20 @@ +package dev.dnpm.etl.processor.config + +import ca.uhn.fhir.context.FhirContext +import com.fasterxml.jackson.core.JsonParser + +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import org.hl7.fhir.instance.model.api.IBaseResource + +class IBaseResourceDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): IBaseResource { + val fhirContext = FhirContext.forR4() + + val jsonNode = p?.readValueAsTree() + val json = jsonNode?.toString() + + return fhirContext.newJsonParser().parseResource(json) as IBaseResource + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/IBaseResourceSerializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/IBaseResourceSerializer.kt new file mode 100644 index 0000000..49b488e --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/IBaseResourceSerializer.kt @@ -0,0 +1,19 @@ +package dev.dnpm.etl.processor.config + +import ca.uhn.fhir.context.FhirContext +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import org.hl7.fhir.instance.model.api.IBaseResource + +class IBaseResourceSerializer : JsonSerializer() { + override fun serialize( + value: IBaseResource, + gen: JsonGenerator, + serializers: SerializerProvider + ) { + val fhirContext = FhirContext.forR4() + val json = fhirContext.newJsonParser().encodeResourceToString(value) + gen.writeRawValue(json) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt new file mode 100644 index 0000000..76f5138 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt @@ -0,0 +1,19 @@ +package dev.dnpm.etl.processor.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule + +@Configuration +class JacksonConfig { + + @Bean + fun objectMapper(): ObjectMapper = + ObjectMapper() + .registerModule(FhirResourceModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).registerModule( + JavaTimeModule() + ); +} diff --git a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java index 41dc2a2..445d930 100644 --- a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java +++ b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java @@ -7,13 +7,18 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat import com.fasterxml.jackson.databind.ObjectMapper; import dev.dnpm.etl.processor.config.AppConfiguration; import dev.dnpm.etl.processor.config.AppFhirConfig; +import dev.dnpm.etl.processor.config.GIcsConfigProperties; +import java.time.Instant; +import java.util.Date; import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.r4.model.OperationOutcome.IssueType; import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -43,6 +48,9 @@ public class GicsConsentServiceTest { @Autowired AppFhirConfig appFhirConfig; + @Autowired + GIcsConfigProperties gIcsConfigProperties; + @BeforeEach public void setUp() { mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate()); @@ -100,4 +108,17 @@ public class GicsConsentServiceTest { var consentStatus = gicsConsentService.getTtpConsentStatus("123456"); assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK); } + + @Test + void buildRequestParameterCurrentPolicyStatesForPersonTest() { + + String pid = "12345678"; + var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(gIcsConfigProperties, + pid, Date.from(Instant.now()),gIcsConfigProperties.getGnomDeConsentDomainName()); + + assertThat(result.getParameter().size()).as("should contain 3 parameter resources").isEqualTo(3); + + assertThat(((StringType)result.getParameter("domain").getValue()).getValue()).isEqualTo(gIcsConfigProperties.getGnomDeConsentDomainName()); + assertThat(((Identifier)result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo(pid); + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt index 487b502..32c443c 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt @@ -19,14 +19,22 @@ package dev.dnpm.etl.processor.services -import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.Diagnosis import de.ukw.ccc.bwhc.dto.Icd10 import de.ukw.ccc.bwhc.dto.MtbFile +import dev.dnpm.etl.processor.config.JacksonConfig +import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import dev.pcvolkmer.mv64e.mtb.Mtb +import dev.pcvolkmer.mv64e.mtb.MvhMetadata +import org.hl7.fhir.instance.model.api.IBaseResource +import org.hl7.fhir.r4.model.CodeableConcept +import org.hl7.fhir.r4.model.Coding +import java.time.Instant +import java.util.Date class TransformationServiceTest { @@ -35,7 +43,7 @@ class TransformationServiceTest { @BeforeEach fun setup() { this.service = TransformationService( - ObjectMapper(), listOf( + JacksonConfig().objectMapper(), listOf( Transformation.of("consent.status") from Consent.Status.ACTIVE to Consent.Status.REJECTED, Transformation.of("diagnoses[*].icd10.version") from "2013" to "2014", ) @@ -92,4 +100,79 @@ class TransformationServiceTest { assertThat(actual.consent.status).isEqualTo(Consent.Status.REJECTED) } + @Test + fun shouldTransformConsentValues() { + val mtbFile = MtbFile.builder().withDiagnoses( + listOf( + Diagnosis.builder().withId("1234").withIcd10(Icd10("F79.9").also { + it.version = "2013" + }).build(), + Diagnosis.builder().withId("5678").withIcd10(Icd10("F79.8").also { + it.version = "2019" + }).build() + ) + ).build() + + val actual = this.service.transform(mtbFile) + + assertThat(actual).isNotNull + assertThat(actual.diagnoses[0].icd10.code).isEqualTo("F79.9") + assertThat(actual.diagnoses[0].icd10.version).isEqualTo("2014") + assertThat(actual.diagnoses[1].icd10.code).isEqualTo("F79.8") + assertThat(actual.diagnoses[1].icd10.version).isEqualTo("2019") + } + + @Test + fun shouldTransformConsent() { + val mvhMetadata = MvhMetadata.builder().transferTan("transfertan12345").build(); + + assertThat(mvhMetadata).isNotNull + mvhMetadata.modelProjectConsent = + ModelProjectConsent.builder().date(Date.from(Instant.now())).version("1").build() + val consent1 = org.hl7.fhir.r4.model.Consent() + consent1.id = "consent 1 id" + consent1.patient.reference = "Patient/1234-pat1" + + consent1.provision.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny")) + consent1.provision.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + consent1.provision.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + + + val addProvision1 = consent1.provision.addProvision() + addProvision1.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("permit")) + addProvision1.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + addProvision1.code.addLast( + CodeableConcept( + Coding( + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "Teilnahme", + "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" + ) + ) + ) + + val addProvision2 = consent1.provision.addProvision() + addProvision2.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny")) + addProvision2.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + addProvision2.code.addLast( + CodeableConcept( + Coding( + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "Rekontaktierung", + "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" + ) + ) + ) + + mvhMetadata.researchConsents = mutableListOf() + mvhMetadata.researchConsents.add(mapOf(consent1.id to consent1 as IBaseResource)) + + val mtbFile = Mtb.builder().metadata(mvhMetadata).build() + + val transformed = service.transform(mtbFile) + assertThat(transformed.metadata.modelProjectConsent.date).isNotNull + + } } \ No newline at end of file