summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorjlidke2025-07-23 22:11:47 +0200
committerGitHub2025-07-23 22:11:47 +0200
commitdfc9de78ceb8753392aac16e99dfc25346634ac9 (patch)
tree8006f5efa5fa35bbf1a802c3ebd754097e5e2f4f /src/main
parent199511e567884bb703277c276b782e54e528f744 (diff)
119 add transaction (#124)
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java2
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java93
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt15
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt4
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt5
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt2
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt17
9 files changed, 132 insertions, 21 deletions
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 e1938ba..8d0d0c1 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java
@@ -23,4 +23,6 @@ public interface Generator {
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 a22100b..6a2b947 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
@@ -23,7 +23,11 @@ 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;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
@@ -42,38 +46,67 @@ public class GpasPseudonymGenerator implements Generator {
private final FhirContext r4Context;
private final String gPasUrl;
- private final String psnTargetDomain;
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.psnTargetDomain = gpasCfg.getTarget();
+ this.pidPsnDomain = gpasCfg.getPatientDomain();
+ this.genomDeTanDomain = gpasCfg.getGenomDeTanDomain();
this.r4Context = appFhirConfig.fhirContext();
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
- log.debug(String.format("%s has been initialized", this.getClass().getName()));
+ log.debug("{} has been initialized", this.getClass().getName());
}
@Override
public String generate(String id) {
- var gPasRequestBody = getGpasRequestBody(id);
- var responseEntity = getGpasPseudonym(gPasRequestBody);
- var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
- .parseResource(responseEntity.getBody());
+ return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN);
+ }
+
+ @Override
+ public String generateGenomDeTan(String id) {
+ return generate(id, PsnDomainType.MULTI_PSN_DOMAIN);
+ }
- return unwrapPseudonym(gPasPseudonymResult);
+ 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));
}
@NotNull
- public static String unwrapPseudonym(Parameters gPasPseudonymResult) {
+ public static String unwrapPseudonym(Parameters gPasPseudonymResult, String targetPartName) {
final var parameters = gPasPseudonymResult.getParameter().stream().findFirst();
if (parameters.isEmpty()) {
@@ -81,7 +114,7 @@ public class GpasPseudonymGenerator implements Generator {
}
final var identifier = (Identifier) parameters.get().getPart().stream()
- .filter(a -> a.getName().equals("pseudonym"))
+ .filter(a -> a.getName().equals(targetPartName))
.findFirst()
.orElseGet(ParametersParameterComponent::new).getValue();
@@ -104,13 +137,14 @@ public class GpasPseudonymGenerator implements Generator {
}
@NotNull
- protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody) {
+ 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(gPasUrl, HttpMethod.POST, requestEntity,
+ ctx -> restTemplate.exchange(targetUrl, HttpMethod.POST, requestEntity,
String.class));
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
@@ -139,16 +173,43 @@ public class GpasPseudonymGenerator implements Generator {
}
- protected String getGpasRequestBody(String id) {
- var requestParameters = new Parameters();
+ 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(psnTargetDomain));
+ .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();
diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java
new file mode 100644
index 0000000..a0fbc93
--- /dev/null
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java
@@ -0,0 +1,12 @@
+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
+}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
index a2ea032..207785e 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -48,7 +48,8 @@ data class PseudonymizeConfigProperties(
@ConfigurationProperties(GPasConfigProperties.NAME)
data class GPasConfigProperties(
val uri: String?,
- val target: String = "etl-processor",
+ val patientDomain: String = "etl-processor",
+ val genomDeTanDomain: String = "ccdn",
val username: String?,
val password: String?,
) {
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt
index c03db12..0537cbb 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt
@@ -21,9 +21,12 @@ package dev.dnpm.etl.processor.pseudonym
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.digest.DigestUtils
-
+import java.security.SecureRandom
class AnonymizingGenerator : Generator {
+ companion object fun getSecureRandom() : SecureRandom {
+ return SecureRandom()
+ }
override fun generate(id: String): String {
return Base32().encodeAsString(DigestUtils.sha256(id))
@@ -31,4 +34,14 @@ class AnonymizingGenerator : Generator {
.lowercase()
}
+ @OptIn(ExperimentalStdlibApi::class)
+ override fun generateGenomDeTan(id: String?): String {
+
+ val bytes = ByteArray(64 / 2)
+ getSecureRandom().nextBytes(bytes)
+
+ return bytes.joinToString("") { "%02x".format(it) }
+
+ }
+
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt
index e80f6ec..96225a9 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt
@@ -35,6 +35,10 @@ class PseudonymizeService(
}
}
+ fun genomDeTan(patientId: PatientId): String {
+ return generator.generateGenomDeTan(patientId.value)
+ }
+
fun prefix(): String {
return configProperties.prefix
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
index 77f3399..50d5b20 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
@@ -349,3 +349,8 @@ fun Mtb.ensureMetaDataIsInitialized() {
this.metadata.modelProjectConsent.provisions.toMutableList()
}
}
+
+infix fun Mtb.addGenomDeTan(pseudonymizeService: PseudonymizeService)
+{
+ this.metadata.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id))
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
index 3841641..11aff57 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
@@ -38,7 +38,7 @@ class ConsentProcessor(
/**
* In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked.
*
- * Logik:
+ * Logic:
* * <c>true</c> IF consent check is disabled.
* * <c>true</c> IF broad consent (BC) has been given.
* * <c>true</c> BC has been asked AND declined but genomDe consent has been consented.
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
index bb226c0..f2e8390 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
@@ -30,8 +30,11 @@ import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
import dev.dnpm.etl.processor.output.*
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
+import dev.dnpm.etl.processor.pseudonym.addGenomDeTan
import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
+import dev.pcvolkmer.mv64e.mtb.ConsentProvision
+import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
import dev.pcvolkmer.mv64e.mtb.Mtb
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.digest.DigestUtils
@@ -76,9 +79,12 @@ class RequestProcessor(
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
val pid = PatientId(extractPatientIdentifier(mtbFile))
- val isConsentOk = consentProcessor != null &&
- consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null
+ val isConsentOk =
+ consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null
if (isConsentOk) {
+ if (isGenomDeConsented(mtbFile)) {
+ mtbFile addGenomDeTan pseudonymizeService
+ }
mtbFile pseudonymizeWith pseudonymizeService
mtbFile anonymizeContentWith pseudonymizeService
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
@@ -93,6 +99,13 @@ class RequestProcessor(
}
}
+ private fun isGenomDeConsented(mtbFile: Mtb): Boolean {
+ val isModelProjectConsented = mtbFile.metadata?.modelProjectConsent?.provisions?.any { p ->
+ p.purpose == ModelProjectConsentPurpose.SEQUENCING && p.type == ConsentProvision.PERMIT
+ } == true
+ return isModelProjectConsented
+ }
+
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
requestService.save(
Request(