1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-07-01 14:12:55 +00:00

chore: add HL7 Fhir serialization to ObjectMapper, so we can add FHIR consent resources to mtb file

This commit is contained in:
Jakub Lidke
2025-06-25 14:17:47 +02:00
parent 60a1a351b1
commit 92f34459c3
9 changed files with 195 additions and 14 deletions

View File

@ -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<String> requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader);
ResponseEntity<String> 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();

View File

@ -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

View File

@ -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
})
}

View File

@ -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())
}
}

View File

@ -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<IBaseResource>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): IBaseResource {
val fhirContext = FhirContext.forR4()
val jsonNode = p?.readValueAsTree<JsonNode>()
val json = jsonNode?.toString()
return fhirContext.newJsonParser().parseResource(json) as IBaseResource
}
}

View File

@ -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<IBaseResource>() {
override fun serialize(
value: IBaseResource,
gen: JsonGenerator,
serializers: SerializerProvider
) {
val fhirContext = FhirContext.forR4()
val json = fhirContext.newJsonParser().encodeResourceToString(value)
gen.writeRawValue(json)
}
}

View File

@ -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()
);
}

View File

@ -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);
}
}

View File

@ -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
}
}