diff options
| author | Paul-Christian Volkmer | 2025-10-22 17:09:14 +0200 |
|---|---|---|
| committer | GitHub | 2025-10-22 17:09:14 +0200 |
| commit | 7ae349930521ed4f9b6255e12c50641ed4987e9d (patch) | |
| tree | 9d69c7a4b5084591e8fbf3c456d2818b78a0de6a /src/main/java/dev/dnpm | |
| parent | 004e1021c8156bf81f85ac5ad1ef6d260392dc6f (diff) | |
build: add spotless for java code (#158)
Diffstat (limited to 'src/main/java/dev/dnpm')
9 files changed, 535 insertions, 564 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 de722e6..9783b43 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -4,6 +4,8 @@ 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.util.Date; import kotlin.random.Random; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; @@ -22,9 +24,6 @@ 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; - /** * Service to request Consent from remote gICS installation * @@ -32,326 +31,322 @@ 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 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( + 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("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( + .setName("ignoreVersionNumber") + .setValue(new BooleanType().setValue(true))) + .addPart( 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)) - ); - - result.addParameter(config); - - return result; - } + .setName("unknownStateIsConsideredAsDecline") + .setValue(new BooleanType().setValue(false))); - private URI endpointUri(String endpoint) { - assert this.gIcsConfigProperties.getUri() != null; - return UriComponentsBuilder.fromUriString(this.gIcsConfigProperties.getUri()).path(endpoint).build().toUri(); - } + result.addParameter(config); - private HttpHeaders headersWithHttpBasicAuth() { - assert this.gIcsConfigProperties.getUri() != null; + return result; + } - var headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_XML); + private URI endpointUri(String endpoint) { + assert this.gIcsConfigProperties.getUri() != null; + return UriComponentsBuilder.fromUriString(this.gIcsConfigProperties.getUri()) + .path(endpoint) + .build() + .toUri(); + } - if ( - StringUtils.isBlank(this.gIcsConfigProperties.getUsername()) - || StringUtils.isBlank(this.gIcsConfigProperties.getPassword()) - ) { - return headers; - } + private HttpHeaders headersWithHttpBasicAuth() { + assert this.gIcsConfigProperties.getUri() != null; - headers.setBasicAuth(this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword()); - return headers; - } + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); - 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; - } + if (StringUtils.isBlank(this.gIcsConfigProperties.getUsername()) + || StringUtils.isBlank(this.gIcsConfigProperties.getPassword())) { + return headers; } - @Override - public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { - var consentStatusResponse = callGicsApi( - getFhirRequestParameters(personIdentifierValue), - GicsConsentService.IS_CONSENTED_ENDPOINT - ); - return evaluateConsentResponse(consentStatusResponse); + headers.setBasicAuth( + this.gIcsConfigProperties.getUsername(), 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; } - - 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); - } + } + + @Override + public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { + var consentStatusResponse = + callGicsApi( + getFhirRequestParameters(personIdentifierValue), + 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."); } - - @NotNull - private String getConsentDomainName(ConsentDomain targetConsentDomain) { - return switch (targetConsentDomain) { - case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName(); - case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName(); - }; + 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); } - - 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", false) - .addParameter("unknownStateIsConsideredAsDecline", false) - .addParameter("requestDate", new DateType().setValue(requestDate)); - - requestParameter.addParameter( - new ParametersParameterComponent().setName("config").addPart().setResource(nestedConfigParameters) - ); - - return requestParameter; + } + + @NotNull + 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", false) + .addParameter("unknownStateIsConsideredAsDecline", false) + .addParameter("requestDate", new DateType().setValue(requestDate)); + + requestParameter.addParameter( + new ParametersParameterComponent() + .setName("config") + .addPart() + .setResource(nestedConfigParameters)); + + return requestParameter; + } + + private TtpConsentStatus evaluateConsentResponse(String consentStatusResponse) { + if (consentStatusResponse == null) { + return TtpConsentStatus.FAILED_TO_ASK; } + try { + var response = fhirContext.newJsonParser().parseResource(consentStatusResponse); - private TtpConsentStatus evaluateConsentResponse(String consentStatusResponse) { - if (consentStatusResponse == null) { - return TtpConsentStatus.FAILED_TO_ASK; + if (response instanceof Parameters responseParameters) { + + var responseValue = responseParameters.getParameter("consented").getValue(); + var isConsented = responseValue.castToBoolean(responseValue); + if (!isConsented.hasValue()) { + 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); + if (isConsented.booleanValue()) { + return TtpConsentStatus.BROAD_CONSENT_GIVEN; + } else { + return TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED; } - return TtpConsentStatus.FAILED_TO_ASK; + } 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); } - - @Override - public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) { - Bundle gIcsResultBundle = currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate); - if (ConsentDomain.BROAD_CONSENT == consentDomain) { - return anonymizeBroadConsent(convertGicsResultToMiiBroadConsent(gIcsResultBundle)); - } - return gIcsResultBundle; + return TtpConsentStatus.FAILED_TO_ASK; + } + + @Override + public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) { + Bundle gIcsResultBundle = + currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate); + if (ConsentDomain.BROAD_CONSENT == consentDomain) { + return anonymizeBroadConsent(convertGicsResultToMiiBroadConsent(gIcsResultBundle)); } + return gIcsResultBundle; + } - protected Bundle convertGicsResultToMiiBroadConsent(Bundle gIcsResultBundle) { - if (gIcsResultBundle == null - || gIcsResultBundle.getEntry().isEmpty() - || !(gIcsResultBundle.getEntry().getFirst().getResource() instanceof Consent)) - return gIcsResultBundle; + protected Bundle convertGicsResultToMiiBroadConsent(Bundle gIcsResultBundle) { + if (gIcsResultBundle == null + || gIcsResultBundle.getEntry().isEmpty() + || !(gIcsResultBundle.getEntry().getFirst().getResource() instanceof Consent)) + return gIcsResultBundle; - Bundle.BundleEntryComponent bundleEntryComponent = gIcsResultBundle.getEntry().getFirst(); + Bundle.BundleEntryComponent bundleEntryComponent = gIcsResultBundle.getEntry().getFirst(); - var consentAsOne = (Consent) bundleEntryComponent.getResource(); - if (consentAsOne.getPolicy().stream().noneMatch(p -> p.getUri().equals(BROAD_CONSENT_POLICY))) { - consentAsOne.addPolicy(new Consent.ConsentPolicyComponent().setUri(BROAD_CONSENT_POLICY)); - } - - if (consentAsOne.getMeta().getProfile().stream().noneMatch(p -> p.getValue().equals(BROAD_CONSENT_PROFILE_URI))) { - consentAsOne.getMeta().addProfile(BROAD_CONSENT_PROFILE_URI); - } - - consentAsOne.setPolicyRule(null); - - gIcsResultBundle.getEntry().stream().skip(1).forEach(c -> consentAsOne.getProvision().addProvision(((Consent) c.getResource()).getProvision().getProvisionFirstRep())); - - gIcsResultBundle.getEntry().clear(); - gIcsResultBundle.addEntry(bundleEntryComponent); - return gIcsResultBundle; + var consentAsOne = (Consent) bundleEntryComponent.getResource(); + if (consentAsOne.getPolicy().stream().noneMatch(p -> p.getUri().equals(BROAD_CONSENT_POLICY))) { + consentAsOne.addPolicy(new Consent.ConsentPolicyComponent().setUri(BROAD_CONSENT_POLICY)); } - protected Bundle anonymizeBroadConsent(Bundle bundle) { - Bundle.BundleEntryComponent bundleEntryComponent = bundle.getEntry().getFirst(); - hashBundleEntry(bundleEntryComponent); - return bundle; + if (consentAsOne.getMeta().getProfile().stream() + .noneMatch(p -> p.getValue().equals(BROAD_CONSENT_PROFILE_URI))) { + consentAsOne.getMeta().addProfile(BROAD_CONSENT_PROFILE_URI); } - 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))); - } + consentAsOne.setPolicyRule(null); + + gIcsResultBundle.getEntry().stream() + .skip(1) + .forEach( + c -> + consentAsOne + .getProvision() + .addProvision( + ((Consent) c.getResource()).getProvision().getProvisionFirstRep())); + + gIcsResultBundle.getEntry().clear(); + gIcsResultBundle.addEntry(bundleEntryComponent); + return gIcsResultBundle; + } + + 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..148592e 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java @@ -5,23 +5,22 @@ 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; <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..a126a51 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java @@ -7,25 +7,25 @@ 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 + 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(); - } + /** + * 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/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..fce8146 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java @@ -21,8 +21,7 @@ package dev.dnpm.etl.processor.pseudonym; public interface Generator { - String generate(String id); - - 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 ea0d20f..48cdc67 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java @@ -23,6 +23,8 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.GPasConfigProperties; +import java.net.URI; +import java.net.URISyntaxException; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; import org.apache.hc.core5.net.URIBuilder; @@ -39,186 +41,186 @@ import org.springframework.web.client.HttpClientErrorException.BadRequest; import org.springframework.web.client.HttpClientErrorException.Unauthorized; import org.springframework.web.client.RestTemplate; -import java.net.URI; -import java.net.URISyntaxException; - 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 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 - public String generate(String id) { - return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN); - } - - @Override - public String generateGenomDeTan(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)); - } - - @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()); + 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 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 + public String generate(String id) { + return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN); + } + + @Override + public String generateGenomDeTan(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)); + } - /** - * 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, "_"); - } - - @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 (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"); } - 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(); + 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, "_"); + } + + @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 (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); } - - 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); + throw new PseudonymRequestFailed( + "API request due unexpected error unsuccessful gPas unsuccessful."); + } + + protected URI buildRequestUrl(String apiEndpoint) throws URISyntaxException { + var gPasUrl1 = gPasUrl; + if (gPasUrl.lastIndexOf("/") == gPasUrl.length() - 1) { + gPasUrl1 = gPasUrl.substring(0, gPasUrl.length() - 1); } - - 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); + 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; } - - @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 } |
