From 3eb1c79cec3704a5b821377c4df3f8e9f703c8a3 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Fri, 15 Aug 2025 12:37:42 +0200 Subject: feat: check consent for DNPM 2.1 requests (#126) Co-authored-by: Jakub Lidke --- .../etl/processor/consent/ConsentByMtbFile.java | 31 --- .../dnpm/etl/processor/consent/ConsentDomain.java | 6 +- .../etl/processor/consent/GicsConsentService.java | 292 +++++++++++---------- .../etl/processor/consent/IConsentService.java | 27 ++ .../dnpm/etl/processor/consent/IGetConsent.java | 27 -- .../processor/consent/MtbFileConsentService.java | 31 +++ 6 files changed, 220 insertions(+), 194 deletions(-) delete mode 100644 src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java create mode 100644 src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java delete mode 100644 src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java create mode 100644 src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java (limited to 'src/main/java/dev/dnpm/etl') diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java deleted file mode 100644 index f7ce39e..0000000 --- a/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.dnpm.etl.processor.consent; - -import java.util.Date; -import org.hl7.fhir.r4.model.Bundle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ConsentByMtbFile implements IGetConsent { - - private static final Logger log = LoggerFactory.getLogger(ConsentByMtbFile.class); - - public ConsentByMtbFile() { - log.info("ConsentCheckFileBased initialized..."); - } - - @Override - public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { - return TtpConsentStatus.UNKNOWN_CHECK_FILE; - } - - /** - * EMPTY METHOD: NOT IMPLEMENTED - * - * @return empty bundle - */ - @Override - public Bundle getConsent(String personIdentifierValue, Date requestDate, - ConsentDomain consentDomain) { - return new Bundle(); - } -} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java index 6d0b160..51bfd50 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java @@ -4,10 +4,10 @@ public enum ConsentDomain { /** * MII Broad consent */ - BroadConsent, + BROAD_CONSENT, /** - * GenomDe Modelvohaben §64e + * GenomDe Modellvorhaben §64e */ - Modelvorhaben64e + MODELLVORHABEN_64E } 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 6f3c987..95e8e8f 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -4,17 +4,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.DataFormatException; import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.GIcsConfigProperties; -import java.util.Date; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.DateType; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.OperationOutcome; -import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; -import org.hl7.fhir.r4.model.StringType; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,112 +14,152 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.retry.TerminatedRetryException; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.net.URI; +import java.util.Date; -public class GicsConsentService implements IGetConsent { +/** + * Service to request Consent from remote gICS installation + * + * @since 0.11 + */ +public class GicsConsentService implements IConsentService { private final Logger log = LoggerFactory.getLogger(GicsConsentService.class); 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; - private final HttpHeaders httpHeader; private final GIcsConfigProperties gIcsConfigProperties; - private String url; - - public GicsConsentService(GIcsConfigProperties gIcsConfigProperties, - RetryTemplate retryTemplate, RestTemplate restTemplate, AppFhirConfig appFhirConfig) { + public GicsConsentService( + GIcsConfigProperties gIcsConfigProperties, + RetryTemplate retryTemplate, + RestTemplate restTemplate, + AppFhirConfig appFhirConfig + ) { this.retryTemplate = retryTemplate; this.restTemplate = restTemplate; this.fhirContext = appFhirConfig.fhirContext(); - httpHeader = buildHeader(gIcsConfigProperties.getUsername(), - gIcsConfigProperties.getPassword()); this.gIcsConfigProperties = gIcsConfigProperties; log.info("GicsConsentService initialized..."); } - public String getGicsUri(String endpoint) { - if (url == null) { - final String gIcsBaseUri = gIcsConfigProperties.getUri(); - if (StringUtils.isBlank(gIcsBaseUri)) { - throw new IllegalArgumentException( - "gICS base URL is empty - should call gICS with false configuration."); - } - url = UriComponentsBuilder.fromUriString(gIcsBaseUri).path(endpoint) - .toUriString(); - } - return url; - } - - @NotNull - private static HttpHeaders buildHeader(String gPasUserName, String gPasPassword) { - var headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_XML); - - if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) { - return headers; - } - - headers.setBasicAuth(gPasUserName, gPasPassword); - return headers; - } - - protected static Parameters getIsConsentedRequestParam(GIcsConfigProperties configProperties, - String personIdentifierValue) { + protected Parameters getFhirRequestParameters( + String personIdentifierValue + ) { var result = new Parameters(); - result.addParameter(new ParametersParameterComponent().setName("personIdentifier").setValue( - new Identifier().setValue(personIdentifierValue) - .setSystem(configProperties.getPersonIdentifierSystem()))); - result.addParameter(new ParametersParameterComponent().setName("domain") - .setValue(new StringType().setValue(configProperties.getBroadConsentDomainName()))); - result.addParameter(new ParametersParameterComponent().setName("policy").setValue( - new Coding().setCode(configProperties.getBroadConsentPolicyCode()) - .setSystem(configProperties.getBroadConsentPolicySystem()))); + result.addParameter( + new ParametersParameterComponent() + .setName("personIdentifier") + .setValue( + new Identifier() + .setValue(personIdentifierValue) + .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem()) + ) + ); + result.addParameter( + new ParametersParameterComponent() + .setName("domain") + .setValue( + new StringType() + .setValue(this.gIcsConfigProperties.getBroadConsentDomainName()) + ) + ); + result.addParameter( + new ParametersParameterComponent() + .setName("policy") + .setValue( + new Coding() + .setCode(this.gIcsConfigProperties.getBroadConsentPolicyCode()) + .setSystem(this.gIcsConfigProperties.getBroadConsentPolicySystem()) + ) + ); /* * is mandatory parameter, but we ignore it via additional configuration parameter * 'ignoreVersionNumber'. */ - result.addParameter(new ParametersParameterComponent().setName("version") - .setValue(new StringType().setValue("1.1"))); + result.addParameter( + new ParametersParameterComponent() + .setName("version") + .setValue(new StringType().setValue("1.1") + ) + ); /* add config parameter with: * ignoreVersionNumber -> true ->> Reason is we cannot know which policy version each patient * has possibly signed or not, therefore we are happy with any version found. * unknownStateIsConsideredAsDecline -> true */ - var config = new ParametersParameterComponent().setName("config").addPart( - new ParametersParameterComponent().setName("ignoreVersionNumber") - .setValue(new BooleanType().setValue(true))).addPart( - new ParametersParameterComponent().setName("unknownStateIsConsideredAsDecline") - .setValue(new BooleanType().setValue(false))); + var config = new ParametersParameterComponent() + .setName("config") + .addPart( + new ParametersParameterComponent() + .setName("ignoreVersionNumber") + .setValue(new BooleanType().setValue(true)) + ) + .addPart( + new ParametersParameterComponent() + .setName("unknownStateIsConsideredAsDecline") + .setValue(new BooleanType().setValue(false)) + ); + result.addParameter(config); return result; } + private URI endpointUri(String endpoint) { + assert this.gIcsConfigProperties.getUri() != null; + return UriComponentsBuilder.fromUriString(this.gIcsConfigProperties.getUri()).path(endpoint).build().toUri(); + } + + private HttpHeaders headersWithHttpBasicAuth() { + assert this.gIcsConfigProperties.getUri() != null; + + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + + if ( + StringUtils.isBlank(this.gIcsConfigProperties.getUsername()) + || StringUtils.isBlank(this.gIcsConfigProperties.getPassword()) + ) { + return headers; + } + + headers.setBasicAuth(this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword()); + return headers; + } + protected String callGicsApi(Parameters parameter, String endpoint) { var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter); - - HttpEntity requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader); - ResponseEntity responseEntity; + HttpEntity requestEntity = new HttpEntity<>(parameterAsXml, this.headersWithHttpBasicAuth()); try { - var url = getGicsUri(endpoint); - - responseEntity = retryTemplate.execute( - ctx -> restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class)); + var responseEntity = retryTemplate.execute( + ctx -> restTemplate.exchange(endpointUri(endpoint), HttpMethod.POST, requestEntity, String.class) + ); + + if (responseEntity.getStatusCode().is2xxSuccessful()) { + return responseEntity.getBody(); + } else { + var msg = String.format( + "Trusted party system reached but request failed! code: '%s' response: '%s'", + responseEntity.getStatusCode(), responseEntity.getBody()); + log.error(msg); + return null; + } } catch (RestClientException e) { var msg = String.format("Get consents status request failed reason: '%s", - e.getMessage()); + e.getMessage()); log.error(msg); return null; @@ -137,39 +169,32 @@ public class GicsConsentService implements IGetConsent { terminatedRetryException.getMessage()); log.error(msg); return null; - - } - if (responseEntity.getStatusCode().is2xxSuccessful()) { - return responseEntity.getBody(); - } else { - var msg = String.format( - "Trusted party system reached but request failed! code: '%s' response: '%s'", - responseEntity.getStatusCode(), responseEntity.getBody()); - log.error(msg); - return null; } } @Override public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { - var parameter = GicsConsentService.getIsConsentedRequestParam(gIcsConfigProperties, - personIdentifierValue); - - var consentStatusResponse = callGicsApi(parameter, - GicsConsentService.IS_CONSENTED_ENDPOINT); + var consentStatusResponse = callGicsApi( + getFhirRequestParameters(personIdentifierValue), + GicsConsentService.IS_CONSENTED_ENDPOINT + ); return evaluateConsentResponse(consentStatusResponse); } - protected Bundle currentConsentForPersonAndTemplate(String personIdentifierValue, - ConsentDomain targetConsentDomain, Date requestDate) { - - String consentDomain = getConsentDomain(targetConsentDomain); + protected Bundle currentConsentForPersonAndTemplate( + String personIdentifierValue, + ConsentDomain consentDomain, + Date requestDate + ) { - var requestParameter = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson( - gIcsConfigProperties, personIdentifierValue, requestDate, consentDomain); + var requestParameter = buildRequestParameterCurrentPolicyStatesForPerson( + personIdentifierValue, + requestDate, + consentDomain + ); var consentDataSerialized = callGicsApi(requestParameter, - GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); + GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); if (consentDataSerialized == null) { // error occurred - should not process further! @@ -177,15 +202,15 @@ public class GicsConsentService implements IGetConsent { "consent data request failed - stopping processing! - try again or fix other problems first."); } var iBaseResource = fhirContext.newJsonParser() - .parseResource(consentDataSerialized); + .parseResource(consentDataSerialized); if (iBaseResource instanceof OperationOutcome) { // log error - very likely a configuration error String errorMessage = "Consent request failed! Check outcome:\n " + consentDataSerialized; log.error(errorMessage); throw new IllegalStateException(errorMessage); - } else if (iBaseResource instanceof Bundle) { - return (Bundle) iBaseResource; + } else if (iBaseResource instanceof Bundle bundle) { + return bundle; } else { String errorMessage = "Consent request failed! Unexpected response received! -> " + consentDataSerialized; @@ -195,40 +220,52 @@ public class GicsConsentService implements IGetConsent { } @NotNull - private String getConsentDomain(ConsentDomain targetConsentDomain) { - String consentDomain; - switch (targetConsentDomain) { - case BroadConsent -> consentDomain = gIcsConfigProperties.getBroadConsentDomainName(); - case Modelvorhaben64e -> - consentDomain = gIcsConfigProperties.getGenomDeConsentDomainName(); - default -> throw new IllegalArgumentException( - "target ConsentDomain is missing but must be provided!"); - } - return consentDomain; + private String getConsentDomainName(ConsentDomain targetConsentDomain) { + return switch (targetConsentDomain) { + case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName(); + case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName(); + }; } - protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson( - GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate, - String targetDomain) { + protected Parameters buildRequestParameterCurrentPolicyStatesForPerson( + String personIdentifierValue, + Date requestDate, + ConsentDomain consentDomain + ) { var requestParameter = new Parameters(); - requestParameter.addParameter(new ParametersParameterComponent().setName("personIdentifier") - .setValue(new Identifier().setValue(personIdentifierValue) - .setSystem(gIcsConfigProperties.getPersonIdentifierSystem()))); - - requestParameter.addParameter(new ParametersParameterComponent().setName("domain") - .setValue(new StringType().setValue(targetDomain))); + requestParameter.addParameter( + new ParametersParameterComponent() + .setName("personIdentifier") + .setValue( + new Identifier() + .setValue(personIdentifierValue) + .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem()) + ) + ); + + requestParameter.addParameter( + new ParametersParameterComponent() + .setName("domain") + .setValue(new StringType().setValue(getConsentDomainName(consentDomain))) + ); Parameters nestedConfigParameters = new Parameters(); - nestedConfigParameters.addParameter( - new ParametersParameterComponent().setName("idMatchingType").setValue( - new Coding().setSystem( - "https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType") - .setCode("AT_LEAST_ONE"))).addParameter("ignoreVersionNumber", false) + nestedConfigParameters + .addParameter( + new ParametersParameterComponent() + .setName("idMatchingType") + .setValue(new Coding() + .setSystem("https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType") + .setCode("AT_LEAST_ONE") + ) + ) + .addParameter("ignoreVersionNumber", false) .addParameter("unknownStateIsConsideredAsDecline", false) .addParameter("requestDate", new DateType().setValue(requestDate)); - requestParameter.addParameter(new ParametersParameterComponent().setName("config").addPart() - .setResource(nestedConfigParameters)); + requestParameter.addParameter( + new ParametersParameterComponent().setName("config").addPart().setResource(nestedConfigParameters) + ); return requestParameter; } @@ -254,7 +291,7 @@ public class GicsConsentService implements IGetConsent { } } else if (response instanceof OperationOutcome outcome) { log.error("failed to get consent status from ttp. probably configuration error. " - + "outcome: '{}'", fhirContext.newJsonParser().encodeToString(outcome)); + + "outcome: '{}'", fhirContext.newJsonParser().encodeToString(outcome)); } } catch (DataFormatException dfe) { @@ -265,17 +302,6 @@ public class GicsConsentService implements IGetConsent { @Override public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) { - switch (consentDomain) { - case BroadConsent -> { - return currentConsentForPersonAndTemplate(patientId, ConsentDomain.BroadConsent, - requestDate); - } - case Modelvorhaben64e -> { - return currentConsentForPersonAndTemplate(patientId, - ConsentDomain.Modelvorhaben64e, requestDate); - } - } - - return new Bundle(); + return currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate); } } diff --git a/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java new file mode 100644 index 0000000..ded3515 --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java @@ -0,0 +1,27 @@ +package dev.dnpm.etl.processor.consent; + +import java.util.Date; +import org.hl7.fhir.r4.model.Bundle; + +public interface IConsentService { + + /** + * Get broad consent status for a patient identifier + * + * @param personIdentifierValue patient identifier used for consent data + * @return status of broad consent + * @apiNote cannot not differ between not asked and rejected + * + */ + TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue); + + /** + * Get broad consent policies with respect to a request date + * + * @param personIdentifierValue patient identifier used for consent data + * @param requestDate target date until consent data should be considered + * @return consent policies as bundle;

if empty patient has not been asked, yet.

+ */ + Bundle getConsent(String personIdentifierValue, Date requestDate, ConsentDomain consentDomain); + +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java b/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java deleted file mode 100644 index 3482b9a..0000000 --- a/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.dnpm.etl.processor.consent; - -import java.util.Date; -import org.hl7.fhir.r4.model.Bundle; - -public interface IGetConsent { - - /** - * Get broad consent status for a patient identifier - * - * @param personIdentifierValue patient identifier used for consent data - * @return status of broad consent - * @apiNote cannot not differ between not asked and rejected - * - */ - TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue); - - /** - * Get broad consent policies with respect to a request date - * - * @param personIdentifierValue patient identifier used for consent data - * @param requestDate target date until consent data should be considered - * @return consent policies as bundle;

if empty patient has not been asked, yet.

- */ - Bundle getConsent(String personIdentifierValue, Date requestDate, ConsentDomain consentDomain); - -} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java new file mode 100644 index 0000000..24cb8f7 --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java @@ -0,0 +1,31 @@ +package dev.dnpm.etl.processor.consent; + +import java.util.Date; +import org.hl7.fhir.r4.model.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MtbFileConsentService implements IConsentService { + + private static final Logger log = LoggerFactory.getLogger(MtbFileConsentService.class); + + public MtbFileConsentService() { + log.info("ConsentCheckFileBased initialized..."); + } + + @Override + public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { + return TtpConsentStatus.UNKNOWN_CHECK_FILE; + } + + /** + * EMPTY METHOD: NOT IMPLEMENTED + * + * @return empty bundle + */ + @Override + public Bundle getConsent(String personIdentifierValue, Date requestDate, + ConsentDomain consentDomain) { + return new Bundle(); + } +} -- cgit v1.2.3