summaryrefslogtreecommitdiff
path: root/src/main/java/dev
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/dev')
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java31
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java13
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java281
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java27
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java38
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java56
6 files changed, 426 insertions, 20 deletions
diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java
new file mode 100644
index 0000000..f7ce39e
--- /dev/null
+++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.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 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
new file mode 100644
index 0000000..6d0b160
--- /dev/null
+++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java
@@ -0,0 +1,13 @@
+package dev.dnpm.etl.processor.consent;
+
+public enum ConsentDomain {
+ /**
+ * MII Broad consent
+ */
+ BroadConsent,
+
+ /**
+ * GenomDe Modelvohaben ยง64e
+ */
+ Modelvorhaben64e
+}
diff --git a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java
new file mode 100644
index 0000000..6f3c987
--- /dev/null
+++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java
@@ -0,0 +1,281 @@
+package dev.dnpm.etl.processor.consent;
+
+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.Parameters.ParametersParameterComponent;
+import org.hl7.fhir.r4.model.StringType;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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;
+
+
+public class GicsConsentService implements IGetConsent {
+
+ 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) {
+
+ 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) {
+ 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())));
+
+ /*
+ * 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))).addPart(
+ new ParametersParameterComponent().setName("unknownStateIsConsideredAsDecline")
+ .setValue(new BooleanType().setValue(false)));
+ result.addParameter(config);
+
+ return result;
+ }
+
+ protected String callGicsApi(Parameters parameter, String endpoint) {
+ var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter);
+
+ HttpEntity<String> requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader);
+ ResponseEntity<String> responseEntity;
+ try {
+ var url = getGicsUri(endpoint);
+
+ responseEntity = retryTemplate.execute(
+ ctx -> restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class));
+ } 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 (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);
+ return evaluateConsentResponse(consentStatusResponse);
+ }
+
+ protected Bundle currentConsentForPersonAndTemplate(String personIdentifierValue,
+ ConsentDomain targetConsentDomain, Date requestDate) {
+
+ String consentDomain = getConsentDomain(targetConsentDomain);
+
+ var requestParameter = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(
+ gIcsConfigProperties, 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) {
+ return (Bundle) iBaseResource;
+ } else {
+ String errorMessage = "Consent request failed! Unexpected response received! -> "
+ + consentDataSerialized;
+ log.error(errorMessage);
+ throw new IllegalStateException(errorMessage);
+ }
+ }
+
+ @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;
+ }
+
+ protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson(
+ GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate,
+ String targetDomain) {
+ 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)));
+
+ 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);
+
+ 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;
+ }
+
+ @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();
+ }
+}
diff --git a/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java b/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java
new file mode 100644
index 0000000..3482b9a
--- /dev/null
+++ b/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java
@@ -0,0 +1,27 @@
+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; <p>if empty patient has not been asked, yet.</p>
+ */
+ Bundle getConsent(String personIdentifierValue, Date requestDate, ConsentDomain consentDomain);
+
+}
diff --git a/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java b/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java
new file mode 100644
index 0000000..2af1683
--- /dev/null
+++ b/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java
@@ -0,0 +1,38 @@
+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,
+
+ 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/GpasPseudonymGenerator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
index 77caa77..a22100b 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
@@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.pseudonym;
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 org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.Identifier;
@@ -32,11 +33,14 @@ 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 static FhirContext r4Context = FhirContext.forR4();
+ private final FhirContext r4Context;
private final String gPasUrl;
private final String psnTargetDomain;
private final HttpHeaders httpHeader;
@@ -45,11 +49,13 @@ public class GpasPseudonymGenerator implements Generator {
private final RestTemplate restTemplate;
- public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) {
+ public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
+ RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
this.retryTemplate = retryTemplate;
this.restTemplate = restTemplate;
this.gPasUrl = gpasCfg.getUri();
this.psnTargetDomain = gpasCfg.getTarget();
+ this.r4Context = appFhirConfig.fhirContext();
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
log.debug(String.format("%s has been initialized", this.getClass().getName()));
@@ -61,7 +67,7 @@ public class GpasPseudonymGenerator implements Generator {
var gPasRequestBody = getGpasRequestBody(id);
var responseEntity = getGpasPseudonym(gPasRequestBody);
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
- .parseResource(responseEntity.getBody());
+ .parseResource(responseEntity.getBody());
return unwrapPseudonym(gPasPseudonymResult);
}
@@ -75,9 +81,9 @@ public class GpasPseudonymGenerator implements Generator {
}
final var identifier = (Identifier) parameters.get().getPart().stream()
- .filter(a -> a.getName().equals("pseudonym"))
- .findFirst()
- .orElseGet(ParametersParameterComponent::new).getValue();
+ .filter(a -> a.getName().equals("pseudonym"))
+ .findFirst()
+ .orElseGet(ParametersParameterComponent::new).getValue();
// pseudonym
return sanitizeValue(identifier.getValue());
@@ -97,38 +103,48 @@ public class GpasPseudonymGenerator implements Generator {
return psnValue.replaceAll(forbiddenCharsRegex, "_");
}
-
@NotNull
protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody) {
HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader);
- ResponseEntity<String> responseEntity;
try {
- responseEntity = retryTemplate.execute(
- ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
- String.class));
-
+ ResponseEntity<String> responseEntity = retryTemplate.execute(
+ ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
+ String.class));
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
- } else {
- log.warn("API request unsuccessful. Response: {}", requestEntity.getBody());
- throw new PseudonymRequestFailed("API request unsuccessful gPas unsuccessful.");
+ 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);
}
-
- return responseEntity;
} catch (Exception unexpected) {
throw new PseudonymRequestFailed(
- "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
+ "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
}
+ throw new PseudonymRequestFailed(
+ "API request due unexpected error unsuccessful gPas unsuccessful.");
+
}
protected String getGpasRequestBody(String id) {
var requestParameters = new Parameters();
requestParameters.addParameter().setName("target")
- .setValue(new StringType().setValue(psnTargetDomain));
+ .setValue(new StringType().setValue(psnTargetDomain));
requestParameters.addParameter().setName("original")
- .setValue(new StringType().setValue(id));
+ .setValue(new StringType().setValue(id));
final IParser iParser = r4Context.newJsonParser();
return iParser.encodeResourceToString(requestParameters);
}