diff options
Diffstat (limited to 'src/main/java/dev')
9 files changed, 592 insertions, 520 deletions
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 51bfd50..e339ba4 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java @@ -1,13 +1,9 @@ package dev.dnpm.etl.processor.consent; public enum ConsentDomain { - /** - * MII Broad consent - */ - BROAD_CONSENT, + /** MII Broad consent */ + BROAD_CONSENT, - /** - * GenomDe Modellvorhaben §64e - */ - MODELLVORHABEN_64E + /** GenomDe Modellvorhaben §64e */ + 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 cc0491d..a69ba53 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -4,11 +4,17 @@ 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.net.URI; +import java.net.URISyntaxException; +import java.util.Date; +import kotlin.random.Random; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hc.core5.net.URIBuilder; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; @@ -20,10 +26,6 @@ import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Date; - /** * Service to request Consent from remote gICS installation * @@ -31,294 +33,372 @@ import java.util.Date; */ 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 GIcsConfigProperties gIcsConfigProperties; - - public GicsConsentService( - GIcsConfigProperties gIcsConfigProperties, - RetryTemplate retryTemplate, - RestTemplate restTemplate, - AppFhirConfig appFhirConfig - ) { - this.retryTemplate = retryTemplate; - this.restTemplate = restTemplate; - this.fhirContext = appFhirConfig.fhirContext(); - this.gIcsConfigProperties = gIcsConfigProperties; - log.info("GicsConsentService initialized..."); - } - - protected Parameters getFhirRequestParameters( - String personIdentifierValue - ) { - var result = new Parameters(); - 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") - ) - ); - - /* 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() + 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 static final String BROAD_CONSENT_PROFILE_URI = + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung"; + private static final String BROAD_CONSENT_POLICY = + "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1791"; + + private final RetryTemplate retryTemplate; + private final RestTemplate restTemplate; + private final FhirContext fhirContext; + private final GIcsConfigProperties gIcsConfigProperties; + + public GicsConsentService( + GIcsConfigProperties gIcsConfigProperties, + RetryTemplate retryTemplate, + RestTemplate restTemplate, + AppFhirConfig appFhirConfig) { + this.retryTemplate = retryTemplate; + this.restTemplate = restTemplate; + this.fhirContext = appFhirConfig.fhirContext(); + this.gIcsConfigProperties = gIcsConfigProperties; + log.info("GicsConsentService initialized..."); + } + + protected Parameters getFhirRequestParameters(String personIdentifierValue) { + var result = new Parameters(); + 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"))); + + /* 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)) - ) + .setValue(new BooleanType().setValue(true))) .addPart( new ParametersParameterComponent() .setName("unknownStateIsConsideredAsDecline") - .setValue(new BooleanType().setValue(false)) - ); + .setValue(new BooleanType().setValue(false))); - result.addParameter(config); + result.addParameter(config); - return result; - } - - private URI endpointUri(String endpoint) throws URISyntaxException { - if (null == this.gIcsConfigProperties.getUri()) { - throw new URISyntaxException("null", "URI must not be null"); - } - var gPasUrl1 = this.gIcsConfigProperties.getUri(); - if (this.gIcsConfigProperties.getUri().lastIndexOf("/") - == this.gIcsConfigProperties.getUri().length() - 1) { - gPasUrl1 = - this.gIcsConfigProperties - .getUri() - .substring(0, this.gIcsConfigProperties.getUri().length() - 1); - } - var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(endpoint); + return result; + } - return urlBuilder.build(); + private URI endpointUri(String endpoint) throws URISyntaxException { + if (null == this.gIcsConfigProperties.getUri()) { + throw new URISyntaxException("null", "URI must not be null"); } + var gPasUrl1 = this.gIcsConfigProperties.getUri(); + if (this.gIcsConfigProperties.getUri().lastIndexOf("/") + == this.gIcsConfigProperties.getUri().length() - 1) { + gPasUrl1 = + this.gIcsConfigProperties + .getUri() + .substring(0, this.gIcsConfigProperties.getUri().length() - 1); + } + var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(endpoint); - private HttpHeaders headersWithHttpBasicAuth() { - assert this.gIcsConfigProperties.getUri() != null; + return urlBuilder.build(); + } - var headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_XML); + private HttpHeaders headersWithHttpBasicAuth() { + assert this.gIcsConfigProperties.getUri() != null; - if ( - StringUtils.isBlank(this.gIcsConfigProperties.getUsername()) - || StringUtils.isBlank(this.gIcsConfigProperties.getPassword()) - ) { - return headers; - } + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); - headers.setBasicAuth(this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword()); - return headers; + if (StringUtils.isBlank(this.gIcsConfigProperties.getUsername()) + || StringUtils.isBlank(this.gIcsConfigProperties.getPassword())) { + return headers; } - protected String callGicsApi(Parameters parameter, String endpoint) { - var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter); - HttpEntity<String> requestEntity = new HttpEntity<>(parameterAsXml, this.headersWithHttpBasicAuth()); - try { - 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()); - log.error(msg); - return null; - - } catch (TerminatedRetryException terminatedRetryException) { - var msg = String.format( - "Get consents status process has been terminated. termination reason: '%s", - terminatedRetryException.getMessage()); - log.error(msg); - return null; - } catch (URISyntaxException e) { + headers.setBasicAuth( + this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword()); + return headers; + } + + @Nullable + protected String callGicsApi(Parameters parameter, String endpoint) { + var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter); + HttpEntity<String> requestEntity = + new HttpEntity<>(parameterAsXml, this.headersWithHttpBasicAuth()); + try { + 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()); + log.error(msg); + return null; + + } catch (TerminatedRetryException terminatedRetryException) { + var msg = + String.format( + "Get consents status process has been terminated. termination reason: '%s", + terminatedRetryException.getMessage()); + log.error(msg); + return null; + } catch (URISyntaxException e) { var msg = String.format("Invalid URI for consents status request: '%s", e.getMessage()); log.error(msg); return null; } - } + } - @Override - public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { - var consentStatusResponse = callGicsApi( + @Override + @NonNull + public TtpConsentStatus getTtpBroadConsentStatus(@NonNull String personIdentifierValue) { + var consentStatusResponse = + callGicsApi( getFhirRequestParameters(personIdentifierValue), - GicsConsentService.IS_CONSENTED_ENDPOINT - ); - return evaluateConsentResponse(consentStatusResponse); + GicsConsentService.IS_CONSENTED_ENDPOINT); + return evaluateConsentResponse(consentStatusResponse); + } + + protected Bundle currentConsentForPersonAndTemplate( + String personIdentifierValue, ConsentDomain consentDomain, Date requestDate) { + + var requestParameter = + buildRequestParameterCurrentPolicyStatesForPerson( + personIdentifierValue, requestDate, consentDomain); + + var consentDataSerialized = + callGicsApi(requestParameter, GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); + + if (consentDataSerialized == null) { + // error occurred - should not process further! + throw new IllegalStateException( + "consent data request failed - stopping processing! - try again or fix other problems first."); + } + var iBaseResource = fhirContext.newJsonParser().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 bundle) { + return bundle; + } else { + String errorMessage = + "Consent request failed! Unexpected response received! -> " + consentDataSerialized; + log.error(errorMessage); + throw new IllegalStateException(errorMessage); + } + } + + @NonNull + private String getConsentDomainName(ConsentDomain targetConsentDomain) { + return switch (targetConsentDomain) { + case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName(); + case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName(); + }; + } + + 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(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", true) + .addParameter("unknownStateIsConsideredAsDecline", false) + .addParameter("requestDate", new DateType().setValue(requestDate)); + + requestParameter.addParameter( + new ParametersParameterComponent() + .setName("config") + .addPart() + .setResource(nestedConfigParameters)); + + return requestParameter; + } + + private TtpConsentStatus evaluateConsentResponse(@Nullable String consentStatusResponse) { + if (consentStatusResponse == null) { + return TtpConsentStatus.FAILED_TO_ASK; } + try { + var response = fhirContext.newJsonParser().parseResource(consentStatusResponse); + + if (response instanceof Parameters responseParameters) { - protected Bundle currentConsentForPersonAndTemplate( - String personIdentifierValue, - ConsentDomain consentDomain, - Date requestDate - ) { - - var requestParameter = buildRequestParameterCurrentPolicyStatesForPerson( - personIdentifierValue, - requestDate, - consentDomain - ); - - var consentDataSerialized = callGicsApi(requestParameter, - GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); - - if (consentDataSerialized == null) { - // error occurred - should not process further! - throw new IllegalStateException( - "consent data request failed - stopping processing! - try again or fix other problems first."); + var responseValue = responseParameters.getParameter("consented").getValue(); + var isConsented = responseValue.castToBoolean(responseValue); + if (!isConsented.hasValue()) { + return TtpConsentStatus.FAILED_TO_ASK; } - var iBaseResource = fhirContext.newJsonParser() - .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 bundle) { - return bundle; + if (isConsented.booleanValue()) { + return TtpConsentStatus.BROAD_CONSENT_GIVEN; } else { - String errorMessage = "Consent request failed! Unexpected response received! -> " - + consentDataSerialized; - log.error(errorMessage); - throw new IllegalStateException(errorMessage); + return TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED; } + } else if (response instanceof OperationOutcome outcome) { + log.error( + "failed to get consent status from ttp. probably configuration error. " + + "outcome: '{}'", + fhirContext.newJsonParser().encodeToString(outcome)); + } + } catch (DataFormatException dfe) { + log.error("failed to parse response to FHIR R4 resource.", dfe); } - - @NotNull - private String getConsentDomainName(ConsentDomain targetConsentDomain) { - return switch (targetConsentDomain) { - case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName(); - case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName(); - }; + return TtpConsentStatus.FAILED_TO_ASK; + } + + @Override + @NonNull + public Bundle getConsent( + @NonNull String patientId, @NonNull Date requestDate, @NonNull ConsentDomain consentDomain) { + Bundle gIcsResultBundle = + currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate); + if (ConsentDomain.BROAD_CONSENT == consentDomain) { + return anonymizeBroadConsent(convertGicsResultToMiiBroadConsent(gIcsResultBundle)); } + return gIcsResultBundle; + } - 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(this.gIcsConfigProperties.getPersonIdentifierSystem()) - ) - ); + protected Bundle convertGicsResultToMiiBroadConsent(Bundle gIcsResultBundle) { + if (gIcsResultBundle == null + || gIcsResultBundle.getEntry().isEmpty() + || !(gIcsResultBundle.getEntry().getFirst().getResource() instanceof Consent)) + return gIcsResultBundle; - requestParameter.addParameter( - new ParametersParameterComponent() - .setName("domain") - .setValue(new StringType().setValue(getConsentDomainName(consentDomain))) - ); + Bundle.BundleEntryComponent bundleEntryComponent = gIcsResultBundle.getEntry().getFirst(); - 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) - .addParameter("unknownStateIsConsideredAsDecline", false) - .addParameter("requestDate", new DateType().setValue(requestDate)); - - requestParameter.addParameter( - new ParametersParameterComponent().setName("config").addPart().setResource(nestedConfigParameters) - ); - - return requestParameter; + var consentAsOne = (Consent) bundleEntryComponent.getResource(); + + if (isMiiConsent(consentAsOne)) { + return gIcsResultBundle; } - private TtpConsentStatus evaluateConsentResponse(String consentStatusResponse) { - if (consentStatusResponse == null) { - return TtpConsentStatus.FAILED_TO_ASK; - } - try { - var response = fhirContext.newJsonParser().parseResource(consentStatusResponse); - - if (response instanceof Parameters responseParameters) { - - var responseValue = responseParameters.getParameter("consented").getValue(); - var isConsented = responseValue.castToBoolean(responseValue); - if (!isConsented.hasValue()) { - return TtpConsentStatus.FAILED_TO_ASK; - } - if (isConsented.booleanValue()) { - return TtpConsentStatus.BROAD_CONSENT_GIVEN; - } else { - return TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED; - } - } else if (response instanceof OperationOutcome outcome) { - log.error("failed to get consent status from ttp. probably configuration error. " - + "outcome: '{}'", fhirContext.newJsonParser().encodeToString(outcome)); - - } - } catch (DataFormatException dfe) { - log.error("failed to parse response to FHIR R4 resource.", dfe); - } - return TtpConsentStatus.FAILED_TO_ASK; + if (consentAsOne.getPolicy().stream().noneMatch(p -> p.getUri().equals(BROAD_CONSENT_POLICY))) { + consentAsOne.addPolicy(new Consent.ConsentPolicyComponent().setUri(BROAD_CONSENT_POLICY)); } - @Override - public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) { - return currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate); + if (consentAsOne.getMeta().getProfile().stream() + .noneMatch(p -> p.getValue().equals(BROAD_CONSENT_PROFILE_URI))) { + consentAsOne.getMeta().addProfile(BROAD_CONSENT_PROFILE_URI); + } + + consentAsOne.setPolicyRule(null); + + consentAsOne + .getCategory() + .removeIf( + category -> + category.hasCoding( + "http://fhir.de/ConsentManagement/CodeSystem/ResultType", "policy")); + + final var miiConsentCategory = new CodeableConcept(); + miiConsentCategory.addCoding( + new Coding() + .setSystem( + "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category") + .setCode("2.16.840.1.113883.3.1937.777.24.2.184")); + consentAsOne.addCategory(miiConsentCategory); + + gIcsResultBundle.getEntry().stream() + .skip(1) + .forEach( + c -> + consentAsOne + .getProvision() + .addProvision( + ((Consent) c.getResource()).getProvision().getProvisionFirstRep())); + + gIcsResultBundle.getEntry().clear(); + gIcsResultBundle.addEntry(bundleEntryComponent); + return gIcsResultBundle; + } + + private static boolean isMiiConsent(Consent consent) { + for (var category : consent.getCategory()) { + for (var categoryCoding : category.getCoding()) { + if ("https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category" + .equals(categoryCoding.getSystem()) + && "2.16.840.1.113883.3.1937.777.24.2.184".equals(categoryCoding.getCode())) { + return true; + } + } } + return false; + } + + protected Bundle anonymizeBroadConsent(Bundle bundle) { + Bundle.BundleEntryComponent bundleEntryComponent = bundle.getEntry().getFirst(); + hashBundleEntry(bundleEntryComponent); + return bundle; + } + + private static void hashBundleEntry(Bundle.BundleEntryComponent entry) { + String id = entry.getResource().getIdPart(); + var hash = DigestUtils.sha256Hex("%s_%s".formatted(Random.Default.toString(), id)); + + entry.getResource().setId(hash); + entry.setFullUrl(entry.getFullUrl().replace(id, hash)); + var consent = (Consent) entry.getResource(); + consent + .getSource() + .setProperty("reference", new StringType("QuestionnaireResponse/%s".formatted(hash))); + } } diff --git a/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java index ded3515..f9230ec 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java @@ -2,26 +2,27 @@ package dev.dnpm.etl.processor.consent; import java.util.Date; import org.hl7.fhir.r4.model.Bundle; +import org.jspecify.annotations.NullMarked; +@NullMarked 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; <p>if empty patient has not been asked, yet.</p> - */ - Bundle getConsent(String personIdentifierValue, Date requestDate, ConsentDomain consentDomain); + /** + * 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; + * <p>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 index 24cb8f7..cc8107f 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java @@ -2,30 +2,35 @@ package dev.dnpm.etl.processor.consent; import java.util.Date; import org.hl7.fhir.r4.model.Bundle; +import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MtbFileConsentService implements IConsentService { - private static final Logger log = LoggerFactory.getLogger(MtbFileConsentService.class); + private static final Logger log = LoggerFactory.getLogger(MtbFileConsentService.class); - public MtbFileConsentService() { - log.info("ConsentCheckFileBased initialized..."); - } + public MtbFileConsentService() { + log.info("ConsentCheckFileBased initialized..."); + } - @Override - public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { - return TtpConsentStatus.UNKNOWN_CHECK_FILE; - } + @Override + @NonNull + public TtpConsentStatus getTtpBroadConsentStatus(@NonNull 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(); - } + /** + * EMPTY METHOD: NOT IMPLEMENTED + * + * @return empty bundle + */ + @Override + @NonNull + public Bundle getConsent( + @NonNull String personIdentifierValue, + @NonNull Date requestDate, + @NonNull ConsentDomain consentDomain) { + return new Bundle(); + } } diff --git a/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java b/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java index 2af1683..b92f58d 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java @@ -1,38 +1,22 @@ package dev.dnpm.etl.processor.consent; public enum TtpConsentStatus { - /** - * Valid consent found - */ - BROAD_CONSENT_GIVEN, - /** - * Missing or rejected...actually unknown - */ - BROAD_CONSENT_MISSING_OR_REJECTED, - /** - * No Broad consent policy found - */ - BROAD_CONSENT_MISSING, - /** - * Research policy has been rejected - */ - BROAD_CONSENT_REJECTED, + /** Valid consent found */ + BROAD_CONSENT_GIVEN, + /** Missing or rejected...actually unknown */ + BROAD_CONSENT_MISSING_OR_REJECTED, + /** No Broad consent policy found */ + BROAD_CONSENT_MISSING, + /** Research policy has been rejected */ + BROAD_CONSENT_REJECTED, - GENOM_DE_CONSENT_SEQUENCING_PERMIT, - /** - * No GenomDE consent policy found - */ - GENOM_DE_CONSENT_MISSING, - /** - * GenomDE consent policy found, but has been rejected - */ - GENOM_DE_SEQUENCING_REJECTED, - /** - * Consent status is validate via file property 'consent.status' - */ - UNKNOWN_CHECK_FILE, - /** - * Due technical problems consent status is unknown - */ - FAILED_TO_ASK + GENOM_DE_CONSENT_SEQUENCING_PERMIT, + /** No GenomDE consent policy found */ + GENOM_DE_CONSENT_MISSING, + /** GenomDE consent policy found, but has been rejected */ + GENOM_DE_SEQUENCING_REJECTED, + /** Consent status is validate via file property 'consent.status' */ + UNKNOWN_CHECK_FILE, + /** Due technical problems consent status is unknown */ + FAILED_TO_ASK } diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java index 8d0d0c1..e72a8f8 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java @@ -19,10 +19,12 @@ package dev.dnpm.etl.processor.pseudonym; -public interface Generator { +import org.jspecify.annotations.NullMarked; - String generate(String id); +@NullMarked +public interface Generator { - String generateGenomDeTan(String id); + String generate(String id); + String generateGenomDeTan(String id); } diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java index 6a2b947..478c5c9 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java @@ -32,194 +32,202 @@ import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.StringType; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.*; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.HttpClientErrorException.BadRequest; import org.springframework.web.client.HttpClientErrorException.Unauthorized; -import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; public class GpasPseudonymGenerator implements Generator { - private final FhirContext r4Context; - private final String gPasUrl; - private final HttpHeaders httpHeader; - private final RetryTemplate retryTemplate; - private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class); - private final RestTemplate restTemplate; - private final @NotNull String genomDeTanDomain; - private final @NotNull String pidPsnDomain; - protected final static String createOrGetPsn = "$pseudonymizeAllowCreate"; - protected final static String createMultiDomainPsn = "$pseudonymize-secondary"; - private final static String SINGLE_PSN_PART_NAME = "pseudonym"; - private final static String MULTI_PSN_PART_NAME = "value"; - - public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, - RestTemplate restTemplate, AppFhirConfig appFhirConfig) { - this.retryTemplate = retryTemplate; - this.restTemplate = restTemplate; - this.gPasUrl = gpasCfg.getUri(); - this.pidPsnDomain = gpasCfg.getPatientDomain(); - this.genomDeTanDomain = gpasCfg.getGenomDeTanDomain(); - this.r4Context = appFhirConfig.fhirContext(); - httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword()); - - log.debug("{} has been initialized", this.getClass().getName()); - + private final FhirContext r4Context; + private final @Nullable String gPasUrl; + private final HttpHeaders httpHeader; + private final RetryTemplate retryTemplate; + private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class); + private final RestTemplate restTemplate; + private final @NonNull String genomDeTanDomain; + private final @NonNull String pidPsnDomain; + protected static final String CREATE_OR_GET_PSN = "$pseudonymizeAllowCreate"; + protected static final String CREATE_MULTI_DOMAIN_PSN = "$pseudonymize-secondary"; + private static final String SINGLE_PSN_PART_NAME = "pseudonym"; + private static final String MULTI_PSN_PART_NAME = "value"; + + public GpasPseudonymGenerator( + GPasConfigProperties gpasCfg, + RetryTemplate retryTemplate, + RestTemplate restTemplate, + AppFhirConfig appFhirConfig) { + this.retryTemplate = retryTemplate; + this.restTemplate = restTemplate; + this.gPasUrl = gpasCfg.getUri(); + this.pidPsnDomain = gpasCfg.getPatientDomain(); + this.genomDeTanDomain = gpasCfg.getGenomDeTanDomain(); + this.r4Context = appFhirConfig.fhirContext(); + httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword()); + + log.debug("{} has been initialized", this.getClass().getName()); + } + + @Override + @NonNull + public String generate(@NonNull String id) { + return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN); + } + + @Override + @NonNull + public String generateGenomDeTan(@NonNull String id) { + return generate(id, PsnDomainType.MULTI_PSN_DOMAIN); + } + + protected String generate(String id, PsnDomainType domainType) { + switch (domainType) { + case SINGLE_PSN_DOMAIN -> { + final var requestBody = createSinglePsnRequestBody(id, pidPsnDomain); + final var responseEntity = getGpasPseudonym(requestBody, CREATE_OR_GET_PSN); + final var gPasPseudonymResult = + (Parameters) r4Context.newJsonParser().parseResource(responseEntity.getBody()); + + return unwrapPseudonym(gPasPseudonymResult, SINGLE_PSN_PART_NAME); + } + case MULTI_PSN_DOMAIN -> { + final var requestBody = createMultiPsnRequestBody(id, genomDeTanDomain); + final var responseEntity = getGpasPseudonym(requestBody, CREATE_MULTI_DOMAIN_PSN); + final var gPasPseudonymResult = + (Parameters) r4Context.newJsonParser().parseResource(responseEntity.getBody()); + + return unwrapPseudonym(gPasPseudonymResult, MULTI_PSN_PART_NAME); + } } + throw new NotImplementedException( + "give domain type '%s' is unexpected and is currently not supported!" + .formatted(domainType)); + } - @Override - public String generate(String id) { - return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN); - } + @NonNull + public static String unwrapPseudonym(Parameters gPasPseudonymResult, String targetPartName) { + final var parameters = gPasPseudonymResult.getParameter().stream().findFirst(); - @Override - public String generateGenomDeTan(String id) { - return generate(id, PsnDomainType.MULTI_PSN_DOMAIN); + if (parameters.isEmpty()) { + throw new PseudonymRequestFailed("Empty HL7 parameters, cannot find first one"); } - protected String generate(String id, PsnDomainType domainType) { - switch (domainType) { - case SINGLE_PSN_DOMAIN -> { - final var requestBody = createSinglePsnRequestBody(id, pidPsnDomain); - final var responseEntity = getGpasPseudonym(requestBody, createOrGetPsn); - final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser() - .parseResource(responseEntity.getBody()); - - return unwrapPseudonym(gPasPseudonymResult, SINGLE_PSN_PART_NAME); - } - case MULTI_PSN_DOMAIN -> { - final var requestBody = createMultiPsnRequestBody(id, genomDeTanDomain); - final var responseEntity = getGpasPseudonym(requestBody, createMultiDomainPsn); - final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser() - .parseResource(responseEntity.getBody()); - - return unwrapPseudonym(gPasPseudonymResult, MULTI_PSN_PART_NAME); - } - } - throw new NotImplementedException( - "give domain type '%s' is unexpected and is currently not supported!".formatted( - domainType)); + final var identifier = + (Identifier) + parameters.get().getPart().stream() + .filter(a -> a.getName().equals(targetPartName)) + .findFirst() + .orElseGet(ParametersParameterComponent::new) + .getValue(); + + // pseudonym + return sanitizeValue(identifier.getValue()); + } + + /** + * Allow only filename friendly values + * + * @param psnValue GAPS pseudonym value + * @return cleaned up value + */ + public static String sanitizeValue(String psnValue) { + // pattern to match forbidden characters + String forbiddenCharsRegex = "[\\\\/:*?\"<>|;]"; + + // Replace all forbidden characters with underscores + return psnValue.replaceAll(forbiddenCharsRegex, "_"); + } + + @NonNull + protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody, String apiEndpoint) { + + HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader); + + try { + var targetUrl = buildRequestUrl(apiEndpoint); + ResponseEntity<String> responseEntity = + retryTemplate.execute( + ctx -> + restTemplate.exchange(targetUrl, HttpMethod.POST, requestEntity, String.class)); + if (responseEntity.getStatusCode().is2xxSuccessful()) { + log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode()); + return responseEntity; + } + } catch (BadRequest e) { + String msg = + "gPas or request configuration is incorrect. Please check both." + e.getMessage(); + log.error(msg); + throw new PseudonymRequestFailed(msg, e); + } catch (Unauthorized e) { + var msg = + "gPas access credentials are invalid check your configuration. msg: '%s" + .formatted(e.getMessage()); + log.error(msg); + throw new PseudonymRequestFailed(msg, e); + } catch (Exception unexpected) { + throw new PseudonymRequestFailed( + "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected); } + throw new PseudonymRequestFailed( + "API request due unexpected error unsuccessful gPas unsuccessful."); + } - @NotNull - public static String unwrapPseudonym(Parameters gPasPseudonymResult, String targetPartName) { - final var parameters = gPasPseudonymResult.getParameter().stream().findFirst(); - - if (parameters.isEmpty()) { - throw new PseudonymRequestFailed("Empty HL7 parameters, cannot find first one"); - } - - final var identifier = (Identifier) parameters.get().getPart().stream() - .filter(a -> a.getName().equals(targetPartName)) - .findFirst() - .orElseGet(ParametersParameterComponent::new).getValue(); - - // pseudonym - return sanitizeValue(identifier.getValue()); + protected URI buildRequestUrl(String apiEndpoint) throws URISyntaxException { + if (null == gPasUrl) { + throw new URISyntaxException("null", "URI must not be null"); } - - /** - * Allow only filename friendly values - * - * @param psnValue GAPS pseudonym value - * @return cleaned up value - */ - public static String sanitizeValue(String psnValue) { - // pattern to match forbidden characters - String forbiddenCharsRegex = "[\\\\/:*?\"<>|;]"; - - // Replace all forbidden characters with underscores - return psnValue.replaceAll(forbiddenCharsRegex, "_"); + var gPasUrl1 = gPasUrl; + if (gPasUrl.lastIndexOf("/") == gPasUrl.length() - 1) { + gPasUrl1 = gPasUrl.substring(0, gPasUrl.length() - 1); } - - @NotNull - protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody, String apiEndpoint) { - - HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader); - - try { - var targetUrl = buildRequestUrl(apiEndpoint); - ResponseEntity<String> responseEntity = retryTemplate.execute( - ctx -> restTemplate.exchange(targetUrl, HttpMethod.POST, requestEntity, - String.class)); - if (responseEntity.getStatusCode().is2xxSuccessful()) { - log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode()); - return responseEntity; - } - } catch (RestClientException rce) { - if (rce instanceof BadRequest) { - String msg = "gPas or request configuration is incorrect. Please check both." - + rce.getMessage(); - log.debug( - msg); - throw new PseudonymRequestFailed(msg, rce); - } - if (rce instanceof Unauthorized) { - var msg = "gPas access credentials are invalid check your configuration. msg: '%s".formatted( - rce.getMessage()); - log.error(msg); - throw new PseudonymRequestFailed(msg, rce); - } - } catch (Exception unexpected) { - throw new PseudonymRequestFailed( - "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected); - } - throw new PseudonymRequestFailed( - "API request due unexpected error unsuccessful gPas unsuccessful."); - + var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(apiEndpoint); + + return urlBuilder.build(); + } + + protected String createSinglePsnRequestBody(String id, String targetDomain) { + final var requestParameters = new Parameters(); + requestParameters + .addParameter() + .setName("target") + .setValue(new StringType().setValue(targetDomain)); + requestParameters.addParameter().setName("original").setValue(new StringType().setValue(id)); + final IParser iParser = r4Context.newJsonParser(); + return iParser.encodeResourceToString(requestParameters); + } + + protected String createMultiPsnRequestBody(String id, String targetDomain) { + final var param = new Parameters(); + ParametersParameterComponent targetParam = param.addParameter().setName("original"); + targetParam.addPart( + new ParametersParameterComponent() + .setName("target") + .setValue(new StringType(targetDomain))); + targetParam.addPart( + new ParametersParameterComponent().setName("value").setValue(new StringType(id))); + targetParam.addPart( + new ParametersParameterComponent().setName("count").setValue(new StringType("1"))); + + final IParser iParser = r4Context.newJsonParser(); + return iParser.encodeResourceToString(param); + } + + @NonNull + protected HttpHeaders getHttpHeaders( + @Nullable String gPasUserName, @Nullable String gPasPassword) { + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) { + return headers; } - protected URI buildRequestUrl(String apiEndpoint) throws URISyntaxException { - var gPasUrl1 = gPasUrl; - if (gPasUrl.lastIndexOf("/") == gPasUrl.length() - 1) { - gPasUrl1 = gPasUrl.substring(0, gPasUrl.length() - 1); - } - var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(apiEndpoint); - - return urlBuilder.build(); - } - - protected String createSinglePsnRequestBody(String id, String targetDomain) { - final var requestParameters = new Parameters(); - requestParameters.addParameter().setName("target") - .setValue(new StringType().setValue(targetDomain)); - requestParameters.addParameter().setName("original") - .setValue(new StringType().setValue(id)); - final IParser iParser = r4Context.newJsonParser(); - return iParser.encodeResourceToString(requestParameters); - } - - protected String createMultiPsnRequestBody(String id, String targetDomain) { - final var param = new Parameters(); - ParametersParameterComponent targetParam = param.addParameter().setName("original"); - targetParam.addPart( - new ParametersParameterComponent().setName("target") - .setValue(new StringType(targetDomain))); - targetParam.addPart( - new ParametersParameterComponent().setName("value").setValue(new StringType(id))); - targetParam - .addPart(new ParametersParameterComponent().setName("count").setValue( - new StringType("1"))); - - final IParser iParser = r4Context.newJsonParser(); - return iParser.encodeResourceToString(param); - } - - - @NotNull - protected HttpHeaders getHttpHeaders(String gPasUserName, String gPasPassword) { - var headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) { - return headers; - } - - headers.setBasicAuth(gPasUserName, gPasPassword); - return headers; - } + headers.setBasicAuth(gPasUserName, gPasPassword); + return headers; + } } diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java index 79b4ba6..397455a 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java @@ -2,11 +2,11 @@ package dev.dnpm.etl.processor.pseudonym; public class PseudonymRequestFailed extends RuntimeException { - public PseudonymRequestFailed(String message) { - super(message); - } + public PseudonymRequestFailed(String message) { + super(message); + } - public PseudonymRequestFailed(String message, Throwable cause) { - super(message, cause); - } + public PseudonymRequestFailed(String message, Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java index a0fbc93..55cb212 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java @@ -1,12 +1,8 @@ package dev.dnpm.etl.processor.pseudonym; public enum PsnDomainType { - /** - * one pseudonym per original value - */ - SINGLE_PSN_DOMAIN, - /** - * multiple pseudonymes for one original value - */ - MULTI_PSN_DOMAIN + /** one pseudonym per original value */ + SINGLE_PSN_DOMAIN, + /** multiple pseudonymes for one original value */ + MULTI_PSN_DOMAIN } |
