mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-07-18 05:12:54 +00:00
Compare commits
2 Commits
63-check-c
...
119-genomd
Author | SHA1 | Date | |
---|---|---|---|
b5b6203f3a | |||
07508679e6 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -39,5 +39,3 @@ out/
|
|||||||
.vscode/
|
.vscode/
|
||||||
/dev/gpas*
|
/dev/gpas*
|
||||||
/deploy/.env
|
/deploy/.env
|
||||||
/dev/gICS*
|
|
||||||
/dev/gPAS*
|
|
||||||
|
69
README.md
69
README.md
@ -26,12 +26,15 @@ Konfigurationsparameter
|
|||||||
|
|
||||||
### Modelvorhaben genomDE §64e
|
### Modelvorhaben genomDE §64e
|
||||||
|
|
||||||
Um die voll Betriebsbereitschaft herzustellen, muss eine erfolgreiche Übertragung mit dem
|
Zusätzlich zur Patienten Identifier Pseudonymisierung müssen Vorgangsummern generiert werden, die
|
||||||
Submission-Typ *Test* erfolgt sein. Über die Umgebungsvariable wird dieser Übertragungsmodus
|
jede Übertragung eindeutig identifizieren aber gleichzeitig dem Patienten zugeordnet werden können.
|
||||||
aktiviert. Alle Datensätze mit erteilter Teilnahme am Modelvorhaben werden mit der Test-Kennung
|
Dies lässt sich durch weitere Pseudonyme abbilden, allerdings werden pro Originalwert mehrere
|
||||||
übertragen.
|
Pseudonyme benötigt.
|
||||||
|
Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werden (siehe auch
|
||||||
|
*APP_PSEUDONYMIZE_GPAS_CCDN*).
|
||||||
|
|
||||||
`APP_GENOM_DE_TEST_SUBMISSION` -> `true` | `false` (falls fehlt wird `true` angenommen)
|
**WICHTIG:** Deaktivierte Pseudonymisierung ist nur für Tests nutzbar. Vorgangsummern sind zufällig
|
||||||
|
und werden anschließend verworfen.
|
||||||
|
|
||||||
### Datenübermittlung über HTTP/REST
|
### Datenübermittlung über HTTP/REST
|
||||||
|
|
||||||
@ -109,61 +112,11 @@ Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfiguri
|
|||||||
|
|
||||||
* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz inklusive Endpoint (z.B.
|
* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz inklusive Endpoint (z.B.
|
||||||
`http://localhost:8080/ttp-fhir/fhir/gpas/$$pseudonymizeAllowCreate`)
|
`http://localhost:8080/ttp-fhir/fhir/gpas/$$pseudonymizeAllowCreate`)
|
||||||
* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname
|
* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname für Patienten ID
|
||||||
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
|
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
|
||||||
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
|
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
|
||||||
|
* `APP_PSEUDONYMIZE_GPAS_GENOM_DE_DOMAIN`: gPas Multi-Pseudonym-Domäne für genomDE Vorgangsnummern (
|
||||||
### Einwilligung gICS
|
Clinical data node)
|
||||||
|
|
||||||
Ab gIcs Version 2.13.0 kann
|
|
||||||
per [REST-Schnittstelle](https://simplifier.net/guide/ttp-fhir-gateway-ig/ImplementationGuide-markdown-Einwilligungsmanagement-Operations-isConsented?version=current)
|
|
||||||
der Einwilligungsstatus abgefragt werden.
|
|
||||||
Vor der MTB-Übertragung kann der zum Sendezeitpunkt verfügbarer Einwilligungsstatus über Endpunkt
|
|
||||||
*isConsented* abgefragt werden.
|
|
||||||
|
|
||||||
Falls Anbindung an gICS aktiviert wurde, wird der Einwilligungsstatus der MTB Datei ignoriert.
|
|
||||||
Stattdessen werden vorhandene Einwilligungen abgefragt und in die MTB Datei eingebettet.
|
|
||||||
|
|
||||||
Es werden zwei Einwilligungsdomänen unterstützt, eine für Broad Consent und als zweites GenomDE
|
|
||||||
Modelvorhaben §64e.
|
|
||||||
|
|
||||||
#### Hinweise
|
|
||||||
|
|
||||||
1. Die aktuelle Impl. nimmt an, dass die hinterlegten Domänen der Einwilligungen ausschließlich für
|
|
||||||
die genannten Art von Einwilligungen genutzt werden. Es finde keine weitere Filterung statt. Wir
|
|
||||||
fragen pro Domäne die Schnittstelle `CurrentPolicyStatesForPerson` - siehe
|
|
||||||
auch [IG TTP-FHIR Gateway
|
|
||||||
](https://www.ths-greifswald.de/wp-content/uploads/tools/fhirgw/ig/2024-3-0/ImplementationGuide-markdown-Einwilligungsmanagement-Operations-currentPolicyStatesForPerson.html)
|
|
||||||
ab.
|
|
||||||
2. Die Einwilligung wird für den Patienten-Identifier der MTB abgerufen und anschließend durch das
|
|
||||||
DNPM Pseudonym ersetzt.
|
|
||||||
3. Abfragen von Einwilligungen über gesonderte Pseudonyme anstatt des MTB-Identifiers fehlt in der
|
|
||||||
ersten Implementierung.
|
|
||||||
4. Bei Verarbeitung von MTB Version 1.x Inhalten ist eine positive Einwilligung für die
|
|
||||||
Weiterverarbeitung notwendig. Das Fehlen einer Einwilligung löst die Löschung des Patienten im
|
|
||||||
Brückenkopf aus.
|
|
||||||
|
|
||||||
#### Konfiguration
|
|
||||||
|
|
||||||
* `APP_CONSENT_GICS_ENABLED`: Aktiviert oder deaktiviert `true` oder `false`, `false` wenn nicht
|
|
||||||
gesetzt.
|
|
||||||
* `APP_CONSENT_GICS_CHECKGNOMEDE`: Aktiviert oder deaktiviert `true` oder `false`, `false` wenn
|
|
||||||
nicht gesetzt. Versuche Einwilligungsdaten zu GENOM DE Modelvorhaben über gIcs abzurufen.
|
|
||||||
* `APP_CONSENT_GICS_CHECKBROADCONSENT`: Aktiviert oder deaktiviert `true` oder `false`, `false` wenn
|
|
||||||
nicht gesetzt. Versuche Einwilligungsdaten zu Broad Consent über gIcs abzurufen.
|
|
||||||
* `APP_CONSENT_GICS_URI`: URI der gICS-Instanz (z.B. `http://localhost:8090/ttp-fhir/fhir/gics`)
|
|
||||||
* `APP_CONSENT_GICS_USERNAME`: gIcs Basic-Auth Benutzername
|
|
||||||
* `APP_CONSENT_GICS_PASSWORD`: gIcs Basic-Auth Passwort
|
|
||||||
* `APP_CONSENT_GICS_PERSONIDENTIFIERSYSTEM`: Derzeit wird nur die PID unterstützt. wenn leer wird
|
|
||||||
`https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID` angenommen
|
|
||||||
* `APP_CONSENT_GICS_BROADCONSENTDOMAINNAME`: Domäne in der gIcs Broad Consent Einwilligungen
|
|
||||||
verwaltet. Falls Wert leer, wird `MII` angenommen.
|
|
||||||
* `APP_CONSENT_GICS_GNOMDECONSENTDOMAINNAME`: Domäne in der gIcs GenomDE Modelvorhaben §64e
|
|
||||||
Einwilligungen verwaltet. Falls Wert leer, wird `GenomDE_MV` angenommen.
|
|
||||||
* `APP_CONSENT_GICS_POLICYCODE`: Die entscheidende Objekt-ID der zu prüfenden Einwilligung-Regel.
|
|
||||||
Falls leer wird `2.16.840.1.113883.3.1937.777.24.5.3.6` angenommen.
|
|
||||||
* `APP_CONSENT_GICS_POLICYSYSTEM`: Das System der Einwilligung-Regel der Objekt-IDs. Falls leer wird
|
|
||||||
`urn:oid:2.16.840.1.113883.3.1937.777.24.5.3` angenommen.
|
|
||||||
|
|
||||||
### Anmeldung mit einem Passwort
|
### Anmeldung mit einem Passwort
|
||||||
|
|
||||||
|
@ -90,6 +90,7 @@ dependencies {
|
|||||||
implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}")
|
implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}")
|
||||||
implementation("org.webjars.npm:echarts:${versions["echarts"]}")
|
implementation("org.webjars.npm:echarts:${versions["echarts"]}")
|
||||||
implementation("org.webjars.npm:htmx.org:${versions["htmx.org"]}")
|
implementation("org.webjars.npm:htmx.org:${versions["htmx.org"]}")
|
||||||
|
implementation ("org.apache.commons:commons-math3:3.6.1")
|
||||||
|
|
||||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
|
@ -16,11 +16,6 @@ services:
|
|||||||
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true
|
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true
|
||||||
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
|
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
|
||||||
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
||||||
healthcheck:
|
|
||||||
test: kafka-topics --bootstrap-server kafka:9092 --list
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
## Use AKHQ as Kafka web frontend
|
## Use AKHQ as Kafka web frontend
|
||||||
akhq:
|
akhq:
|
||||||
|
@ -2,55 +2,31 @@ version: '3.7'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
zoo:
|
zoo1:
|
||||||
image: zookeeper:3.9.2
|
image: zookeeper:3.8.0
|
||||||
restart: unless-stopped
|
hostname: zoo1
|
||||||
ports:
|
ports:
|
||||||
- "2181:2181"
|
- "2181:2181"
|
||||||
environment:
|
environment:
|
||||||
ZOO_MY_ID: 1
|
ZOO_MY_ID: 1
|
||||||
ZOO_PORT: 2181
|
ZOO_PORT: 2181
|
||||||
ZOO_SERVERS: server.1=zoo:2888:3888;2181
|
ZOO_SERVERS: server.1=zoo1:2888:3888;2181
|
||||||
|
|
||||||
kafka:
|
kafka1:
|
||||||
image: confluentinc/cp-kafka:7.6.1
|
image: confluentinc/cp-kafka:7.2.1
|
||||||
|
hostname: kafka1
|
||||||
ports:
|
ports:
|
||||||
- "9092:9092"
|
- "9092:9092"
|
||||||
environment:
|
environment:
|
||||||
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka:19092,LISTENER_DOCKER_EXTERNAL://172.17.0.1:9093,LISTENER_EXTERNAL://127.0.0.1:9092
|
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka1:19092,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092
|
||||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT,LISTENER_EXTERNAL:PLAINTEXT
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT
|
||||||
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
|
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
|
||||||
KAFKA_ZOOKEEPER_CONNECT: zoo:2181
|
KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181"
|
||||||
KAFKA_BROKER_ID: 1
|
KAFKA_BROKER_ID: 1
|
||||||
KAFKA_LOG4J_LOGGERS: kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO
|
KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO"
|
||||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||||
KAFKA_MESSAGE_MAX_BYTES: 5242880
|
|
||||||
KAFKA_REPLICA_FETCH_MAX_BYTES: 5242880
|
|
||||||
KAFKA_COMPRESSION_TYPE: gzip
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- zoo
|
- zoo1
|
||||||
healthcheck:
|
|
||||||
test: kafka-topics --bootstrap-server kafka:9092 --list
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
akhq:
|
|
||||||
image: tchiotludo/akhq:0.25.0
|
|
||||||
environment:
|
|
||||||
AKHQ_CONFIGURATION: |
|
|
||||||
akhq:
|
|
||||||
ui-options:
|
|
||||||
topic.show-all-consumer-groups: true
|
|
||||||
topic-data.sort: NEWEST
|
|
||||||
connections:
|
|
||||||
docker-kafka-server:
|
|
||||||
properties:
|
|
||||||
bootstrap.servers: "kafka:19092"
|
|
||||||
ports:
|
|
||||||
- "9000:8080"
|
|
||||||
depends_on:
|
|
||||||
- kafka
|
|
||||||
|
|
||||||
kafka-rest-proxy:
|
kafka-rest-proxy:
|
||||||
image: confluentinc/cp-kafka-rest:7.2.1
|
image: confluentinc/cp-kafka-rest:7.2.1
|
||||||
@ -64,8 +40,8 @@ services:
|
|||||||
KAFKA_REST_HOST_NAME: kafka-rest-proxy
|
KAFKA_REST_HOST_NAME: kafka-rest-proxy
|
||||||
KAFKA_REST_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:19092
|
KAFKA_REST_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:19092
|
||||||
depends_on:
|
depends_on:
|
||||||
- zoo
|
- zoo1
|
||||||
- kafka
|
- kafka1
|
||||||
|
|
||||||
kafka-connect:
|
kafka-connect:
|
||||||
image: confluentinc/cp-kafka-connect:7.2.1
|
image: confluentinc/cp-kafka-connect:7.2.1
|
||||||
@ -91,6 +67,24 @@ services:
|
|||||||
#volumes:
|
#volumes:
|
||||||
# - ./connectors:/etc/kafka-connect/jars/
|
# - ./connectors:/etc/kafka-connect/jars/
|
||||||
depends_on:
|
depends_on:
|
||||||
- zoo
|
- zoo1
|
||||||
- kafka
|
- kafka1
|
||||||
- kafka-rest-proxy
|
- kafka-rest-proxy
|
||||||
|
|
||||||
|
akhq:
|
||||||
|
image: tchiotludo/akhq:0.21.0
|
||||||
|
environment:
|
||||||
|
AKHQ_CONFIGURATION: |
|
||||||
|
akhq:
|
||||||
|
connections:
|
||||||
|
docker-kafka-server:
|
||||||
|
properties:
|
||||||
|
bootstrap.servers: "kafka1:19092"
|
||||||
|
connect:
|
||||||
|
- name: "kafka-connect"
|
||||||
|
url: "http://kafka-connect:8083"
|
||||||
|
ports:
|
||||||
|
- "8084:8080"
|
||||||
|
depends_on:
|
||||||
|
- kafka1
|
||||||
|
- kafka-connect
|
||||||
|
@ -23,9 +23,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.anyValueClass
|
import dev.dnpm.etl.processor.anyValueClass
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import dev.dnpm.etl.processor.consent.ConsentByMtbFile
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.consent.IGetConsent
|
|
||||||
import dev.dnpm.etl.processor.security.TokenRepository
|
import dev.dnpm.etl.processor.security.TokenRepository
|
||||||
import dev.dnpm.etl.processor.security.UserRoleRepository
|
import dev.dnpm.etl.processor.security.UserRoleRepository
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
@ -34,7 +31,10 @@ import org.junit.jupiter.api.Nested
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.never
|
||||||
|
import org.mockito.kotlin.times
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
@ -54,8 +54,7 @@ import org.springframework.test.web.servlet.post
|
|||||||
@ContextConfiguration(
|
@ContextConfiguration(
|
||||||
classes = [
|
classes = [
|
||||||
MtbFileRestController::class,
|
MtbFileRestController::class,
|
||||||
AppSecurityConfiguration::class,
|
AppSecurityConfiguration::class
|
||||||
ConsentByMtbFile::class, IGetConsent::class
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(types = [TokenRepository::class, RequestProcessor::class])
|
@MockitoBean(types = [TokenRepository::class, RequestProcessor::class])
|
||||||
@ -64,8 +63,7 @@ import org.springframework.test.web.servlet.post
|
|||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
"app.security.admin-user=admin",
|
"app.security.admin-user=admin",
|
||||||
"app.security.admin-password={noop}very-secret",
|
"app.security.admin-password={noop}very-secret",
|
||||||
"app.security.enable-tokens=true",
|
"app.security.enable-tokens=true"
|
||||||
"app.consent.gics.enabled=false"
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
class MtbFileRestControllerTest {
|
class MtbFileRestControllerTest {
|
||||||
@ -143,7 +141,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE))
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -154,7 +152,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processDeletion(anyValueClass(), any())
|
verify(requestProcessor, never()).processDeletion(anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@ -165,8 +163,7 @@ class MtbFileRestControllerTest {
|
|||||||
"app.security.admin-user=admin",
|
"app.security.admin-user=admin",
|
||||||
"app.security.admin-password={noop}very-secret",
|
"app.security.admin-password={noop}very-secret",
|
||||||
"app.security.enable-tokens=true",
|
"app.security.enable-tokens=true",
|
||||||
"app.security.enable-oidc=true",
|
"app.security.enable-oidc=true"
|
||||||
"app.consent.gics.enabled=false"
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class WithOidcEnabled {
|
inner class WithOidcEnabled {
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.pseudonym
|
package dev.dnpm.etl.processor.pseudonym
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.AppFhirConfig
|
|
||||||
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
@ -43,22 +42,20 @@ class GpasPseudonymGeneratorTest {
|
|||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
private lateinit var generator: GpasPseudonymGenerator
|
private lateinit var generator: GpasPseudonymGenerator
|
||||||
private lateinit var restTemplate: RestTemplate
|
private lateinit var restTemplate: RestTemplate
|
||||||
private var appFhirConfig: AppFhirConfig = AppFhirConfig()
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
val gPasConfigProperties = GPasConfigProperties(
|
val gPasConfigProperties = GPasConfigProperties(
|
||||||
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
||||||
"test",
|
"test", "test2",
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
this.restTemplate = RestTemplate()
|
this.restTemplate = RestTemplate()
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
this.generator =
|
this.generator = GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate)
|
||||||
GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate, appFhirConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -22,7 +22,6 @@ package dev.dnpm.etl.processor.web
|
|||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
import dev.dnpm.etl.processor.monitoring.GIcsConnectionCheckService
|
|
||||||
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
@ -90,8 +89,7 @@ abstract class MockSink : Sinks.Many<Boolean>
|
|||||||
RequestProcessor::class,
|
RequestProcessor::class,
|
||||||
TransformationService::class,
|
TransformationService::class,
|
||||||
GPasConnectionCheckService::class,
|
GPasConnectionCheckService::class,
|
||||||
RestConnectionCheckService::class,
|
RestConnectionCheckService::class
|
||||||
GIcsConnectionCheckService::class
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
class ConfigControllerTest {
|
class ConfigControllerTest {
|
||||||
@ -184,13 +182,7 @@ class ConfigControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testShouldNotSaveTokenWithExstingName() {
|
fun testShouldNotSaveTokenWithExstingName() {
|
||||||
whenever(tokenService.addToken(anyString())).thenReturn(
|
whenever(tokenService.addToken(anyString())).thenReturn(Result.failure(RuntimeException("Testfailure")))
|
||||||
Result.failure(
|
|
||||||
RuntimeException(
|
|
||||||
"Testfailure"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
mockMvc.post("/configs/tokens") {
|
mockMvc.post("/configs/tokens") {
|
||||||
with(user("admin").roles("ADMIN"))
|
with(user("admin").roles("ADMIN"))
|
||||||
@ -311,10 +303,7 @@ class ConfigControllerTest {
|
|||||||
|
|
||||||
val idCaptor = argumentCaptor<Long>()
|
val idCaptor = argumentCaptor<Long>()
|
||||||
val roleCaptor = argumentCaptor<Role>()
|
val roleCaptor = argumentCaptor<Role>()
|
||||||
verify(userRoleService, times(1)).updateUserRole(
|
verify(userRoleService, times(1)).updateUserRole(idCaptor.capture(), roleCaptor.capture())
|
||||||
idCaptor.capture(),
|
|
||||||
roleCaptor.capture()
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(idCaptor.firstValue).isEqualTo(42)
|
assertThat(idCaptor.firstValue).isEqualTo(42)
|
||||||
assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN)
|
assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN)
|
||||||
@ -352,23 +341,20 @@ class ConfigControllerTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
applicationContext: WebApplicationContext
|
applicationContext: WebApplicationContext,
|
||||||
) {
|
) {
|
||||||
this.webClient = MockMvcWebTestClient
|
this.webClient = MockMvcWebTestClient
|
||||||
.bindToApplicationContext(applicationContext).build()
|
.bindToApplicationContext(applicationContext).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testShouldRequestGPasSSE() {
|
fun testShouldRequestSSE() {
|
||||||
val expectedEvent =
|
val expectedEvent = ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
|
||||||
ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
|
|
||||||
|
|
||||||
connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
|
connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
|
||||||
connectionCheckUpdateProducer.emitComplete { _, _ -> true }
|
connectionCheckUpdateProducer.emitComplete { _, _ -> true }
|
||||||
|
|
||||||
val result =
|
val result = webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM).exchange()
|
||||||
webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM)
|
|
||||||
.exchange()
|
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
||||||
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
|
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class ConsentByMtbFile implements IGetConsent {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ConsentByMtbFile.class);
|
|
||||||
|
|
||||||
public ConsentByMtbFile() {
|
|
||||||
log.info("ConsentCheckFileBased initialized...");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
|
|
||||||
return TtpConsentStatus.UNKNOWN_CHECK_FILE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle getBroadConsent(String personIdentifierValue, Date requestDate) {
|
|
||||||
return IGetConsent.super.getBroadConsent(personIdentifierValue, requestDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle getGenomDeConsent(String personIdentifierValue, Date requestDate) {
|
|
||||||
return IGetConsent.super.getGenomDeConsent(personIdentifierValue, requestDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle currentConsentForPersonAndTemplate(String personIdentifierValue,
|
|
||||||
ConsentDomain targetConsentDomain, Date requestDate) {
|
|
||||||
return new Bundle();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
public enum ConsentDomain {
|
|
||||||
/**
|
|
||||||
* MII Broad consent
|
|
||||||
*/
|
|
||||||
BroadConsent,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GenomDe Modelvohaben §64e
|
|
||||||
*/
|
|
||||||
Modelvorhaben64e
|
|
||||||
}
|
|
@ -1,266 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
|
||||||
|
|
||||||
public interface IGetConsent {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get broad consent status for a patient identifier
|
|
||||||
*
|
|
||||||
* @param personIdentifierValue patient identifier used for consent data
|
|
||||||
* @return status of broad consent
|
|
||||||
* @apiNote cannot not differ between not asked and rejected
|
|
||||||
*/
|
|
||||||
TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get broad consent policies with respect to a request date
|
|
||||||
*
|
|
||||||
* @param personIdentifierValue patient identifier used for consent data
|
|
||||||
* @param requestDate target date until consent data should be considered
|
|
||||||
* @return consent policies as bundle; <p>if empty patient has not been asked, yet.</p>
|
|
||||||
*/
|
|
||||||
default Bundle getBroadConsent(String personIdentifierValue, Date requestDate) {
|
|
||||||
return currentConsentForPersonAndTemplate(personIdentifierValue, ConsentDomain.BroadConsent,
|
|
||||||
requestDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get 'GenomDe Modelvorhaben §64e' 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>
|
|
||||||
*/
|
|
||||||
default Bundle getGenomDeConsent(String personIdentifierValue, Date requestDate) {
|
|
||||||
return currentConsentForPersonAndTemplate(personIdentifierValue,
|
|
||||||
ConsentDomain.Modelvorhaben64e, requestDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get consent policies with respect to a request date
|
|
||||||
*
|
|
||||||
* @param personIdentifierValue patient identifier used for consent data
|
|
||||||
* @param targetConsentDomain domain which should be used to request consent
|
|
||||||
* @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 currentConsentForPersonAndTemplate(String personIdentifierValue,
|
|
||||||
ConsentDomain targetConsentDomain, Date requestDate);
|
|
||||||
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -23,4 +23,6 @@ public interface Generator {
|
|||||||
|
|
||||||
String generate(String id);
|
String generate(String id);
|
||||||
|
|
||||||
|
String generateGenomDeTan(String id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ package dev.dnpm.etl.processor.pseudonym;
|
|||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.parser.IParser;
|
import ca.uhn.fhir.parser.IParser;
|
||||||
import dev.dnpm.etl.processor.config.AppFhirConfig;
|
|
||||||
import dev.dnpm.etl.processor.config.GPasConfigProperties;
|
import dev.dnpm.etl.processor.config.GPasConfigProperties;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
@ -33,38 +32,44 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
import org.springframework.retry.support.RetryTemplate;
|
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;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
public class GpasPseudonymGenerator implements Generator {
|
public class GpasPseudonymGenerator implements Generator {
|
||||||
|
|
||||||
private final FhirContext r4Context;
|
private final static FhirContext r4Context = FhirContext.forR4();
|
||||||
private final String gPasUrl;
|
private final String gPasUrl;
|
||||||
private final String psnTargetDomain;
|
|
||||||
private final HttpHeaders httpHeader;
|
private final HttpHeaders httpHeader;
|
||||||
private final RetryTemplate retryTemplate;
|
private final RetryTemplate retryTemplate;
|
||||||
private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
|
private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
|
||||||
|
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
private final @NotNull String genomDeDomain;
|
||||||
|
private final @NotNull String psnTargetDomain;
|
||||||
|
|
||||||
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
|
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
|
||||||
RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
|
RestTemplate restTemplate) {
|
||||||
this.retryTemplate = retryTemplate;
|
this.retryTemplate = retryTemplate;
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
this.gPasUrl = gpasCfg.getUri();
|
this.gPasUrl = gpasCfg.getUri();
|
||||||
this.psnTargetDomain = gpasCfg.getTarget();
|
this.psnTargetDomain = gpasCfg.getTarget();
|
||||||
this.r4Context = appFhirConfig.fhirContext();
|
this.genomDeDomain = gpasCfg.getGenomDeDomain();
|
||||||
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
|
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
|
@Override
|
||||||
public String generate(String id) {
|
public String generate(String id) {
|
||||||
var gPasRequestBody = getGpasRequestBody(id);
|
return generate(id, psnTargetDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateGenomDeTan(String id) {
|
||||||
|
return generate(id, genomDeDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String generate(String id, String targetDomain) {
|
||||||
|
var gPasRequestBody = getGpasRequestBody(id, targetDomain);
|
||||||
var responseEntity = getGpasPseudonym(gPasRequestBody);
|
var responseEntity = getGpasPseudonym(gPasRequestBody);
|
||||||
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
||||||
.parseResource(responseEntity.getBody());
|
.parseResource(responseEntity.getBody());
|
||||||
@ -103,46 +108,36 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
return psnValue.replaceAll(forbiddenCharsRegex, "_");
|
return psnValue.replaceAll(forbiddenCharsRegex, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody) {
|
protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody) {
|
||||||
|
|
||||||
HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader);
|
HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader);
|
||||||
|
ResponseEntity<String> responseEntity;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ResponseEntity<String> responseEntity = retryTemplate.execute(
|
responseEntity = retryTemplate.execute(
|
||||||
ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
|
ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
|
||||||
String.class));
|
String.class));
|
||||||
|
|
||||||
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||||
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
|
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;
|
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) {
|
} catch (Exception unexpected) {
|
||||||
throw new PseudonymRequestFailed(
|
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) {
|
protected String getGpasRequestBody(String id, String targetDomain) {
|
||||||
var requestParameters = new Parameters();
|
var requestParameters = new Parameters();
|
||||||
requestParameters.addParameter().setName("target")
|
requestParameters.addParameter().setName("target")
|
||||||
.setValue(new StringType().setValue(psnTargetDomain));
|
.setValue(new StringType().setValue(targetDomain));
|
||||||
requestParameters.addParameter().setName("original")
|
requestParameters.addParameter().setName("original")
|
||||||
.setValue(new StringType().setValue(id));
|
.setValue(new StringType().setValue(id));
|
||||||
final IParser iParser = r4Context.newJsonParser();
|
final IParser iParser = r4Context.newJsonParser();
|
||||||
|
@ -27,8 +27,7 @@ data class AppConfigProperties(
|
|||||||
var bwhcUri: String?,
|
var bwhcUri: String?,
|
||||||
var transformations: List<TransformationProperties> = listOf(),
|
var transformations: List<TransformationProperties> = listOf(),
|
||||||
var maxRetryAttempts: Int = 3,
|
var maxRetryAttempts: Int = 3,
|
||||||
var duplicationDetection: Boolean = true,
|
var duplicationDetection: Boolean = true
|
||||||
var genomDeTestSubmission: Boolean = true
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val NAME = "app"
|
const val NAME = "app"
|
||||||
@ -49,6 +48,7 @@ data class PseudonymizeConfigProperties(
|
|||||||
data class GPasConfigProperties(
|
data class GPasConfigProperties(
|
||||||
val uri: String?,
|
val uri: String?,
|
||||||
val target: String = "etl-processor",
|
val target: String = "etl-processor",
|
||||||
|
val genomDeDomain: String = "ccdn",
|
||||||
val username: String?,
|
val username: String?,
|
||||||
val password: String?,
|
val password: String?,
|
||||||
) {
|
) {
|
||||||
@ -57,70 +57,6 @@ data class GPasConfigProperties(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConfigurationProperties(GIcsConfigProperties.NAME)
|
|
||||||
data class GIcsConfigProperties(
|
|
||||||
/**
|
|
||||||
* Base URL to gICS System
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
val uri: String?,
|
|
||||||
val username: String?,
|
|
||||||
val password: String?,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If value is 'true' valid consent at processing time is mandatory for transmission of DNPM
|
|
||||||
* files otherwise they will be flagged and skipped.
|
|
||||||
* If value 'false' or missing consent status is assumed to be valid.
|
|
||||||
*/
|
|
||||||
val enabled: Boolean?,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gICS specific system
|
|
||||||
* **/
|
|
||||||
val personIdentifierSystem: String =
|
|
||||||
"https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain of broad consent resources
|
|
||||||
**/
|
|
||||||
val broadConsentDomainName: String = "MII",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain of Modelvorhaben 64e consent resources
|
|
||||||
**/
|
|
||||||
val genomDeConsentDomainName: String = "GenomDE_MV",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Value to expect in case of positiv consent
|
|
||||||
*/
|
|
||||||
val broadConsentPolicyCode: String = "2.16.840.1.113883.3.1937.777.24.5.3.6",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent Policy which should be used for consent check
|
|
||||||
*/
|
|
||||||
val broadConsentPolicySystem: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Value to expect in case of positiv consent
|
|
||||||
*/
|
|
||||||
val genomeDePolicyCode: String = "sequencing",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent Policy which should be used for consent check
|
|
||||||
*/
|
|
||||||
val genomeDePolicySystem: String = "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent version (fixed version)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
val genomeDeConsentVersion: String = "2.0"
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
const val NAME = "app.consent.gics"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConfigurationProperties(RestTargetProperties.NAME)
|
@ConfigurationProperties(RestTargetProperties.NAME)
|
||||||
data class RestTargetProperties(
|
data class RestTargetProperties(
|
||||||
val uri: String?,
|
val uri: String?,
|
||||||
|
@ -20,17 +20,16 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
package dev.dnpm.etl.processor.config
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import dev.dnpm.etl.processor.consent.ConsentByMtbFile
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
import dev.dnpm.etl.processor.consent.IGetConsent
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.monitoring.*
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||||
import dev.dnpm.etl.processor.security.TokenRepository
|
import dev.dnpm.etl.processor.security.TokenRepository
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import dev.dnpm.etl.processor.services.ConsentProcessor
|
|
||||||
import dev.dnpm.etl.processor.services.Transformation
|
import dev.dnpm.etl.processor.services.Transformation
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -61,8 +60,7 @@ import kotlin.time.toJavaDuration
|
|||||||
value = [
|
value = [
|
||||||
AppConfigProperties::class,
|
AppConfigProperties::class,
|
||||||
PseudonymizeConfigProperties::class,
|
PseudonymizeConfigProperties::class,
|
||||||
GPasConfigProperties::class,
|
GPasConfigProperties::class
|
||||||
GIcsConfigProperties::class
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@ -75,27 +73,13 @@ class AppConfiguration {
|
|||||||
return RestTemplate()
|
return RestTemplate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun appFhirConfig(): AppFhirConfig {
|
|
||||||
return AppFhirConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
|
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
|
||||||
@Bean
|
@Bean
|
||||||
fun gpasPseudonymGenerator(
|
fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
||||||
configProperties: GPasConfigProperties,
|
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
||||||
retryTemplate: RetryTemplate,
|
|
||||||
restTemplate: RestTemplate,
|
|
||||||
appFhirConfig: AppFhirConfig
|
|
||||||
): Generator {
|
|
||||||
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
|
||||||
value = ["app.pseudonymize.generator"],
|
|
||||||
havingValue = "BUILDIN",
|
|
||||||
matchIfMissing = true
|
|
||||||
)
|
|
||||||
@Bean
|
@Bean
|
||||||
fun buildinPseudonymGenerator(): Generator {
|
fun buildinPseudonymGenerator(): Generator {
|
||||||
return AnonymizingGenerator()
|
return AnonymizingGenerator()
|
||||||
@ -110,21 +94,17 @@ class AppConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun reportService(): ReportService {
|
fun reportService(objectMapper: ObjectMapper): ReportService {
|
||||||
return ReportService(getObjectMapper())
|
return ReportService(objectMapper)
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun getObjectMapper(): ObjectMapper {
|
|
||||||
return JacksonConfig().objectMapper()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun transformationService(
|
fun transformationService(
|
||||||
|
objectMapper: ObjectMapper,
|
||||||
configProperties: AppConfigProperties
|
configProperties: AppConfigProperties
|
||||||
): TransformationService {
|
): TransformationService {
|
||||||
logger.info("Apply ${configProperties.transformations.size} transformation rules")
|
logger.info("Apply ${configProperties.transformations.size} transformation rules")
|
||||||
return TransformationService(getObjectMapper(), configProperties.transformations.map {
|
return TransformationService(objectMapper, configProperties.transformations.map {
|
||||||
Transformation.of(it.path) from it.from to it.to
|
Transformation.of(it.path) from it.from to it.to
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -143,11 +123,7 @@ class AppConfiguration {
|
|||||||
callback: RetryCallback<T, E>,
|
callback: RetryCallback<T, E>,
|
||||||
throwable: Throwable
|
throwable: Throwable
|
||||||
) {
|
) {
|
||||||
logger.warn(
|
logger.warn("Error occured: {}. Retrying {}", throwable.message, context.retryCount)
|
||||||
"Error occured: {}. Retrying {}",
|
|
||||||
throwable.message,
|
|
||||||
context.retryCount
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
@ -155,11 +131,7 @@ class AppConfiguration {
|
|||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true")
|
@ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true")
|
||||||
@Bean
|
@Bean
|
||||||
fun tokenService(
|
fun tokenService(userDetailsManager: InMemoryUserDetailsManager, passwordEncoder: PasswordEncoder, tokenRepository: TokenRepository): TokenService {
|
||||||
userDetailsManager: InMemoryUserDetailsManager,
|
|
||||||
passwordEncoder: PasswordEncoder,
|
|
||||||
tokenRepository: TokenRepository
|
|
||||||
): TokenService {
|
|
||||||
return TokenService(userDetailsManager, passwordEncoder, tokenRepository)
|
return TokenService(userDetailsManager, passwordEncoder, tokenRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,11 +152,7 @@ class AppConfiguration {
|
|||||||
gPasConfigProperties: GPasConfigProperties,
|
gPasConfigProperties: GPasConfigProperties,
|
||||||
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
): ConnectionCheckService {
|
): ConnectionCheckService {
|
||||||
return GPasConnectionCheckService(
|
return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
|
||||||
restTemplate,
|
|
||||||
gPasConfigProperties,
|
|
||||||
connectionCheckUpdateProducer
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
|
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
|
||||||
@ -195,68 +163,12 @@ class AppConfiguration {
|
|||||||
gPasConfigProperties: GPasConfigProperties,
|
gPasConfigProperties: GPasConfigProperties,
|
||||||
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
): ConnectionCheckService {
|
): ConnectionCheckService {
|
||||||
return GPasConnectionCheckService(
|
return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
|
||||||
restTemplate,
|
|
||||||
gPasConfigProperties,
|
|
||||||
connectionCheckUpdateProducer
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun jdbcConfiguration(): AbstractJdbcConfiguration {
|
fun jdbcConfiguration(): AbstractJdbcConfiguration {
|
||||||
return AppJdbcConfiguration()
|
return AppJdbcConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
|
|
||||||
fun gicsConsentService(
|
|
||||||
gIcsConfigProperties: GIcsConfigProperties,
|
|
||||||
retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig
|
|
||||||
): IGetConsent {
|
|
||||||
return GicsConsentService(
|
|
||||||
gIcsConfigProperties,
|
|
||||||
retryTemplate,
|
|
||||||
restTemplate,
|
|
||||||
appFhirConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
|
|
||||||
fun gicsConsentProcessor(
|
|
||||||
configProperties: AppConfigProperties,
|
|
||||||
gIcsConfigProperties: GIcsConfigProperties,
|
|
||||||
getObjectMapper: ObjectMapper,
|
|
||||||
appFhirConfig: AppFhirConfig,
|
|
||||||
gicsConsentService: IGetConsent
|
|
||||||
): ConsentProcessor {
|
|
||||||
return ConsentProcessor(
|
|
||||||
configProperties,
|
|
||||||
gIcsConfigProperties,
|
|
||||||
getObjectMapper,
|
|
||||||
appFhirConfig.fhirContext(),
|
|
||||||
gicsConsentService
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
|
|
||||||
@Bean
|
|
||||||
fun gIcsConnectionCheckService(
|
|
||||||
restTemplate: RestTemplate,
|
|
||||||
gIcsConfigProperties: GIcsConfigProperties,
|
|
||||||
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
|
||||||
): ConnectionCheckService {
|
|
||||||
return GIcsConnectionCheckService(
|
|
||||||
restTemplate,
|
|
||||||
gIcsConfigProperties,
|
|
||||||
connectionCheckUpdateProducer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
fun iGetConsentService(): IGetConsent {
|
|
||||||
return ConsentByMtbFile()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
class AppFhirConfig {
|
|
||||||
private val fhirCtx: FhirContext = FhirContext.forR4()
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun fhirContext(): FhirContext {
|
|
||||||
return fhirCtx
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext
|
|
||||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode
|
|
||||||
import org.hl7.fhir.r4.model.Consent
|
|
||||||
|
|
||||||
class ConsentResourceDeserializer : JsonDeserializer<Consent>() {
|
|
||||||
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Consent {
|
|
||||||
|
|
||||||
val jsonNode = p?.readValueAsTree<JsonNode>()
|
|
||||||
val json = jsonNode?.toString()
|
|
||||||
|
|
||||||
return JacksonConfig.fhirContext().newJsonParser().parseResource(json) as Consent
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer
|
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider
|
|
||||||
import org.hl7.fhir.r4.model.Consent
|
|
||||||
|
|
||||||
class ConsentResourceSerializer : JsonSerializer<Consent>() {
|
|
||||||
override fun serialize(
|
|
||||||
value: Consent, gen: JsonGenerator, serializers: SerializerProvider
|
|
||||||
) {
|
|
||||||
val json = JacksonConfig.fhirContext().newJsonParser().encodeResourceToString(value)
|
|
||||||
gen.writeRawValue(json)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
|
||||||
import org.hl7.fhir.r4.model.Consent
|
|
||||||
|
|
||||||
class FhirResourceModule : SimpleModule() {
|
|
||||||
init {
|
|
||||||
addSerializer(Consent::class.java, ConsentResourceSerializer())
|
|
||||||
addDeserializer(Consent::class.java, ConsentResourceDeserializer())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
class JacksonConfig {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var fhirContext: FhirContext = FhirContext.forR4()
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun fhirContext(): FhirContext {
|
|
||||||
return fhirContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun objectMapper(): ObjectMapper = ObjectMapper().registerModule(FhirResourceModule())
|
|
||||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).registerModule(
|
|
||||||
JavaTimeModule()
|
|
||||||
)
|
|
||||||
}
|
|
@ -25,7 +25,6 @@ import de.ukw.ccc.bwhc.dto.MtbFile
|
|||||||
import dev.dnpm.etl.processor.CustomMediaType
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -77,13 +76,9 @@ class KafkaInputListener(
|
|||||||
} else {
|
} else {
|
||||||
logger.debug("Accepted MTB File and process deletion")
|
logger.debug("Accepted MTB File and process deletion")
|
||||||
if (requestId.isBlank()) {
|
if (requestId.isBlank()) {
|
||||||
requestProcessor.processDeletion(patientId, TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
requestProcessor.processDeletion(patientId)
|
||||||
} else {
|
} else {
|
||||||
requestProcessor.processDeletion(
|
requestProcessor.processDeletion(patientId, requestId)
|
||||||
patientId,
|
|
||||||
requestId,
|
|
||||||
TtpConsentStatus.UNKNOWN_CHECK_FILE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,6 @@ import de.ukw.ccc.bwhc.dto.Consent
|
|||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.dnpm.etl.processor.consent.IGetConsent
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -35,7 +33,7 @@ import org.springframework.web.bind.annotation.*
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(path = ["mtbfile", "mtb"])
|
@RequestMapping(path = ["mtbfile", "mtb"])
|
||||||
class MtbFileRestController(
|
class MtbFileRestController(
|
||||||
private val requestProcessor: RequestProcessor, private val iGetConsent: IGetConsent
|
private val requestProcessor: RequestProcessor,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
||||||
@ -47,36 +45,17 @@ class MtbFileRestController(
|
|||||||
|
|
||||||
@PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] )
|
@PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] )
|
||||||
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
||||||
val consentStatusBooleanPair = checkConsentStatus(mtbFile)
|
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
||||||
val ttpConsentStatus = consentStatusBooleanPair.first
|
|
||||||
val isConsentOK = consentStatusBooleanPair.second
|
|
||||||
if (isConsentOK) {
|
|
||||||
logger.debug("Accepted MTB File (bwHC V1) for processing")
|
logger.debug("Accepted MTB File (bwHC V1) for processing")
|
||||||
requestProcessor.processMtbFile(mtbFile)
|
requestProcessor.processMtbFile(mtbFile)
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
logger.debug("Accepted MTB File (bwHC V1) and process deletion")
|
logger.debug("Accepted MTB File (bwHC V1) and process deletion")
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
val patientId = PatientId(mtbFile.patient.id)
|
||||||
requestProcessor.processDeletion(patientId, ttpConsentStatus)
|
requestProcessor.processDeletion(patientId)
|
||||||
}
|
}
|
||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkConsentStatus(mtbFile: MtbFile): Pair<TtpConsentStatus, Boolean> {
|
|
||||||
var ttpConsentStatus = iGetConsent.getTtpBroadConsentStatus(mtbFile.patient.id)
|
|
||||||
|
|
||||||
val isConsentOK =
|
|
||||||
(ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.ACTIVE) ||
|
|
||||||
ttpConsentStatus.equals(
|
|
||||||
TtpConsentStatus.BROAD_CONSENT_GIVEN
|
|
||||||
)
|
|
||||||
if (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.REJECTED) {
|
|
||||||
// in case ttp check is disabled - we propagate rejected status anyway
|
|
||||||
ttpConsentStatus = TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED
|
|
||||||
}
|
|
||||||
return Pair(ttpConsentStatus, isConsentOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping( consumes = [ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] )
|
@PostMapping( consumes = [ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] )
|
||||||
fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> {
|
fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> {
|
||||||
logger.debug("Accepted MTB File (DNPM V2) for processing")
|
logger.debug("Accepted MTB File (DNPM V2) for processing")
|
||||||
@ -87,7 +66,7 @@ class MtbFileRestController(
|
|||||||
@DeleteMapping(path = ["{patientId}"])
|
@DeleteMapping(path = ["{patientId}"])
|
||||||
fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> {
|
fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> {
|
||||||
logger.debug("Accepted patient ID to process deletion")
|
logger.debug("Accepted patient ID to process deletion")
|
||||||
requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
requestProcessor.processDeletion(PatientId(patientId))
|
||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.monitoring
|
package dev.dnpm.etl.processor.monitoring
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.GIcsConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import jakarta.annotation.PostConstruct
|
import jakarta.annotation.PostConstruct
|
||||||
@ -69,12 +68,6 @@ sealed class ConnectionCheckResult {
|
|||||||
override val timestamp: Instant,
|
override val timestamp: Instant,
|
||||||
override val lastChange: Instant
|
override val lastChange: Instant
|
||||||
) : ConnectionCheckResult()
|
) : ConnectionCheckResult()
|
||||||
|
|
||||||
data class GIcsConnectionCheckResult(
|
|
||||||
override val available: Boolean,
|
|
||||||
override val timestamp: Instant,
|
|
||||||
override val lastChange: Instant
|
|
||||||
) : ConnectionCheckResult()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class KafkaConnectionCheckService(
|
class KafkaConnectionCheckService(
|
||||||
@ -215,56 +208,3 @@ class GPasConnectionCheckService(
|
|||||||
return this.result
|
return this.result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GIcsConnectionCheckService(
|
|
||||||
private val restTemplate: RestTemplate,
|
|
||||||
private val gIcsConfigProperties: GIcsConfigProperties,
|
|
||||||
@Qualifier("connectionCheckUpdateProducer")
|
|
||||||
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
|
||||||
) : ConnectionCheckService {
|
|
||||||
|
|
||||||
private var result = ConnectionCheckResult.GIcsConnectionCheckResult(false, Instant.now(), Instant.now())
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
@Scheduled(cron = "0 * * * * *")
|
|
||||||
fun check() {
|
|
||||||
result = try {
|
|
||||||
|
|
||||||
val uri = UriComponentsBuilder.fromUriString(
|
|
||||||
gIcsConfigProperties.uri.toString()).path("/metadata").build().toUri()
|
|
||||||
|
|
||||||
val headers = HttpHeaders()
|
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
|
||||||
if (!gIcsConfigProperties.username.isNullOrBlank() && !gIcsConfigProperties.password.isNullOrBlank()) {
|
|
||||||
headers.setBasicAuth(gIcsConfigProperties.username, gIcsConfigProperties.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
val available = restTemplate.exchange(
|
|
||||||
uri,
|
|
||||||
HttpMethod.GET,
|
|
||||||
HttpEntity<Void>(headers),
|
|
||||||
Void::class.java
|
|
||||||
).statusCode == HttpStatus.OK
|
|
||||||
|
|
||||||
ConnectionCheckResult.GIcsConnectionCheckResult(
|
|
||||||
available,
|
|
||||||
Instant.now(),
|
|
||||||
if (result.available == available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
ConnectionCheckResult.GIcsConnectionCheckResult(
|
|
||||||
false,
|
|
||||||
Instant.now(),
|
|
||||||
if (!result.available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
connectionCheckUpdateProducer.emitNext(
|
|
||||||
result,
|
|
||||||
Sinks.EmitFailureHandler.FAIL_FAST
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun connectionAvailable(): ConnectionCheckResult.GIcsConnectionCheckResult {
|
|
||||||
return this.result
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,6 +24,5 @@ enum class RequestStatus(val value: String) {
|
|||||||
WARNING("warning"),
|
WARNING("warning"),
|
||||||
ERROR("error"),
|
ERROR("error"),
|
||||||
UNKNOWN("unknown"),
|
UNKNOWN("unknown"),
|
||||||
DUPLICATION("duplication"),
|
DUPLICATION("duplication")
|
||||||
NO_CONSENT("no-consent")
|
|
||||||
}
|
}
|
@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.pseudonym
|
|||||||
|
|
||||||
import org.apache.commons.codec.binary.Base32
|
import org.apache.commons.codec.binary.Base32
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
|
import org.apache.commons.math3.random.RandomDataGenerator
|
||||||
|
|
||||||
|
|
||||||
class AnonymizingGenerator : Generator {
|
class AnonymizingGenerator : Generator {
|
||||||
@ -31,4 +32,9 @@ class AnonymizingGenerator : Generator {
|
|||||||
.lowercase()
|
.lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun generateGenomDeTan(id: String?): String? {
|
||||||
|
val randomDataGenerator = RandomDataGenerator()
|
||||||
|
return randomDataGenerator.nextSecureHexString(64).lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -35,6 +35,10 @@ class PseudonymizeService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun genomDeTan(patientId: PatientId): String {
|
||||||
|
return generator.generateGenomDeTan(patientId.value)
|
||||||
|
}
|
||||||
|
|
||||||
fun prefix(): String {
|
fun prefix(): String {
|
||||||
return configProperties.prefix
|
return configProperties.prefix
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,7 @@ package dev.dnpm.etl.processor.pseudonym
|
|||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
import dev.pcvolkmer.mv64e.mtb.MvhMetadata
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
|
|
||||||
/** Replaces patient ID with generated patient pseudonym
|
/** Replaces patient ID with generated patient pseudonym
|
||||||
@ -291,16 +289,6 @@ infix fun Mtb.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
|||||||
this.followUps?.forEach {
|
this.followUps?.forEach {
|
||||||
it.patient.id = patientPseudonym
|
it.patient.id = patientPseudonym
|
||||||
}
|
}
|
||||||
|
|
||||||
this.metadata?.researchConsents?.forEach { it ->
|
|
||||||
val entry = it ?: return@forEach
|
|
||||||
if (entry.contains("patient")) {
|
|
||||||
// here we expect only a patient reference any other data like display
|
|
||||||
// need to be removed, since may contain unsecure data
|
|
||||||
entry.remove("patient")
|
|
||||||
entry["patient"] = mapOf("reference" to "Patient/$patientPseudonym")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -330,22 +318,7 @@ infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
|||||||
// TODO all other properties
|
// TODO all other properties
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Mtb.ensureMetaDataIsInitialized() {
|
infix fun Mtb.addGenomDeTan(pseudonymizeService: PseudonymizeService)
|
||||||
// init metadata if necessary
|
{
|
||||||
if (this.metadata == null) {
|
this.metadata.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id))
|
||||||
val mvhMetadata = MvhMetadata.builder().build()
|
|
||||||
this.metadata = mvhMetadata
|
|
||||||
}
|
|
||||||
if (this.metadata.researchConsents == null) {
|
|
||||||
this.metadata.researchConsents = mutableListOf()
|
|
||||||
}
|
|
||||||
if (this.metadata.modelProjectConsent == null) {
|
|
||||||
this.metadata.modelProjectConsent = ModelProjectConsent()
|
|
||||||
this.metadata.modelProjectConsent.provisions = mutableListOf()
|
|
||||||
} else
|
|
||||||
if (this.metadata.modelProjectConsent.provisions != null) {
|
|
||||||
// make sure list can be changed
|
|
||||||
this.metadata.modelProjectConsent.provisions =
|
|
||||||
this.metadata.modelProjectConsent.provisions.toMutableList()
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,276 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.services
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.config.GIcsConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.consent.ConsentDomain
|
|
||||||
import dev.dnpm.etl.processor.consent.IGetConsent
|
|
||||||
import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
|
||||||
import org.apache.commons.lang3.NotImplementedException
|
|
||||||
import org.hl7.fhir.r4.model.*
|
|
||||||
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent
|
|
||||||
import org.hl7.fhir.r4.model.Coding
|
|
||||||
import org.hl7.fhir.r4.model.Consent.ConsentState
|
|
||||||
import org.hl7.fhir.r4.model.Consent.ProvisionComponent
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Clock
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class ConsentProcessor(
|
|
||||||
private val appConfigProperties: AppConfigProperties,
|
|
||||||
private val gIcsConfigProperties: GIcsConfigProperties,
|
|
||||||
private val objectMapper: ObjectMapper,
|
|
||||||
private val fhirContext: FhirContext,
|
|
||||||
private val consentService: IGetConsent?
|
|
||||||
) {
|
|
||||||
private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked.
|
|
||||||
*
|
|
||||||
* Logik:
|
|
||||||
* * <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.
|
|
||||||
* * ELSE <c>false</c> is returned.
|
|
||||||
*
|
|
||||||
* @param mtbFile File v2 (will be enriched with consent data)
|
|
||||||
* @return true if consent is given
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean {
|
|
||||||
if (consentService == null) {
|
|
||||||
// consent check seems to be disabled
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
mtbFile.ensureMetaDataIsInitialized()
|
|
||||||
|
|
||||||
val personIdentifierValue = mtbFile.patient.id
|
|
||||||
val requestDate = Date.from(Instant.now(Clock.systemUTC()))
|
|
||||||
|
|
||||||
// 1. Broad consent Entry exists?
|
|
||||||
// 1.1. -> yes and research consent is given -> send mtb file
|
|
||||||
// 1.2. -> no -> return status error - consent has not been asked
|
|
||||||
// 2. -> Broad consent found but rejected -> is GenomDe consent provision 'sequencing' given?
|
|
||||||
// 2.1 -> yes -> send mtb file
|
|
||||||
// 2.2 -> no -> warn/info no consent given
|
|
||||||
|
|
||||||
/*
|
|
||||||
* broad consent
|
|
||||||
*/
|
|
||||||
val broadConsent = consentService.getBroadConsent(personIdentifierValue, requestDate)
|
|
||||||
val broadConsentHasBeenAsked = !broadConsent.entry.isEmpty()
|
|
||||||
|
|
||||||
// fast exit - if patient has not been asked, we can skip and exit
|
|
||||||
if (!broadConsentHasBeenAsked) return false
|
|
||||||
|
|
||||||
val genomeDeConsent = consentService.getGenomDeConsent(
|
|
||||||
personIdentifierValue, requestDate
|
|
||||||
)
|
|
||||||
|
|
||||||
addGenomeDbProvisions(mtbFile, genomeDeConsent)
|
|
||||||
|
|
||||||
|
|
||||||
if (!genomeDeConsent.entry.isEmpty()) setGenomDeSubmissionType(mtbFile)
|
|
||||||
|
|
||||||
embedBroadConsentResources(mtbFile, broadConsent)
|
|
||||||
|
|
||||||
val broadConsentStatus = getProvisionTypeByPolicyCode(
|
|
||||||
broadConsent, requestDate, ConsentDomain.BroadConsent
|
|
||||||
)
|
|
||||||
|
|
||||||
val genomDeSequencingStatus = getProvisionTypeByPolicyCode(
|
|
||||||
genomeDeConsent, requestDate, ConsentDomain.Modelvorhaben64e
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Consent.ConsentProvisionType.NULL == broadConsentStatus) {
|
|
||||||
// bc not asked
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (Consent.ConsentProvisionType.PERMIT == broadConsentStatus || Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) {
|
|
||||||
for (entry in broadConsent.getEntry()) {
|
|
||||||
val resource = entry.getResource()
|
|
||||||
if (resource is Consent) {
|
|
||||||
// since jackson convertValue does not work here,
|
|
||||||
// we need another step to back to string, before we convert to object map
|
|
||||||
val asJsonString = fhirContext.newJsonParser().encodeResourceToString(resource)
|
|
||||||
try {
|
|
||||||
val mapOfJson: HashMap<String?, Any?>? =
|
|
||||||
objectMapper.readValue<HashMap<String?, Any?>?>(
|
|
||||||
asJsonString, object : TypeReference<HashMap<String?, Any?>?>() {})
|
|
||||||
mtbFile.metadata.researchConsents.add(mapOfJson)
|
|
||||||
} catch (e: JsonProcessingException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) {
|
|
||||||
for (entry in consentGnomeDe.getEntry()) {
|
|
||||||
val resource = entry.getResource()
|
|
||||||
if (resource !is Consent) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect only one provision in collection, therefore get first or none
|
|
||||||
val provisions = resource.getProvision().getProvision()
|
|
||||||
if (provisions.isEmpty()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val provisionComponent: ProvisionComponent = provisions.first()
|
|
||||||
|
|
||||||
var provisionCode: String? = null
|
|
||||||
if (provisionComponent.getCode() != null && !provisionComponent.getCode().isEmpty()) {
|
|
||||||
val codableConcept: CodeableConcept = provisionComponent.getCode().first()
|
|
||||||
if (codableConcept.getCoding() != null && !codableConcept.getCoding().isEmpty()) {
|
|
||||||
provisionCode = codableConcept.getCoding().first().getCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provisionCode != null) {
|
|
||||||
try {
|
|
||||||
val modelProjectConsentPurpose =
|
|
||||||
ModelProjectConsentPurpose.forValue(provisionCode)
|
|
||||||
|
|
||||||
if (ModelProjectConsentPurpose.SEQUENCING == modelProjectConsentPurpose) {
|
|
||||||
// CONVENTION: wrapping date is date of SEQUENCING consent
|
|
||||||
mtbFile.metadata.modelProjectConsent.date = resource.getDateTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
val provision = Provision.builder()
|
|
||||||
.type(ConsentProvision.valueOf(provisionComponent.getType().name))
|
|
||||||
.date(provisionComponent.getPeriod().getStart())
|
|
||||||
.purpose(modelProjectConsentPurpose).build()
|
|
||||||
|
|
||||||
mtbFile.metadata.modelProjectConsent.provisions.add(provision)
|
|
||||||
} catch (ioe: IOException) {
|
|
||||||
logger.error(
|
|
||||||
"Provision code '$provisionCode' is unknown and cannot be mapped.",
|
|
||||||
ioe.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mtbFile.metadata.modelProjectConsent.provisions.isEmpty()) {
|
|
||||||
mtbFile.metadata.modelProjectConsent.version =
|
|
||||||
gIcsConfigProperties.genomeDeConsentVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* fixme: currently we do not have information about submission type
|
|
||||||
*/
|
|
||||||
private fun setGenomDeSubmissionType(mtbFile: Mtb) {
|
|
||||||
if (appConfigProperties.genomDeTestSubmission) {
|
|
||||||
// fixme: uncomment when data model is updated
|
|
||||||
// mtbFile.metadata.type = MvhSubmissionType.Test
|
|
||||||
logger.info("genomeDe submission mit TEST")
|
|
||||||
} else {
|
|
||||||
mtbFile.metadata.type = MvhSubmissionType.INITIAL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param consentBundle consent resource
|
|
||||||
* @param requestDate date which must be within validation period of provision
|
|
||||||
* @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found.
|
|
||||||
*/
|
|
||||||
fun getProvisionTypeByPolicyCode(
|
|
||||||
consentBundle: Bundle, requestDate: Date?, consentDomain: ConsentDomain
|
|
||||||
): Consent.ConsentProvisionType {
|
|
||||||
val code: String?
|
|
||||||
val system: String?
|
|
||||||
if (ConsentDomain.BroadConsent == consentDomain) {
|
|
||||||
code = gIcsConfigProperties.broadConsentPolicyCode
|
|
||||||
system = gIcsConfigProperties.broadConsentPolicySystem
|
|
||||||
} else if (ConsentDomain.Modelvorhaben64e == consentDomain) {
|
|
||||||
code = gIcsConfigProperties.genomeDePolicyCode
|
|
||||||
system = gIcsConfigProperties.genomeDePolicySystem
|
|
||||||
} else {
|
|
||||||
throw NotImplementedException("unknown consent domain " + consentDomain.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
val provisionTypeByPolicyCode = getProvisionTypeByPolicyCode(
|
|
||||||
consentBundle, code, system, requestDate
|
|
||||||
)
|
|
||||||
return provisionTypeByPolicyCode
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param consentBundle consent resource
|
|
||||||
* @param policyAndProvisionCode policyRule and provision code value
|
|
||||||
* @param policyAndProvisionSystem policyRule and provision system value
|
|
||||||
* @param requestDate date which must be within validation period of provision
|
|
||||||
* @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found.
|
|
||||||
*/
|
|
||||||
fun getProvisionTypeByPolicyCode(
|
|
||||||
consentBundle: Bundle,
|
|
||||||
policyAndProvisionCode: String?,
|
|
||||||
policyAndProvisionSystem: String?,
|
|
||||||
requestDate: Date?
|
|
||||||
): Consent.ConsentProvisionType {
|
|
||||||
val entriesOfInterest = consentBundle.entry.filter { entry ->
|
|
||||||
entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent && (entry.resource as Consent).status == ConsentState.ACTIVE && checkCoding(
|
|
||||||
policyAndProvisionCode,
|
|
||||||
policyAndProvisionSystem,
|
|
||||||
(entry.resource as Consent).policyRule.codingFirstRep
|
|
||||||
) && isIsRequestDateInRange(
|
|
||||||
requestDate, (entry.resource as Consent).provision.period
|
|
||||||
)
|
|
||||||
}.map { consentWithTargetPolicy: BundleEntryComponent ->
|
|
||||||
val provision = (consentWithTargetPolicy.getResource() as Consent).getProvision()
|
|
||||||
val provisionComponentByCode =
|
|
||||||
provision.getProvision().stream().filter { prov: ProvisionComponent? ->
|
|
||||||
checkCoding(
|
|
||||||
policyAndProvisionCode,
|
|
||||||
policyAndProvisionSystem,
|
|
||||||
prov!!.getCodeFirstRep().getCodingFirstRep()
|
|
||||||
) && isIsRequestDateInRange(
|
|
||||||
requestDate, prov.getPeriod()
|
|
||||||
)
|
|
||||||
}.findFirst()
|
|
||||||
if (provisionComponentByCode.isPresent) {
|
|
||||||
// actual provision we search for
|
|
||||||
return@map provisionComponentByCode.get().getType()
|
|
||||||
} else {
|
|
||||||
if (provision.type != null) return provision.type
|
|
||||||
|
|
||||||
}
|
|
||||||
return Consent.ConsentProvisionType.NULL
|
|
||||||
}.firstOrNull()
|
|
||||||
|
|
||||||
if (entriesOfInterest == null) return Consent.ConsentProvisionType.NULL
|
|
||||||
return entriesOfInterest
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkCoding(
|
|
||||||
researchAllowedPolicyOid: String?, researchAllowedPolicySystem: String?, coding: Coding
|
|
||||||
): Boolean {
|
|
||||||
return coding.getSystem() == researchAllowedPolicySystem && (coding.getCode() == researchAllowedPolicyOid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isIsRequestDateInRange(requestdate: Date?, provPeriod: Period): Boolean {
|
|
||||||
val isRequestDateAfterOrEqualStart = provPeriod.getStart().compareTo(requestdate)
|
|
||||||
val isRequestDateBeforeOrEqualEnd = provPeriod.getEnd().compareTo(requestdate)
|
|
||||||
return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -23,23 +23,22 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.*
|
import dev.dnpm.etl.processor.*
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Report
|
import dev.dnpm.etl.processor.monitoring.Report
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import dev.dnpm.etl.processor.output.*
|
import dev.dnpm.etl.processor.output.*
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
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.anonymizeContentWith
|
||||||
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
|
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 dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
import org.apache.commons.codec.binary.Base32
|
import org.apache.commons.codec.binary.Base32
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.lang.RuntimeException
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -51,11 +50,9 @@ class RequestProcessor(
|
|||||||
private val requestService: RequestService,
|
private val requestService: RequestService,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||||
private val appConfigProperties: AppConfigProperties,
|
private val appConfigProperties: AppConfigProperties
|
||||||
private val consentProcessor: ConsentProcessor?
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var logger: Logger = LoggerFactory.getLogger("RequestProcessor")
|
|
||||||
fun processMtbFile(mtbFile: MtbFile) {
|
fun processMtbFile(mtbFile: MtbFile) {
|
||||||
processMtbFile(mtbFile, randomRequestId())
|
processMtbFile(mtbFile, randomRequestId())
|
||||||
}
|
}
|
||||||
@ -72,25 +69,19 @@ class RequestProcessor(
|
|||||||
processMtbFile(mtbFile, randomRequestId())
|
processMtbFile(mtbFile, randomRequestId())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
|
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
|
||||||
val pid = PatientId(extractPatientIdentifier(mtbFile))
|
val pid = PatientId(mtbFile.patient.id)
|
||||||
|
val isModelProjectConsented = mtbFile.metadata?.modelProjectConsent?.provisions?.any { p ->
|
||||||
val isConsentOk = consentProcessor != null &&
|
p.purpose == ModelProjectConsentPurpose.SEQUENCING
|
||||||
consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null
|
&& p.type == ConsentProvision.PERMIT
|
||||||
if (isConsentOk) {
|
} == true
|
||||||
|
if (isModelProjectConsented) {
|
||||||
|
mtbFile addGenomDeTan pseudonymizeService
|
||||||
|
}
|
||||||
mtbFile pseudonymizeWith pseudonymizeService
|
mtbFile pseudonymizeWith pseudonymizeService
|
||||||
mtbFile anonymizeContentWith pseudonymizeService
|
mtbFile anonymizeContentWith pseudonymizeService
|
||||||
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
||||||
saveAndSend(request, pid)
|
saveAndSend(request, pid)
|
||||||
} else {
|
|
||||||
logger.warn("consent check failed file will not be processed further!")
|
|
||||||
applicationEventPublisher.publishEvent(
|
|
||||||
ResponseEvent(
|
|
||||||
requestId, Instant.now(), RequestStatus.NO_CONSENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
|
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
|
||||||
@ -108,7 +99,9 @@ class RequestProcessor(
|
|||||||
if (appConfigProperties.duplicationDetection && isDuplication(request)) {
|
if (appConfigProperties.duplicationDetection && isDuplication(request)) {
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
request.requestId, Instant.now(), RequestStatus.DUPLICATION
|
request.requestId,
|
||||||
|
Instant.now(),
|
||||||
|
RequestStatus.DUPLICATION
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@ -140,28 +133,21 @@ class RequestProcessor(
|
|||||||
val isLastRequestDeletion =
|
val isLastRequestDeletion =
|
||||||
requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
||||||
|
|
||||||
return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint(
|
return null != lastMtbFileRequestForPatient
|
||||||
|
&& !isLastRequestDeletion
|
||||||
|
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(
|
||||||
pseudonymizedMtbFileRequest
|
pseudonymizedMtbFileRequest
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) {
|
fun processDeletion(patientId: PatientId) {
|
||||||
processDeletion(patientId, randomRequestId(), isConsented)
|
processDeletion(patientId, randomRequestId())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) {
|
fun processDeletion(patientId: PatientId, requestId: RequestId) {
|
||||||
try {
|
try {
|
||||||
val patientPseudonym = pseudonymizeService.patientPseudonym(patientId)
|
val patientPseudonym = pseudonymizeService.patientPseudonym(patientId)
|
||||||
|
|
||||||
val requestStatus: RequestStatus = when (isConsented) {
|
|
||||||
TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, TtpConsentStatus.BROAD_CONSENT_MISSING, TtpConsentStatus.BROAD_CONSENT_REJECTED -> RequestStatus.NO_CONSENT
|
|
||||||
TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR
|
|
||||||
TtpConsentStatus.BROAD_CONSENT_GIVEN, TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN
|
|
||||||
TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, TtpConsentStatus.GENOM_DE_CONSENT_MISSING, TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED -> {
|
|
||||||
throw RuntimeException("processDelete should never deal with '" + isConsented.name + "' consent status. This is a bug and need to be fixed!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
requestId,
|
requestId,
|
||||||
@ -169,7 +155,7 @@ class RequestProcessor(
|
|||||||
patientId,
|
patientId,
|
||||||
fingerprint(patientPseudonym.value),
|
fingerprint(patientPseudonym.value),
|
||||||
RequestType.DELETE,
|
RequestType.DELETE,
|
||||||
requestStatus
|
RequestStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -177,14 +163,17 @@ class RequestProcessor(
|
|||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
requestId, Instant.now(), responseStatus.status, when (responseStatus.status) {
|
requestId,
|
||||||
|
Instant.now(),
|
||||||
|
responseStatus.status,
|
||||||
|
when (responseStatus.status) {
|
||||||
RequestStatus.WARNING, RequestStatus.ERROR -> Optional.of(responseStatus.body)
|
RequestStatus.WARNING, RequestStatus.ERROR -> Optional.of(responseStatus.body)
|
||||||
else -> Optional.empty()
|
else -> Optional.empty()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
uuid = requestId,
|
uuid = requestId,
|
||||||
@ -208,10 +197,10 @@ class RequestProcessor(
|
|||||||
|
|
||||||
private fun fingerprint(s: String): Fingerprint {
|
private fun fingerprint(s: String): Fingerprint {
|
||||||
return Fingerprint(
|
return Fingerprint(
|
||||||
Base32().encodeAsString(DigestUtils.sha256(s)).replace("=", "").lowercase()
|
Base32().encodeAsString(DigestUtils.sha256(s))
|
||||||
|
.replace("=", "")
|
||||||
|
.lowercase()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractPatientIdentifier(mtbFile: Mtb): String = mtbFile.patient.id
|
|
||||||
|
@ -70,12 +70,6 @@ class ResponseProcessor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestStatus.NO_CONSENT -> {
|
|
||||||
it.report = Report(
|
|
||||||
"Einwilligung Status fehlt, widerrufen oder ungeklärt."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
logger.error("Cannot process response: Unknown response!")
|
logger.error("Cannot process response: Unknown response!")
|
||||||
return@ifPresentOrElse
|
return@ifPresentOrElse
|
||||||
|
@ -19,7 +19,10 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.monitoring.*
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
||||||
|
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
||||||
|
import dev.dnpm.etl.processor.monitoring.OutputConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
import dev.dnpm.etl.processor.security.Role
|
import dev.dnpm.etl.processor.security.Role
|
||||||
@ -58,15 +61,11 @@ class ConfigController(
|
|||||||
val gPasConnectionAvailable =
|
val gPasConnectionAvailable =
|
||||||
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
||||||
|
|
||||||
val gIcsConnectionAvailable =
|
|
||||||
connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
|
||||||
|
|
||||||
model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName)
|
model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName)
|
||||||
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
||||||
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
||||||
model.addAttribute("outputConnectionAvailable", outputConnectionAvailable)
|
model.addAttribute("outputConnectionAvailable", outputConnectionAvailable)
|
||||||
model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable)
|
model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable)
|
||||||
model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable)
|
|
||||||
model.addAttribute("tokensEnabled", tokenService != null)
|
model.addAttribute("tokensEnabled", tokenService != null)
|
||||||
if (tokenService != null) {
|
if (tokenService != null) {
|
||||||
model.addAttribute("tokens", tokenService.findAll())
|
model.addAttribute("tokens", tokenService.findAll())
|
||||||
@ -120,24 +119,6 @@ class ConfigController(
|
|||||||
return "configs/gPasConnectionAvailable"
|
return "configs/gPasConnectionAvailable"
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(params = ["gIcsConnectionAvailable"])
|
|
||||||
fun gIcsConnectionAvailable(model: Model): String {
|
|
||||||
val gIcsConnectionAvailable =
|
|
||||||
connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
|
||||||
|
|
||||||
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
|
||||||
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
|
||||||
model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable)
|
|
||||||
if (tokenService != null) {
|
|
||||||
model.addAttribute("tokensEnabled", true)
|
|
||||||
model.addAttribute("tokens", tokenService.findAll())
|
|
||||||
} else {
|
|
||||||
model.addAttribute("tokens", listOf<Token>())
|
|
||||||
}
|
|
||||||
|
|
||||||
return "configs/gIcsConnectionAvailable"
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(path = ["tokens"])
|
@PostMapping(path = ["tokens"])
|
||||||
fun addToken(@ModelAttribute("name") name: String, model: Model): String {
|
fun addToken(@ModelAttribute("name") name: String, model: Model): String {
|
||||||
if (tokenService == null) {
|
if (tokenService == null) {
|
||||||
@ -209,7 +190,6 @@ class ConfigController(
|
|||||||
is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check"
|
is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check"
|
||||||
is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check"
|
is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check"
|
||||||
is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check"
|
is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check"
|
||||||
is ConnectionCheckResult.GIcsConnectionCheckResult -> "gics-connection-check"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerSentEvent.builder<Any>()
|
ServerSentEvent.builder<Any>()
|
||||||
|
@ -16,7 +16,6 @@ spring:
|
|||||||
content:
|
content:
|
||||||
enabled: true
|
enabled: true
|
||||||
paths: /**/*.js,/**/*.css,/**/*.svg,/**/*.jpeg
|
paths: /**/*.js,/**/*.css,/**/*.svg,/**/*.jpeg
|
||||||
app:
|
|
||||||
isGenomDeTestSubmission: true
|
|
||||||
server:
|
server:
|
||||||
forward-headers-strategy: framework
|
forward-headers-strategy: framework
|
@ -49,11 +49,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
|
|
||||||
<div th:insert="~{configs/gIcsConnectionAvailable.html}" th:hx-get="@{/configs?gIcsConnectionAvailable}" hx-trigger="sse:gics-connection-check">
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
|
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
|
||||||
<div th:insert="~{configs/outputConnectionAvailable.html}" th:hx-get="@{/configs?outputConnectionAvailable}" hx-trigger="sse:output-connection-check">
|
<div th:insert="~{configs/outputConnectionAvailable.html}" th:hx-get="@{/configs?outputConnectionAvailable}" hx-trigger="sse:output-connection-check">
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
<th:block th:if="${gIcsConnectionAvailable == null}">
|
|
||||||
<h2><span>🟦</span> gICS nicht konfiguriert - Einwilligung wird über Dateiinhalt geprüft</h2>
|
|
||||||
</th:block>
|
|
||||||
<th:block th:if="${gIcsConnectionAvailable != null}">
|
|
||||||
<h2><span th:if="${gIcsConnectionAvailable.available}">✅</span><span th:if="${not(gIcsConnectionAvailable.available)}">⚡</span> Verbindung zu gICS</h2>
|
|
||||||
<div>
|
|
||||||
Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gIcsConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(gIcsConnectionAvailable.timestamp)}"></time>
|
|
||||||
|
|
|
||||||
Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gIcsConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(gIcsConnectionAvailable.lastChange)}"></time>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Die Verbindung ist aktuell</span>
|
|
||||||
<strong th:if="${gIcsConnectionAvailable.available}" style="color: green">verfügbar.</strong>
|
|
||||||
<strong th:if="${not(gIcsConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
|
|
||||||
</div>
|
|
||||||
<div class="connection-display border">
|
|
||||||
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
|
||||||
<span class="connection" th:classappend="${gIcsConnectionAvailable.available ? 'available' : ''}"></span>
|
|
||||||
<img th:src="@{/server.png}" alt="gICS" />
|
|
||||||
<span>ETL-Processor</span>
|
|
||||||
<span></span>
|
|
||||||
<span>gICS</span>
|
|
||||||
</div>
|
|
||||||
</th:block>
|
|
@ -1,123 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
|
||||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration;
|
|
||||||
import dev.dnpm.etl.processor.config.AppFhirConfig;
|
|
||||||
import dev.dnpm.etl.processor.config.GIcsConfigProperties;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Date;
|
|
||||||
import org.hl7.fhir.r4.model.BooleanType;
|
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome.IssueType;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent;
|
|
||||||
import org.hl7.fhir.r4.model.Parameters;
|
|
||||||
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.test.context.ContextConfiguration;
|
|
||||||
import org.springframework.test.context.TestPropertySource;
|
|
||||||
import org.springframework.test.web.client.MockRestServiceServer;
|
|
||||||
|
|
||||||
@ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class})
|
|
||||||
@TestPropertySource(properties = {"app.consent.gics.enabled=true",
|
|
||||||
"app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"})
|
|
||||||
@RestClientTest
|
|
||||||
public class GicsConsentServiceTest {
|
|
||||||
|
|
||||||
public static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics";
|
|
||||||
@Autowired
|
|
||||||
MockRestServiceServer mockRestServiceServer;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
GicsConsentService gicsConsentService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
AppConfiguration appConfiguration;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
AppFhirConfig appFhirConfig;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
GIcsConfigProperties gIcsConfigProperties;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setUp() {
|
|
||||||
mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getTtpBroadConsentStatus() {
|
|
||||||
final Parameters responseConsented = new Parameters().addParameter(
|
|
||||||
new ParametersParameterComponent().setName("consented")
|
|
||||||
.setValue(new BooleanType().setValue(true)));
|
|
||||||
|
|
||||||
mockRestServiceServer.expect(requestTo(
|
|
||||||
"http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT))
|
|
||||||
.andRespond(withSuccess(appFhirConfig.fhirContext().newJsonParser()
|
|
||||||
.encodeResourceToString(responseConsented), MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
|
|
||||||
assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void consentRevoced() {
|
|
||||||
final Parameters responseRevoced = new Parameters().addParameter(
|
|
||||||
new ParametersParameterComponent().setName("consented")
|
|
||||||
.setValue(new BooleanType().setValue(false)));
|
|
||||||
|
|
||||||
mockRestServiceServer.expect(requestTo(
|
|
||||||
"http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT))
|
|
||||||
.andRespond(withSuccess(
|
|
||||||
appFhirConfig.fhirContext().newJsonParser().encodeResourceToString(responseRevoced),
|
|
||||||
MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
|
|
||||||
assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void gicsParameterInvalid() {
|
|
||||||
final OperationOutcome responseErrorOutcome = new OperationOutcome().addIssue(
|
|
||||||
new OperationOutcomeIssueComponent().setSeverity(IssueSeverity.ERROR)
|
|
||||||
.setCode(IssueType.PROCESSING).setDiagnostics("Invalid policy parameter..."));
|
|
||||||
|
|
||||||
mockRestServiceServer.expect(
|
|
||||||
requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
|
|
||||||
withSuccess(appFhirConfig.fhirContext().newJsonParser()
|
|
||||||
.encodeResourceToString(responseErrorOutcome), MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
|
|
||||||
assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void buildRequestParameterCurrentPolicyStatesForPersonTest() {
|
|
||||||
|
|
||||||
String pid = "12345678";
|
|
||||||
var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(
|
|
||||||
gIcsConfigProperties, pid, Date.from(Instant.now()),
|
|
||||||
gIcsConfigProperties.getGenomDeConsentDomainName());
|
|
||||||
|
|
||||||
assertThat(result.getParameter().size()).as("should contain 3 parameter resources")
|
|
||||||
.isEqualTo(3);
|
|
||||||
|
|
||||||
assertThat(((StringType) result.getParameter("domain").getValue()).getValue()).isEqualTo(
|
|
||||||
gIcsConfigProperties.getGenomDeConsentDomainName());
|
|
||||||
assertThat(
|
|
||||||
((Identifier) result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo(
|
|
||||||
pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -23,7 +23,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
import de.ukw.ccc.bwhc.dto.Patient
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||||
@ -35,7 +34,10 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.anyValueClass
|
||||||
|
import org.mockito.kotlin.times
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
@ -47,7 +49,7 @@ class KafkaInputListenerTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Mock requestProcessor: RequestProcessor,
|
@Mock requestProcessor: RequestProcessor
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
this.objectMapper = ObjectMapper()
|
this.objectMapper = ObjectMapper()
|
||||||
@ -92,10 +94,7 @@ class KafkaInputListenerTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(),
|
|
||||||
eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -148,8 +147,7 @@ class KafkaInputListenerTest {
|
|||||||
Optional.empty()
|
Optional.empty()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass(), eq(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass())
|
||||||
TtpConsentStatus.UNKNOWN_CHECK_FILE))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -180,8 +178,7 @@ class KafkaInputListenerTest {
|
|||||||
Optional.empty()
|
Optional.empty()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
verify(requestProcessor, times(0)).processDeletion(anyValueClass(), anyValueClass(), eq(
|
verify(requestProcessor, times(0)).processDeletion(anyValueClass(), anyValueClass())
|
||||||
TtpConsentStatus.UNKNOWN_CHECK_FILE))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,29 +21,21 @@ package dev.dnpm.etl.processor.input
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Consent.Status
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.consent.ConsentByMtbFile
|
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource
|
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.times
|
import org.mockito.Mockito.times
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.anyValueClass
|
import org.mockito.kotlin.anyValueClass
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import org.springframework.core.io.ClassPathResource
|
import org.springframework.core.io.ClassPathResource
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.context.TestPropertySource
|
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.delete
|
import org.springframework.test.web.servlet.delete
|
||||||
import org.springframework.test.web.servlet.post
|
import org.springframework.test.web.servlet.post
|
||||||
@ -61,22 +53,19 @@ class MtbFileRestControllerTest {
|
|||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
private lateinit var requestProcessor: RequestProcessor
|
||||||
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Mock requestProcessor: RequestProcessor
|
@Mock requestProcessor: RequestProcessor
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
val controller = MtbFileRestController(requestProcessor,
|
val controller = MtbFileRestController(requestProcessor)
|
||||||
ConsentByMtbFile()
|
|
||||||
)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequest() {
|
fun shouldProcessPostRequest() {
|
||||||
mockMvc.post("/mtbfile") {
|
mockMvc.post("/mtbfile") {
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE))
|
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE))
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
@ -90,8 +79,7 @@ class MtbFileRestControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequestWithRejectedConsent() {
|
fun shouldProcessPostRequestWithRejectedConsent() {
|
||||||
mockMvc.post("/mtbfile") {
|
mockMvc.post("/mtbfile") {
|
||||||
content =
|
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
@ -99,10 +87,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(),
|
|
||||||
org.mockito.kotlin.eq(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -113,100 +98,10 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(),
|
|
||||||
org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = ["app.consent.gics.enabled=true",
|
|
||||||
"app.consent.gics.gIcsBaseUri=http://localhost:8090/ttp-fhir/fhir/gics"]
|
|
||||||
)
|
|
||||||
@Nested
|
|
||||||
inner class BwhcRequestsCheckConsentViaTtp {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
|
||||||
|
|
||||||
private lateinit var gicsConsentService: GicsConsentService
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Mock requestProcessor: RequestProcessor,
|
|
||||||
@Mock gicsConsentService: GicsConsentService
|
|
||||||
) {
|
|
||||||
this.requestProcessor = requestProcessor
|
|
||||||
val controller = MtbFileRestController(requestProcessor, gicsConsentService)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
|
||||||
this.gicsConsentService = gicsConsentService
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ValueSource(strings = ["ACTIVE", "REJECTED"])
|
|
||||||
fun shouldProcessPostRequest(status: String) {
|
|
||||||
|
|
||||||
whenever(gicsConsentService.getTtpBroadConsentStatus(any())).thenReturn(TtpConsentStatus.BROAD_CONSENT_GIVEN)
|
|
||||||
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
content =
|
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.valueOf(status)))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ValueSource(strings = ["ACTIVE", "REJECTED"])
|
|
||||||
fun shouldProcessPostRequestWithRejectedConsent(status: String) {
|
|
||||||
|
|
||||||
whenever(gicsConsentService.getTtpBroadConsentStatus(any())).thenReturn(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED)
|
|
||||||
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
content =
|
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.valueOf(status)))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// consent status from ttp should override file consent value
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
|
||||||
anyValueClass(),
|
|
||||||
org.mockito.kotlin.eq(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessDeleteRequest() {
|
|
||||||
|
|
||||||
mockMvc.delete("/mtbfile/TEST_12345678").andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
|
||||||
anyValueClass(),
|
|
||||||
org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
|
||||||
)
|
|
||||||
verify(gicsConsentService, times(0)).getTtpBroadConsentStatus(any())
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class BwhcRequestsWithAlias {
|
inner class BwhcRequestsWithAlias {
|
||||||
|
|
||||||
@ -219,16 +114,14 @@ class MtbFileRestControllerTest {
|
|||||||
@Mock requestProcessor: RequestProcessor
|
@Mock requestProcessor: RequestProcessor
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
val controller = MtbFileRestController(requestProcessor,
|
val controller = MtbFileRestController(requestProcessor)
|
||||||
ConsentByMtbFile()
|
|
||||||
)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequest() {
|
fun shouldProcessPostRequest() {
|
||||||
mockMvc.post("/mtb") {
|
mockMvc.post("/mtb") {
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE))
|
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE))
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
@ -242,8 +135,7 @@ class MtbFileRestControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequestWithRejectedConsent() {
|
fun shouldProcessPostRequestWithRejectedConsent() {
|
||||||
mockMvc.post("/mtb") {
|
mockMvc.post("/mtb") {
|
||||||
content =
|
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
@ -251,11 +143,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(), org.mockito.kotlin.eq(
|
|
||||||
TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -266,11 +154,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(), org.mockito.kotlin.eq(
|
|
||||||
TtpConsentStatus.UNKNOWN_CHECK_FILE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,21 +167,16 @@ class MtbFileRestControllerTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Mock requestProcessor: RequestProcessor,
|
@Mock requestProcessor: RequestProcessor
|
||||||
@Mock gicsConsentService: GicsConsentService
|
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
val controller = MtbFileRestController(requestProcessor,
|
val controller = MtbFileRestController(requestProcessor)
|
||||||
gicsConsentService
|
|
||||||
)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldRespondPostRequest() {
|
fun shouldRespondPostRequest() {
|
||||||
val mtbFileContent =
|
val mtbFileContent = ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes().toString(Charsets.UTF_8)
|
||||||
ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes()
|
|
||||||
.toString(Charsets.UTF_8)
|
|
||||||
|
|
||||||
mockMvc.post("/mtb") {
|
mockMvc.post("/mtb") {
|
||||||
content = mtbFileContent
|
content = mtbFileContent
|
||||||
@ -314,7 +193,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun bwhcMtbFileContent(consentStatus: Status) = MtbFile.builder()
|
fun bwhcMtbFileContent(consentStatus: Consent.Status) = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("TEST_12345678")
|
.withId("TEST_12345678")
|
||||||
|
@ -19,18 +19,11 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.pseudonym
|
package dev.dnpm.etl.processor.pseudonym
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
import de.ukw.ccc.bwhc.dto.Patient
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.config.GIcsConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.config.JacksonConfig
|
|
||||||
import dev.dnpm.etl.processor.services.ConsentProcessor
|
|
||||||
import dev.dnpm.etl.processor.services.ConsentProcessorTest
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.hl7.fhir.r4.model.Bundle
|
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
@ -46,9 +39,6 @@ import java.util.*
|
|||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class ExtensionsTest {
|
class ExtensionsTest {
|
||||||
fun getObjectMapper(): ObjectMapper {
|
|
||||||
return JacksonConfig().objectMapper()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class UsingBwhcDatamodel {
|
inner class UsingBwhcDatamodel {
|
||||||
@ -56,14 +46,13 @@ class ExtensionsTest {
|
|||||||
val FAKE_MTB_FILE_PATH = "fake_MTBFile.json"
|
val FAKE_MTB_FILE_PATH = "fake_MTBFile.json"
|
||||||
val CLEAN_PATIENT_ID = "5dad2f0b-49c6-47d8-a952-7b9e9e0f7549"
|
val CLEAN_PATIENT_ID = "5dad2f0b-49c6-47d8-a952-7b9e9e0f7549"
|
||||||
|
|
||||||
|
|
||||||
private fun fakeMtbFile(): MtbFile {
|
private fun fakeMtbFile(): MtbFile {
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
||||||
return getObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
return ObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MtbFile.serialized(): String {
|
private fun MtbFile.serialized(): String {
|
||||||
return getObjectMapper().writeValueAsString(this)
|
return ObjectMapper().writeValueAsString(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -97,9 +86,7 @@ class ExtensionsTest {
|
|||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
mtbFile.anonymizeContentWith(pseudonymizeService)
|
||||||
|
|
||||||
val pattern =
|
val pattern = "\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex().toPattern()
|
||||||
"\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex()
|
|
||||||
.toPattern()
|
|
||||||
val matcher = pattern.matcher(mtbFile.serialized())
|
val matcher = pattern.matcher(mtbFile.serialized())
|
||||||
|
|
||||||
assertThrows<IllegalStateException> {
|
assertThrows<IllegalStateException> {
|
||||||
@ -224,11 +211,11 @@ class ExtensionsTest {
|
|||||||
|
|
||||||
private fun fakeMtbFile(): Mtb {
|
private fun fakeMtbFile(): Mtb {
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
||||||
return getObjectMapper().readValue(mtbFile, Mtb::class.java)
|
return ObjectMapper().readValue(mtbFile, Mtb::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Mtb.serialized(): String {
|
private fun Mtb.serialized(): String {
|
||||||
return getObjectMapper().writeValueAsString(this)
|
return ObjectMapper().writeValueAsString(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -239,8 +226,6 @@ class ExtensionsTest {
|
|||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
||||||
|
|
||||||
val mtbFile = fakeMtbFile()
|
val mtbFile = fakeMtbFile()
|
||||||
mtbFile.ensureMetaDataIsInitialized()
|
|
||||||
addConsentData(mtbFile)
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
|
|
||||||
@ -248,25 +233,6 @@ class ExtensionsTest {
|
|||||||
assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID)
|
assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addConsentData(mtbFile: Mtb) {
|
|
||||||
val gIcsConfigProperties = GIcsConfigProperties("", "", "", true)
|
|
||||||
val appConfigProperties = AppConfigProperties(null, emptyList())
|
|
||||||
|
|
||||||
val bundle = Bundle()
|
|
||||||
val dummyConsent = ConsentProcessorTest.getDummyGenomDeConsent()
|
|
||||||
dummyConsent.patient.reference = "Patient/$CLEAN_PATIENT_ID"
|
|
||||||
bundle.addEntry().resource = dummyConsent
|
|
||||||
|
|
||||||
ConsentProcessor(
|
|
||||||
appConfigProperties,
|
|
||||||
gIcsConfigProperties,
|
|
||||||
JacksonConfig().objectMapper(),
|
|
||||||
FhirContext.forR4(),
|
|
||||||
null
|
|
||||||
).embedBroadConsentResources(mtbFile, bundle)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
|
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
|
@ -90,4 +90,10 @@ class PseudonymizeServiceTest {
|
|||||||
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
|
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldReturnDifferentValues() {
|
||||||
|
val ag = AnonymizingGenerator()
|
||||||
|
val tan = ag.generateGenomDeTan("12345")
|
||||||
|
assertThat(tan).hasSize(64)
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,168 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.services
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.config.GIcsConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.config.JacksonConfig
|
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.hl7.fhir.r4.model.Bundle
|
|
||||||
import org.hl7.fhir.r4.model.CodeableConcept
|
|
||||||
import org.hl7.fhir.r4.model.Coding
|
|
||||||
import org.hl7.fhir.r4.model.Consent
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
|
||||||
import org.junit.jupiter.params.provider.CsvSource
|
|
||||||
import org.mockito.Mock
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
|
||||||
import org.mockito.kotlin.any
|
|
||||||
import org.mockito.kotlin.doAnswer
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import org.springframework.core.io.ClassPathResource
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
|
||||||
class ConsentProcessorTest {
|
|
||||||
|
|
||||||
private lateinit var appConfigProperties: AppConfigProperties
|
|
||||||
private lateinit var gicsConsentService: GicsConsentService
|
|
||||||
private lateinit var objectMapper: ObjectMapper
|
|
||||||
private lateinit var gIcsConfigProperties: GIcsConfigProperties
|
|
||||||
private lateinit var fhirContext: FhirContext
|
|
||||||
private lateinit var consentProcessor: ConsentProcessor
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setups(
|
|
||||||
@Mock gicsConsentService: GicsConsentService,
|
|
||||||
) {
|
|
||||||
|
|
||||||
this.gIcsConfigProperties = GIcsConfigProperties(null, null, null, true)
|
|
||||||
val jacksonConfig = JacksonConfig()
|
|
||||||
this.objectMapper = jacksonConfig.objectMapper()
|
|
||||||
this.fhirContext = JacksonConfig.fhirContext()
|
|
||||||
this.gicsConsentService = gicsConsentService
|
|
||||||
this.appConfigProperties = AppConfigProperties(null, emptyList())
|
|
||||||
this.consentProcessor =
|
|
||||||
ConsentProcessor(
|
|
||||||
appConfigProperties,
|
|
||||||
gIcsConfigProperties,
|
|
||||||
objectMapper,
|
|
||||||
fhirContext,
|
|
||||||
gicsConsentService
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun consentOk() {
|
|
||||||
assertThat(consentProcessor.toString()).isNotNull
|
|
||||||
// prep gICS response
|
|
||||||
doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService)
|
|
||||||
.getBroadConsent(any(), any())
|
|
||||||
doAnswer { Bundle() }.whenever(gicsConsentService)
|
|
||||||
.getGenomDeConsent(any(), any())
|
|
||||||
|
|
||||||
val inputMtb = Mtb.builder()
|
|
||||||
.patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build()
|
|
||||||
val checkResult = consentProcessor.consentGatedCheckAndTryEmbedding(inputMtb)
|
|
||||||
|
|
||||||
assertThat(checkResult).isTrue
|
|
||||||
assertThat(inputMtb.metadata.researchConsents).hasSize(13)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getDummyGenomDeConsent(): Consent {
|
|
||||||
val consent = Consent()
|
|
||||||
consent.id = "consent 1 id"
|
|
||||||
consent.patient.reference = "Patient/1234-pat1"
|
|
||||||
|
|
||||||
consent.provision.setType(
|
|
||||||
Consent.ConsentProvisionType.fromCode(
|
|
||||||
"deny"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
consent.provision.period.start =
|
|
||||||
Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
|
|
||||||
consent.provision.period.end =
|
|
||||||
Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
|
|
||||||
|
|
||||||
val addProvision1 = consent.provision.addProvision()
|
|
||||||
addProvision1.setType(Consent.ConsentProvisionType.fromCode("permit"))
|
|
||||||
addProvision1.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
|
|
||||||
addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
|
|
||||||
addProvision1.code.addLast(
|
|
||||||
CodeableConcept(
|
|
||||||
Coding(
|
|
||||||
"https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"Teilnahme",
|
|
||||||
"Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val addProvision2 = consent.provision.addProvision()
|
|
||||||
addProvision2.setType(Consent.ConsentProvisionType.fromCode("deny"))
|
|
||||||
addProvision2.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
|
|
||||||
addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
|
|
||||||
addProvision2.code.addLast(
|
|
||||||
CodeableConcept(
|
|
||||||
Coding(
|
|
||||||
"https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"Rekontaktierung",
|
|
||||||
"Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return consent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@CsvSource(
|
|
||||||
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,PERMIT,expect permit",
|
|
||||||
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on start",
|
|
||||||
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2055-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on end",
|
|
||||||
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2021-06-23T00:00:00+02:00,NULL,date is before start",
|
|
||||||
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2060-06-23T00:00:00+02:00,NULL,date is after end",
|
|
||||||
"2.16.840.1.113883.3.1937.777.24.5.3.8,XXXX,2025-07-23T00:00:00+02:00,NULL,system not found - therefore expect NULL",
|
|
||||||
"2.16.840.1.113883.3.1937.777.24.5.3.27,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,DENY,provision is denied"
|
|
||||||
)
|
|
||||||
fun getProvisionTypeByPolicyCode(
|
|
||||||
code: String?, system: String?, timeStamp: String, expected: String?,
|
|
||||||
desc: String?
|
|
||||||
) {
|
|
||||||
val testData = getDummyBroadConsentBundle()
|
|
||||||
|
|
||||||
val requestDate = Date.from(OffsetDateTime.parse(timeStamp).toInstant())
|
|
||||||
|
|
||||||
val result: Consent.ConsentProvisionType =
|
|
||||||
consentProcessor.getProvisionTypeByPolicyCode(testData, code, system, requestDate)
|
|
||||||
assertThat(result).isNotNull()
|
|
||||||
|
|
||||||
|
|
||||||
assertThat(result).`as`(desc)
|
|
||||||
.isEqualTo(Consent.ConsentProvisionType.valueOf(expected!!))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDummyBroadConsentBundle(): Bundle {
|
|
||||||
val bundle: InputStream?
|
|
||||||
try {
|
|
||||||
bundle = ClassPathResource(
|
|
||||||
"fake_broadConsent_gics_response_permit.json"
|
|
||||||
).getInputStream()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return FhirContext.forR4().newJsonParser()
|
|
||||||
.parseResource<Bundle>(Bundle::class.java, bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -25,8 +25,6 @@ import dev.dnpm.etl.processor.Fingerprint
|
|||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
@ -60,7 +58,7 @@ class RequestProcessorTest {
|
|||||||
private lateinit var requestService: RequestService
|
private lateinit var requestService: RequestService
|
||||||
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
||||||
private lateinit var appConfigProperties: AppConfigProperties
|
private lateinit var appConfigProperties: AppConfigProperties
|
||||||
private lateinit var consentProcessor: ConsentProcessor
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
private lateinit var requestProcessor: RequestProcessor
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -69,8 +67,7 @@ class RequestProcessorTest {
|
|||||||
@Mock transformationService: TransformationService,
|
@Mock transformationService: TransformationService,
|
||||||
@Mock sender: RestMtbFileSender,
|
@Mock sender: RestMtbFileSender,
|
||||||
@Mock requestService: RequestService,
|
@Mock requestService: RequestService,
|
||||||
@Mock applicationEventPublisher: ApplicationEventPublisher,
|
@Mock applicationEventPublisher: ApplicationEventPublisher
|
||||||
@Mock consentProcessor: ConsentProcessor
|
|
||||||
) {
|
) {
|
||||||
this.pseudonymizeService = pseudonymizeService
|
this.pseudonymizeService = pseudonymizeService
|
||||||
this.transformationService = transformationService
|
this.transformationService = transformationService
|
||||||
@ -78,7 +75,6 @@ class RequestProcessorTest {
|
|||||||
this.requestService = requestService
|
this.requestService = requestService
|
||||||
this.applicationEventPublisher = applicationEventPublisher
|
this.applicationEventPublisher = applicationEventPublisher
|
||||||
this.appConfigProperties = AppConfigProperties(null)
|
this.appConfigProperties = AppConfigProperties(null)
|
||||||
this.consentProcessor = consentProcessor
|
|
||||||
|
|
||||||
val objectMapper = ObjectMapper()
|
val objectMapper = ObjectMapper()
|
||||||
|
|
||||||
@ -89,8 +85,7 @@ class RequestProcessorTest {
|
|||||||
requestService,
|
requestService,
|
||||||
objectMapper,
|
objectMapper,
|
||||||
applicationEventPublisher,
|
applicationEventPublisher,
|
||||||
appConfigProperties,
|
appConfigProperties
|
||||||
consentProcessor
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,10 +343,7 @@ class RequestProcessorTest {
|
|||||||
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
TEST_PATIENT_ID,
|
|
||||||
isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE
|
|
||||||
)
|
|
||||||
|
|
||||||
val requestCaptor = argumentCaptor<Request>()
|
val requestCaptor = argumentCaptor<Request>()
|
||||||
verify(requestService, times(1)).save(requestCaptor.capture())
|
verify(requestService, times(1)).save(requestCaptor.capture())
|
||||||
@ -369,10 +361,7 @@ class RequestProcessorTest {
|
|||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
TEST_PATIENT_ID,
|
|
||||||
isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE
|
|
||||||
)
|
|
||||||
|
|
||||||
val eventCaptor = argumentCaptor<ResponseEvent>()
|
val eventCaptor = argumentCaptor<ResponseEvent>()
|
||||||
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
||||||
@ -390,10 +379,7 @@ class RequestProcessorTest {
|
|||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
TEST_PATIENT_ID,
|
|
||||||
isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE
|
|
||||||
)
|
|
||||||
|
|
||||||
val eventCaptor = argumentCaptor<ResponseEvent>()
|
val eventCaptor = argumentCaptor<ResponseEvent>()
|
||||||
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
||||||
@ -405,10 +391,7 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
|
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
|
||||||
doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
TEST_PATIENT_ID,
|
|
||||||
isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE
|
|
||||||
)
|
|
||||||
|
|
||||||
val requestCaptor = argumentCaptor<Request>()
|
val requestCaptor = argumentCaptor<Request>()
|
||||||
verify(requestService, times(1)).save(requestCaptor.capture())
|
verify(requestService, times(1)).save(requestCaptor.capture())
|
||||||
|
@ -19,23 +19,14 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.Diagnosis
|
import de.ukw.ccc.bwhc.dto.Diagnosis
|
||||||
import de.ukw.ccc.bwhc.dto.Icd10
|
import de.ukw.ccc.bwhc.dto.Icd10
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.config.JacksonConfig
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.ConsentProvision
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.MvhMetadata
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.Provision
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class TransformationServiceTest {
|
class TransformationServiceTest {
|
||||||
|
|
||||||
@ -44,7 +35,7 @@ class TransformationServiceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
this.service = TransformationService(
|
this.service = TransformationService(
|
||||||
JacksonConfig().objectMapper(), listOf(
|
ObjectMapper(), listOf(
|
||||||
Transformation.of("consent.status") from Consent.Status.ACTIVE to Consent.Status.REJECTED,
|
Transformation.of("consent.status") from Consent.Status.ACTIVE to Consent.Status.REJECTED,
|
||||||
Transformation.of("diagnoses[*].icd10.version") from "2013" to "2014",
|
Transformation.of("diagnoses[*].icd10.version") from "2013" to "2014",
|
||||||
)
|
)
|
||||||
@ -101,59 +92,4 @@ class TransformationServiceTest {
|
|||||||
assertThat(actual.consent.status).isEqualTo(Consent.Status.REJECTED)
|
assertThat(actual.consent.status).isEqualTo(Consent.Status.REJECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldTransformConsentValues() {
|
|
||||||
val mtbFile = MtbFile.builder().withDiagnoses(
|
|
||||||
listOf(
|
|
||||||
Diagnosis.builder().withId("1234").withIcd10(Icd10("F79.9").also {
|
|
||||||
it.version = "2013"
|
|
||||||
}).build(),
|
|
||||||
Diagnosis.builder().withId("5678").withIcd10(Icd10("F79.8").also {
|
|
||||||
it.version = "2019"
|
|
||||||
}).build()
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
|
|
||||||
val actual = this.service.transform(mtbFile)
|
|
||||||
|
|
||||||
assertThat(actual).isNotNull
|
|
||||||
assertThat(actual.diagnoses[0].icd10.code).isEqualTo("F79.9")
|
|
||||||
assertThat(actual.diagnoses[0].icd10.version).isEqualTo("2014")
|
|
||||||
assertThat(actual.diagnoses[1].icd10.code).isEqualTo("F79.8")
|
|
||||||
assertThat(actual.diagnoses[1].icd10.version).isEqualTo("2019")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldTransformConsent() {
|
|
||||||
val mvhMetadata = MvhMetadata.builder().transferTan("transfertan12345").build()
|
|
||||||
|
|
||||||
assertThat(mvhMetadata).isNotNull
|
|
||||||
mvhMetadata.modelProjectConsent =
|
|
||||||
ModelProjectConsent.builder().date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z")))
|
|
||||||
.version("1").provisions(
|
|
||||||
listOf(
|
|
||||||
Provision.builder().type(ConsentProvision.PERMIT)
|
|
||||||
.purpose(ModelProjectConsentPurpose.SEQUENCING)
|
|
||||||
.date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(),
|
|
||||||
Provision.builder().type(ConsentProvision.PERMIT)
|
|
||||||
.purpose(ModelProjectConsentPurpose.REIDENTIFICATION)
|
|
||||||
.date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(),
|
|
||||||
Provision.builder().type(ConsentProvision.DENY)
|
|
||||||
.purpose(ModelProjectConsentPurpose.CASE_IDENTIFICATION)
|
|
||||||
.date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build()
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
val consent = ConsentProcessorTest.getDummyGenomDeConsent()
|
|
||||||
|
|
||||||
mvhMetadata.researchConsents = mutableListOf()
|
|
||||||
mvhMetadata.researchConsents.add(mapOf(consent.id to consent as IBaseResource))
|
|
||||||
|
|
||||||
val mtbFile = Mtb.builder().metadata(mvhMetadata).build()
|
|
||||||
|
|
||||||
val transformed = service.transform(mtbFile)
|
|
||||||
assertThat(transformed.metadata.modelProjectConsent.date).isNotNull
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,333 +0,0 @@
|
|||||||
{
|
|
||||||
"resourceType": "Bundle",
|
|
||||||
"type": "collection",
|
|
||||||
"entry": [
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673204-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "24673204-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:58:27.178+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 999999"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Teilnahme",
|
|
||||||
"display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Teilnahme",
|
|
||||||
"display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673913-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "24673913-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:58:27.194+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 999999"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Fallidentifizierung",
|
|
||||||
"display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Fallidentifizierung",
|
|
||||||
"display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673da9-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "24673da9-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:58:27.211+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 999999"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Rekontaktierung",
|
|
||||||
"display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Rekontaktierung",
|
|
||||||
"display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,333 +0,0 @@
|
|||||||
{
|
|
||||||
"resourceType": "Bundle",
|
|
||||||
"type": "collection",
|
|
||||||
"entry": [
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121a8368-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "121a8368-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:55:42.079+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 12345678"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Teilnahme",
|
|
||||||
"display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "permit",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Teilnahme",
|
|
||||||
"display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121aad40-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "121aad40-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:55:42.096+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 12345678"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Fallidentifizierung",
|
|
||||||
"display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "permit",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Fallidentifizierung",
|
|
||||||
"display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121ac5f8-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "121ac5f8-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:55:42.110+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 12345678"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Rekontaktierung",
|
|
||||||
"display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "permit",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Rekontaktierung",
|
|
||||||
"display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
Reference in New Issue
Block a user