From cc525d3f4f2a32ca7b5d3b6386f58821e5404028 Mon Sep 17 00:00:00 2001 From: Jakub Lidke Date: Thu, 10 Jul 2025 11:43:37 +0200 Subject: [PATCH] fix: added missing genomeDe consent version and date; moved embedding consent resources into base class of GicsConsentService --- .../processor/consent/BaseConsentService.java | 91 +++++++++++++++++++ .../consent/ConsentCheckFileBased.java | 14 ++- .../processor/consent/GicsConsentService.java | 7 +- .../etl/processor/consent/ICheckConsent.java | 19 ++-- .../processor/config/AppConfigProperties.kt | 8 +- .../processor/services/RequestProcessor.kt | 75 ++++----------- 6 files changed, 142 insertions(+), 72 deletions(-) create mode 100644 src/main/java/dev/dnpm/etl/processor/consent/BaseConsentService.java diff --git a/src/main/java/dev/dnpm/etl/processor/consent/BaseConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/BaseConsentService.java new file mode 100644 index 0000000..31a75e7 --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/BaseConsentService.java @@ -0,0 +1,91 @@ +package dev.dnpm.etl.processor.consent; + +import dev.dnpm.etl.processor.config.GIcsConfigProperties; +import dev.pcvolkmer.mv64e.mtb.ConsentProvision; +import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose; +import dev.pcvolkmer.mv64e.mtb.Mtb; +import dev.pcvolkmer.mv64e.mtb.Provision; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.Consent.ProvisionComponent; +import org.hl7.fhir.r4.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class BaseConsentService implements ICheckConsent { + + protected final GIcsConfigProperties gIcsConfigProperties; + protected Logger logger = LoggerFactory.getLogger(BaseConsentService.class); + + public BaseConsentService(GIcsConfigProperties gIcsConfigProperties) { + this.gIcsConfigProperties = gIcsConfigProperties; + } + + public void embedBroadConsentResources(Mtb mtbFile, Bundle broadConsent) { + for (Bundle.BundleEntryComponent entry : broadConsent.getEntry()) { + Resource resource = entry.getResource(); + if (resource instanceof Consent) { + Map consentMap = new HashMap<>(); + consentMap.put(resource.getIdElement().getIdPart(), resource); + mtbFile.getMetadata().getResearchConsents().add(consentMap); + } + } + } + + public void addGenomeDbProvisions(Mtb mtbFile, Bundle consentGnomeDe) { + for (Bundle.BundleEntryComponent entry : consentGnomeDe.getEntry()) { + Resource resource = entry.getResource(); + if (!(resource instanceof Consent consentFhirResource)) { + continue; + } + + // We expect only one provision in collection, therefore get first or none + List provisions = consentFhirResource.getProvision().getProvision(); + if (provisions.isEmpty()) { + continue; + } + + var provisionComponent = provisions.getFirst(); + + String provisionCode = null; + if (provisionComponent.getCode() != null && !provisionComponent.getCode().isEmpty()) { + CodeableConcept codeableConcept = provisionComponent.getCode().getFirst(); + if (codeableConcept.getCoding() != null && !codeableConcept.getCoding().isEmpty()) { + provisionCode = codeableConcept.getCoding().getFirst().getCode(); + } + } + + if (provisionCode != null) { + try { + ModelProjectConsentPurpose modelProjectConsentPurpose = + ModelProjectConsentPurpose.forValue(provisionCode); + + if (ModelProjectConsentPurpose.SEQUENCING.equals(modelProjectConsentPurpose)) { + // CONVENTION: wrapping date is date of SEQUENCING consent + mtbFile.getMetadata().getModelProjectConsent().setDate(consentFhirResource.getDateTime()); + } + + Provision provision = Provision.builder() + .type(ConsentProvision.valueOf(provisionComponent.getType().name())) + .date(provisionComponent.getPeriod().getStart()) + .purpose(modelProjectConsentPurpose) + .build(); + + mtbFile.getMetadata().getModelProjectConsent().getProvisions().add(provision); + + } catch (IOException ioe) { + logger.error("Provision code '" + provisionCode + "' is unknown and cannot be mapped.", ioe.toString()); + } + } + + if (!mtbFile.getMetadata().getModelProjectConsent().getProvisions().isEmpty()) { + mtbFile.getMetadata().getModelProjectConsent().setVersion(gIcsConfigProperties.getGenomeDeConsentVersion()); + } + } + } +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckFileBased.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckFileBased.java index a18baca..df1209b 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckFileBased.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckFileBased.java @@ -1,12 +1,14 @@ package dev.dnpm.etl.processor.consent; +import dev.pcvolkmer.mv64e.mtb.Mtb; import java.util.Date; +import org.apache.commons.lang3.NotImplementedException; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Consent.ConsentProvisionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ConsentCheckFileBased implements ICheckConsent{ +public class ConsentCheckFileBased implements ICheckConsent { private static final Logger log = LoggerFactory.getLogger(ConsentCheckFileBased.class); @@ -40,4 +42,14 @@ public class ConsentCheckFileBased implements ICheckConsent{ Date requestDate, ConsentDomain consentDomain) { return ConsentProvisionType.NULL; } + + @Override + public void embedBroadConsentResources(Mtb mtbFile, Bundle broadConsent) { + throw new NotImplementedException("not intended to be implemented here!"); + } + + @Override + public void addGenomeDbProvisions(Mtb mtbFile, Bundle consentGnomeDe) { + throw new NotImplementedException("not intended to be implemented here!"); + } } 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 b6288d3..937bea0 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -38,12 +38,10 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; -public class GicsConsentService implements ICheckConsent { +public class GicsConsentService extends BaseConsentService { private final Logger log = LoggerFactory.getLogger(GicsConsentService.class); - 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; @@ -54,7 +52,8 @@ public class GicsConsentService implements ICheckConsent { public GicsConsentService(GIcsConfigProperties gIcsConfigProperties, RetryTemplate retryTemplate, RestTemplate restTemplate, AppFhirConfig appFhirConfig) { - this.gIcsConfigProperties = gIcsConfigProperties; + super(gIcsConfigProperties); + this.retryTemplate = retryTemplate; this.restTemplate = restTemplate; this.fhirContext = appFhirConfig.fhirContext(); diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ICheckConsent.java b/src/main/java/dev/dnpm/etl/processor/consent/ICheckConsent.java index 99697f1..b230a3c 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/ICheckConsent.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/ICheckConsent.java @@ -1,6 +1,6 @@ package dev.dnpm.etl.processor.consent; - +import dev.pcvolkmer.mv64e.mtb.Mtb; import java.util.Date; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Consent.ConsentProvisionType; @@ -36,8 +36,8 @@ public interface ICheckConsent { * @return consent policies as bundle;

if empty patient has not been asked, yet.

*/ default Bundle getGenomDeConsent(String personIdentifierValue, Date requestDate) { - return currentConsentForPersonAndTemplate(personIdentifierValue, ConsentDomain.Modelvorhaben64e, - requestDate); + return currentConsentForPersonAndTemplate(personIdentifierValue, + ConsentDomain.Modelvorhaben64e, requestDate); } /** @@ -50,12 +50,17 @@ public interface ICheckConsent { */ Bundle currentConsentForPersonAndTemplate(String personIdentifierValue, ConsentDomain targetConsentDomain, Date requestDate); + /** - * * @param consentBundle consent resource - * @param requestDate date which must be within validation period of provision + * @param requestDate date which must be within validation period of provision * @return type of provision, will be {@link ConsentProvisionType#NULL} if none is found. */ - ConsentProvisionType getProvisionTypeByPolicyCode(Bundle consentBundle, - Date requestDate, ConsentDomain consentDomain); + ConsentProvisionType getProvisionTypeByPolicyCode(Bundle consentBundle, Date requestDate, + ConsentDomain consentDomain); + + + void embedBroadConsentResources(Mtb mtbFile, Bundle broadConsent); + + void addGenomeDbProvisions(Mtb mtbFile, Bundle consentGnomeDe); } 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 5010cfb..35eafd5 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -107,7 +107,13 @@ data class GIcsConfigProperties( /** * Consent Policy which should be used for consent check */ - val genomeDePolicySystem: String = "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV" + val genomeDePolicySystem: String = "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + + /** + * Consent version (fixed version) + * + */ + val genomeDeConsentVersion: String = "2.0" ) { companion object { const val NAME = "app.consent.gics" diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt index cdbba90..e2d5e1c 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -34,21 +34,16 @@ import dev.dnpm.etl.processor.output.* import dev.dnpm.etl.processor.pseudonym.PseudonymizeService import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith -import dev.pcvolkmer.mv64e.mtb.ConsentProvision import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent -import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose import dev.pcvolkmer.mv64e.mtb.Mtb import dev.pcvolkmer.mv64e.mtb.MvhMetadata -import dev.pcvolkmer.mv64e.mtb.Provision import org.apache.commons.codec.binary.Base32 import org.apache.commons.codec.digest.DigestUtils -import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Consent import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service -import java.io.IOException import java.lang.RuntimeException import java.time.Clock import java.time.Instant @@ -96,7 +91,7 @@ class RequestProcessor( * @return true if consent is given * */ - fun consentGatedCheck(mtbFile: Mtb): Boolean { + fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean { if (consentService == null) { // consent check seems to be disabled return true @@ -127,8 +122,8 @@ class RequestProcessor( personIdentifierValue, requestDate ) - addGenomeDbProvisions(mtbFile, genomeDeConsent) - embedBroadConsentResources(mtbFile, broadConsent) + consentService.addGenomeDbProvisions(mtbFile, genomeDeConsent) + consentService.embedBroadConsentResources(mtbFile, broadConsent) val broadConsentStatus = consentService.getProvisionTypeByPolicyCode( broadConsent, @@ -157,20 +152,25 @@ class RequestProcessor( if (mtbFile.metadata == null) { val mvhMetadata = MvhMetadata.builder().build() mtbFile.metadata = mvhMetadata - if (mtbFile.metadata.researchConsents == null) { - mtbFile.metadata.researchConsents = mutableListOf() - } - if (mtbFile.metadata.modelProjectConsent == null) { - mtbFile.metadata.modelProjectConsent = ModelProjectConsent() - mtbFile.metadata.modelProjectConsent.provisions = mutableListOf() - } } + if (mtbFile.metadata.researchConsents == null) { + mtbFile.metadata.researchConsents = mutableListOf() + } + if (mtbFile.metadata.modelProjectConsent == null) { + mtbFile.metadata.modelProjectConsent = ModelProjectConsent() + mtbFile.metadata.modelProjectConsent.provisions = mutableListOf() + } else + if (mtbFile.metadata.modelProjectConsent.provisions != null) { + // make sure list can be changed + mtbFile.metadata.modelProjectConsent.provisions = + mtbFile.metadata.modelProjectConsent.provisions.toMutableList() + } } fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { val pid = PatientId(extractPatientIdentifier(mtbFile)) - if (consentGatedCheck(mtbFile)) { + if (consentGatedCheckAndTryEmbedding(mtbFile)) { mtbFile pseudonymizeWith pseudonymizeService mtbFile anonymizeContentWith pseudonymizeService val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) @@ -186,49 +186,6 @@ class RequestProcessor( } - fun embedBroadConsentResources( - mtbFile: Mtb, broadConsent: Bundle - ) { - broadConsent.entry.forEach { it -> - mtbFile.metadata.researchConsents.add(mapOf(it.resource.id to it.resource as Consent)) - } - } - - fun addGenomeDbProvisions( - mtbFile: Mtb, consentGnomeDe: Bundle - ) { - consentGnomeDe.entry.forEach { it -> - { - val consentFhirResource = it.resource as Consent - - // we expect only one provision in collection, therefore get first or none - val provisionComponent = consentFhirResource.provision.provision.firstOrNull() - val provisionCode = - provisionComponent?.code?.firstOrNull()?.coding?.firstOrNull()?.code - - if (provisionCode != null) { - try { - val modelProjectConsentPurpose: ModelProjectConsentPurpose = - ModelProjectConsentPurpose.valueOf(provisionCode) - mtbFile.metadata.modelProjectConsent.provisions.add( - Provision.builder().type( - ConsentProvision.forValue(provisionComponent.type.name) - ).date(provisionComponent.period.start).purpose( - modelProjectConsentPurpose - ).build() - ) - } catch (ioe: IOException) { - logger.error( - "provision code '$provisionCode' is unknown and cannot be mapped.", - ioe.toString() - ) - } - } - - } - } - } - private fun saveAndSend(request: MtbFileRequest, pid: PatientId) { requestService.save( Request(