mirror of
https://github.com/pcvolkmer/mv64e-etl-processor
synced 2025-09-13 09:02:50 +00:00
Compare commits
29 Commits
v0.10.0
...
v0.11.0-rc
Author | SHA1 | Date | |
---|---|---|---|
eed0972018 | |||
|
3b66f42eb2 | ||
c40fd7f816 | |||
1759729931 | |||
|
7f80224eac | ||
3eb1c79cec | |||
|
be513f305a | ||
2e88157893 | |||
bf898e5c25 | |||
e5693736d8 | |||
|
dfc9de78ce | ||
|
199511e567 | ||
1319be8b3f | |||
1a5737189c | |||
7543785116 | |||
858189aa59 | |||
17f4dc3512 | |||
1dd601e8db | |||
b748603c06 | |||
b939b2bf57 | |||
c6b37fda69 | |||
8e3de6a220 | |||
c5c553f817 | |||
7d97365aea | |||
48b1e62e22 | |||
66cc818755 | |||
9d4786fae3 | |||
b78dc3519b | |||
46015c5b66 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -39,3 +39,5 @@ out/
|
|||||||
.vscode/
|
.vscode/
|
||||||
/dev/gpas*
|
/dev/gpas*
|
||||||
/deploy/.env
|
/deploy/.env
|
||||||
|
/dev/gICS*
|
||||||
|
/dev/gPAS*
|
||||||
|
354
README.md
354
README.md
@@ -1,12 +1,15 @@
|
|||||||
# ETL-Processor for DNPM:DIP [](https://github.com/pcvolkmer/etl-processor/actions/workflows/test.yml)
|
# ETL-Processor für das MV gem. §64e und DNPM:DIP
|
||||||
|
[](https://github.com/pcvolkmer/etl-processor/actions/workflows/test.yml)
|
||||||
|
|
||||||
Diese Anwendung versendet ein bwHC-MTB-File im bwHC-Datenmodell 1.0 an DNPM:DIP und pseudonymisiert die Patienten-ID.
|
Diese Anwendung pseudonymisiert/anonymisiert Daten im DNPM-Datenmodell 2.1 für das Modellvorhaben
|
||||||
|
Genomsequenzierung nach §64e unter Beachtung des Consents und sendet sie an DNPM:DIP.
|
||||||
|
|
||||||
## Einordnung innerhalb einer DNPM-ETL-Strecke
|
## Einordnung innerhalb einer DNPM-ETL-Strecke
|
||||||
|
|
||||||
Diese Anwendung erlaubt das Entgegennehmen von HTTP/REST-Anfragen aus dem Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)**.
|
Diese Anwendung erlaubt das Entgegennehmen von HTTP/REST-Anfragen aus dem Onkostar-Plugin
|
||||||
|
**[mv64e-onkostar-plugin-export](https://github.com/pcvolkmer/mv64e-onkostar-plugin-export)**.
|
||||||
|
|
||||||
Der Inhalt einer Anfrage, wenn ein bwHC-MTBFile, wird pseudonymisiert und auf Duplikate geprüft.
|
Der Inhalt einer Anfrage, wenn ein MTB-File, wird pseudonymisiert und auf Duplikate geprüft.
|
||||||
Duplikate werden verworfen, Änderungen werden weitergeleitet.
|
Duplikate werden verworfen, Änderungen werden weitergeleitet.
|
||||||
|
|
||||||
Löschanfragen werden immer als Löschanfrage an DNPM:DIP weitergeleitet.
|
Löschanfragen werden immer als Löschanfrage an DNPM:DIP weitergeleitet.
|
||||||
@@ -15,16 +18,48 @@ Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### 🔥 Wichtige Änderungen in Version 0.11
|
||||||
|
|
||||||
|
Ab Version 0.11 wird ausschließlich [DNPM:DIP](https://github.com/dnpm-dip) unterstützt.
|
||||||
|
|
||||||
|
Zudem wurde der Name des Pakets in **mv64e-etl-processor** geändert.
|
||||||
|
|
||||||
|
## Funktionsweise
|
||||||
|
|
||||||
### Duplikaterkennung
|
### Duplikaterkennung
|
||||||
|
|
||||||
Die Erkennung von Duplikaten ist normalerweise immer aktiv, kann jedoch über den Konfigurationsparameter
|
Die Erkennung von Duplikaten ist normalerweise immer aktiv, kann jedoch über den
|
||||||
|
Konfigurationsparameter
|
||||||
`APP_DUPLICATION_DETECTION=false` deaktiviert werden.
|
`APP_DUPLICATION_DETECTION=false` deaktiviert werden.
|
||||||
|
|
||||||
|
### Modelvorhaben genomDE §64e
|
||||||
|
|
||||||
|
#### Vorgangsummern
|
||||||
|
Zusätzlich zur Patienten Identifier Pseudonymisierung müssen Vorgangsummern generiert werden, die
|
||||||
|
jede Übertragung eindeutig identifizieren aber gleichzeitig dem Patienten zugeordnet werden können.
|
||||||
|
Dies lässt sich durch weitere Pseudonyme abbilden, allerdings werden pro Originalwert mehrere
|
||||||
|
Pseudonyme benötigt.
|
||||||
|
Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werden (siehe auch
|
||||||
|
*APP_PSEUDONYMIZE_GPAS_CCDN*).
|
||||||
|
|
||||||
|
**WICHTIG:** Deaktivierte Pseudonymisierung ist nur für Tests nutzbar. Vorgangsummern sind zufällig
|
||||||
|
und werden anschließend verworfen.
|
||||||
|
|
||||||
|
#### Test Betriebsbereitschaft
|
||||||
|
Um die voll Betriebsbereitschaft herzustellen, muss eine erfolgreiche Übertragung mit dem
|
||||||
|
Submission-Typ *Test* erfolgt sein. Über die Umgebungsvariable wird dieser Übertragungsmodus
|
||||||
|
aktiviert. Alle Datensätze mit erteilter Teilnahme am Modelvorhaben werden mit der Test-Submission-Kennung
|
||||||
|
übertragen, unabhängig vom ursprünglichen Wert.
|
||||||
|
|
||||||
|
`APP_GENOM_DE_TEST_SUBMISSION` -> `true` | `false` (falls fehlt, wird `false` angenommen)
|
||||||
|
|
||||||
### Datenübermittlung über HTTP/REST
|
### Datenübermittlung über HTTP/REST
|
||||||
|
|
||||||
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an DNPM:DIP gesendet.
|
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an DNPM:DIP
|
||||||
|
gesendet.
|
||||||
|
|
||||||
Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.example.com` an nachfolgende URLs gesendet werden:
|
Ein HTTP-Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.example.com` an
|
||||||
|
nachfolgende URLs gesendet werden:
|
||||||
|
|
||||||
| HTTP-Request | URL | Consent-Status im Datensatz | Bemerkung |
|
| HTTP-Request | URL | Consent-Status im Datensatz | Bemerkung |
|
||||||
|--------------|-----------------------------------------|-----------------------------|---------------------------------------------------------------------------------|
|
|--------------|-----------------------------------------|-----------------------------|---------------------------------------------------------------------------------|
|
||||||
@@ -32,34 +67,20 @@ Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.e
|
|||||||
| `POST` | `https://dnpm.example.com/mtb` | `REJECT` | Die Anwendung sendet einen Lösch-Request für die im Datensatz angegebene Pat-ID |
|
| `POST` | `https://dnpm.example.com/mtb` | `REJECT` | Die Anwendung sendet einen Lösch-Request für die im Datensatz angegebene Pat-ID |
|
||||||
| `DELETE` | `https://dnpm.example.com/mtb/12345678` | - | Die Anwendung sendet einen Lösch-Request für Pat-ID `12345678` |
|
| `DELETE` | `https://dnpm.example.com/mtb/12345678` | - | Die Anwendung sendet einen Lösch-Request für Pat-ID `12345678` |
|
||||||
|
|
||||||
Anstelle des Pfads `/mtb` kann auch, wie in Version 0.9 und älter üblich, `/mtbfile` verwendet werden.
|
Anstelle des Pfads `/mtb` kann auch, wie in Version 0.9 und älter üblich, `/mtbfile` verwendet
|
||||||
|
werden.
|
||||||
|
|
||||||
### Datenübermittlung mit Apache Kafka
|
### Datenübermittlung mit Apache Kafka
|
||||||
|
|
||||||
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung an Apache Kafka übergeben.
|
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung an Apache Kafka
|
||||||
Eine Antwort wird dabei ebenfalls mithilfe von Apache Kafka übermittelt und nach der Entgegennahme verarbeitet.
|
übergeben.
|
||||||
|
Eine Antwort wird dabei ebenfalls mithilfe von Apache Kafka übermittelt und nach der Entgegennahme
|
||||||
|
verarbeitet.
|
||||||
|
|
||||||
Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc
|
Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
### 🔥 Wichtige Änderungen in Version 0.10
|
|
||||||
|
|
||||||
Ab Version 0.10 wird [DNPM:DIP](https://github.com/dnpm-dip) unterstützt und als Standardendpunkt verwendet.
|
|
||||||
Soll noch das alte bwHC-Backend verwendet werden, so ist die Umgebungsvariable `APP_REST_IS_BWHC` auf `true` zu setzen.
|
|
||||||
|
|
||||||
### 🔥 Breaking Changes nach Version 0.10
|
|
||||||
|
|
||||||
In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen entfernt:
|
|
||||||
|
|
||||||
* `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`: Nutzen Sie hier, wie unter [_Integration eines eigenen Root CA
|
|
||||||
Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben, das Einbinden eigener Zertifikate.
|
|
||||||
* `APP_KAFKA_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_TOPIC`
|
|
||||||
* `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`
|
|
||||||
|
|
||||||
Der Pfad zum Versenden von MTB-Daten ist nun offiziell `/mtb`.
|
|
||||||
In Versionen **nach Version 0.10** wird die Unterstützung des Pfads `/mtbfile` entfernt.
|
|
||||||
|
|
||||||
### Pseudonymisierung der Patienten-ID
|
### Pseudonymisierung der Patienten-ID
|
||||||
|
|
||||||
Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) angegeben ist, wird diese verwendet.
|
Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) angegeben ist, wird diese verwendet.
|
||||||
@@ -68,40 +89,93 @@ Ist diese nicht gesetzt. wird intern eine Anonymisierung der Patienten-ID vorgen
|
|||||||
* `APP_PSEUDONYMIZE_PREFIX`: Standortbezogenes Präfix - `UNKNOWN`, wenn nicht gesetzt
|
* `APP_PSEUDONYMIZE_PREFIX`: Standortbezogenes Präfix - `UNKNOWN`, wenn nicht gesetzt
|
||||||
* `APP_PSEUDONYMIZE_GENERATOR`: `BUILDIN` oder `GPAS` - `BUILDIN`, wenn nicht gesetzt
|
* `APP_PSEUDONYMIZE_GENERATOR`: `BUILDIN` oder `GPAS` - `BUILDIN`, wenn nicht gesetzt
|
||||||
|
|
||||||
**Hinweise**:
|
**Hinweis**
|
||||||
|
|
||||||
* Der alte Konfigurationsparameter `APP_PSEUDONYMIZER` mit den Werten `GPAS` oder `BUILDIN` sollte nicht mehr verwendet
|
Die Pseudonymisierung erfolgt im ETL-Prozessor nur für die Patienten-ID.
|
||||||
werden.
|
Andere IDs werden mithilfe des standortbezogenen Präfixes (erneut) anonymisiert, um für den
|
||||||
* Die Pseudonymisierung erfolgt im ETL-Prozessor nur für die Patienten-ID.
|
aktuellen Kontext nicht
|
||||||
Andere IDs werden mithilfe des standortbezogenen Präfixes (erneut) anonymisiert, um für den aktuellen Kontext nicht
|
vergleichbare IDs bereitzustellen.
|
||||||
vergleichbare IDs bereitzustellen.
|
|
||||||
|
|
||||||
#### Eingebaute Anonymisierung
|
#### Eingebaute Anonymisierung
|
||||||
|
|
||||||
Wurde keine oder die Verwendung der eingebauten Anonymisierung konfiguriert, so wird für die Patienten-ID der
|
Wurde keine oder die Verwendung der eingebauten Anonymisierung konfiguriert, so wird für die
|
||||||
entsprechende SHA-256-Hash gebildet und Base64-codiert - hier ohne endende "=" - zuzüglich des konfigurierten Präfixes
|
Patienten-ID der entsprechende SHA-256-Hash gebildet und Base64-codiert - hier ohne endende
|
||||||
als Patienten-Pseudonym verwendet.
|
"=" - zuzüglich des konfigurierten Präfixes als Patienten-Pseudonym verwendet.
|
||||||
|
|
||||||
#### Pseudonymisierung mit gPAS
|
#### Pseudonymisierung mit gPAS
|
||||||
|
|
||||||
Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfigurieren.
|
Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfigurieren.
|
||||||
|
|
||||||
* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz inklusive Endpoint (z.B. `http://localhost:8080/ttp-fhir/fhir/gpas/$$pseudonymizeAllowCreate`)
|
Ab Version 2025.1 (Multi-Pseudonym Support)
|
||||||
* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname
|
|
||||||
|
* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz REST API (e.g. http://127.0.0.1:9990/ttp-fhir/fhir/gpas)
|
||||||
* `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_SSLCALOCATION`~~: **Veraltet** - Root Zertifikat für gPas, falls es dediziert hinzugefügt werden muss.
|
* `APP_PSEUDONYMIZE_GPAS_PID_DOMAIN`: gPas Domänenname für Patienten ID
|
||||||
**Wird in nach Version 0.10 entfernt**
|
* `APP_PSEUDONYMIZE_GPAS_GENOM_DE_TAN_DOMAIN`: gPas Multi-Pseudonym-Domäne für genomDE Vorgangsnummern (
|
||||||
|
Clinical data node)
|
||||||
|
|
||||||
Der Konfigurationsparameter `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION` sollte nicht mehr verwendet werden und wird nach
|
### (Externe) Consent-Services
|
||||||
Version 0.10 entfernt.
|
|
||||||
Stattdessen sollte das Root Zertifikat wie unter [_Integration eines eigenen Root CA
|
Consent-Services können konfiguriert werden.
|
||||||
Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben eingebunden werden.
|
|
||||||
|
* `APP_CONSENT_SERVICE`: Zu verwendender (externer) Consent-Service:
|
||||||
|
* `NONE`: Verwende Consent-Angaben im MTB-File v1 und ändere diese nicht. Für MTB-File v2 wird
|
||||||
|
die Prüfung übersprungen.
|
||||||
|
* `GICS`: Verwende gICS der Greiswalder Tools (siehe unten).
|
||||||
|
|
||||||
|
#### Einwilligung gICS
|
||||||
|
|
||||||
|
Ab gIcs Version 2.13.0 kann im ETL-Processor
|
||||||
|
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* (MTB-File v1) und *currentPolicyStatesForPerson* (MTB-File v2) 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_SERVICE`: Muss Wert `GICS` gesetzt sein um die Abfragen zu aktivieren. Der Wert
|
||||||
|
`NONE` deaktiviert die Abfrage in gICS.
|
||||||
|
* `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
|
||||||
|
|
||||||
Ein initialer Administrator-Account kann optional konfiguriert werden und sorgt dafür, dass bestimmte Bereiche nur nach
|
Ein initialer Administrator-Account kann optional konfiguriert werden und sorgt dafür, dass
|
||||||
einem erfolgreichen Login erreichbar sind.
|
bestimmte Bereiche nur nach einem erfolgreichen Login erreichbar sind.
|
||||||
|
|
||||||
* `APP_SECURITY_ADMIN_USER`: Muss angegeben werden zur Aktivierung der Zugriffsbeschränkung.
|
* `APP_SECURITY_ADMIN_USER`: Muss angegeben werden zur Aktivierung der Zugriffsbeschränkung.
|
||||||
* `APP_SECURITY_ADMIN_PASSWORD`: Das Passwort für den Administrator (Empfohlen).
|
* `APP_SECURITY_ADMIN_PASSWORD`: Das Passwort für den Administrator (Empfohlen).
|
||||||
@@ -114,27 +188,34 @@ Hier Beispiele für das Beispielpasswort `very-secret`:
|
|||||||
* `{bcrypt}$2y$05$CCkfsMr/wbTleMyjVIK8g.Aa3RCvrvoLXVAsL.f6KeouS88vXD9b6`
|
* `{bcrypt}$2y$05$CCkfsMr/wbTleMyjVIK8g.Aa3RCvrvoLXVAsL.f6KeouS88vXD9b6`
|
||||||
* `{sha256}9a34717f0646b5e9cfcba70055de62edb026ff4f68671ba3db96aa29297d2df5f1a037d58c745657`
|
* `{sha256}9a34717f0646b5e9cfcba70055de62edb026ff4f68671ba3db96aa29297d2df5f1a037d58c745657`
|
||||||
|
|
||||||
Wird kein Administrator-Passwort angegeben, wird ein zufälliger Wert generiert und beim Start der Anwendung in den Logs
|
Wird kein Administrator-Passwort angegeben, wird ein zufälliger Wert generiert und beim Start der
|
||||||
|
Anwendung in den Logs
|
||||||
angezeigt.
|
angezeigt.
|
||||||
|
|
||||||
#### Weitere (nicht administrative) Nutzer mit OpenID Connect
|
#### Weitere (nicht administrative) Nutzer mit OpenID Connect
|
||||||
|
|
||||||
Die folgenden Konfigurationsparameter werden benötigt, um die Authentifizierung weiterer Benutzer an einen OIDC-Provider
|
Die folgenden Konfigurationsparameter werden benötigt, um die Authentifizierung weiterer Benutzer an
|
||||||
|
einen OIDC-Provider
|
||||||
zu delegieren.
|
zu delegieren.
|
||||||
Ein Admin-Benutzer muss dabei konfiguriert sein.
|
Ein Admin-Benutzer muss dabei konfiguriert sein.
|
||||||
|
|
||||||
* `APP_SECURITY_ENABLE_OIDC`: Aktiviert die Nutzung von OpenID Connect. Damit sind weitere Parameter erforderlich
|
* `APP_SECURITY_ENABLE_OIDC`: Aktiviert die Nutzung von OpenID Connect. Damit sind weitere Parameter
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_NAME`: Name. Wird beim zusätzlichen Loginbutton angezeigt.
|
erforderlich
|
||||||
|
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_NAME`: Name. Wird beim zusätzlichen
|
||||||
|
Loginbutton angezeigt.
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_ID`: Client-ID
|
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_ID`: Client-ID
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SECRET`: Client-Secret
|
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SECRET`: Client-Secret
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SCOPE[0]`: Hier sollte immer `openid` angegeben werden.
|
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SCOPE[0]`: Hier sollte immer `openid`
|
||||||
|
angegeben werden.
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_ISSUER_URI`: Die URI des Providers,
|
* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_ISSUER_URI`: Die URI des Providers,
|
||||||
z.B. `https://auth.example.com/realm/example`
|
z.B. `https://auth.example.com/realm/example`
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_USER_NAME_ATTRIBUTE`: Name des Attributes, welches den Benutzernamen
|
* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_USER_NAME_ATTRIBUTE`: Name des Attributes, welches
|
||||||
|
den Benutzernamen
|
||||||
enthält.
|
enthält.
|
||||||
Oft verwendet: `preferred_username`
|
Oft verwendet: `preferred_username`
|
||||||
|
|
||||||
Ist die Nutzung von OpenID Connect konfiguriert, erscheint ein zusätzlicher Login-Button zur Nutzung mit OpenID Connect
|
Ist die Nutzung von OpenID Connect konfiguriert, erscheint ein zusätzlicher Login-Button zur Nutzung
|
||||||
|
mit OpenID Connect
|
||||||
und dem konfigurierten `CLIENT_NAME`.
|
und dem konfigurierten `CLIENT_NAME`.
|
||||||
|
|
||||||

|

|
||||||
@@ -145,62 +226,75 @@ zu finden.
|
|||||||
|
|
||||||
#### Rollenbasierte Berechtigungen
|
#### Rollenbasierte Berechtigungen
|
||||||
|
|
||||||
Wird OpenID Connect verwendet, gibt es eine rollenbasierte Berechtigungszuweisung.
|
Wird OpenID Connect verwendet, gibt es eine rollenbasierte Berechtigungszuweisung.
|
||||||
|
|
||||||
Die Standardrolle für neue OIDC-Benutzer kann mit der Option `APP_SECURITY_DEFAULT_USER_ROLE` festgelegt werden.
|
Die Standardrolle für neue OIDC-Benutzer kann mit der Option `APP_SECURITY_DEFAULT_USER_ROLE`
|
||||||
|
festgelegt werden.
|
||||||
Mögliche Werte sind `user` oder `guest`. Standardwert ist `user`.
|
Mögliche Werte sind `user` oder `guest`. Standardwert ist `user`.
|
||||||
|
|
||||||
Benutzer mit der Rolle "Gast" sehen nur die Inhalte, die auch nicht angemeldete Benutzer sehen.
|
Benutzer mit der Rolle "Gast" sehen nur die Inhalte, die auch nicht angemeldete Benutzer sehen.
|
||||||
|
|
||||||
Hierdurch ist es möglich, einzelne Benutzer einzuschränken oder durch Änderung der Standardrolle auf `guest` nur
|
Hierdurch ist es möglich, einzelne Benutzer einzuschränken oder durch Änderung der Standardrolle auf
|
||||||
|
`guest` nur
|
||||||
einzelne Benutzer als vollwertige Nutzer zuzulassen.
|
einzelne Benutzer als vollwertige Nutzer zuzulassen.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Benutzer werden nach dem Entfernen oder der Änderung der vergebenen Rolle automatisch abgemeldet und müssen sich neu anmelden.
|
Benutzer werden nach dem Entfernen oder der Änderung der vergebenen Rolle automatisch abgemeldet und
|
||||||
|
müssen sich neu anmelden.
|
||||||
Sie bekommen dabei wieder die Standardrolle zugewiesen.
|
Sie bekommen dabei wieder die Standardrolle zugewiesen.
|
||||||
|
|
||||||
#### Auswirkungen auf den dargestellten Inhalt
|
#### Auswirkungen auf den dargestellten Inhalt
|
||||||
|
|
||||||
Nur Administratoren haben Zugriff auf den Konfigurationsbereich, nur angemeldete Benutzer können die anonymisierte oder
|
Nur Administratoren haben Zugriff auf den Konfigurationsbereich, nur angemeldete Benutzer können die
|
||||||
|
anonymisierte oder
|
||||||
pseudonymisierte Patienten-ID sowie den Qualitätsbericht von DNPM:DIP einsehen.
|
pseudonymisierte Patienten-ID sowie den Qualitätsbericht von DNPM:DIP einsehen.
|
||||||
|
|
||||||
Wurde kein Administrator-Account konfiguriert, sind diese Inhalte generell nicht verfügbar.
|
Wurde kein Administrator-Account konfiguriert, sind diese Inhalte generell nicht verfügbar.
|
||||||
|
|
||||||
### Tokenbasierte Authentifizierung für MTBFile-Endpunkt
|
### Tokenbasierte Authentifizierung für MTBFile-Endpunkt
|
||||||
|
|
||||||
Die Anwendung unterstützt das Erstellen und Nutzen einer tokenbasierten Authentifizierung für den MTB-File-Endpunkt.
|
Die Anwendung unterstützt das Erstellen und Nutzen einer tokenbasierten Authentifizierung für den
|
||||||
|
MTB-File-Endpunkt.
|
||||||
|
|
||||||
Dies kann mit der Umgebungsvariable `APP_SECURITY_ENABLE_TOKENS` aktiviert (`true` oder `false`) werden
|
Dies kann mit der Umgebungsvariable `APP_SECURITY_ENABLE_TOKENS` aktiviert (`true` oder `false`)
|
||||||
|
werden
|
||||||
und ist als Standardeinstellung nicht aktiv.
|
und ist als Standardeinstellung nicht aktiv.
|
||||||
|
|
||||||
Ist diese Einstellung aktiviert worden, ist es Administratoren möglich, Zugriffstokens für Onkostar zu erstellen, die
|
Ist diese Einstellung aktiviert worden, ist es Administratoren möglich, Zugriffstokens für Onkostar
|
||||||
|
zu erstellen, die
|
||||||
zur Nutzung des MTB-File-Endpunkts eine HTTP-Basic-Authentifizierung voraussetzen.
|
zur Nutzung des MTB-File-Endpunkts eine HTTP-Basic-Authentifizierung voraussetzen.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
In diesem Fall kann der Endpunkt für das Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt konfiguriert werden:
|
In diesem Fall kann der Endpunkt für das Onkostar-Plugin *
|
||||||
|
*[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt
|
||||||
|
konfiguriert werden:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile
|
https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile
|
||||||
```
|
```
|
||||||
|
|
||||||
Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information abgelehnt.
|
Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information
|
||||||
|
abgelehnt.
|
||||||
|
|
||||||
Alternativ kann eine Authentifizierung über Benutzername/Passwort oder OIDC erfolgen.
|
Alternativ kann eine Authentifizierung über Benutzername/Passwort oder OIDC erfolgen.
|
||||||
|
|
||||||
### Transformation von Werten
|
### Transformation von Werten
|
||||||
|
|
||||||
In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst wurde und dadurch nicht dem Wert entspricht,
|
In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst
|
||||||
|
wurde und dadurch nicht dem Wert entspricht,
|
||||||
der von DNPM:DIP akzeptiert wird.
|
der von DNPM:DIP akzeptiert wird.
|
||||||
|
|
||||||
Diese Anwendung bietet daher die Möglichkeit, eine Transformation vorzunehmen. Hierzu muss der "Pfad" innerhalb des JSON-MTB-Files angegeben werden und
|
Diese Anwendung bietet daher die Möglichkeit, eine Transformation vorzunehmen. Hierzu muss der "Pfad"
|
||||||
welcher Wert wie ersetzt werden soll.
|
innerhalb des JSON-MTB-Files angegeben werden und welcher Wert wie ersetzt werden soll.
|
||||||
|
|
||||||
Hier ein Beispiel für die erste (Index 0 - weitere dann mit 1,2, ...) Transformationsregel:
|
Hier ein Beispiel für die erste (Index 0 - weitere dann mit 1,2, ...) Transformationsregel:
|
||||||
|
|
||||||
* `APP_TRANSFORMATIONS_0_PATH`: Pfad zum Wert in der JSON-MTB-Datei. Beispiel: `diagnoses[*].icd10.version` für **alle** Diagnosen
|
* `APP_TRANSFORMATIONS_0_PATH`: Pfad zum Wert in der JSON-MTB-Datei. Beispiel:
|
||||||
* `APP_TRANSFORMATIONS_0_FROM`: Angabe des Werts, der ersetzt werden soll. Andere Werte bleiben dabei unverändert.
|
`diagnoses[*].icd10.version` für **alle** Diagnosen
|
||||||
|
* `APP_TRANSFORMATIONS_0_FROM`: Angabe des Werts, der ersetzt werden soll. Andere Werte bleiben
|
||||||
|
dabei unverändert.
|
||||||
* `APP_TRANSFORMATIONS_0_TO`: Angabe des neuen Werts.
|
* `APP_TRANSFORMATIONS_0_TO`: Angabe des neuen Werts.
|
||||||
|
|
||||||
### Mögliche Endpunkte zur Datenübermittlung
|
### Mögliche Endpunkte zur Datenübermittlung
|
||||||
@@ -212,56 +306,61 @@ Werden sowohl REST als auch Kafka-Endpunkt konfiguriert, wird nur der REST-Endpu
|
|||||||
|
|
||||||
#### REST
|
#### REST
|
||||||
|
|
||||||
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an DNPM:DIP gesendet wird:
|
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein MTB-File an DNPM:DIP gesendet wird:
|
||||||
|
|
||||||
* `APP_REST_URI`: URI der zu benutzenden API der Backend-Instanz. Zum Beispiel:
|
* `APP_REST_URI`: URI der zu benutzenden API der Backend-Instanz. Zum Beispiel `http://localhost:9000/api`
|
||||||
* `http://localhost:9000/bwhc/etl/api` für **bwHC Backend**
|
|
||||||
* `http://localhost:9000/api` für **dnpm:dip**
|
|
||||||
* `APP_REST_USERNAME`: Basic-Auth-Benutzername für den REST-Endpunkt
|
* `APP_REST_USERNAME`: Basic-Auth-Benutzername für den REST-Endpunkt
|
||||||
* `APP_REST_PASSWORD`: Basic-Auth-Passwort für den REST-Endpunkt
|
* `APP_REST_PASSWORD`: Basic-Auth-Passwort für den REST-Endpunkt
|
||||||
* `APP_REST_IS_BWHC`: `true` für **bwHC Backend**, weglassen oder `false` für **dnpm:dip**
|
|
||||||
|
|
||||||
#### Kafka-Topics
|
#### Kafka-Topics
|
||||||
|
|
||||||
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an ein Kafka-Topic übermittelt wird:
|
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein MTB-File an ein Kafka-Topic
|
||||||
|
übermittelt wird:
|
||||||
|
|
||||||
* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen.
|
* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen.
|
||||||
Ersetzt ~~`APP_KAFKA_TOPIC`~~, **welches nach Version 0.10 entfernt wird**.
|
* `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`: Topic mit Antworten über den Erfolg des Versendens.
|
||||||
* `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`: Topic mit Antworten über den Erfolg des Versendens. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_response".
|
Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_response".
|
||||||
Ersetzt ~~`APP_KAFKA_RESPONSE_TOPIC`~~, **welches nach Version 0.10 entfernt wird**.
|
* `APP_KAFKA_GROUP_ID`: Kafka GroupID des Consumers. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_
|
||||||
* `APP_KAFKA_GROUP_ID`: Kafka GroupID des Consumers. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_group".
|
group".
|
||||||
* `APP_KAFKA_SERVERS`: Zu verwendende Kafka-Bootstrap-Server als kommagetrennte Liste
|
* `APP_KAFKA_SERVERS`: Zu verwendende Kafka-Bootstrap-Server als kommagetrennte Liste
|
||||||
|
|
||||||
Wird keine Rückantwort über Apache Kafka empfangen und es gibt keine weitere Möglichkeit den Status festzustellen, verbleibt der Status auf `UNKNOWN`.
|
Wird keine Rückantwort über Apache Kafka empfangen und es gibt keine weitere Möglichkeit den Status
|
||||||
|
festzustellen, verbleibt der Status auf `UNKNOWN`.
|
||||||
|
|
||||||
Weitere Einstellungen können über die Parameter von Spring Kafka konfiguriert werden.
|
Weitere Einstellungen können über die Parameter von Spring Kafka konfiguriert werden.
|
||||||
|
|
||||||
Lässt sich keine Verbindung zu dem Backend aufbauen, wird eine Rückantwort mit Status-Code `900` erwartet, welchen es
|
Lässt sich keine Verbindung zu dem Backend aufbauen, wird eine Rückantwort mit Status-Code `900`
|
||||||
|
erwartet, welchen es
|
||||||
für HTTP nicht gibt.
|
für HTTP nicht gibt.
|
||||||
|
|
||||||
Wird die Umgebungsvariable `APP_KAFKA_INPUT_TOPIC` gesetzt, kann eine Nachricht auch über dieses Kafka-Topic an den ETL-Prozessor übermittelt werden.
|
Wird die Umgebungsvariable `APP_KAFKA_INPUT_TOPIC` gesetzt, kann eine Nachricht auch über dieses
|
||||||
|
Kafka-Topic an den ETL-Prozessor übermittelt werden.
|
||||||
|
|
||||||
##### Retention Time
|
##### Retention Time
|
||||||
|
|
||||||
Generell werden in Apache Kafka alle Records entsprechend der Konfiguration vorgehalten.
|
Generell werden in Apache Kafka alle Records entsprechend der Konfiguration vorgehalten.
|
||||||
So wird ohne spezielle Konfiguration ein Record für 7 Tage in Apache Kafka gespeichert.
|
So wird ohne spezielle Konfiguration ein Record für 7 Tage in Apache Kafka gespeichert.
|
||||||
Es sind innerhalb dieses Zeitraums auch alte Informationen weiterhin enthalten, wenn der Consent später abgelehnt wurde.
|
Es sind innerhalb dieses Zeitraums auch alte Informationen weiterhin enthalten, wenn der Consent
|
||||||
|
später abgelehnt wurde.
|
||||||
|
|
||||||
Durch eine entsprechende Konfiguration des Topics kann dies verhindert werden.
|
Durch eine entsprechende Konfiguration des Topics kann dies verhindert werden.
|
||||||
|
|
||||||
Beispiel - auszuführen innerhalb des Kafka-Containers: Löschen alter Records nach einem Tag
|
Beispiel - auszuführen innerhalb des Kafka-Containers: Löschen alter Records nach einem Tag
|
||||||
|
|
||||||
```
|
```
|
||||||
kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config retention.ms=86400000
|
kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config retention.ms=86400000
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Key based Retention
|
##### Key based Retention
|
||||||
|
|
||||||
Möchten Sie hingegen immer nur die letzte Meldung für einen Patienten und eine Erkrankung in Apache Kafka vorhalten,
|
Möchten Sie hingegen immer nur die letzte Meldung für einen Patienten und eine Erkrankung in Apache
|
||||||
|
Kafka vorhalten,
|
||||||
so ist die nachfolgend genannte Konfiguration der Kafka-Topics hilfreich.
|
so ist die nachfolgend genannte Konfiguration der Kafka-Topics hilfreich.
|
||||||
|
|
||||||
|
* `retention.ms`: Möglichst kurze Zeit in der alte Records noch erhalten bleiben, z.B. 10 Sekunden
|
||||||
* `retention.ms`: Möglichst kurze Zeit in der alte Records noch erhalten bleiben, z.B. 10 Sekunden 10000
|
10000
|
||||||
* `cleanup.policy`: Löschen alter Records und Beibehalten des letzten Records zu einem Key [delete,compact]
|
* `cleanup.policy`: Löschen alter Records und Beibehalten des letzten Records zu einem
|
||||||
|
Key [delete,compact]
|
||||||
|
|
||||||
Beispiele für ein Topic `test`, hier bitte an die verwendeten Topics anpassen.
|
Beispiele für ein Topic `test`, hier bitte an die verwendeten Topics anpassen.
|
||||||
|
|
||||||
@@ -270,32 +369,28 @@ kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-co
|
|||||||
kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config cleanup.policy=[delete,compact]
|
kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config cleanup.policy=[delete,compact]
|
||||||
```
|
```
|
||||||
|
|
||||||
Da als Key eines Records die (pseudonymisierte) Patienten-ID verwendet wird, stehen mit obiger Konfiguration
|
Da als Key eines Records die (pseudonymisierte) Patienten-ID verwendet wird, stehen mit obiger
|
||||||
der Kafka-Topics nach 10 Sekunden nur noch der jeweils letzte Eintrag für den entsprechenden Key zur Verfügung.
|
Konfiguration
|
||||||
|
der Kafka-Topics nach 10 Sekunden nur noch der jeweils letzte Eintrag für den entsprechenden Key zur
|
||||||
|
Verfügung.
|
||||||
|
|
||||||
Da der Key sowohl für die Records in Richtung DNPM:DIP, als auch für die Rückantwort identisch aufgebaut ist, lassen sich so
|
Da der Key sowohl für die Records in Richtung DNPM:DIP, als auch für die Rückantwort identisch
|
||||||
auch im Falle eines Consent-Widerspruchs die enthaltenen Daten als auch die Offenlegung durch Verifikationsdaten in der
|
aufgebaut ist, lassen sich so
|
||||||
|
auch im Falle eines Consent-Widerspruchs die enthaltenen Daten als auch die Offenlegung durch
|
||||||
|
Verifikationsdaten in der
|
||||||
Antwort effektiv verhindern, da diese nach 10 Sekunden gelöscht werden.
|
Antwort effektiv verhindern, da diese nach 10 Sekunden gelöscht werden.
|
||||||
|
|
||||||
Es steht dann nur noch die jeweils letzten Information zur Verfügung, dass für einen Patienten/eine Erkrankung
|
Es steht dann nur noch die jeweils letzten Information zur Verfügung, dass für einen Patienten/eine
|
||||||
|
Erkrankung
|
||||||
ein Consent-Widerspruch erfolgte.
|
ein Consent-Widerspruch erfolgte.
|
||||||
|
|
||||||
Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen verwenden möchten.
|
Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen
|
||||||
|
verwenden möchten.
|
||||||
|
|
||||||
### Antworten und Statusauswertung
|
### Antworten und Statusauswertung
|
||||||
|
|
||||||
Anfragen an das bwHC-Backend aus Versionen bis 0.9.x wurden wie folgt behandelt:
|
Seit Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthaltene höchste
|
||||||
|
Severity-Stufe als Ergebnis verwendet.
|
||||||
| HTTP-Response | Status |
|
|
||||||
|----------------|-----------|
|
|
||||||
| `HTTP 200` | `SUCCESS` |
|
|
||||||
| `HTTP 201` | `WARNING` |
|
|
||||||
| `HTTP 400-...` | `ERROR` |
|
|
||||||
|
|
||||||
Dies konnte dazu führen, dass zwar mit einem `HTTP 201` geantwortet wurde, aber dennoch in der Issue-Liste die
|
|
||||||
Severity `error` aufgetaucht ist.
|
|
||||||
|
|
||||||
Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthaltene höchste Severity-Stufe als Ergebnis verwendet.
|
|
||||||
|
|
||||||
| Höchste Severity | Status |
|
| Höchste Severity | Status |
|
||||||
|------------------|-----------|
|
|------------------|-----------|
|
||||||
@@ -305,9 +400,10 @@ Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthal
|
|||||||
|
|
||||||
## Docker-Images
|
## Docker-Images
|
||||||
|
|
||||||
Diese Anwendung ist auch als Docker-Image verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-processor
|
Diese Anwendung ist auch als Docker-Image
|
||||||
|
verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-processor
|
||||||
|
|
||||||
### Images lokal bauen
|
### Images lokal bauen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew bootBuildImage
|
./gradlew bootBuildImage
|
||||||
@@ -315,20 +411,25 @@ Diese Anwendung ist auch als Docker-Image verfügbar: https://github.com/pcvolkm
|
|||||||
|
|
||||||
### Integration eines eigenen Root CA Zertifikats
|
### Integration eines eigenen Root CA Zertifikats
|
||||||
|
|
||||||
Wird eine eigene Root CA verwendet, die nicht offiziell signiert ist, wird es zu Problemen beim SSL-Handshake kommen, wenn z.B. gPAS zur Generierung von Pseudonymen verwendet wird.
|
Wird eine eigene Root CA verwendet, die nicht offiziell signiert ist, wird es zu Problemen beim
|
||||||
|
SSL-Handshake kommen, wenn z.B. gPAS zur Generierung von Pseudonymen verwendet wird.
|
||||||
|
|
||||||
Hier bietet es sich an, das Root CA Zertifikat in das Image zu integrieren.
|
Hier bietet es sich an, das Root CA Zertifikat in das Image zu integrieren.
|
||||||
|
|
||||||
#### Integration beim Bauen des Images
|
#### Integration beim Bauen des Images
|
||||||
|
|
||||||
Hier muss die Zeile `"BP_EMBED_CERTS" to "true"` in der Datei `build.gradle.kts` verwendet werden und darf nicht als Kommentar verwendet werden.
|
Hier muss die Zeile `"BP_EMBED_CERTS" to "true"` in der Datei `build.gradle.kts` verwendet werden
|
||||||
|
und darf nicht als Kommentar verwendet werden.
|
||||||
|
|
||||||
Die PEM-Datei mit dem/den Root CA Zertifikat(en) muss dabei im vorbereiteten Verzeichnis [`bindings/ca-certificates`](bindings/ca-certificates) enthalten sein.
|
Die PEM-Datei mit dem/den Root CA Zertifikat(en) muss dabei im vorbereiteten Verzeichnis [
|
||||||
|
`bindings/ca-certificates`](bindings/ca-certificates) enthalten sein.
|
||||||
|
|
||||||
#### Integration zur Laufzeit
|
#### Integration zur Laufzeit
|
||||||
|
|
||||||
Hier muss die Umgebungsvariable `SERVICE_BINDING_ROOT` z.B. auf den Wert `/bindings` gesetzt sein.
|
Hier muss die Umgebungsvariable `SERVICE_BINDING_ROOT` z.B. auf den Wert `/bindings` gesetzt sein.
|
||||||
Zudem muss ein Verzeichnis `bindings/ca-certificates` - analog zum Verzeichnis [`bindings/ca-certificates`](bindings/ca-certificates) mit einer PEM-Datei als Docker-Volume eingebunden werden.
|
Zudem muss ein Verzeichnis `bindings/ca-certificates` - analog zum Verzeichnis [
|
||||||
|
`bindings/ca-certificates`](bindings/ca-certificates) mit einer PEM-Datei als Docker-Volume
|
||||||
|
eingebunden werden.
|
||||||
|
|
||||||
Beispiel für Docker-Compose:
|
Beispiel für Docker-Compose:
|
||||||
|
|
||||||
@@ -343,12 +444,14 @@ Beispiel für Docker-Compose:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
*Ausführen als Docker Container:*
|
*Ausführen als Docker Container:*
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ./deploy
|
cd ./deploy
|
||||||
cp env-sample.env .env
|
cp env-sample.env .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Wenn gewünscht, Änderungen in der `.env` vornehmen.
|
Wenn gewünscht, Änderungen in der `.env` vornehmen.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -357,15 +460,19 @@ docker compose up -d
|
|||||||
|
|
||||||
### Einfaches Beispiel für ein eigenes Docker-Compose-File
|
### Einfaches Beispiel für ein eigenes Docker-Compose-File
|
||||||
|
|
||||||
Die Datei [`docs/docker-compose.yml`](docs/docker-compose.yml) zeigt eine einfache Konfiguration für REST-Requests basierend
|
Die Datei [`docs/docker-compose.yml`](docs/docker-compose.yml) zeigt eine einfache Konfiguration für
|
||||||
|
REST-Requests basierend
|
||||||
auf Docker-Compose mit der gestartet werden kann.
|
auf Docker-Compose mit der gestartet werden kann.
|
||||||
|
|
||||||
### Betrieb hinter einem Reverse-Proxy
|
### Betrieb hinter einem Reverse-Proxy
|
||||||
|
|
||||||
Die Anwendung verarbeitet `X-Forwarded`-HTTP-Header und kann daher auch hinter einem Reverse-Proxy betrieben werden.
|
Die Anwendung verarbeitet `X-Forwarded`-HTTP-Header und kann daher auch hinter einem Reverse-Proxy
|
||||||
|
betrieben werden.
|
||||||
|
|
||||||
Dabei werden, je nachdem welche Header durch den Reverse-Proxy gesendet werden auch Protokoll, Host oder auch Path-Präfix
|
Dabei werden, je nachdem welche Header durch den Reverse-Proxy gesendet werden auch Protokoll, Host
|
||||||
automatisch erkannt und verwendet werden. Dadurch ist z.B. eine abweichende Angabe des Pfads problemlos möglich.
|
oder auch Path-Präfix
|
||||||
|
automatisch erkannt und verwendet werden. Dadurch ist z.B. eine abweichende Angabe des Pfads
|
||||||
|
problemlos möglich.
|
||||||
|
|
||||||
#### Beispiel *Traefik* (mit Docker-Labels):
|
#### Beispiel *Traefik* (mit Docker-Labels):
|
||||||
|
|
||||||
@@ -401,13 +508,17 @@ Das folgende Beispiel zeigt die Konfiguration einer _location_ in einer nginx-Ko
|
|||||||
|
|
||||||
## Entwicklungssetup
|
## Entwicklungssetup
|
||||||
|
|
||||||
Zum Starten einer lokalen Entwicklungs- und Testumgebung kann die beiliegende Datei `dev-compose.yml` verwendet werden.
|
Zum Starten einer lokalen Entwicklungs- und Testumgebung kann die beiliegende Datei
|
||||||
|
`dev-compose.yml` verwendet werden.
|
||||||
Diese kann zur Nutzung der Datenbanken **MariaDB** als auch **PostgreSQL** angepasst werden.
|
Diese kann zur Nutzung der Datenbanken **MariaDB** als auch **PostgreSQL** angepasst werden.
|
||||||
|
|
||||||
Zur Nutzung von Apache Kafka muss dazu ein Eintrag im hosts-File vorgenommen werden und der Hostname `kafka` auf die lokale
|
Zur Nutzung von Apache Kafka muss dazu ein Eintrag im hosts-File vorgenommen werden und der Hostname
|
||||||
IP-Adresse verweisen. Ohne diese Einstellung ist eine Nutzung von Apache Kafka außerhalb der Docker-Umgebung nicht möglich.
|
`kafka` auf die lokale
|
||||||
|
IP-Adresse verweisen. Ohne diese Einstellung ist eine Nutzung von Apache Kafka außerhalb der
|
||||||
|
Docker-Umgebung nicht möglich.
|
||||||
|
|
||||||
Beim Start der Anwendung mit dem Profil `dev` wird die in `dev-compose.yml` definierte Umgebung beim Start der
|
Beim Start der Anwendung mit dem Profil `dev` wird die in `dev-compose.yml` definierte Umgebung beim
|
||||||
|
Start der
|
||||||
Anwendung mit gestartet:
|
Anwendung mit gestartet:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -419,4 +530,5 @@ Die Datei `application-dev.yml` enthält hierzu die Konfiguration für das Profi
|
|||||||
Beim Ausführen der Integrationstests wird eine Testdatenbank in einem Docker-Container gestartet.
|
Beim Ausführen der Integrationstests wird eine Testdatenbank in einem Docker-Container gestartet.
|
||||||
Siehe hier auch die Klasse `AbstractTestcontainerTest` unter `src/integrationTest`.
|
Siehe hier auch die Klasse `AbstractTestcontainerTest` unter `src/integrationTest`.
|
||||||
|
|
||||||
Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar.
|
Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe
|
||||||
|
von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar.
|
||||||
|
@@ -5,7 +5,7 @@ import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
war
|
war
|
||||||
id("org.springframework.boot") version "3.3.10"
|
id("org.springframework.boot") version "3.5.3"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
kotlin("jvm") version "1.9.25"
|
kotlin("jvm") version "1.9.25"
|
||||||
kotlin("plugin.spring") version "1.9.25"
|
kotlin("plugin.spring") version "1.9.25"
|
||||||
@@ -13,12 +13,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.dnpm"
|
group = "dev.dnpm"
|
||||||
version = "0.10.0-SNAPSHOT"
|
version = "0.11.0-SNAPSHOT"
|
||||||
|
|
||||||
var versions = mapOf(
|
var versions = mapOf(
|
||||||
"bwhc-dto-java" to "0.4.0",
|
"mtb-dto" to "0.1.0-SNAPSHOT",
|
||||||
"hapi-fhir" to "7.6.0",
|
"hapi-fhir" to "7.6.1",
|
||||||
"commons-compress" to "1.26.2",
|
|
||||||
"mockito-kotlin" to "5.4.0",
|
"mockito-kotlin" to "5.4.0",
|
||||||
"archunit" to "1.3.0",
|
"archunit" to "1.3.0",
|
||||||
// Webjars
|
// Webjars
|
||||||
@@ -49,9 +48,18 @@ configurations {
|
|||||||
compileOnly {
|
compileOnly {
|
||||||
extendsFrom(configurations.annotationProcessor.get())
|
extendsFrom(configurations.annotationProcessor.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
all {
|
||||||
|
resolutionStrategy {
|
||||||
|
cacheChangingModulesFor(5, "minutes")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
maven {
|
||||||
|
url = uri("https://git.dnpm.dev/api/packages/public-snapshots/maven")
|
||||||
|
}
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://git.dnpm.dev/api/packages/public/maven")
|
url = uri("https://git.dnpm.dev/api/packages/public/maven")
|
||||||
}
|
}
|
||||||
@@ -72,7 +80,7 @@ dependencies {
|
|||||||
implementation("org.flywaydb:flyway-mysql")
|
implementation("org.flywaydb:flyway-mysql")
|
||||||
implementation("commons-codec:commons-codec")
|
implementation("commons-codec:commons-codec")
|
||||||
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||||
implementation("de.ukw.ccc:bwhc-dto-java:${versions["bwhc-dto-java"]}")
|
implementation("dev.pcvolkmer.mv64e:mtb-dto:${versions["mtb-dto"]}") { isChanging = true }
|
||||||
implementation("ca.uhn.hapi.fhir:hapi-fhir-base:${versions["hapi-fhir"]}")
|
implementation("ca.uhn.hapi.fhir:hapi-fhir-base:${versions["hapi-fhir"]}")
|
||||||
implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:${versions["hapi-fhir"]}")
|
implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:${versions["hapi-fhir"]}")
|
||||||
implementation("org.apache.httpcomponents.client5:httpclient5")
|
implementation("org.apache.httpcomponents.client5:httpclient5")
|
||||||
@@ -80,6 +88,8 @@ 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"]}")
|
||||||
|
// Fix for CVE-2025-48924
|
||||||
|
implementation("org.apache.commons:commons-lang3:3.18.0")
|
||||||
|
|
||||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
@@ -99,10 +109,8 @@ dependencies {
|
|||||||
integrationTestImplementation("org.testcontainers:junit-jupiter")
|
integrationTestImplementation("org.testcontainers:junit-jupiter")
|
||||||
integrationTestImplementation("org.testcontainers:postgresql")
|
integrationTestImplementation("org.testcontainers:postgresql")
|
||||||
integrationTestImplementation("com.tngtech.archunit:archunit:${versions["archunit"]}")
|
integrationTestImplementation("com.tngtech.archunit:archunit:${versions["archunit"]}")
|
||||||
integrationTestImplementation("net.sourceforge.htmlunit:htmlunit")
|
integrationTestImplementation("org.htmlunit:htmlunit")
|
||||||
integrationTestImplementation("org.springframework:spring-webflux")
|
integrationTestImplementation("org.springframework:spring-webflux")
|
||||||
// Override dependency version from org.testcontainers:junit-jupiter - CVE-2024-26308, CVE-2024-25710
|
|
||||||
integrationTestImplementation("org.apache.commons:commons-compress:${versions["commons-compress"]}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
@@ -119,8 +127,9 @@ tasks.withType<Test> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task<Test>("integrationTest") {
|
tasks.register<Test>("integrationTest") {
|
||||||
description = "Runs integration tests"
|
description = "Runs integration tests"
|
||||||
|
group = "verification"
|
||||||
|
|
||||||
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
||||||
classpath = sourceSets["integrationTest"].runtimeClasspath
|
classpath = sourceSets["integrationTest"].runtimeClasspath
|
||||||
@@ -145,7 +154,7 @@ tasks.jacocoTestReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<BootBuildImage>("bootBuildImage") {
|
tasks.named<BootBuildImage>("bootBuildImage") {
|
||||||
imageName.set("ghcr.io/pcvolkmer/etl-processor")
|
imageName.set("ghcr.io/pcvolkmer/mv64e-etl-processor")
|
||||||
|
|
||||||
// Binding for CA Certs
|
// Binding for CA Certs
|
||||||
bindings.set(listOf(
|
bindings.set(listOf(
|
||||||
@@ -155,8 +164,8 @@ tasks.named<BootBuildImage>("bootBuildImage") {
|
|||||||
environment.set(environment.get() + mapOf(
|
environment.set(environment.get() + mapOf(
|
||||||
// Enable this line to embed CA Certs into image on build time
|
// Enable this line to embed CA Certs into image on build time
|
||||||
//"BP_EMBED_CERTS" to "true",
|
//"BP_EMBED_CERTS" to "true",
|
||||||
"BP_OCI_SOURCE" to "https://github.com/pcvolkmer/etl-processor",
|
"BP_OCI_SOURCE" to "https://github.com/pcvolkmer/mv64e-etl-processor",
|
||||||
"BP_OCI_LICENSES" to "AGPLv3",
|
"BP_OCI_LICENSES" to "AGPLv3",
|
||||||
"BP_OCI_DESCRIPTION" to "ETL Processor for bwHC MTB files"
|
"BP_OCI_DESCRIPTION" to "ETL Processor for MV § 64e and DNPM:DIP"
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,11 @@ 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:
|
||||||
@@ -53,4 +58,4 @@ services:
|
|||||||
# environment:
|
# environment:
|
||||||
# POSTGRES_DB: dev
|
# POSTGRES_DB: dev
|
||||||
# POSTGRES_USER: dev
|
# POSTGRES_USER: dev
|
||||||
# POSTGRES_PASSWORD: dev
|
# POSTGRES_PASSWORD: dev
|
||||||
|
@@ -2,31 +2,55 @@ version: '3.7'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
zoo1:
|
zoo:
|
||||||
image: zookeeper:3.8.0
|
image: zookeeper:3.9.2
|
||||||
hostname: zoo1
|
restart: unless-stopped
|
||||||
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=zoo1:2888:3888;2181
|
ZOO_SERVERS: server.1=zoo:2888:3888;2181
|
||||||
|
|
||||||
kafka1:
|
kafka:
|
||||||
image: confluentinc/cp-kafka:7.2.1
|
image: confluentinc/cp-kafka:7.6.1
|
||||||
hostname: kafka1
|
|
||||||
ports:
|
ports:
|
||||||
- "9092:9092"
|
- "9092:9092"
|
||||||
environment:
|
environment:
|
||||||
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka1:19092,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092
|
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka:19092,LISTENER_DOCKER_EXTERNAL://172.17.0.1:9093,LISTENER_EXTERNAL://127.0.0.1:9092
|
||||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT,LISTENER_EXTERNAL:PLAINTEXT
|
||||||
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
|
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
|
||||||
KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181"
|
KAFKA_ZOOKEEPER_CONNECT: zoo: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:
|
||||||
- zoo1
|
- zoo
|
||||||
|
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
|
||||||
@@ -40,8 +64,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:
|
||||||
- zoo1
|
- zoo
|
||||||
- kafka1
|
- kafka
|
||||||
|
|
||||||
kafka-connect:
|
kafka-connect:
|
||||||
image: confluentinc/cp-kafka-connect:7.2.1
|
image: confluentinc/cp-kafka-connect:7.2.1
|
||||||
@@ -67,24 +91,6 @@ services:
|
|||||||
#volumes:
|
#volumes:
|
||||||
# - ./connectors:/etc/kafka-connect/jars/
|
# - ./connectors:/etc/kafka-connect/jars/
|
||||||
depends_on:
|
depends_on:
|
||||||
- zoo1
|
- zoo
|
||||||
- kafka1
|
- kafka
|
||||||
- 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
|
|
||||||
|
@@ -1 +1 @@
|
|||||||
rootProject.name = "etl-processor"
|
rootProject.name = "mv64e-etl-processor"
|
||||||
|
@@ -20,10 +20,11 @@
|
|||||||
package dev.dnpm.etl.processor
|
package dev.dnpm.etl.processor
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import dev.dnpm.etl.processor.output.DnpmV2MtbFileRequest
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
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.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
@@ -33,10 +34,10 @@ import org.mockito.kotlin.*
|
|||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.post
|
import org.springframework.test.web.servlet.post
|
||||||
@@ -45,11 +46,12 @@ import org.testcontainers.junit.jupiter.Testcontainers
|
|||||||
@Testcontainers
|
@Testcontainers
|
||||||
@ExtendWith(SpringExtension::class)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@MockBean(MtbFileSender::class)
|
@MockitoBean(types = [MtbFileSender::class])
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.rest.uri=http://example.com",
|
"app.rest.uri=http://example.com",
|
||||||
"app.pseudonymize.generator=buildin"
|
"app.pseudonymize.generator=buildin",
|
||||||
|
"app.consent.service=none"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
||||||
@@ -66,14 +68,15 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin",
|
"app.pseudonymize.generator=buildin",
|
||||||
"app.transformations[0].path=diagnoses[*].icd10.version",
|
"app.consent.service=none",
|
||||||
|
"app.transformations[0].path=diagnoses[*].code.version",
|
||||||
"app.transformations[0].from=2013",
|
"app.transformations[0].from=2013",
|
||||||
"app.transformations[0].to=2014",
|
"app.transformations[0].to=2014",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class TransformationTest {
|
inner class TransformationTest {
|
||||||
|
|
||||||
@MockBean
|
@MockitoBean
|
||||||
private lateinit var mtbFileSender: MtbFileSender
|
private lateinit var mtbFileSender: MtbFileSender
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -91,36 +94,33 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
fun mtbFileIsTransformed() {
|
fun mtbFileIsTransformed() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(RequestStatus.SUCCESS)
|
MtbFileSender.Response(RequestStatus.SUCCESS)
|
||||||
}.whenever(mtbFileSender).send(any<MtbFileSender.MtbFileRequest>())
|
}.whenever(mtbFileSender).send(any<DnpmV2MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = Mtb.builder()
|
||||||
.withPatient(
|
.patient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("TEST_12345678")
|
.id("TEST_12345678")
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withConsent(
|
.metadata(
|
||||||
Consent.builder()
|
MvhMetadata
|
||||||
.withId("1")
|
.builder()
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
.modelProjectConsent(
|
||||||
.withPatient("TEST_12345678")
|
ModelProjectConsent
|
||||||
|
.builder()
|
||||||
|
.provisions(
|
||||||
|
listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build())
|
||||||
|
).build()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withEpisode(
|
.diagnoses(
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("TEST_12345678")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withDiagnoses(
|
|
||||||
listOf(
|
listOf(
|
||||||
Diagnosis.builder()
|
MtbDiagnosis.builder()
|
||||||
.withId("1234")
|
.id("1234")
|
||||||
.withIcd10(Icd10.builder().withCode("F79.9").withVersion("2013").build())
|
.patient(Reference.builder().id("TEST_12345678").build())
|
||||||
.build()
|
.code(Coding.builder().code("F79.9").version("2013").build())
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
@@ -134,10 +134,10 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val captor = argumentCaptor<MtbFileSender.MtbFileRequest>()
|
val captor = argumentCaptor<DnpmV2MtbFileRequest>()
|
||||||
verify(mtbFileSender).send(captor.capture())
|
verify(mtbFileSender).send(captor.capture())
|
||||||
assertThat(captor.firstValue.mtbFile.diagnoses).hasSize(1).allMatch { diagnosis ->
|
assertThat(captor.firstValue.content.diagnoses).hasSize(1).allMatch { diagnosis ->
|
||||||
diagnosis.icd10.version == "2014"
|
diagnosis.code.version == "2014"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,15 +20,18 @@
|
|||||||
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.ConsentEvaluator
|
||||||
|
import dev.dnpm.etl.processor.consent.GicsConsentService
|
||||||
|
import dev.dnpm.etl.processor.consent.MtbFileConsentService
|
||||||
import dev.dnpm.etl.processor.input.KafkaInputListener
|
import dev.dnpm.etl.processor.input.KafkaInputListener
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import dev.dnpm.etl.processor.output.KafkaMtbFileSender
|
import dev.dnpm.etl.processor.output.KafkaMtbFileSender
|
||||||
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
||||||
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
|
||||||
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.RequestProcessor
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -36,24 +39,26 @@ import org.junit.jupiter.api.assertThrows
|
|||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException
|
||||||
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
|
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBeans
|
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@ContextConfiguration(classes = [
|
@ContextConfiguration(
|
||||||
AppConfiguration::class,
|
classes = [
|
||||||
AppSecurityConfiguration::class,
|
AppConfiguration::class,
|
||||||
KafkaAutoConfiguration::class,
|
AppSecurityConfiguration::class,
|
||||||
AppKafkaConfiguration::class,
|
KafkaAutoConfiguration::class,
|
||||||
AppRestConfiguration::class
|
AppKafkaConfiguration::class,
|
||||||
])
|
AppRestConfiguration::class,
|
||||||
@MockBean(ObjectMapper::class)
|
ConsentEvaluator::class
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@MockitoBean(types = [ObjectMapper::class])
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
@@ -86,7 +91,7 @@ class AppConfigurationTest {
|
|||||||
"app.kafka.group-id=test"
|
"app.kafka.group-id=test"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(RequestRepository::class)
|
@MockitoBean(types = [RequestRepository::class])
|
||||||
inner class AppConfigurationKafkaTest(private val context: ApplicationContext) {
|
inner class AppConfigurationKafkaTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -145,7 +150,7 @@ class AppConfigurationTest {
|
|||||||
"app.kafka.group-id=test"
|
"app.kafka.group-id=test"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(RequestProcessor::class)
|
@MockitoBean(types = [RequestProcessor::class])
|
||||||
inner class AppConfigurationUsingKafkaInputTest(private val context: ApplicationContext) {
|
inner class AppConfigurationUsingKafkaInputTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -181,40 +186,7 @@ class AppConfigurationTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=",
|
"app.pseudonymize.generator=buildin"
|
||||||
"app.pseudonymizer=buildin",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class AppConfigurationPseudonymizerBuildinTest(private val context: ApplicationContext) {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldUseConfiguredGenerator() {
|
|
||||||
assertThat(context.getBean(AnonymizingGenerator::class.java)).isNotNull
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.pseudonymize.generator=",
|
|
||||||
"app.pseudonymizer=gpas",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class AppConfigurationPseudonymizerGpasTest(private val context: ApplicationContext) {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldUseConfiguredGenerator() {
|
|
||||||
assertThat(context.getBean(GpasPseudonymGenerator::class.java)).isNotNull
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.pseudonymize.generator=buildin",
|
|
||||||
"app.pseudonymizer=",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class AppConfigurationPseudonymizeGeneratorBuildinTest(private val context: ApplicationContext) {
|
inner class AppConfigurationPseudonymizeGeneratorBuildinTest(private val context: ApplicationContext) {
|
||||||
@@ -229,8 +201,7 @@ class AppConfigurationTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=gpas",
|
"app.pseudonymize.generator=gpas"
|
||||||
"app.pseudonymizer=",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class AppConfigurationPseudonymizeGeneratorGpasTest(private val context: ApplicationContext) {
|
inner class AppConfigurationPseudonymizeGeneratorGpasTest(private val context: ApplicationContext) {
|
||||||
@@ -248,11 +219,13 @@ class AppConfigurationTest {
|
|||||||
"app.security.enable-tokens=true"
|
"app.security.enable-tokens=true"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBeans(value = [
|
@MockitoBean(
|
||||||
MockBean(InMemoryUserDetailsManager::class),
|
types = [
|
||||||
MockBean(PasswordEncoder::class),
|
InMemoryUserDetailsManager::class,
|
||||||
MockBean(TokenRepository::class)
|
PasswordEncoder::class,
|
||||||
])
|
TokenRepository::class
|
||||||
|
]
|
||||||
|
)
|
||||||
inner class AppConfigurationTokenEnabledTest(private val context: ApplicationContext) {
|
inner class AppConfigurationTokenEnabledTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -263,11 +236,13 @@ class AppConfigurationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@MockBeans(value = [
|
@MockitoBean(
|
||||||
MockBean(InMemoryUserDetailsManager::class),
|
types = [
|
||||||
MockBean(PasswordEncoder::class),
|
InMemoryUserDetailsManager::class,
|
||||||
MockBean(TokenRepository::class)
|
PasswordEncoder::class,
|
||||||
])
|
TokenRepository::class
|
||||||
|
]
|
||||||
|
)
|
||||||
inner class AppConfigurationTokenDisabledTest(private val context: ApplicationContext) {
|
inner class AppConfigurationTokenDisabledTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -305,4 +280,30 @@ class AppConfigurationTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@Nested
|
||||||
|
@TestPropertySource(
|
||||||
|
properties = [
|
||||||
|
"app.consent.service=GICS",
|
||||||
|
"app.consent.gics.uri=http://localhost:9000",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
inner class AppConfigurationConsentGicsTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldUseConfiguredGenerator() {
|
||||||
|
assertThat(context.getBean(GicsConsentService::class.java)).isNotNull
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class AppConfigurationConsentBuildinTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldUseConfiguredGenerator() {
|
||||||
|
assertThat(context.getBean(MtbFileConsentService::class.java)).isNotNull
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@@ -20,44 +20,47 @@
|
|||||||
package dev.dnpm.etl.processor.input
|
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 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.ConsentEvaluation
|
||||||
|
import dev.dnpm.etl.processor.consent.ConsentEvaluator
|
||||||
|
import dev.dnpm.etl.processor.consent.MtbFileConsentService
|
||||||
|
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
||||||
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
|
||||||
|
import dev.pcvolkmer.mv64e.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.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.*
|
||||||
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.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
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
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@WebMvcTest(controllers = [MtbFileRestController::class])
|
@WebMvcTest(controllers = [MtbFileRestController::class])
|
||||||
@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
|
@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
|
||||||
@ContextConfiguration(
|
@ContextConfiguration(
|
||||||
classes = [
|
classes = [
|
||||||
MtbFileRestController::class,
|
MtbFileRestController::class,
|
||||||
AppSecurityConfiguration::class
|
AppSecurityConfiguration::class,
|
||||||
|
MtbFileConsentService::class
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(TokenRepository::class, RequestProcessor::class)
|
@MockitoBean(types = [TokenRepository::class, RequestProcessor::class, ConsentEvaluator::class])
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
@@ -68,17 +71,23 @@ import org.springframework.test.web.servlet.post
|
|||||||
)
|
)
|
||||||
class MtbFileRestControllerTest {
|
class MtbFileRestControllerTest {
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
lateinit var mockMvc: MockMvc
|
||||||
|
lateinit var requestProcessor: RequestProcessor
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
lateinit var consentEvaluator: ConsentEvaluator
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Autowired mockMvc: MockMvc,
|
@Autowired mockMvc: MockMvc,
|
||||||
@Autowired requestProcessor: RequestProcessor
|
@Autowired requestProcessor: RequestProcessor,
|
||||||
|
@Autowired consentEvaluator: ConsentEvaluator
|
||||||
) {
|
) {
|
||||||
this.mockMvc = mockMvc
|
this.mockMvc = mockMvc
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
|
this.consentEvaluator = consentEvaluator
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true)
|
||||||
|
}.whenever(consentEvaluator).check(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -91,7 +100,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -104,7 +113,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -117,7 +126,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any())
|
verify(requestProcessor, never()).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -130,7 +139,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isForbidden() }
|
status { isForbidden() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any())
|
verify(requestProcessor, never()).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -141,7 +150,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -152,11 +161,11 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processDeletion(anyValueClass())
|
verify(requestProcessor, never()).processDeletion(anyValueClass(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@MockBean(UserRoleRepository::class, ClientRegistrationRepository::class)
|
@MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::class])
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
@@ -177,7 +186,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -190,33 +199,26 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val mtbFile: MtbFile = MtbFile.builder()
|
val mtbFile = Mtb.builder()
|
||||||
.withPatient(
|
.patient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("PID")
|
.id("PID")
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withConsent(
|
.episodesOfCare(
|
||||||
Consent.builder()
|
listOf(
|
||||||
.withId("1")
|
MtbEpisodeOfCare.builder()
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
.id("1")
|
||||||
.withPatient("PID")
|
.patient(Reference.builder().id("PID").build())
|
||||||
.build()
|
.period(PeriodDate.builder().start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))).build())
|
||||||
)
|
.build()
|
||||||
.withEpisode(
|
)
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("PID")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@@ -27,8 +27,8 @@ import org.junit.jupiter.api.extension.ExtendWith
|
|||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
|
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
|
||||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers
|
import org.testcontainers.junit.jupiter.Testcontainers
|
||||||
@@ -39,7 +39,7 @@ import java.time.Instant
|
|||||||
@DataJdbcTest
|
@DataJdbcTest
|
||||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||||
@Transactional
|
@Transactional
|
||||||
@MockBean(MtbFileSender::class)
|
@MockitoBean(types = [MtbFileSender::class])
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin",
|
"app.pseudonymize.generator=buildin",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
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
|
||||||
@@ -42,30 +43,37 @@ 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(
|
||||||
"http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
"https://localhost:9990/ttp-fhir/fhir/gpas",
|
||||||
"test",
|
"test", "test2",
|
||||||
null,
|
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
this.restTemplate = RestTemplate()
|
this.restTemplate = RestTemplate()
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
this.generator = GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate)
|
this.generator =
|
||||||
|
GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate, appFhirConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldReturnExpectedPseudonym() {
|
fun shouldReturnExpectedPseudonym() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890"))
|
withStatus(HttpStatus.OK).body(
|
||||||
|
getDummyResponseBody(
|
||||||
|
"1234",
|
||||||
|
"test",
|
||||||
|
"test1234ABCDEF567890"
|
||||||
|
)
|
||||||
|
)
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +84,7 @@ class GpasPseudonymGeneratorTest {
|
|||||||
fun shouldThrowExceptionIfGpasNotAvailable() {
|
fun shouldThrowExceptionIfGpasNotAvailable() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withException(IOException("Simulated IO error")).createResponse(it)
|
withException(IOException("Simulated IO error")).createResponse(it)
|
||||||
}
|
}
|
||||||
@@ -88,10 +96,13 @@ class GpasPseudonymGeneratorTest {
|
|||||||
fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
|
fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withStatus(HttpStatus.FOUND)
|
withStatus(HttpStatus.FOUND)
|
||||||
.header(HttpHeaders.LOCATION, "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
.header(
|
||||||
|
HttpHeaders.LOCATION,
|
||||||
|
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate"
|
||||||
|
)
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -31,8 +31,8 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers
|
import org.testcontainers.junit.jupiter.Testcontainers
|
||||||
@@ -42,7 +42,7 @@ import java.time.Instant
|
|||||||
@ExtendWith(SpringExtension::class)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@Transactional
|
@Transactional
|
||||||
@MockBean(MtbFileSender::class)
|
@MockitoBean(types = [MtbFileSender::class])
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin",
|
"app.pseudonymize.generator=buildin",
|
||||||
|
@@ -19,21 +19,22 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
import com.gargoylesoftware.htmlunit.WebClient
|
|
||||||
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
|
||||||
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
|
||||||
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
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
|
||||||
import dev.dnpm.etl.processor.security.UserRoleService
|
import dev.dnpm.etl.processor.security.UserRoleService
|
||||||
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.htmlunit.WebClient
|
||||||
|
import org.htmlunit.html.HtmlPage
|
||||||
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
|
||||||
@@ -46,7 +47,6 @@ import org.mockito.kotlin.verify
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
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.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
||||||
@@ -55,6 +55,7 @@ import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequ
|
|||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.test.web.servlet.*
|
import org.springframework.test.web.servlet.*
|
||||||
@@ -81,14 +82,17 @@ abstract class MockSink : Sinks.Many<Boolean>
|
|||||||
"app.pseudonymize.generator=BUILDIN"
|
"app.pseudonymize.generator=BUILDIN"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(name = "configsUpdateProducer", classes = [MockSink::class])
|
@MockitoBean(name = "configsUpdateProducer", types = [MockSink::class])
|
||||||
@MockBean(
|
@MockitoBean(
|
||||||
Generator::class,
|
types = [
|
||||||
MtbFileSender::class,
|
Generator::class,
|
||||||
RequestProcessor::class,
|
MtbFileSender::class,
|
||||||
TransformationService::class,
|
RequestProcessor::class,
|
||||||
GPasConnectionCheckService::class,
|
TransformationService::class,
|
||||||
RestConnectionCheckService::class,
|
GPasConnectionCheckService::class,
|
||||||
|
RestConnectionCheckService::class,
|
||||||
|
GIcsConnectionCheckService::class
|
||||||
|
]
|
||||||
)
|
)
|
||||||
class ConfigControllerTest {
|
class ConfigControllerTest {
|
||||||
|
|
||||||
@@ -143,8 +147,10 @@ class ConfigControllerTest {
|
|||||||
"app.security.admin-user=admin"
|
"app.security.admin-user=admin"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(
|
@MockitoBean(
|
||||||
TokenService::class
|
types = [
|
||||||
|
TokenService::class
|
||||||
|
]
|
||||||
)
|
)
|
||||||
inner class WithTokensEnabled {
|
inner class WithTokensEnabled {
|
||||||
private lateinit var tokenService: TokenService
|
private lateinit var tokenService: TokenService
|
||||||
@@ -178,7 +184,13 @@ class ConfigControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testShouldNotSaveTokenWithExstingName() {
|
fun testShouldNotSaveTokenWithExstingName() {
|
||||||
whenever(tokenService.addToken(anyString())).thenReturn(Result.failure(RuntimeException("Testfailure")))
|
whenever(tokenService.addToken(anyString())).thenReturn(
|
||||||
|
Result.failure(
|
||||||
|
RuntimeException(
|
||||||
|
"Testfailure"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
mockMvc.post("/configs/tokens") {
|
mockMvc.post("/configs/tokens") {
|
||||||
with(user("admin").roles("ADMIN"))
|
with(user("admin").roles("ADMIN"))
|
||||||
@@ -252,8 +264,10 @@ class ConfigControllerTest {
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(
|
@MockitoBean(
|
||||||
UserRoleService::class
|
types = [
|
||||||
|
UserRoleService::class
|
||||||
|
]
|
||||||
)
|
)
|
||||||
inner class WithUserRolesEnabled {
|
inner class WithUserRolesEnabled {
|
||||||
private lateinit var userRoleService: UserRoleService
|
private lateinit var userRoleService: UserRoleService
|
||||||
@@ -297,7 +311,10 @@ class ConfigControllerTest {
|
|||||||
|
|
||||||
val idCaptor = argumentCaptor<Long>()
|
val idCaptor = argumentCaptor<Long>()
|
||||||
val roleCaptor = argumentCaptor<Role>()
|
val roleCaptor = argumentCaptor<Role>()
|
||||||
verify(userRoleService, times(1)).updateUserRole(idCaptor.capture(), roleCaptor.capture())
|
verify(userRoleService, times(1)).updateUserRole(
|
||||||
|
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)
|
||||||
@@ -335,23 +352,26 @@ 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 testShouldRequestSSE() {
|
fun testShouldRequestGPasSSE() {
|
||||||
val expectedEvent = ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
|
val expectedEvent =
|
||||||
|
ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
|
||||||
|
|
||||||
connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
|
connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
|
||||||
connectionCheckUpdateProducer.emitComplete { _, _ -> true }
|
connectionCheckUpdateProducer.emitComplete { _, _ -> true }
|
||||||
|
|
||||||
val result = webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM).exchange()
|
val result =
|
||||||
.expectStatus().isOk()
|
webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM)
|
||||||
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
.exchange()
|
||||||
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
|
.expectStatus().isOk()
|
||||||
|
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
||||||
|
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
|
||||||
|
|
||||||
StepVerifier.create(result.responseBody)
|
StepVerifier.create(result.responseBody)
|
||||||
.expectNext(expectedEvent)
|
.expectNext(expectedEvent)
|
||||||
|
@@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
import com.gargoylesoftware.htmlunit.WebClient
|
|
||||||
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
|
||||||
import dev.dnpm.etl.processor.*
|
import dev.dnpm.etl.processor.*
|
||||||
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
|
||||||
@@ -30,6 +28,8 @@ 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.services.RequestService
|
import dev.dnpm.etl.processor.services.RequestService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.htmlunit.WebClient
|
||||||
|
import org.htmlunit.html.HtmlPage
|
||||||
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
|
||||||
@@ -40,13 +40,13 @@ import org.mockito.kotlin.any
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
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.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.PageImpl
|
import org.springframework.data.domain.PageImpl
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.security.test.context.support.WithMockUser
|
import org.springframework.security.test.context.support.WithMockUser
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
@@ -71,8 +71,8 @@ import java.util.*
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(
|
@MockitoBean(
|
||||||
RequestService::class
|
types = [RequestService::class]
|
||||||
)
|
)
|
||||||
class HomeControllerTest {
|
class HomeControllerTest {
|
||||||
|
|
||||||
|
@@ -19,21 +19,21 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
import com.gargoylesoftware.htmlunit.WebClient
|
|
||||||
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
|
||||||
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.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.htmlunit.WebClient
|
||||||
|
import org.htmlunit.html.HtmlPage
|
||||||
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 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.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.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
@@ -56,8 +56,8 @@ import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
|
|||||||
"app.security.enable-tokens=true"
|
"app.security.enable-tokens=true"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(
|
@MockitoBean(
|
||||||
TokenService::class,
|
types = [TokenService::class]
|
||||||
)
|
)
|
||||||
class LoginControllerTest {
|
class LoginControllerTest {
|
||||||
|
|
||||||
|
@@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
import com.gargoylesoftware.htmlunit.WebClient
|
|
||||||
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 org.htmlunit.WebClient
|
||||||
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 org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
@@ -41,10 +41,10 @@ import org.mockito.kotlin.doAnswer
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
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.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
@@ -74,8 +74,8 @@ import java.time.temporal.ChronoUnit
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockBean(
|
@MockitoBean(
|
||||||
RequestService::class
|
types = [RequestService::class]
|
||||||
)
|
)
|
||||||
class StatisticsRestControllerTest {
|
class StatisticsRestControllerTest {
|
||||||
|
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.dnpm.etl.processor.consent;
|
||||||
|
|
||||||
|
public enum ConsentDomain {
|
||||||
|
/**
|
||||||
|
* MII Broad consent
|
||||||
|
*/
|
||||||
|
BROAD_CONSENT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GenomDe Modellvorhaben §64e
|
||||||
|
*/
|
||||||
|
MODELLVORHABEN_64E
|
||||||
|
}
|
@@ -0,0 +1,307 @@
|
|||||||
|
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 org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.hl7.fhir.r4.model.*;
|
||||||
|
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to request Consent from remote gICS installation
|
||||||
|
*
|
||||||
|
* @since 0.11
|
||||||
|
*/
|
||||||
|
public class GicsConsentService implements IConsentService {
|
||||||
|
|
||||||
|
private final Logger log = LoggerFactory.getLogger(GicsConsentService.class);
|
||||||
|
|
||||||
|
public static final String IS_CONSENTED_ENDPOINT = "/$isConsented";
|
||||||
|
public static final String IS_POLICY_STATES_FOR_PERSON_ENDPOINT = "/$currentPolicyStatesForPerson";
|
||||||
|
|
||||||
|
private final RetryTemplate retryTemplate;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
private final FhirContext fhirContext;
|
||||||
|
private final GIcsConfigProperties gIcsConfigProperties;
|
||||||
|
|
||||||
|
public GicsConsentService(
|
||||||
|
GIcsConfigProperties gIcsConfigProperties,
|
||||||
|
RetryTemplate retryTemplate,
|
||||||
|
RestTemplate restTemplate,
|
||||||
|
AppFhirConfig appFhirConfig
|
||||||
|
) {
|
||||||
|
this.retryTemplate = retryTemplate;
|
||||||
|
this.restTemplate = restTemplate;
|
||||||
|
this.fhirContext = appFhirConfig.fhirContext();
|
||||||
|
this.gIcsConfigProperties = gIcsConfigProperties;
|
||||||
|
log.info("GicsConsentService initialized...");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Parameters getFhirRequestParameters(
|
||||||
|
String personIdentifierValue
|
||||||
|
) {
|
||||||
|
var result = new Parameters();
|
||||||
|
result.addParameter(
|
||||||
|
new ParametersParameterComponent()
|
||||||
|
.setName("personIdentifier")
|
||||||
|
.setValue(
|
||||||
|
new Identifier()
|
||||||
|
.setValue(personIdentifierValue)
|
||||||
|
.setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
result.addParameter(
|
||||||
|
new ParametersParameterComponent()
|
||||||
|
.setName("domain")
|
||||||
|
.setValue(
|
||||||
|
new StringType()
|
||||||
|
.setValue(this.gIcsConfigProperties.getBroadConsentDomainName())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
result.addParameter(
|
||||||
|
new ParametersParameterComponent()
|
||||||
|
.setName("policy")
|
||||||
|
.setValue(
|
||||||
|
new Coding()
|
||||||
|
.setCode(this.gIcsConfigProperties.getBroadConsentPolicyCode())
|
||||||
|
.setSystem(this.gIcsConfigProperties.getBroadConsentPolicySystem())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* is mandatory parameter, but we ignore it via additional configuration parameter
|
||||||
|
* 'ignoreVersionNumber'.
|
||||||
|
*/
|
||||||
|
result.addParameter(
|
||||||
|
new ParametersParameterComponent()
|
||||||
|
.setName("version")
|
||||||
|
.setValue(new StringType().setValue("1.1")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* add config parameter with:
|
||||||
|
* ignoreVersionNumber -> true ->> Reason is we cannot know which policy version each patient
|
||||||
|
* has possibly signed or not, therefore we are happy with any version found.
|
||||||
|
* unknownStateIsConsideredAsDecline -> true
|
||||||
|
*/
|
||||||
|
var config = new ParametersParameterComponent()
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI endpointUri(String endpoint) {
|
||||||
|
assert this.gIcsConfigProperties.getUri() != null;
|
||||||
|
return UriComponentsBuilder.fromUriString(this.gIcsConfigProperties.getUri()).path(endpoint).build().toUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpHeaders headersWithHttpBasicAuth() {
|
||||||
|
assert this.gIcsConfigProperties.getUri() != null;
|
||||||
|
|
||||||
|
var headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_XML);
|
||||||
|
|
||||||
|
if (
|
||||||
|
StringUtils.isBlank(this.gIcsConfigProperties.getUsername())
|
||||||
|
|| StringUtils.isBlank(this.gIcsConfigProperties.getPassword())
|
||||||
|
) {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.setBasicAuth(this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword());
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String callGicsApi(Parameters parameter, String endpoint) {
|
||||||
|
var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter);
|
||||||
|
HttpEntity<String> requestEntity = new HttpEntity<>(parameterAsXml, this.headersWithHttpBasicAuth());
|
||||||
|
try {
|
||||||
|
var responseEntity = retryTemplate.execute(
|
||||||
|
ctx -> restTemplate.exchange(endpointUri(endpoint), HttpMethod.POST, requestEntity, String.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||||
|
return responseEntity.getBody();
|
||||||
|
} else {
|
||||||
|
var msg = String.format(
|
||||||
|
"Trusted party system reached but request failed! code: '%s' response: '%s'",
|
||||||
|
responseEntity.getStatusCode(), responseEntity.getBody());
|
||||||
|
log.error(msg);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (RestClientException e) {
|
||||||
|
var msg = String.format("Get consents status request failed reason: '%s",
|
||||||
|
e.getMessage());
|
||||||
|
log.error(msg);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
} catch (TerminatedRetryException terminatedRetryException) {
|
||||||
|
var msg = String.format(
|
||||||
|
"Get consents status process has been terminated. termination reason: '%s",
|
||||||
|
terminatedRetryException.getMessage());
|
||||||
|
log.error(msg);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
|
||||||
|
var consentStatusResponse = callGicsApi(
|
||||||
|
getFhirRequestParameters(personIdentifierValue),
|
||||||
|
GicsConsentService.IS_CONSENTED_ENDPOINT
|
||||||
|
);
|
||||||
|
return evaluateConsentResponse(consentStatusResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Bundle currentConsentForPersonAndTemplate(
|
||||||
|
String personIdentifierValue,
|
||||||
|
ConsentDomain consentDomain,
|
||||||
|
Date requestDate
|
||||||
|
) {
|
||||||
|
|
||||||
|
var requestParameter = buildRequestParameterCurrentPolicyStatesForPerson(
|
||||||
|
personIdentifierValue,
|
||||||
|
requestDate,
|
||||||
|
consentDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
var consentDataSerialized = callGicsApi(requestParameter,
|
||||||
|
GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT);
|
||||||
|
|
||||||
|
if (consentDataSerialized == null) {
|
||||||
|
// error occurred - should not process further!
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"consent data request failed - stopping processing! - try again or fix other problems first.");
|
||||||
|
}
|
||||||
|
var iBaseResource = fhirContext.newJsonParser()
|
||||||
|
.parseResource(consentDataSerialized);
|
||||||
|
if (iBaseResource instanceof OperationOutcome) {
|
||||||
|
// log error - very likely a configuration error
|
||||||
|
String errorMessage =
|
||||||
|
"Consent request failed! Check outcome:\n " + consentDataSerialized;
|
||||||
|
log.error(errorMessage);
|
||||||
|
throw new IllegalStateException(errorMessage);
|
||||||
|
} else if (iBaseResource instanceof Bundle bundle) {
|
||||||
|
return bundle;
|
||||||
|
} else {
|
||||||
|
String errorMessage = "Consent request failed! Unexpected response received! -> "
|
||||||
|
+ consentDataSerialized;
|
||||||
|
log.error(errorMessage);
|
||||||
|
throw new IllegalStateException(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private String getConsentDomainName(ConsentDomain targetConsentDomain) {
|
||||||
|
return switch (targetConsentDomain) {
|
||||||
|
case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName();
|
||||||
|
case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Parameters buildRequestParameterCurrentPolicyStatesForPerson(
|
||||||
|
String personIdentifierValue,
|
||||||
|
Date requestDate,
|
||||||
|
ConsentDomain consentDomain
|
||||||
|
) {
|
||||||
|
var requestParameter = new Parameters();
|
||||||
|
requestParameter.addParameter(
|
||||||
|
new ParametersParameterComponent()
|
||||||
|
.setName("personIdentifier")
|
||||||
|
.setValue(
|
||||||
|
new Identifier()
|
||||||
|
.setValue(personIdentifierValue)
|
||||||
|
.setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
requestParameter.addParameter(
|
||||||
|
new ParametersParameterComponent()
|
||||||
|
.setName("domain")
|
||||||
|
.setValue(new StringType().setValue(getConsentDomainName(consentDomain)))
|
||||||
|
);
|
||||||
|
|
||||||
|
Parameters nestedConfigParameters = new Parameters();
|
||||||
|
nestedConfigParameters
|
||||||
|
.addParameter(
|
||||||
|
new ParametersParameterComponent()
|
||||||
|
.setName("idMatchingType")
|
||||||
|
.setValue(new Coding()
|
||||||
|
.setSystem("https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType")
|
||||||
|
.setCode("AT_LEAST_ONE")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addParameter("ignoreVersionNumber", false)
|
||||||
|
.addParameter("unknownStateIsConsideredAsDecline", false)
|
||||||
|
.addParameter("requestDate", new DateType().setValue(requestDate));
|
||||||
|
|
||||||
|
requestParameter.addParameter(
|
||||||
|
new ParametersParameterComponent().setName("config").addPart().setResource(nestedConfigParameters)
|
||||||
|
);
|
||||||
|
|
||||||
|
return requestParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TtpConsentStatus evaluateConsentResponse(String consentStatusResponse) {
|
||||||
|
if (consentStatusResponse == null) {
|
||||||
|
return TtpConsentStatus.FAILED_TO_ASK;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var response = fhirContext.newJsonParser().parseResource(consentStatusResponse);
|
||||||
|
|
||||||
|
if (response instanceof Parameters responseParameters) {
|
||||||
|
|
||||||
|
var responseValue = responseParameters.getParameter("consented").getValue();
|
||||||
|
var isConsented = responseValue.castToBoolean(responseValue);
|
||||||
|
if (!isConsented.hasValue()) {
|
||||||
|
return TtpConsentStatus.FAILED_TO_ASK;
|
||||||
|
}
|
||||||
|
if (isConsented.booleanValue()) {
|
||||||
|
return TtpConsentStatus.BROAD_CONSENT_GIVEN;
|
||||||
|
} else {
|
||||||
|
return TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED;
|
||||||
|
}
|
||||||
|
} else if (response instanceof OperationOutcome outcome) {
|
||||||
|
log.error("failed to get consent status from ttp. probably configuration error. "
|
||||||
|
+ "outcome: '{}'", fhirContext.newJsonParser().encodeToString(outcome));
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (DataFormatException dfe) {
|
||||||
|
log.error("failed to parse response to FHIR R4 resource.", dfe);
|
||||||
|
}
|
||||||
|
return TtpConsentStatus.FAILED_TO_ASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) {
|
||||||
|
return currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
package dev.dnpm.etl.processor.consent;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
|
|
||||||
|
public interface IConsentService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get broad consent status for a patient identifier
|
||||||
|
*
|
||||||
|
* @param personIdentifierValue patient identifier used for consent data
|
||||||
|
* @return status of broad consent
|
||||||
|
* @apiNote cannot not differ between not asked and rejected
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get broad consent policies with respect to a request date
|
||||||
|
*
|
||||||
|
* @param personIdentifierValue patient identifier used for consent data
|
||||||
|
* @param requestDate target date until consent data should be considered
|
||||||
|
* @return consent policies as bundle; <p>if empty patient has not been asked, yet.</p>
|
||||||
|
*/
|
||||||
|
Bundle getConsent(String personIdentifierValue, Date requestDate, ConsentDomain consentDomain);
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.dnpm.etl.processor.consent;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class MtbFileConsentService implements IConsentService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MtbFileConsentService.class);
|
||||||
|
|
||||||
|
public MtbFileConsentService() {
|
||||||
|
log.info("ConsentCheckFileBased initialized...");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
|
||||||
|
return TtpConsentStatus.UNKNOWN_CHECK_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EMPTY METHOD: NOT IMPLEMENTED
|
||||||
|
*
|
||||||
|
* @return empty bundle
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Bundle getConsent(String personIdentifierValue, Date requestDate,
|
||||||
|
ConsentDomain consentDomain) {
|
||||||
|
return new Bundle();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package dev.dnpm.etl.processor.consent;
|
||||||
|
|
||||||
|
public enum TtpConsentStatus {
|
||||||
|
/**
|
||||||
|
* Valid consent found
|
||||||
|
*/
|
||||||
|
BROAD_CONSENT_GIVEN,
|
||||||
|
/**
|
||||||
|
* Missing or rejected...actually unknown
|
||||||
|
*/
|
||||||
|
BROAD_CONSENT_MISSING_OR_REJECTED,
|
||||||
|
/**
|
||||||
|
* No Broad consent policy found
|
||||||
|
*/
|
||||||
|
BROAD_CONSENT_MISSING,
|
||||||
|
/**
|
||||||
|
* Research policy has been rejected
|
||||||
|
*/
|
||||||
|
BROAD_CONSENT_REJECTED,
|
||||||
|
|
||||||
|
GENOM_DE_CONSENT_SEQUENCING_PERMIT,
|
||||||
|
/**
|
||||||
|
* No GenomDE consent policy found
|
||||||
|
*/
|
||||||
|
GENOM_DE_CONSENT_MISSING,
|
||||||
|
/**
|
||||||
|
* GenomDE consent policy found, but has been rejected
|
||||||
|
*/
|
||||||
|
GENOM_DE_SEQUENCING_REJECTED,
|
||||||
|
/**
|
||||||
|
* Consent status is validate via file property 'consent.status'
|
||||||
|
*/
|
||||||
|
UNKNOWN_CHECK_FILE,
|
||||||
|
/**
|
||||||
|
* Due technical problems consent status is unknown
|
||||||
|
*/
|
||||||
|
FAILED_TO_ASK
|
||||||
|
}
|
@@ -23,4 +23,6 @@ public interface Generator {
|
|||||||
|
|
||||||
String generate(String id);
|
String generate(String id);
|
||||||
|
|
||||||
|
String generateGenomDeTan(String id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -21,8 +21,13 @@ 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 java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.hc.core5.net.URIBuilder;
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
import org.hl7.fhir.r4.model.Parameters;
|
import org.hl7.fhir.r4.model.Parameters;
|
||||||
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
||||||
@@ -32,42 +37,76 @@ 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 static FhirContext r4Context = FhirContext.forR4();
|
private final FhirContext r4Context;
|
||||||
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 genomDeTanDomain;
|
||||||
|
private final @NotNull String pidPsnDomain;
|
||||||
|
protected final static String createOrGetPsn = "$pseudonymizeAllowCreate";
|
||||||
|
protected final static String createMultiDomainPsn = "$pseudonymize-secondary";
|
||||||
|
private final static String SINGLE_PSN_PART_NAME = "pseudonym";
|
||||||
|
private final static String MULTI_PSN_PART_NAME = "value";
|
||||||
|
|
||||||
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) {
|
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
|
||||||
|
RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
|
||||||
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.pidPsnDomain = gpasCfg.getPatientDomain();
|
||||||
|
this.genomDeTanDomain = gpasCfg.getGenomDeTanDomain();
|
||||||
|
this.r4Context = appFhirConfig.fhirContext();
|
||||||
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, PsnDomainType.SINGLE_PSN_DOMAIN);
|
||||||
var responseEntity = getGpasPseudonym(gPasRequestBody);
|
}
|
||||||
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
|
||||||
.parseResource(responseEntity.getBody());
|
|
||||||
|
|
||||||
return unwrapPseudonym(gPasPseudonymResult);
|
@Override
|
||||||
|
public String generateGenomDeTan(String id) {
|
||||||
|
return generate(id, PsnDomainType.MULTI_PSN_DOMAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String generate(String id, PsnDomainType domainType) {
|
||||||
|
switch (domainType) {
|
||||||
|
case SINGLE_PSN_DOMAIN -> {
|
||||||
|
final var requestBody = createSinglePsnRequestBody(id, pidPsnDomain);
|
||||||
|
final var responseEntity = getGpasPseudonym(requestBody, createOrGetPsn);
|
||||||
|
final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
||||||
|
.parseResource(responseEntity.getBody());
|
||||||
|
|
||||||
|
return unwrapPseudonym(gPasPseudonymResult, SINGLE_PSN_PART_NAME);
|
||||||
|
}
|
||||||
|
case MULTI_PSN_DOMAIN -> {
|
||||||
|
final var requestBody = createMultiPsnRequestBody(id, genomDeTanDomain);
|
||||||
|
final var responseEntity = getGpasPseudonym(requestBody, createMultiDomainPsn);
|
||||||
|
final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
||||||
|
.parseResource(responseEntity.getBody());
|
||||||
|
|
||||||
|
return unwrapPseudonym(gPasPseudonymResult, MULTI_PSN_PART_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NotImplementedException(
|
||||||
|
"give domain type '%s' is unexpected and is currently not supported!".formatted(
|
||||||
|
domainType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static String unwrapPseudonym(Parameters gPasPseudonymResult) {
|
public static String unwrapPseudonym(Parameters gPasPseudonymResult, String targetPartName) {
|
||||||
final var parameters = gPasPseudonymResult.getParameter().stream().findFirst();
|
final var parameters = gPasPseudonymResult.getParameter().stream().findFirst();
|
||||||
|
|
||||||
if (parameters.isEmpty()) {
|
if (parameters.isEmpty()) {
|
||||||
@@ -75,9 +114,9 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final var identifier = (Identifier) parameters.get().getPart().stream()
|
final var identifier = (Identifier) parameters.get().getPart().stream()
|
||||||
.filter(a -> a.getName().equals("pseudonym"))
|
.filter(a -> a.getName().equals(targetPartName))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseGet(ParametersParameterComponent::new).getValue();
|
.orElseGet(ParametersParameterComponent::new).getValue();
|
||||||
|
|
||||||
// pseudonym
|
// pseudonym
|
||||||
return sanitizeValue(identifier.getValue());
|
return sanitizeValue(identifier.getValue());
|
||||||
@@ -97,42 +136,80 @@ 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, String apiEndpoint) {
|
||||||
|
|
||||||
HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader);
|
HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader);
|
||||||
ResponseEntity<String> responseEntity;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
responseEntity = retryTemplate.execute(
|
var targetUrl = buildRequestUrl(apiEndpoint);
|
||||||
ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
|
ResponseEntity<String> responseEntity = retryTemplate.execute(
|
||||||
String.class));
|
ctx -> restTemplate.exchange(targetUrl, HttpMethod.POST, requestEntity,
|
||||||
|
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 {
|
return responseEntity;
|
||||||
log.warn("API request unsuccessful. Response: {}", requestEntity.getBody());
|
}
|
||||||
throw new PseudonymRequestFailed("API request unsuccessful gPas unsuccessful.");
|
} catch (RestClientException rce) {
|
||||||
|
if (rce instanceof BadRequest) {
|
||||||
|
String msg = "gPas or request configuration is incorrect. Please check both."
|
||||||
|
+ rce.getMessage();
|
||||||
|
log.debug(
|
||||||
|
msg);
|
||||||
|
throw new PseudonymRequestFailed(msg, rce);
|
||||||
|
}
|
||||||
|
if (rce instanceof Unauthorized) {
|
||||||
|
var msg = "gPas access credentials are invalid check your configuration. msg: '%s".formatted(
|
||||||
|
rce.getMessage());
|
||||||
|
log.error(msg);
|
||||||
|
throw new PseudonymRequestFailed(msg, rce);
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseEntity;
|
|
||||||
} catch (Exception unexpected) {
|
} 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 URI buildRequestUrl(String apiEndpoint) throws URISyntaxException {
|
||||||
var requestParameters = new Parameters();
|
var gPasUrl1 = gPasUrl;
|
||||||
|
if (gPasUrl.lastIndexOf("/") == gPasUrl.length() - 1) {
|
||||||
|
gPasUrl1 = gPasUrl.substring(0, gPasUrl.length() - 1);
|
||||||
|
}
|
||||||
|
var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(apiEndpoint);
|
||||||
|
|
||||||
|
return urlBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String createSinglePsnRequestBody(String id, String targetDomain) {
|
||||||
|
final var requestParameters = new Parameters();
|
||||||
requestParameters.addParameter().setName("target")
|
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();
|
||||||
return iParser.encodeResourceToString(requestParameters);
|
return iParser.encodeResourceToString(requestParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String createMultiPsnRequestBody(String id, String targetDomain) {
|
||||||
|
final var param = new Parameters();
|
||||||
|
ParametersParameterComponent targetParam = param.addParameter().setName("original");
|
||||||
|
targetParam.addPart(
|
||||||
|
new ParametersParameterComponent().setName("target")
|
||||||
|
.setValue(new StringType(targetDomain)));
|
||||||
|
targetParam.addPart(
|
||||||
|
new ParametersParameterComponent().setName("value").setValue(new StringType(id)));
|
||||||
|
targetParam
|
||||||
|
.addPart(new ParametersParameterComponent().setName("count").setValue(
|
||||||
|
new StringType("1")));
|
||||||
|
|
||||||
|
final IParser iParser = r4Context.newJsonParser();
|
||||||
|
return iParser.encodeResourceToString(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
protected HttpHeaders getHttpHeaders(String gPasUserName, String gPasPassword) {
|
protected HttpHeaders getHttpHeaders(String gPasUserName, String gPasPassword) {
|
||||||
var headers = new HttpHeaders();
|
var headers = new HttpHeaders();
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.dnpm.etl.processor.pseudonym;
|
||||||
|
|
||||||
|
public enum PsnDomainType {
|
||||||
|
/**
|
||||||
|
* one pseudonym per original value
|
||||||
|
*/
|
||||||
|
SINGLE_PSN_DOMAIN,
|
||||||
|
/**
|
||||||
|
* multiple pseudonymes for one original value
|
||||||
|
*/
|
||||||
|
MULTI_PSN_DOMAIN
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -21,19 +21,13 @@ package dev.dnpm.etl.processor.config
|
|||||||
|
|
||||||
import dev.dnpm.etl.processor.security.Role
|
import dev.dnpm.etl.processor.security.Role
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty
|
|
||||||
|
|
||||||
@ConfigurationProperties(AppConfigProperties.NAME)
|
@ConfigurationProperties(AppConfigProperties.NAME)
|
||||||
data class AppConfigProperties(
|
data class AppConfigProperties(
|
||||||
var bwhcUri: String?,
|
|
||||||
@get:DeprecatedConfigurationProperty(
|
|
||||||
reason = "Deprecated in favor of 'app.pseudonymize.generator'",
|
|
||||||
replacement = "app.pseudonymize.generator"
|
|
||||||
)
|
|
||||||
var pseudonymizer: PseudonymGenerator = PseudonymGenerator.BUILDIN,
|
|
||||||
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 = false
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val NAME = "app"
|
const val NAME = "app"
|
||||||
@@ -53,25 +47,87 @@ data class PseudonymizeConfigProperties(
|
|||||||
@ConfigurationProperties(GPasConfigProperties.NAME)
|
@ConfigurationProperties(GPasConfigProperties.NAME)
|
||||||
data class GPasConfigProperties(
|
data class GPasConfigProperties(
|
||||||
val uri: String?,
|
val uri: String?,
|
||||||
val target: String = "etl-processor",
|
val patientDomain: String = "etl-processor",
|
||||||
|
val genomDeTanDomain: String = "ccdn",
|
||||||
val username: String?,
|
val username: String?,
|
||||||
val password: String?,
|
val password: String?,
|
||||||
@get:DeprecatedConfigurationProperty(
|
|
||||||
reason = "Deprecated in favor of including Root CA"
|
|
||||||
)
|
|
||||||
val sslCaLocation: String?
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val NAME = "app.pseudonymize.gpas"
|
const val NAME = "app.pseudonymize.gpas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ConfigurationProperties(ConsentConfigProperties.NAME)
|
||||||
|
data class ConsentConfigProperties(
|
||||||
|
var service: ConsentService = ConsentService.NONE
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val NAME = "app.consent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigurationProperties(GIcsConfigProperties.NAME)
|
||||||
|
data class GIcsConfigProperties(
|
||||||
|
/**
|
||||||
|
* Base URL to gICS System
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
val uri: String?,
|
||||||
|
val username: String? = null,
|
||||||
|
val password: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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?,
|
||||||
val username: String?,
|
val username: String?,
|
||||||
val password: String?,
|
val password: String?
|
||||||
val isBwhc: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val NAME = "app.rest"
|
const val NAME = "app.rest"
|
||||||
@@ -82,18 +138,8 @@ data class RestTargetProperties(
|
|||||||
data class KafkaProperties(
|
data class KafkaProperties(
|
||||||
val inputTopic: String?,
|
val inputTopic: String?,
|
||||||
val outputTopic: String = "etl-processor",
|
val outputTopic: String = "etl-processor",
|
||||||
@get:DeprecatedConfigurationProperty(
|
|
||||||
reason = "Deprecated",
|
|
||||||
replacement = "outputTopic"
|
|
||||||
)
|
|
||||||
val topic: String = outputTopic,
|
|
||||||
val outputResponseTopic: String = "${outputTopic}_response",
|
val outputResponseTopic: String = "${outputTopic}_response",
|
||||||
@get:DeprecatedConfigurationProperty(
|
val groupId: String = "${outputTopic}_group",
|
||||||
reason = "Deprecated",
|
|
||||||
replacement = "outputResponseTopic"
|
|
||||||
)
|
|
||||||
val responseTopic: String = outputResponseTopic,
|
|
||||||
val groupId: String = "${topic}_group",
|
|
||||||
val servers: String = ""
|
val servers: String = ""
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -119,8 +165,13 @@ enum class PseudonymGenerator {
|
|||||||
GPAS
|
GPAS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class ConsentService {
|
||||||
|
NONE,
|
||||||
|
GICS
|
||||||
|
}
|
||||||
|
|
||||||
data class TransformationProperties(
|
data class TransformationProperties(
|
||||||
val path: String,
|
val path: String,
|
||||||
val from: String,
|
val from: String,
|
||||||
val to: String
|
val to: String
|
||||||
)
|
)
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -20,32 +20,29 @@
|
|||||||
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.monitoring.ConnectionCheckResult
|
import dev.dnpm.etl.processor.consent.MtbFileConsentService
|
||||||
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.consent.IConsentService
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.*
|
||||||
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.apache.hc.client5.http.impl.classic.HttpClients
|
|
||||||
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager
|
|
||||||
import org.apache.hc.client5.http.socket.ConnectionSocketFactory
|
|
||||||
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory
|
|
||||||
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory
|
|
||||||
import org.apache.hc.core5.http.config.RegistryBuilder
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Conditional
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.context.annotation.ConfigurationCondition
|
||||||
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
|
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
|
|
||||||
import org.springframework.retry.RetryCallback
|
import org.springframework.retry.RetryCallback
|
||||||
import org.springframework.retry.RetryContext
|
import org.springframework.retry.RetryContext
|
||||||
import org.springframework.retry.RetryListener
|
import org.springframework.retry.RetryListener
|
||||||
@@ -58,13 +55,6 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
|||||||
import org.springframework.web.client.HttpClientErrorException
|
import org.springframework.web.client.HttpClientErrorException
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.security.KeyStore
|
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import javax.net.ssl.SSLContext
|
|
||||||
import javax.net.ssl.TrustManagerFactory
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.toJavaDuration
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
@@ -74,7 +64,9 @@ import kotlin.time.toJavaDuration
|
|||||||
value = [
|
value = [
|
||||||
AppConfigProperties::class,
|
AppConfigProperties::class,
|
||||||
PseudonymizeConfigProperties::class,
|
PseudonymizeConfigProperties::class,
|
||||||
GPasConfigProperties::class
|
GPasConfigProperties::class,
|
||||||
|
ConsentConfigProperties::class,
|
||||||
|
GIcsConfigProperties::class
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@@ -87,113 +79,31 @@ 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(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
fun gpasPseudonymGenerator(
|
||||||
try {
|
configProperties: GPasConfigProperties,
|
||||||
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
retryTemplate: RetryTemplate,
|
||||||
return GpasPseudonymGenerator(
|
restTemplate: RestTemplate,
|
||||||
configProperties,
|
appFhirConfig: AppFhirConfig
|
||||||
retryTemplate,
|
): Generator {
|
||||||
createCustomGpasRestTemplate(configProperties)
|
logger.info("Selected 'GpasPseudonym Generator'")
|
||||||
)
|
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig)
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
|
@ConditionalOnProperty(
|
||||||
|
value = ["app.pseudonymize.generator"],
|
||||||
|
havingValue = "BUILDIN",
|
||||||
|
matchIfMissing = true
|
||||||
|
)
|
||||||
@Bean
|
@Bean
|
||||||
fun buildinPseudonymGenerator(): Generator {
|
fun buildinPseudonymGenerator(): Generator {
|
||||||
return AnonymizingGenerator()
|
logger.info("Selected 'BUILDIN Pseudonym Generator'")
|
||||||
}
|
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
@Bean
|
|
||||||
fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
|
||||||
try {
|
|
||||||
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
|
||||||
return GpasPseudonymGenerator(
|
|
||||||
configProperties,
|
|
||||||
retryTemplate,
|
|
||||||
createCustomGpasRestTemplate(configProperties)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createCustomGpasRestTemplate(configProperties: GPasConfigProperties): RestTemplate {
|
|
||||||
fun getSslContext(certificateLocation: String): SSLContext? {
|
|
||||||
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
|
|
||||||
|
|
||||||
val fis = FileInputStream(certificateLocation)
|
|
||||||
val ca = CertificateFactory.getInstance("X.509")
|
|
||||||
.generateCertificate(BufferedInputStream(fis)) as X509Certificate
|
|
||||||
|
|
||||||
ks.load(null, null)
|
|
||||||
ks.setCertificateEntry(1.toString(), ca)
|
|
||||||
|
|
||||||
val tmf = TrustManagerFactory.getInstance(
|
|
||||||
TrustManagerFactory.getDefaultAlgorithm()
|
|
||||||
)
|
|
||||||
tmf.init(ks)
|
|
||||||
|
|
||||||
val sslContext = SSLContext.getInstance("TLS")
|
|
||||||
sslContext.init(null, tmf.trustManagers, null)
|
|
||||||
|
|
||||||
return sslContext
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCustomRestTemplate(customSslContext: SSLContext): RestTemplate {
|
|
||||||
val sslsf = SSLConnectionSocketFactory(customSslContext)
|
|
||||||
val socketFactoryRegistry = RegistryBuilder.create<ConnectionSocketFactory>()
|
|
||||||
.register("https", sslsf).register("http", PlainConnectionSocketFactory()).build()
|
|
||||||
|
|
||||||
val connectionManager = BasicHttpClientConnectionManager(
|
|
||||||
socketFactoryRegistry
|
|
||||||
)
|
|
||||||
val httpClient = HttpClients.custom()
|
|
||||||
.setConnectionManager(connectionManager).build()
|
|
||||||
|
|
||||||
val requestFactory = HttpComponentsClientHttpRequestFactory(
|
|
||||||
httpClient
|
|
||||||
)
|
|
||||||
return RestTemplate(requestFactory)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
|
||||||
val customSslContext = getSslContext(configProperties.sslCaLocation)
|
|
||||||
logger.warn(
|
|
||||||
String.format(
|
|
||||||
"%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.",
|
|
||||||
this.javaClass.name, configProperties.sslCaLocation
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (customSslContext != null) {
|
|
||||||
return getCustomRestTemplate(customSslContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw RuntimeException("Custom SSL configuration for gPAS not usable")
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "BUILDIN")
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
@Bean
|
|
||||||
fun buildinPseudonymGeneratorOnDeprecatedProperty(): Generator {
|
|
||||||
return AnonymizingGenerator()
|
return AnonymizingGenerator()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,17 +116,21 @@ class AppConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun reportService(objectMapper: ObjectMapper): ReportService {
|
fun reportService(): ReportService {
|
||||||
return ReportService(objectMapper)
|
return ReportService(getObjectMapper())
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(objectMapper, configProperties.transformations.map {
|
return TransformationService(getObjectMapper(), configProperties.transformations.map {
|
||||||
Transformation.of(it.path) from it.from to it.to
|
Transformation.of(it.path) from it.from to it.to
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -235,7 +149,11 @@ class AppConfiguration {
|
|||||||
callback: RetryCallback<T, E>,
|
callback: RetryCallback<T, E>,
|
||||||
throwable: Throwable
|
throwable: Throwable
|
||||||
) {
|
) {
|
||||||
logger.warn("Error occured: {}. Retrying {}", throwable.message, context.retryCount)
|
logger.warn(
|
||||||
|
"Error occured: {}. Retrying {}",
|
||||||
|
throwable.message,
|
||||||
|
context.retryCount
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
@@ -243,7 +161,11 @@ class AppConfiguration {
|
|||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true")
|
@ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true")
|
||||||
@Bean
|
@Bean
|
||||||
fun tokenService(userDetailsManager: InMemoryUserDetailsManager, passwordEncoder: PasswordEncoder, tokenRepository: TokenRepository): TokenService {
|
fun tokenService(
|
||||||
|
userDetailsManager: InMemoryUserDetailsManager,
|
||||||
|
passwordEncoder: PasswordEncoder,
|
||||||
|
tokenRepository: TokenRepository
|
||||||
|
): TokenService {
|
||||||
return TokenService(userDetailsManager, passwordEncoder, tokenRepository)
|
return TokenService(userDetailsManager, passwordEncoder, tokenRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +186,11 @@ class AppConfiguration {
|
|||||||
gPasConfigProperties: GPasConfigProperties,
|
gPasConfigProperties: GPasConfigProperties,
|
||||||
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
): ConnectionCheckService {
|
): ConnectionCheckService {
|
||||||
return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
|
return GPasConnectionCheckService(
|
||||||
|
restTemplate,
|
||||||
|
gPasConfigProperties,
|
||||||
|
connectionCheckUpdateProducer
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
|
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
|
||||||
@@ -275,12 +201,81 @@ class AppConfiguration {
|
|||||||
gPasConfigProperties: GPasConfigProperties,
|
gPasConfigProperties: GPasConfigProperties,
|
||||||
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
): ConnectionCheckService {
|
): ConnectionCheckService {
|
||||||
return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
|
return GPasConnectionCheckService(
|
||||||
|
restTemplate,
|
||||||
|
gPasConfigProperties,
|
||||||
|
connectionCheckUpdateProducer
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun jdbcConfiguration(): AbstractJdbcConfiguration {
|
fun jdbcConfiguration(): AbstractJdbcConfiguration {
|
||||||
return AppJdbcConfiguration()
|
return AppJdbcConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Conditional(GicsEnabledCondition::class)
|
||||||
|
@Bean
|
||||||
|
fun gicsConsentService(
|
||||||
|
gIcsConfigProperties: GIcsConfigProperties,
|
||||||
|
retryTemplate: RetryTemplate,
|
||||||
|
restTemplate: RestTemplate,
|
||||||
|
appFhirConfig: AppFhirConfig
|
||||||
|
): IConsentService {
|
||||||
|
return GicsConsentService(
|
||||||
|
gIcsConfigProperties,
|
||||||
|
retryTemplate,
|
||||||
|
restTemplate,
|
||||||
|
appFhirConfig
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Conditional(GicsEnabledCondition::class)
|
||||||
|
@Bean
|
||||||
|
fun consentProcessor(
|
||||||
|
configProperties: AppConfigProperties,
|
||||||
|
gIcsConfigProperties: GIcsConfigProperties,
|
||||||
|
getObjectMapper: ObjectMapper,
|
||||||
|
appFhirConfig: AppFhirConfig,
|
||||||
|
gicsConsentService: IConsentService
|
||||||
|
): ConsentProcessor {
|
||||||
|
return ConsentProcessor(
|
||||||
|
configProperties,
|
||||||
|
gIcsConfigProperties,
|
||||||
|
getObjectMapper,
|
||||||
|
appFhirConfig.fhirContext(),
|
||||||
|
gicsConsentService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Conditional(GicsEnabledCondition::class)
|
||||||
|
@Bean
|
||||||
|
fun gIcsConnectionCheckService(
|
||||||
|
restTemplate: RestTemplate,
|
||||||
|
gIcsConfigProperties: GIcsConfigProperties,
|
||||||
|
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
|
): ConnectionCheckService {
|
||||||
|
return GIcsConnectionCheckService(
|
||||||
|
restTemplate,
|
||||||
|
gIcsConfigProperties,
|
||||||
|
connectionCheckUpdateProducer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
fun iGetConsentService(): IConsentService {
|
||||||
|
return MtbFileConsentService()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GicsEnabledCondition :
|
||||||
|
AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) {
|
||||||
|
|
||||||
|
@ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics")
|
||||||
|
@ConditionalOnProperty(name = ["app.consent.gics.uri"])
|
||||||
|
class OnGicsServiceSelected {
|
||||||
|
// Just for Condition
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
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,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
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.ConsentEvaluator
|
||||||
import dev.dnpm.etl.processor.input.KafkaInputListener
|
import dev.dnpm.etl.processor.input.KafkaInputListener
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
||||||
@@ -71,7 +72,7 @@ class AppKafkaConfiguration {
|
|||||||
kafkaProperties: KafkaProperties,
|
kafkaProperties: KafkaProperties,
|
||||||
kafkaResponseProcessor: KafkaResponseProcessor
|
kafkaResponseProcessor: KafkaResponseProcessor
|
||||||
): KafkaMessageListenerContainer<String, String> {
|
): KafkaMessageListenerContainer<String, String> {
|
||||||
val containerProperties = ContainerProperties(kafkaProperties.responseTopic)
|
val containerProperties = ContainerProperties(kafkaProperties.outputResponseTopic)
|
||||||
containerProperties.messageListener = kafkaResponseProcessor
|
containerProperties.messageListener = kafkaResponseProcessor
|
||||||
return KafkaMessageListenerContainer(consumerFactory, containerProperties)
|
return KafkaMessageListenerContainer(consumerFactory, containerProperties)
|
||||||
}
|
}
|
||||||
@@ -100,9 +101,10 @@ class AppKafkaConfiguration {
|
|||||||
@ConditionalOnProperty(value = ["app.kafka.input-topic"])
|
@ConditionalOnProperty(value = ["app.kafka.input-topic"])
|
||||||
fun kafkaInputListener(
|
fun kafkaInputListener(
|
||||||
requestProcessor: RequestProcessor,
|
requestProcessor: RequestProcessor,
|
||||||
objectMapper: ObjectMapper
|
objectMapper: ObjectMapper,
|
||||||
|
consentEvaluator: ConsentEvaluator
|
||||||
): KafkaInputListener {
|
): KafkaInputListener {
|
||||||
return KafkaInputListener(requestProcessor, objectMapper)
|
return KafkaInputListener(requestProcessor, consentEvaluator, objectMapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -113,4 +115,4 @@ class AppKafkaConfiguration {
|
|||||||
return KafkaConnectionCheckService(consumerFactory.createConsumer(), connectionCheckUpdateProducer)
|
return KafkaConnectionCheckService(consumerFactory.createConsumer(), connectionCheckUpdateProducer)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,6 @@ import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
|||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
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
|
||||||
import dev.dnpm.etl.processor.output.RestBwhcMtbFileSender
|
|
||||||
import dev.dnpm.etl.processor.output.RestDipMtbFileSender
|
import dev.dnpm.etl.processor.output.RestDipMtbFileSender
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||||
@@ -57,11 +56,6 @@ class AppRestConfiguration {
|
|||||||
retryTemplate: RetryTemplate,
|
retryTemplate: RetryTemplate,
|
||||||
reportService: ReportService,
|
reportService: ReportService,
|
||||||
): MtbFileSender {
|
): MtbFileSender {
|
||||||
if (restTargetProperties.isBwhc) {
|
|
||||||
logger.info("Selected 'RestBwhcMtbFileSender'")
|
|
||||||
return RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Selected 'RestDipMtbFileSender'")
|
logger.info("Selected 'RestDipMtbFileSender'")
|
||||||
return RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
return RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -87,11 +87,17 @@ class AppSecurityConfiguration(
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
||||||
fun filterChainOidc(http: HttpSecurity, passwordEncoder: PasswordEncoder, userRoleRepository: UserRoleRepository, sessionRegistry: SessionRegistry): SecurityFilterChain {
|
fun filterChainOidc(
|
||||||
|
http: HttpSecurity,
|
||||||
|
passwordEncoder: PasswordEncoder,
|
||||||
|
userRoleRepository: UserRoleRepository,
|
||||||
|
sessionRegistry: SessionRegistry
|
||||||
|
): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
authorizeRequests {
|
authorizeHttpRequests {
|
||||||
authorize("/configs/**", hasRole("ADMIN"))
|
authorize("/configs/**", hasRole("ADMIN"))
|
||||||
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
|
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
|
||||||
|
authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
|
||||||
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
|
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
|
||||||
authorize("*.css", permitAll)
|
authorize("*.css", permitAll)
|
||||||
authorize("*.ico", permitAll)
|
authorize("*.ico", permitAll)
|
||||||
@@ -127,13 +133,22 @@ class AppSecurityConfiguration(
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
||||||
fun grantedAuthoritiesMapper(userRoleRepository: UserRoleRepository, appSecurityConfigProperties: SecurityConfigProperties): GrantedAuthoritiesMapper {
|
fun grantedAuthoritiesMapper(
|
||||||
|
userRoleRepository: UserRoleRepository,
|
||||||
|
appSecurityConfigProperties: SecurityConfigProperties
|
||||||
|
): GrantedAuthoritiesMapper {
|
||||||
return GrantedAuthoritiesMapper { grantedAuthority ->
|
return GrantedAuthoritiesMapper { grantedAuthority ->
|
||||||
grantedAuthority.filterIsInstance<OidcUserAuthority>()
|
grantedAuthority.filterIsInstance<OidcUserAuthority>()
|
||||||
.onEach {
|
.onEach {
|
||||||
val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername)
|
val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername)
|
||||||
if (userRole.isEmpty) {
|
if (userRole.isEmpty) {
|
||||||
userRoleRepository.save(UserRole(null, it.userInfo.preferredUsername, appSecurityConfigProperties.defaultNewUserRole))
|
userRoleRepository.save(
|
||||||
|
UserRole(
|
||||||
|
null,
|
||||||
|
it.userInfo.preferredUsername,
|
||||||
|
appSecurityConfigProperties.defaultNewUserRole
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
@@ -147,9 +162,10 @@ class AppSecurityConfiguration(
|
|||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "false", matchIfMissing = true)
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "false", matchIfMissing = true)
|
||||||
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
|
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
authorizeRequests {
|
authorizeHttpRequests {
|
||||||
authorize("/configs/**", hasRole("ADMIN"))
|
authorize("/configs/**", hasRole("ADMIN"))
|
||||||
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
|
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
|
||||||
|
authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN"))
|
||||||
authorize("/report/**", hasRole("ADMIN"))
|
authorize("/report/**", hasRole("ADMIN"))
|
||||||
authorize(anyRequest, permitAll)
|
authorize(anyRequest, permitAll)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ETL-Processor
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.dnpm.etl.processor.consent
|
||||||
|
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.ConsentProvision
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates consent using provided consent service and file based consent information
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
class ConsentEvaluator(
|
||||||
|
private val consentService: IConsentService
|
||||||
|
) {
|
||||||
|
fun check(mtbFile: Mtb): ConsentEvaluation {
|
||||||
|
val ttpConsentStatus = consentService.getTtpBroadConsentStatus(mtbFile.patient.id)
|
||||||
|
val consentGiven = ttpConsentStatus == TtpConsentStatus.BROAD_CONSENT_GIVEN
|
||||||
|
|| ttpConsentStatus == TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT
|
||||||
|
// Aktuell nur Modellvorhaben Consent im File
|
||||||
|
|| ttpConsentStatus == TtpConsentStatus.UNKNOWN_CHECK_FILE && mtbFile.metadata?.modelProjectConsent?.provisions?.any {
|
||||||
|
it.purpose == ModelProjectConsentPurpose.SEQUENCING
|
||||||
|
&& it.type == ConsentProvision.PERMIT
|
||||||
|
} == true
|
||||||
|
|
||||||
|
return ConsentEvaluation(ttpConsentStatus, consentGiven)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ConsentEvaluation(private val ttpConsentStatus: TtpConsentStatus, private val consentGiven: Boolean) {
|
||||||
|
/**
|
||||||
|
* Checks if any required consent is present
|
||||||
|
*/
|
||||||
|
fun hasConsent(): Boolean {
|
||||||
|
return consentGiven
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the consent status
|
||||||
|
*/
|
||||||
|
fun getStatus(): TtpConsentStatus {
|
||||||
|
if (ttpConsentStatus == TtpConsentStatus.UNKNOWN_CHECK_FILE) {
|
||||||
|
// in case ttp check is disabled - we propagate rejected status anyway
|
||||||
|
return TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED
|
||||||
|
}
|
||||||
|
return ttpConsentStatus
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -20,32 +20,58 @@
|
|||||||
package dev.dnpm.etl.processor.input
|
package dev.dnpm.etl.processor.input
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
|
||||||
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.ConsentEvaluator
|
||||||
|
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 org.apache.kafka.clients.consumer.ConsumerRecord
|
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.kafka.listener.MessageListener
|
import org.springframework.kafka.listener.MessageListener
|
||||||
|
|
||||||
class KafkaInputListener(
|
class KafkaInputListener(
|
||||||
private val requestProcessor: RequestProcessor,
|
private val requestProcessor: RequestProcessor,
|
||||||
|
private val consentEvaluator: ConsentEvaluator,
|
||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
) : MessageListener<String, String> {
|
) : MessageListener<String, String> {
|
||||||
private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java)
|
private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java)
|
||||||
|
|
||||||
override fun onMessage(data: ConsumerRecord<String, String>) {
|
override fun onMessage(record: ConsumerRecord<String, String>) {
|
||||||
val mtbFile = objectMapper.readValue(data.value(), MtbFile::class.java)
|
when (guessMimeType(record)) {
|
||||||
|
MediaType.APPLICATION_JSON_VALUE -> handleDnpmV2Message(record)
|
||||||
|
CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE -> handleDnpmV2Message(record)
|
||||||
|
else -> {
|
||||||
|
/* ignore other messages */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun guessMimeType(record: ConsumerRecord<String, String>): String {
|
||||||
|
if (record.headers().headers("contentType").toList().isEmpty()) {
|
||||||
|
// Fallback if no contentType set (old behavior)
|
||||||
|
return MediaType.APPLICATION_JSON_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.headers().headers("contentType")?.firstOrNull()?.value().contentToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDnpmV2Message(record: ConsumerRecord<String, String>) {
|
||||||
|
// Do not handle DNPM-V2 for now
|
||||||
|
logger.warn("Ignoring MTB File in DNPM V2 format: Not implemented yet")
|
||||||
|
|
||||||
|
val mtbFile = objectMapper.readValue(record.value(), Mtb::class.java)
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
val patientId = PatientId(mtbFile.patient.id)
|
||||||
val firstRequestIdHeader = data.headers().headers("requestId")?.firstOrNull()
|
val firstRequestIdHeader = record.headers().headers("requestId")?.firstOrNull()
|
||||||
val requestId = if (null != firstRequestIdHeader) {
|
val requestId = if (null != firstRequestIdHeader) {
|
||||||
RequestId(String(firstRequestIdHeader.value()))
|
RequestId(String(firstRequestIdHeader.value()))
|
||||||
} else {
|
} else {
|
||||||
RequestId("")
|
RequestId("")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
if (consentEvaluator.check(mtbFile).hasConsent()) {
|
||||||
logger.debug("Accepted MTB File for processing")
|
logger.debug("Accepted MTB File for processing")
|
||||||
if (requestId.isBlank()) {
|
if (requestId.isBlank()) {
|
||||||
requestProcessor.processMtbFile(mtbFile)
|
requestProcessor.processMtbFile(mtbFile)
|
||||||
@@ -55,10 +81,15 @@ 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)
|
requestProcessor.processDeletion(patientId, TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
||||||
} else {
|
} else {
|
||||||
requestProcessor.processDeletion(patientId, requestId)
|
requestProcessor.processDeletion(
|
||||||
|
patientId,
|
||||||
|
requestId,
|
||||||
|
TtpConsentStatus.UNKNOWN_CHECK_FILE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
@@ -19,11 +19,14 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.input
|
package dev.dnpm.etl.processor.input
|
||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
|
import dev.dnpm.etl.processor.consent.ConsentEvaluator
|
||||||
|
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 org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
@@ -31,8 +34,8 @@ import org.springframework.web.bind.annotation.*
|
|||||||
@RequestMapping(path = ["mtbfile", "mtb"])
|
@RequestMapping(path = ["mtbfile", "mtb"])
|
||||||
class MtbFileRestController(
|
class MtbFileRestController(
|
||||||
private val requestProcessor: RequestProcessor,
|
private val requestProcessor: RequestProcessor,
|
||||||
|
private val consentEvaluator: ConsentEvaluator
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -40,15 +43,16 @@ class MtbFileRestController(
|
|||||||
return ResponseEntity.ok("Test")
|
return ResponseEntity.ok("Test")
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE])
|
||||||
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> {
|
||||||
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
val consentEvaluation = consentEvaluator.check(mtbFile)
|
||||||
logger.debug("Accepted MTB File for processing")
|
if (consentEvaluation.hasConsent()) {
|
||||||
|
logger.debug("Accepted MTB File (DNPM V2) for processing")
|
||||||
requestProcessor.processMtbFile(mtbFile)
|
requestProcessor.processMtbFile(mtbFile)
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Accepted MTB File and process deletion")
|
logger.debug("Accepted MTB File (DNPM V2) and process deletion")
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
val patientId = PatientId(mtbFile.patient.id)
|
||||||
requestProcessor.processDeletion(patientId)
|
requestProcessor.processDeletion(patientId, consentEvaluation.getStatus())
|
||||||
}
|
}
|
||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
@@ -56,8 +60,8 @@ 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))
|
requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
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
|
||||||
@@ -68,6 +69,12 @@ 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(
|
||||||
@@ -121,15 +128,11 @@ class RestConnectionCheckService(
|
|||||||
fun check() {
|
fun check() {
|
||||||
result = try {
|
result = try {
|
||||||
val available = restTemplate.getForEntity(
|
val available = restTemplate.getForEntity(
|
||||||
if (restTargetProperties.isBwhc) {
|
UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString())
|
||||||
UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString()).path("").toUriString()
|
.pathSegment("mtb")
|
||||||
} else {
|
.pathSegment("kaplan-meier")
|
||||||
UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString())
|
.pathSegment("config")
|
||||||
.pathSegment("mtb")
|
.toUriString(),
|
||||||
.pathSegment("kaplan-meier")
|
|
||||||
.pathSegment("config")
|
|
||||||
.toUriString()
|
|
||||||
},
|
|
||||||
String::class.java
|
String::class.java
|
||||||
).statusCode == HttpStatus.OK
|
).statusCode == HttpStatus.OK
|
||||||
|
|
||||||
@@ -207,4 +210,57 @@ class GPasConnectionCheckService(
|
|||||||
override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult {
|
override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult {
|
||||||
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,5 +24,6 @@ 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")
|
||||||
}
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -20,12 +20,14 @@
|
|||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
import dev.dnpm.etl.processor.config.KafkaProperties
|
import dev.dnpm.etl.processor.config.KafkaProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.MvhMetadata
|
||||||
|
import org.apache.kafka.clients.producer.ProducerRecord
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.springframework.kafka.core.KafkaTemplate
|
||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
|
|
||||||
@@ -38,14 +40,24 @@ class KafkaMtbFileSender(
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java)
|
private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java)
|
||||||
|
|
||||||
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
||||||
return try {
|
return try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val result = kafkaTemplate.send(
|
val record =
|
||||||
kafkaProperties.topic,
|
ProducerRecord(
|
||||||
key(request),
|
kafkaProperties.outputTopic,
|
||||||
objectMapper.writeValueAsString(Data(request.requestId, request.mtbFile))
|
key(request),
|
||||||
)
|
objectMapper.writeValueAsString(request)
|
||||||
|
)
|
||||||
|
when (request) {
|
||||||
|
is DnpmV2MtbFileRequest -> record.headers()
|
||||||
|
.add(
|
||||||
|
"contentType",
|
||||||
|
CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = kafkaTemplate.send(record)
|
||||||
if (result.get() != null) {
|
if (result.get() != null) {
|
||||||
logger.debug("Sent file via KafkaMtbFileSender")
|
logger.debug("Sent file via KafkaMtbFileSender")
|
||||||
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
||||||
@@ -59,24 +71,27 @@ class KafkaMtbFileSender(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
|
override fun send(request: DeleteRequest): MtbFileSender.Response {
|
||||||
val dummyMtbFile = MtbFile.builder()
|
val dummyMtbFile = Mtb.builder()
|
||||||
.withConsent(
|
.metadata(MvhMetadata())
|
||||||
Consent.builder()
|
|
||||||
.withPatient(request.patientId.value)
|
|
||||||
.withStatus(Consent.Status.REJECTED)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val result = kafkaTemplate.send(
|
val record =
|
||||||
kafkaProperties.topic,
|
ProducerRecord(
|
||||||
key(request),
|
kafkaProperties.outputTopic,
|
||||||
objectMapper.writeValueAsString(Data(request.requestId, dummyMtbFile))
|
key(request),
|
||||||
)
|
// Always use old BwhcV1FileRequest with Consent REJECT
|
||||||
|
objectMapper.writeValueAsString(
|
||||||
|
DnpmV2MtbFileRequest(
|
||||||
|
request.requestId,
|
||||||
|
dummyMtbFile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = kafkaTemplate.send(record)
|
||||||
if (result.get() != null) {
|
if (result.get() != null) {
|
||||||
logger.debug("Sent deletion request via KafkaMtbFileSender")
|
logger.debug("Sent deletion request via KafkaMtbFileSender")
|
||||||
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
||||||
@@ -91,16 +106,14 @@ class KafkaMtbFileSender(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun endpoint(): String {
|
override fun endpoint(): String {
|
||||||
return "${this.kafkaProperties.servers} (${this.kafkaProperties.topic}/${this.kafkaProperties.responseTopic})"
|
return "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun key(request: MtbFileSender.MtbFileRequest): String {
|
private fun key(request: MtbRequest): String {
|
||||||
return "{\"pid\": \"${request.mtbFile.patient.id}\"}"
|
return when (request) {
|
||||||
|
is DnpmV2MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}"
|
||||||
|
is DeleteRequest -> "{\"pid\": \"${request.patientId.value}\"}"
|
||||||
|
else -> throw IllegalArgumentException("Unsupported request type: ${request::class.simpleName}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun key(request: MtbFileSender.DeleteRequest): String {
|
|
||||||
return "{\"pid\": \"${request.patientId.value}\"}"
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Data(val requestId: RequestId, val content: MtbFile)
|
|
||||||
}
|
|
||||||
|
@@ -19,25 +19,17 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import org.springframework.http.HttpStatusCode
|
import org.springframework.http.HttpStatusCode
|
||||||
|
|
||||||
interface MtbFileSender {
|
interface MtbFileSender {
|
||||||
fun send(request: MtbFileRequest): Response
|
fun <T> send(request: MtbFileRequest<T>): Response
|
||||||
|
|
||||||
fun send(request: DeleteRequest): Response
|
fun send(request: DeleteRequest): Response
|
||||||
|
|
||||||
fun endpoint(): String
|
fun endpoint(): String
|
||||||
|
|
||||||
data class Response(val status: RequestStatus, val body: String = "")
|
data class Response(val status: RequestStatus, val body: String = "")
|
||||||
|
|
||||||
data class MtbFileRequest(val requestId: RequestId, val mtbFile: MtbFile)
|
|
||||||
|
|
||||||
data class DeleteRequest(val requestId: RequestId, val patientId: PatientPseudonym)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Int.asRequestStatus(): RequestStatus {
|
fun Int.asRequestStatus(): RequestStatus {
|
||||||
@@ -51,4 +43,4 @@ fun Int.asRequestStatus(): RequestStatus {
|
|||||||
|
|
||||||
fun HttpStatusCode.asRequestStatus(): RequestStatus {
|
fun HttpStatusCode.asRequestStatus(): RequestStatus {
|
||||||
return this.value().asRequestStatus()
|
return this.value().asRequestStatus()
|
||||||
}
|
}
|
||||||
|
49
src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt
Normal file
49
src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ETL-Processor
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
|
import dev.dnpm.etl.processor.RequestId
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
|
|
||||||
|
interface MtbRequest {
|
||||||
|
val requestId: RequestId
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface MtbFileRequest<out T> : MtbRequest {
|
||||||
|
override val requestId: RequestId
|
||||||
|
val content: T
|
||||||
|
|
||||||
|
fun patientPseudonym(): PatientPseudonym
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DnpmV2MtbFileRequest(
|
||||||
|
override val requestId: RequestId,
|
||||||
|
override val content: Mtb
|
||||||
|
) : MtbFileRequest<Mtb> {
|
||||||
|
override fun patientPseudonym(): PatientPseudonym {
|
||||||
|
return PatientPseudonym(content.patient.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeleteRequest(
|
||||||
|
override val requestId: RequestId,
|
||||||
|
val patientId: PatientPseudonym
|
||||||
|
) : MtbRequest
|
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
|
||||||
import org.springframework.retry.support.RetryTemplate
|
|
||||||
import org.springframework.web.client.RestTemplate
|
|
||||||
import org.springframework.web.util.UriComponentsBuilder
|
|
||||||
|
|
||||||
class RestBwhcMtbFileSender(
|
|
||||||
restTemplate: RestTemplate,
|
|
||||||
private val restTargetProperties: RestTargetProperties,
|
|
||||||
retryTemplate: RetryTemplate,
|
|
||||||
reportService: ReportService,
|
|
||||||
) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) {
|
|
||||||
|
|
||||||
override fun sendUrl(): String {
|
|
||||||
return UriComponentsBuilder
|
|
||||||
.fromUriString(restTargetProperties.uri.toString())
|
|
||||||
.pathSegment("MTBFile")
|
|
||||||
.toUriString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteUrl(patientId: PatientPseudonym): String {
|
|
||||||
return UriComponentsBuilder
|
|
||||||
.fromUriString(restTargetProperties.uri.toString())
|
|
||||||
.pathSegment("Patient")
|
|
||||||
.pathSegment(patientId.value)
|
|
||||||
.toUriString()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -19,10 +19,11 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
@@ -46,11 +47,11 @@ abstract class RestMtbFileSender(
|
|||||||
|
|
||||||
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
||||||
|
|
||||||
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders()
|
val headers = getHttpHeaders(request)
|
||||||
val entityReq = HttpEntity(request.mtbFile, headers)
|
val entityReq = HttpEntity(request.content, headers)
|
||||||
val response = restTemplate.postForEntity(
|
val response = restTemplate.postForEntity(
|
||||||
sendUrl(),
|
sendUrl(),
|
||||||
entityReq,
|
entityReq,
|
||||||
@@ -76,10 +77,10 @@ abstract class RestMtbFileSender(
|
|||||||
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
|
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
|
override fun send(request: DeleteRequest): MtbFileSender.Response {
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders()
|
val headers = getHttpHeaders(request)
|
||||||
val entityReq = HttpEntity(null, headers)
|
val entityReq = HttpEntity(null, headers)
|
||||||
restTemplate.delete(
|
restTemplate.delete(
|
||||||
deleteUrl(request.patientId),
|
deleteUrl(request.patientId),
|
||||||
@@ -102,11 +103,14 @@ abstract class RestMtbFileSender(
|
|||||||
return this.restTargetProperties.uri.orEmpty()
|
return this.restTargetProperties.uri.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHttpHeaders(): HttpHeaders {
|
private fun getHttpHeaders(request: MtbRequest): HttpHeaders {
|
||||||
val username = restTargetProperties.username
|
val username = restTargetProperties.username
|
||||||
val password = restTargetProperties.password
|
val password = restTargetProperties.password
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
headers.contentType = when (request) {
|
||||||
|
is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
||||||
|
else -> MediaType.APPLICATION_JSON
|
||||||
|
}
|
||||||
|
|
||||||
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
||||||
return headers
|
return headers
|
||||||
@@ -116,4 +120,4 @@ abstract class RestMtbFileSender(
|
|||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -21,9 +21,12 @@ 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 java.security.SecureRandom
|
||||||
|
|
||||||
class AnonymizingGenerator : Generator {
|
class AnonymizingGenerator : Generator {
|
||||||
|
companion object fun getSecureRandom() : SecureRandom {
|
||||||
|
return SecureRandom()
|
||||||
|
}
|
||||||
|
|
||||||
override fun generate(id: String): String {
|
override fun generate(id: String): String {
|
||||||
return Base32().encodeAsString(DigestUtils.sha256(id))
|
return Base32().encodeAsString(DigestUtils.sha256(id))
|
||||||
@@ -31,4 +34,14 @@ class AnonymizingGenerator : Generator {
|
|||||||
.lowercase()
|
.lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
override fun generateGenomDeTan(id: String?): String {
|
||||||
|
|
||||||
|
val bytes = ByteArray(64 / 2)
|
||||||
|
getSecureRandom().nextBytes(bytes)
|
||||||
|
|
||||||
|
return bytes.joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -19,54 +19,96 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.pseudonym
|
package dev.dnpm.etl.processor.pseudonym
|
||||||
|
|
||||||
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.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
|
||||||
*
|
*
|
||||||
* @param pseudonymizeService The pseudonymizeService to be used
|
* @since 0.11.0
|
||||||
*
|
*
|
||||||
|
* @param pseudonymizeService The pseudonymizeService to be used
|
||||||
* @return The MTB file containing patient pseudonymes
|
* @return The MTB file containing patient pseudonymes
|
||||||
*/
|
*/
|
||||||
infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
infix fun Mtb.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
||||||
val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value
|
val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value
|
||||||
|
|
||||||
this.episode?.patient = patientPseudonym
|
this.episodesOfCare?.forEach { it.patient.id = patientPseudonym }
|
||||||
this.carePlans?.forEach { it.patient = patientPseudonym }
|
this.carePlans?.forEach {
|
||||||
this.patient.id = patientPseudonym
|
it.patient.id = patientPseudonym
|
||||||
this.claims?.forEach { it.patient = patientPseudonym }
|
it.rebiopsyRequests?.forEach { it.patient.id = patientPseudonym }
|
||||||
this.consent?.patient = patientPseudonym
|
it.histologyReevaluationRequests?.forEach { it.patient.id = patientPseudonym }
|
||||||
this.claimResponses?.forEach { it.patient = patientPseudonym }
|
it.medicationRecommendations.forEach { it.patient.id = patientPseudonym }
|
||||||
this.diagnoses?.forEach { it.patient = patientPseudonym }
|
it.studyEnrollmentRecommendations?.forEach { it.patient.id = patientPseudonym }
|
||||||
this.ecogStatus?.forEach { it.patient = patientPseudonym }
|
it.procedureRecommendations?.forEach { it.patient.id = patientPseudonym }
|
||||||
this.familyMemberDiagnoses?.forEach { it.patient = patientPseudonym }
|
it.geneticCounselingRecommendation.patient.id = patientPseudonym
|
||||||
this.geneticCounsellingRequests?.forEach { it.patient = patientPseudonym }
|
}
|
||||||
this.histologyReevaluationRequests?.forEach { it.patient = patientPseudonym }
|
this.diagnoses?.forEach { it.patient.id = patientPseudonym }
|
||||||
this.histologyReports?.forEach {
|
this.guidelineTherapies?.forEach { it.patient.id = patientPseudonym }
|
||||||
it.patient = patientPseudonym
|
this.guidelineProcedures?.forEach { it.patient.id = patientPseudonym }
|
||||||
it.tumorMorphology?.patient = patientPseudonym
|
this.patient.id = patientPseudonym
|
||||||
|
this.claims?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
this.claimResponses?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
this.diagnoses?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
this.familyMemberHistories?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
this.histologyReports?.forEach {
|
||||||
|
it.patient.id = patientPseudonym
|
||||||
|
it.results.tumorMorphology?.patient?.id = patientPseudonym
|
||||||
|
it.results.tumorCellContent?.patient?.id = patientPseudonym
|
||||||
|
}
|
||||||
|
this.ngsReports?.forEach {
|
||||||
|
it.patient.id = patientPseudonym
|
||||||
|
it.results.simpleVariants?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
it.results.copyNumberVariants?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
it.results.dnaFusions?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
it.results.rnaFusions?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
it.results.tumorCellContent?.patient?.id = patientPseudonym
|
||||||
|
it.results.brcaness?.patient?.id = patientPseudonym
|
||||||
|
it.results.tmb?.patient?.id = patientPseudonym
|
||||||
|
it.results.hrdScore?.patient?.id = patientPseudonym
|
||||||
|
}
|
||||||
|
this.ihcReports?.forEach {
|
||||||
|
it.patient.id = patientPseudonym
|
||||||
|
it.results.msiMmr?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
it.results.proteinExpression?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
}
|
||||||
|
this.responses?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
this.specimens?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
this.priorDiagnosticReports?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
this.performanceStatus?.forEach { it.patient.id = patientPseudonym }
|
||||||
|
this.systemicTherapies?.forEach {
|
||||||
|
it.history?.forEach {
|
||||||
|
it.patient.id = patientPseudonym
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.followUps?.forEach {
|
||||||
|
it.patient.id = patientPseudonym
|
||||||
|
}
|
||||||
|
|
||||||
|
this.msiFindings?.forEach { it -> 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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.lastGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
|
||||||
this.molecularPathologyFindings?.forEach { it.patient = patientPseudonym }
|
|
||||||
this.molecularTherapies?.forEach { molecularTherapy -> molecularTherapy.history.forEach { it.patient = patientPseudonym } }
|
|
||||||
this.ngsReports?.forEach { it.patient = patientPseudonym }
|
|
||||||
this.previousGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
|
||||||
this.rebiopsyRequests?.forEach { it.patient = patientPseudonym }
|
|
||||||
this.recommendations?.forEach { it.patient = patientPseudonym }
|
|
||||||
this.responses?.forEach { it.patient = patientPseudonym }
|
|
||||||
this.studyInclusionRequests?.forEach { it.patient = patientPseudonym }
|
|
||||||
this.specimens?.forEach { it.patient = patientPseudonym }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new hash of content IDs with given prefix except for patient IDs
|
* Creates new hash of content IDs with given prefix except for patient IDs
|
||||||
*
|
*
|
||||||
* @param pseudonymizeService The pseudonymizeService to be used
|
* @since 0.11.0
|
||||||
*
|
*
|
||||||
|
* @param pseudonymizeService The pseudonymizeService to be used
|
||||||
* @return The MTB file containing rehashed content IDs
|
* @return The MTB file containing rehashed content IDs
|
||||||
*/
|
*/
|
||||||
infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
||||||
val prefix = pseudonymizeService.prefix()
|
val prefix = pseudonymizeService.prefix()
|
||||||
|
|
||||||
fun anonymize(id: String): String {
|
fun anonymize(id: String): String {
|
||||||
@@ -74,153 +116,199 @@ infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService)
|
|||||||
return "$prefix$hash"
|
return "$prefix$hash"
|
||||||
}
|
}
|
||||||
|
|
||||||
this.episode?.apply {
|
this.episodesOfCare?.forEach {
|
||||||
id = id?.let {
|
it?.apply { id = id?.let(::anonymize) }
|
||||||
anonymize(it)
|
it.diagnoses?.forEach { it ->
|
||||||
|
it?.id = it.id?.let(::anonymize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.carePlans?.onEach { carePlan ->
|
this.carePlans?.onEach { carePlan ->
|
||||||
carePlan?.apply {
|
carePlan?.apply {
|
||||||
id = id?.let { anonymize(it) }
|
id = id?.let { anonymize(it) }
|
||||||
diagnosis = diagnosis?.let { anonymize(it) }
|
|
||||||
geneticCounsellingRequest = geneticCounsellingRequest?.let { anonymize(it) }
|
diagnoses?.forEach { it -> it?.id = it.id?.let(::anonymize) }
|
||||||
rebiopsyRequests = rebiopsyRequests.map { it?.let { anonymize(it) } }
|
geneticCounselingRecommendation?.apply {
|
||||||
recommendations = recommendations.map { it?.let { anonymize(it) } }
|
id = geneticCounselingRecommendation.id?.let(::anonymize)
|
||||||
studyInclusionRequests = studyInclusionRequests.map { it?.let { anonymize(it) } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.claims?.onEach { claim ->
|
|
||||||
claim?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
therapy = therapy?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.claimResponses?.onEach { claimResponse ->
|
|
||||||
claimResponse?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
claim = claim?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.consent?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
this.diagnoses?.onEach { diagnosis ->
|
|
||||||
diagnosis?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
histologyResults = histologyResults?.map { it?.let { anonymize(it) } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.ecogStatus?.onEach { ecogStatus ->
|
|
||||||
ecogStatus?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.familyMemberDiagnoses?.onEach { familyMemberDiagnosis ->
|
|
||||||
familyMemberDiagnosis?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.geneticCounsellingRequests?.onEach { geneticCounsellingRequest ->
|
|
||||||
geneticCounsellingRequest?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.histologyReevaluationRequests?.onEach { histologyReevaluationRequest ->
|
|
||||||
histologyReevaluationRequest?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
specimen = specimen?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.histologyReports?.onEach { histologyReport ->
|
|
||||||
histologyReport?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
specimen = specimen?.let { anonymize(it) }
|
|
||||||
tumorMorphology?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
specimen = specimen?.let { anonymize(it) }
|
|
||||||
}
|
}
|
||||||
tumorCellContent?.apply {
|
rebiopsyRequests?.forEach { it ->
|
||||||
id = id?.let { anonymize(it) }
|
it.id = it.id?.let(::anonymize)
|
||||||
specimen = specimen?.let { anonymize(it) }
|
it.tumorEntity?.id = it.tumorEntity?.id?.let(::anonymize)
|
||||||
}
|
}
|
||||||
}
|
histologyReevaluationRequests?.forEach { it ->
|
||||||
}
|
it.id = it?.id?.let(::anonymize)
|
||||||
this.lastGuidelineTherapies?.onEach { lastGuidelineTherapy ->
|
it.specimen?.id = it.specimen?.id?.let(::anonymize)
|
||||||
lastGuidelineTherapy?.apply {
|
}
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
diagnosis = diagnosis?.let { anonymize(it) }
|
medicationRecommendations?.forEach { it ->
|
||||||
}
|
it.id = it?.id?.let(::anonymize)
|
||||||
}
|
it.supportingVariants?.forEach { it ->
|
||||||
this.molecularPathologyFindings?.onEach { molecularPathologyFinding ->
|
it.variant?.id = it.variant?.id?.let(::anonymize)
|
||||||
molecularPathologyFinding?.apply {
|
}
|
||||||
id = id?.let { anonymize(it) }
|
it.reason?.id = it.reason?.id?.let(::anonymize)
|
||||||
specimen = specimen?.let { anonymize(it) }
|
}
|
||||||
}
|
reason?.id = reason?.id?.let(::anonymize)
|
||||||
}
|
studyEnrollmentRecommendations?.forEach { it ->
|
||||||
this.molecularTherapies?.onEach { molecularTherapy ->
|
it?.reason?.id = it.reason?.id?.let(::anonymize)
|
||||||
molecularTherapy?.apply {
|
}
|
||||||
history?.onEach { history ->
|
|
||||||
history?.apply {
|
procedureRecommendations?.forEach { it ->
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
basedOn = basedOn?.let { anonymize(it) }
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.supportingVariants?.forEach { it ->
|
||||||
|
it.variant?.id = it.variant?.id?.let(::anonymize)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.reason?.id = it.reason?.id?.let(::anonymize)
|
||||||
|
|
||||||
|
studyEnrollmentRecommendations?.forEach { it ->
|
||||||
|
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.supportingVariants.forEach { it ->
|
||||||
|
it.variant?.id = it?.variant?.id?.let(::anonymize)
|
||||||
|
}
|
||||||
|
responses?.forEach { it ->
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.ngsReports?.onEach { ngsReport ->
|
|
||||||
ngsReport?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
this.responses?.forEach { it ->
|
||||||
specimen = specimen?.let { anonymize(it) }
|
|
||||||
tumorCellContent?.apply {
|
it?.id = it.id?.let(::anonymize)
|
||||||
id = id?.let { anonymize(it) }
|
it?.therapy?.id = it.therapy?.id?.let(::anonymize)
|
||||||
specimen = specimen?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
simpleVariants?.onEach { simpleVariant ->
|
|
||||||
simpleVariant?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.previousGuidelineTherapies?.onEach { previousGuidelineTherapy ->
|
|
||||||
previousGuidelineTherapy?.apply {
|
this.diagnoses?.forEach { it ->
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
diagnosis = diagnosis?.let { anonymize(it) }
|
it.id = it?.id?.let(::anonymize)
|
||||||
medication.forEach { medication ->
|
it.histology?.forEach { it -> it.id = it?.id?.let(::anonymize) }
|
||||||
medication?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.rebiopsyRequests?.onEach { rebiopsyRequest ->
|
|
||||||
rebiopsyRequest?.apply {
|
this.ngsReports?.forEach { it ->
|
||||||
id = id?.let { anonymize(it) }
|
it.id = it?.id?.let(::anonymize)
|
||||||
specimen = specimen?.let { anonymize(it) }
|
it.results?.tumorCellContent?.id = it.results.tumorCellContent?.id?.let(::anonymize)
|
||||||
|
it.results?.tumorCellContent?.specimen?.id =
|
||||||
|
it.results?.tumorCellContent?.specimen?.id?.let(::anonymize)
|
||||||
|
it.results?.rnaFusions?.forEach { it ->
|
||||||
|
it?.id = it.id?.let(::anonymize)
|
||||||
}
|
}
|
||||||
}
|
it.results?.simpleVariants?.forEach { it ->
|
||||||
this.recommendations?.onEach { recommendation ->
|
it?.id = it.id?.let(::anonymize)
|
||||||
recommendation?.apply {
|
it?.transcriptId?.value = it.transcriptId?.value?.let(::anonymize)
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
diagnosis = diagnosis?.let { anonymize(it) }
|
|
||||||
ngsReport = ngsReport?.let { anonymize(it) }
|
|
||||||
}
|
}
|
||||||
|
it.results?.tmb?.id = it.results?.tmb?.id?.let(::anonymize)
|
||||||
|
it.results?.tmb?.specimen?.id = it.results?.tmb?.specimen?.id?.let(::anonymize)
|
||||||
|
|
||||||
|
it.results?.brcaness?.id = it.results?.brcaness?.id?.let(::anonymize)
|
||||||
|
it.results?.brcaness?.specimen?.id = it.results?.brcaness?.specimen?.id?.let(::anonymize)
|
||||||
|
it.results?.copyNumberVariants?.forEach { it -> it?.id = it.id?.let(::anonymize) }
|
||||||
|
it.results?.hrdScore?.id = it.results?.hrdScore?.id?.let(::anonymize)
|
||||||
|
it.results?.hrdScore?.specimen?.id = it.results?.hrdScore?.specimen?.id?.let(::anonymize)
|
||||||
|
it.results?.rnaSeqs?.forEach { it -> it?.id = it.id?.let(::anonymize) }
|
||||||
|
it.results?.dnaFusions?.forEach { it -> it?.id = it.id?.let(::anonymize) }
|
||||||
|
it.specimen?.id = it?.specimen?.id?.let(::anonymize)
|
||||||
|
|
||||||
}
|
}
|
||||||
this.responses?.onEach { response ->
|
|
||||||
response?.apply {
|
this.histologyReports?.forEach { it ->
|
||||||
id = id?.let { anonymize(it) }
|
it.id = it?.id?.let(::anonymize)
|
||||||
therapy = therapy?.let { anonymize(it) }
|
it.results?.tumorCellContent?.id = it.results?.tumorCellContent?.id?.let(::anonymize)
|
||||||
|
it.results?.tumorCellContent?.specimen?.id =
|
||||||
|
it.results?.tumorCellContent?.specimen?.id?.let(::anonymize)
|
||||||
|
|
||||||
|
it.results?.tumorMorphology?.id = it.results?.tumorMorphology?.id?.let(::anonymize)
|
||||||
|
it.results?.tumorMorphology?.specimen?.id =
|
||||||
|
it.results?.tumorMorphology?.specimen?.id?.let(::anonymize)
|
||||||
|
it.specimen?.id = it.specimen?.id?.let(::anonymize)
|
||||||
|
|
||||||
|
}
|
||||||
|
this.claimResponses?.forEach { it ->
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.claim?.id = it.claim?.id?.let(::anonymize)
|
||||||
|
}
|
||||||
|
this.claims?.forEach { it ->
|
||||||
|
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.recommendation?.id = it.recommendation?.id?.let(::anonymize)
|
||||||
|
|
||||||
|
}
|
||||||
|
this.familyMemberHistories?.forEach { it -> it.id = it?.id?.let(::anonymize) }
|
||||||
|
this.guidelineProcedures?.forEach { it ->
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.reason?.id = it.reason?.id?.let(::anonymize)
|
||||||
|
it.basedOn?.id = it.basedOn?.id?.let(::anonymize)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.guidelineTherapies?.forEach { it ->
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.reason?.id = it.reason?.id?.let(::anonymize)
|
||||||
|
it.basedOn?.id = it.basedOn?.id?.let(::anonymize)
|
||||||
|
}
|
||||||
|
this.ihcReports?.forEach { it ->
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.specimen?.id = it.specimen?.id?.let(::anonymize)
|
||||||
|
it.results.proteinExpression.forEach { it -> it?.id = it.id.let(::anonymize) }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.msiFindings?.forEach { it ->
|
||||||
|
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.specimen?.id = it.specimen?.id?.let(::anonymize)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.performanceStatus?.forEach { it -> it.id = it?.id?.let(::anonymize) }
|
||||||
|
|
||||||
|
this.priorDiagnosticReports?.forEach { it ->
|
||||||
|
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.specimen?.id = it.specimen?.id?.let(::anonymize)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.specimens?.forEach { it ->
|
||||||
|
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.diagnosis?.id = it.diagnosis?.id?.let(::anonymize)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.systemicTherapies?.forEach { it ->
|
||||||
|
|
||||||
|
it.history?.forEach { it ->
|
||||||
|
|
||||||
|
it.id = it?.id?.let(::anonymize)
|
||||||
|
it.reason?.id = it.reason?.id?.let(::anonymize)
|
||||||
|
it.basedOn?.id = it.basedOn?.id?.let(::anonymize)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
this.studyInclusionRequests?.onEach { studyInclusionRequest ->
|
}
|
||||||
studyInclusionRequest?.apply {
|
|
||||||
id = id?.let { anonymize(it) }
|
fun Mtb.ensureMetaDataIsInitialized() {
|
||||||
reason = reason?.let { anonymize(it) }
|
// init metadata if necessary
|
||||||
}
|
if (this.metadata == null) {
|
||||||
|
val mvhMetadata = MvhMetadata.builder().build()
|
||||||
|
this.metadata = mvhMetadata
|
||||||
}
|
}
|
||||||
this.specimens?.onEach { specimen ->
|
if (this.metadata.researchConsents == null) {
|
||||||
specimen?.apply {
|
this.metadata.researchConsents = mutableListOf()
|
||||||
id = id?.let { anonymize(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
infix fun Mtb.addGenomDeTan(pseudonymizeService: PseudonymizeService) {
|
||||||
|
this.metadata.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id))
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,273 @@
|
|||||||
|
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.IConsentService
|
||||||
|
import dev.dnpm.etl.processor.consent.MtbFileConsentService
|
||||||
|
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: IConsentService
|
||||||
|
) {
|
||||||
|
private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked.
|
||||||
|
*
|
||||||
|
* Logic:
|
||||||
|
* * <c>true</c> IF consent check is disabled.
|
||||||
|
* * <c>true</c> IF broad consent (BC) has been given.
|
||||||
|
* * <c>true</c> BC has been asked AND declined but genomDe consent has been consented.
|
||||||
|
* * 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 is MtbFileConsentService) {
|
||||||
|
// consent check is 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.getConsent(
|
||||||
|
personIdentifierValue, requestDate, ConsentDomain.BROAD_CONSENT
|
||||||
|
)
|
||||||
|
val broadConsentHasBeenAsked = broadConsent.entry.isNotEmpty()
|
||||||
|
|
||||||
|
// fast exit - if patient has not been asked, we can skip and exit
|
||||||
|
if (!broadConsentHasBeenAsked) return false
|
||||||
|
|
||||||
|
val genomeDeConsent = consentService.getConsent(
|
||||||
|
personIdentifierValue, requestDate, ConsentDomain.MODELLVORHABEN_64E
|
||||||
|
)
|
||||||
|
|
||||||
|
addGenomeDbProvisions(mtbFile, genomeDeConsent)
|
||||||
|
|
||||||
|
if (genomeDeConsent.entry.isNotEmpty()) setGenomDeSubmissionType(mtbFile)
|
||||||
|
|
||||||
|
embedBroadConsentResources(mtbFile, broadConsent)
|
||||||
|
|
||||||
|
val broadConsentStatus = getProvisionTypeByPolicyCode(
|
||||||
|
broadConsent, requestDate, ConsentDomain.BROAD_CONSENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val genomDeSequencingStatus = getProvisionTypeByPolicyCode(
|
||||||
|
genomeDeConsent, requestDate, ConsentDomain.MODELLVORHABEN_64E
|
||||||
|
)
|
||||||
|
|
||||||
|
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.entry) {
|
||||||
|
val resource = entry.resource
|
||||||
|
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.entry) {
|
||||||
|
val resource = entry.resource
|
||||||
|
if (resource !is Consent) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect only one provision in collection, therefore get first or none
|
||||||
|
val provisions = resource.provision.provision
|
||||||
|
if (provisions.isEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val provisionComponent: ProvisionComponent = provisions.first()
|
||||||
|
|
||||||
|
var provisionCode: String? = null
|
||||||
|
if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) {
|
||||||
|
val codableConcept: CodeableConcept = provisionComponent.code.first()
|
||||||
|
if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) {
|
||||||
|
provisionCode = codableConcept.coding.first().code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.dateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
val provision = Provision.builder()
|
||||||
|
.type(ConsentProvision.valueOf(provisionComponent.type.name))
|
||||||
|
.date(provisionComponent.period.start)
|
||||||
|
.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.isNotEmpty()) {
|
||||||
|
mtbFile.metadata.modelProjectConsent.version =
|
||||||
|
gIcsConfigProperties.genomeDeConsentVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setGenomDeSubmissionType(mtbFile: Mtb) {
|
||||||
|
if (appConfigProperties.genomDeTestSubmission) {
|
||||||
|
mtbFile.metadata.type = MvhSubmissionType.TEST
|
||||||
|
logger.info("genomeDe submission mit TEST")
|
||||||
|
} else {
|
||||||
|
mtbFile.metadata.type = when (mtbFile.metadata.type) {
|
||||||
|
null -> MvhSubmissionType.INITIAL
|
||||||
|
else -> mtbFile.metadata.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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.BROAD_CONSENT == consentDomain) {
|
||||||
|
code = gIcsConfigProperties.broadConsentPolicyCode
|
||||||
|
system = gIcsConfigProperties.broadConsentPolicySystem
|
||||||
|
} else if (ConsentDomain.MODELLVORHABEN_64E == 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 targetCode policyRule and provision code value
|
||||||
|
* @param targetSystem 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, targetCode: String?, targetSystem: String?, requestDate: Date?
|
||||||
|
): Consent.ConsentProvisionType {
|
||||||
|
val entriesOfInterest = consentBundle.entry.filter { entry ->
|
||||||
|
val isConsentResource =
|
||||||
|
entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent
|
||||||
|
val consentIsActive = (entry.resource as Consent).status == ConsentState.ACTIVE
|
||||||
|
|
||||||
|
isConsentResource && consentIsActive && checkCoding(
|
||||||
|
targetCode, targetSystem, (entry.resource as Consent).policyRule.coding
|
||||||
|
) && isRequestDateInRange(requestDate, (entry.resource as Consent).provision.period)
|
||||||
|
}.map { entry: BundleEntryComponent ->
|
||||||
|
val consent = (entry.getResource() as Consent)
|
||||||
|
consent.provision.provision.filter { subProvision ->
|
||||||
|
isRequestDateInRange(requestDate, subProvision.period)
|
||||||
|
// search coding entries of current provision for code and system
|
||||||
|
subProvision.code.map { c -> c.coding }.flatten().firstOrNull { code ->
|
||||||
|
targetCode.equals(code.code) && targetSystem.equals(code.system)
|
||||||
|
} != null
|
||||||
|
}.map { subProvision ->
|
||||||
|
subProvision
|
||||||
|
}
|
||||||
|
}.flatten()
|
||||||
|
|
||||||
|
if (entriesOfInterest.isNotEmpty()) {
|
||||||
|
return entriesOfInterest.first().type
|
||||||
|
}
|
||||||
|
return Consent.ConsentProvisionType.NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkCoding(
|
||||||
|
researchAllowedPolicyOid: String?,
|
||||||
|
researchAllowedPolicySystem: String?,
|
||||||
|
policyRules: Collection<Coding>
|
||||||
|
): Boolean {
|
||||||
|
return policyRules.find { code ->
|
||||||
|
researchAllowedPolicySystem.equals(code.getSystem()) && (researchAllowedPolicyOid.equals(
|
||||||
|
code.getCode()
|
||||||
|
))
|
||||||
|
} != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean {
|
||||||
|
val isRequestDateAfterOrEqualStart = provPeriod.start.compareTo(requestDate)
|
||||||
|
val isRequestDateBeforeOrEqualEnd = provPeriod.end.compareTo(requestDate)
|
||||||
|
return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -20,19 +20,28 @@
|
|||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
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.DeleteRequest
|
||||||
|
import dev.dnpm.etl.processor.output.DnpmV2MtbFileRequest
|
||||||
|
import dev.dnpm.etl.processor.output.MtbFileRequest
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
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 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.time.Instant
|
import java.time.Instant
|
||||||
@@ -46,40 +55,63 @@ 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?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: MtbFile) {
|
private var logger: Logger = LoggerFactory.getLogger("RequestProcessor")
|
||||||
|
|
||||||
|
fun processMtbFile(mtbFile: Mtb) {
|
||||||
processMtbFile(mtbFile, randomRequestId())
|
processMtbFile(mtbFile, randomRequestId())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
|
|
||||||
val pid = PatientId(mtbFile.patient.id)
|
|
||||||
|
|
||||||
mtbFile pseudonymizeWith pseudonymizeService
|
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
|
||||||
mtbFile anonymizeContentWith pseudonymizeService
|
val pid = PatientId(extractPatientIdentifier(mtbFile))
|
||||||
|
|
||||||
val request = MtbFileSender.MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
val isConsentOk =
|
||||||
|
consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null
|
||||||
|
if (isConsentOk) {
|
||||||
|
if (isGenomDeConsented(mtbFile)) {
|
||||||
|
mtbFile addGenomDeTan pseudonymizeService
|
||||||
|
}
|
||||||
|
mtbFile pseudonymizeWith pseudonymizeService
|
||||||
|
mtbFile anonymizeContentWith pseudonymizeService
|
||||||
|
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
||||||
|
saveAndSend(request, pid)
|
||||||
|
} else {
|
||||||
|
logger.warn("consent check failed file will not be processed further!")
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
|
ResponseEvent(
|
||||||
|
requestId, Instant.now(), RequestStatus.NO_CONSENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val patientPseudonym = PatientPseudonym(request.mtbFile.patient.id)
|
private fun isGenomDeConsented(mtbFile: Mtb): Boolean {
|
||||||
|
val isModelProjectConsented = mtbFile.metadata?.modelProjectConsent?.provisions?.any { p ->
|
||||||
|
p.purpose == ModelProjectConsentPurpose.SEQUENCING && p.type == ConsentProvision.PERMIT
|
||||||
|
} == true
|
||||||
|
return isModelProjectConsented
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
|
||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
requestId,
|
request.requestId,
|
||||||
patientPseudonym,
|
request.patientPseudonym(),
|
||||||
pid,
|
pid,
|
||||||
fingerprint(request.mtbFile),
|
fingerprint(request),
|
||||||
RequestType.MTB_FILE,
|
RequestType.MTB_FILE,
|
||||||
RequestStatus.UNKNOWN
|
RequestStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (appConfigProperties.duplicationDetection && isDuplication(mtbFile)) {
|
if (appConfigProperties.duplicationDetection && isDuplication(request)) {
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
requestId,
|
request.requestId, Instant.now(), RequestStatus.DUPLICATION
|
||||||
Instant.now(),
|
|
||||||
RequestStatus.DUPLICATION
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -89,7 +121,7 @@ class RequestProcessor(
|
|||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
requestId,
|
request.requestId,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
responseStatus.status,
|
responseStatus.status,
|
||||||
when (responseStatus.status) {
|
when (responseStatus.status) {
|
||||||
@@ -100,26 +132,38 @@ class RequestProcessor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isDuplication(pseudonymizedMtbFile: MtbFile): Boolean {
|
private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean {
|
||||||
val patientPseudonym = PatientPseudonym(pseudonymizedMtbFile.patient.id)
|
val patientPseudonym = when (pseudonymizedMtbFileRequest) {
|
||||||
|
is DnpmV2MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
||||||
|
}
|
||||||
|
|
||||||
val lastMtbFileRequestForPatient =
|
val lastMtbFileRequestForPatient =
|
||||||
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
||||||
val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
val isLastRequestDeletion =
|
||||||
|
requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
||||||
|
|
||||||
return null != lastMtbFileRequestForPatient
|
return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint(
|
||||||
&& !isLastRequestDeletion
|
pseudonymizedMtbFileRequest
|
||||||
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFile)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId) {
|
fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) {
|
||||||
processDeletion(patientId, randomRequestId())
|
processDeletion(patientId, randomRequestId(), isConsented)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId, requestId: RequestId) {
|
fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) {
|
||||||
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,
|
||||||
@@ -127,25 +171,22 @@ class RequestProcessor(
|
|||||||
patientId,
|
patientId,
|
||||||
fingerprint(patientPseudonym.value),
|
fingerprint(patientPseudonym.value),
|
||||||
RequestType.DELETE,
|
RequestType.DELETE,
|
||||||
RequestStatus.UNKNOWN
|
requestStatus
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val responseStatus = sender.send(MtbFileSender.DeleteRequest(requestId, patientPseudonym))
|
val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym))
|
||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
requestId,
|
requestId, Instant.now(), responseStatus.status, when (responseStatus.status) {
|
||||||
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 (e: Exception) {
|
} catch (_: Exception) {
|
||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
uuid = requestId,
|
uuid = requestId,
|
||||||
@@ -160,16 +201,18 @@ class RequestProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fingerprint(mtbFile: MtbFile): Fingerprint {
|
private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint {
|
||||||
return fingerprint(objectMapper.writeValueAsString(mtbFile))
|
return when (request) {
|
||||||
|
is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fingerprint(s: String): Fingerprint {
|
private fun fingerprint(s: String): Fingerprint {
|
||||||
return Fingerprint(
|
return Fingerprint(
|
||||||
Base32().encodeAsString(DigestUtils.sha256(s))
|
Base32().encodeAsString(DigestUtils.sha256(s)).replace("=", "").lowercase()
|
||||||
.replace("=", "")
|
|
||||||
.lowercase()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun extractPatientIdentifier(mtbFile: Mtb): String = mtbFile.patient.id
|
||||||
|
@@ -70,6 +70,12 @@ 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
|
||||||
|
@@ -22,11 +22,17 @@ package dev.dnpm.etl.processor.services
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.jayway.jsonpath.JsonPath
|
import com.jayway.jsonpath.JsonPath
|
||||||
import com.jayway.jsonpath.PathNotFoundException
|
import com.jayway.jsonpath.PathNotFoundException
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
|
|
||||||
class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) {
|
class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) {
|
||||||
fun transform(mtbFile: MtbFile): MtbFile {
|
|
||||||
var json = objectMapper.writeValueAsString(mtbFile)
|
fun transform(mtbFile: Mtb): Mtb {
|
||||||
|
val json = transform(objectMapper.writeValueAsString(mtbFile))
|
||||||
|
return objectMapper.readValue(json, Mtb::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transform(content: String): String {
|
||||||
|
var json = content
|
||||||
|
|
||||||
transformations.forEach { transformation ->
|
transformations.forEach { transformation ->
|
||||||
val jsonPath = JsonPath.parse(json)
|
val jsonPath = JsonPath.parse(json)
|
||||||
@@ -48,7 +54,7 @@ class TransformationService(private val objectMapper: ObjectMapper, private val
|
|||||||
json = jsonPath.jsonString()
|
json = jsonPath.jsonString()
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(json, MtbFile::class.java)
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTransformations(): List<Transformation> {
|
fun getTransformations(): List<Transformation> {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor
|
package dev.dnpm.etl.processor
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Fingerprint(val value: String) {
|
class Fingerprint(val value: String) {
|
||||||
@@ -46,4 +47,17 @@ value class PatientId(val value: String)
|
|||||||
@JvmInline
|
@JvmInline
|
||||||
value class PatientPseudonym(val value: String)
|
value class PatientPseudonym(val value: String)
|
||||||
|
|
||||||
fun emptyPatientPseudonym() = PatientPseudonym("")
|
fun emptyPatientPseudonym() = PatientPseudonym("")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom MediaTypes
|
||||||
|
*
|
||||||
|
* @since 0.11.0
|
||||||
|
*/
|
||||||
|
object CustomMediaType {
|
||||||
|
val APPLICATION_VND_DNPM_V2_MTB_JSON = MediaType("application", "vnd.dnpm.v2.mtb+json")
|
||||||
|
const val APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE = "application/vnd.dnpm.v2.mtb+json"
|
||||||
|
|
||||||
|
val APPLICATION_VND_DNPM_V2_RD_JSON = MediaType("application", "vnd.dnpm.v2.rd+json")
|
||||||
|
const val APPLICATION_VND_DNPM_V2_RD_JSON_VALUE = "application/vnd.dnpm.v2.rd+json"
|
||||||
|
}
|
||||||
|
@@ -19,10 +19,7 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
import dev.dnpm.etl.processor.monitoring.*
|
||||||
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
|
||||||
@@ -61,11 +58,15 @@ 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())
|
||||||
@@ -119,6 +120,24 @@ 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) {
|
||||||
@@ -190,6 +209,7 @@ 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>()
|
||||||
|
@@ -20,7 +20,7 @@ spring:
|
|||||||
|
|
||||||
app:
|
app:
|
||||||
rest:
|
rest:
|
||||||
uri: http://localhost:9000/bwhc/etl/api
|
uri: http://localhost/api
|
||||||
#kafka:
|
#kafka:
|
||||||
# topic: test
|
# topic: test
|
||||||
# response-topic: test_response
|
# response-topic: test_response
|
||||||
|
@@ -16,6 +16,7 @@ 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,6 +49,11 @@
|
|||||||
</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>
|
||||||
|
@@ -0,0 +1,24 @@
|
|||||||
|
<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>
|
@@ -0,0 +1,181 @@
|
|||||||
|
package dev.dnpm.etl.processor.consent;
|
||||||
|
|
||||||
|
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 org.hl7.fhir.r4.model.*;
|
||||||
|
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.ParametersParameterComponent;
|
||||||
|
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.retry.support.RetryTemplate;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
import org.springframework.test.web.client.MockRestServiceServer;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
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.withServerError;
|
||||||
|
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||||
|
|
||||||
|
@ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class})
|
||||||
|
@TestPropertySource(properties = {
|
||||||
|
"app.consent.service=gics",
|
||||||
|
"app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"
|
||||||
|
})
|
||||||
|
@RestClientTest
|
||||||
|
class GicsConsentServiceTest {
|
||||||
|
|
||||||
|
static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics";
|
||||||
|
|
||||||
|
MockRestServiceServer mockRestServiceServer;
|
||||||
|
AppFhirConfig appFhirConfig;
|
||||||
|
GIcsConfigProperties gIcsConfigProperties;
|
||||||
|
|
||||||
|
GicsConsentService gicsConsentService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(
|
||||||
|
@Autowired AppFhirConfig appFhirConfig,
|
||||||
|
@Autowired GIcsConfigProperties gIcsConfigProperties
|
||||||
|
) {
|
||||||
|
this.appFhirConfig = appFhirConfig;
|
||||||
|
this.gIcsConfigProperties = gIcsConfigProperties;
|
||||||
|
|
||||||
|
var restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate);
|
||||||
|
this.gicsConsentService = new GicsConsentService(
|
||||||
|
this.gIcsConfigProperties,
|
||||||
|
RetryTemplate.builder().maxAttempts(1).build(),
|
||||||
|
restTemplate,
|
||||||
|
this.appFhirConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTtpBroadConsentStatus() {
|
||||||
|
final Parameters consentedResponse = 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(consentedResponse),
|
||||||
|
MediaType.APPLICATION_JSON
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
|
||||||
|
assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnRevokedConsent() {
|
||||||
|
final Parameters revokedResponse = 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(revokedResponse),
|
||||||
|
MediaType.APPLICATION_JSON)
|
||||||
|
);
|
||||||
|
|
||||||
|
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
|
||||||
|
assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnInvalidParameterResponse() {
|
||||||
|
final OperationOutcome responseWithErrorOutcome = 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(responseWithErrorOutcome),
|
||||||
|
MediaType.APPLICATION_JSON
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
|
||||||
|
assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnRequestError() {
|
||||||
|
mockRestServiceServer
|
||||||
|
.expect(
|
||||||
|
requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT)
|
||||||
|
)
|
||||||
|
.andRespond(
|
||||||
|
withServerError()
|
||||||
|
);
|
||||||
|
|
||||||
|
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
|
||||||
|
assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildRequestParameterCurrentPolicyStatesForPersonTest() {
|
||||||
|
String pid = "12345678";
|
||||||
|
var result = gicsConsentService
|
||||||
|
.buildRequestParameterCurrentPolicyStatesForPerson(
|
||||||
|
pid,
|
||||||
|
Date.from(Instant.now()),
|
||||||
|
ConsentDomain.MODELLVORHABEN_64E
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(result.getParameter())
|
||||||
|
.as("should contain 3 parameter resources")
|
||||||
|
.hasSize(3);
|
||||||
|
|
||||||
|
assertThat(((StringType) result.getParameter("domain").getValue()).getValue())
|
||||||
|
.isEqualTo(
|
||||||
|
gIcsConfigProperties.getGenomDeConsentDomainName()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(((Identifier) result.getParameter("personIdentifier").getValue()).getValue())
|
||||||
|
.isEqualTo(
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,287 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ETL-Processor
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.dnpm.etl.processor.consent
|
||||||
|
|
||||||
|
import dev.dnpm.etl.processor.ArgProvider
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Nested
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
|
import org.junit.jupiter.params.provider.ArgumentsSource
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension::class)
|
||||||
|
class Dnpm21BasedConsentEvaluatorTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class WithGicsConsentEnabled {
|
||||||
|
|
||||||
|
lateinit var consentService: GicsConsentService
|
||||||
|
lateinit var consentEvaluator: ConsentEvaluator
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp(
|
||||||
|
@Mock consentService: GicsConsentService
|
||||||
|
) {
|
||||||
|
this.consentService = consentService
|
||||||
|
this.consentEvaluator = ConsentEvaluator(consentService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ArgumentsSource(WithGicsMtbFileProvider::class)
|
||||||
|
fun test(
|
||||||
|
mtbFile: Mtb,
|
||||||
|
ttpConsentStatus: TtpConsentStatus,
|
||||||
|
expectedConsentEvaluation: ConsentEvaluation
|
||||||
|
) {
|
||||||
|
whenever(consentService.getTtpBroadConsentStatus(anyString())).thenReturn(
|
||||||
|
ttpConsentStatus
|
||||||
|
)
|
||||||
|
assertThat(consentEvaluator.check(mtbFile)).isEqualTo(expectedConsentEvaluation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class WithFileConsentOnly {
|
||||||
|
|
||||||
|
lateinit var consentService: MtbFileConsentService
|
||||||
|
lateinit var consentEvaluator: ConsentEvaluator
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
this.consentService = MtbFileConsentService()
|
||||||
|
this.consentEvaluator = ConsentEvaluator(consentService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ArgumentsSource(MtbFileProvider::class)
|
||||||
|
fun test(mtbFile: Mtb, expectedConsentEvaluation: ConsentEvaluation) {
|
||||||
|
assertThat(consentEvaluator.check(mtbFile)).isEqualTo(expectedConsentEvaluation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Util classes
|
||||||
|
|
||||||
|
class WithGicsMtbFileProvider : ArgProvider(
|
||||||
|
// Has file ModelProjectConsent and broad consent => consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true)
|
||||||
|
),
|
||||||
|
// Has file ModelProjectConsent and broad consent missing => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_MISSING,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_MISSING, false)
|
||||||
|
),
|
||||||
|
// Has file ModelProjectConsent and broad consent missing or rejected => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, false)
|
||||||
|
),
|
||||||
|
// Has file ModelProjectConsent and MV consent => consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, true)
|
||||||
|
),
|
||||||
|
// Has file ModelProjectConsent and MV consent rejected => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED, false)
|
||||||
|
),
|
||||||
|
// Has file ModelProjectConsent and MV consent missing => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
TtpConsentStatus.GENOM_DE_CONSENT_MISSING,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.GENOM_DE_CONSENT_MISSING, false)
|
||||||
|
),
|
||||||
|
// Has file ModelProjectConsent and no broad consent result => consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
TtpConsentStatus.UNKNOWN_CHECK_FILE,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, true)
|
||||||
|
),
|
||||||
|
// Has file ModelProjectConsent and failed to ask => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
TtpConsentStatus.FAILED_TO_ASK,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.FAILED_TO_ASK, false)
|
||||||
|
),
|
||||||
|
// File ModelProjectConsent rejected and broad consent => consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true)
|
||||||
|
),
|
||||||
|
// File ModelProjectConsent rejected and broad consent missing => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_MISSING,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_MISSING, false)
|
||||||
|
),
|
||||||
|
// File ModelProjectConsent rejected and broad consent missing or rejected => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, false)
|
||||||
|
),
|
||||||
|
// File ModelProjectConsent rejected and MV consent => consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, true)
|
||||||
|
),
|
||||||
|
// File ModelProjectConsent rejected and MV consent rejected => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED, false)
|
||||||
|
),
|
||||||
|
// File ModelProjectConsent rejected and MV consent missing => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
TtpConsentStatus.GENOM_DE_CONSENT_MISSING,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.GENOM_DE_CONSENT_MISSING, false)
|
||||||
|
),
|
||||||
|
// File ModelProjectConsent rejected and no broad consent result => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
TtpConsentStatus.UNKNOWN_CHECK_FILE,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false)
|
||||||
|
),
|
||||||
|
// File ModelProjectConsent rejected and failed to ask => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
TtpConsentStatus.FAILED_TO_ASK,
|
||||||
|
ConsentEvaluation(TtpConsentStatus.FAILED_TO_ASK, false)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun buildMtb(consentProvision: ConsentProvision): Mtb {
|
||||||
|
return Mtb.builder()
|
||||||
|
.patient(
|
||||||
|
Patient.builder().id("TEST_12345678")
|
||||||
|
.birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))).gender(
|
||||||
|
GenderCoding.builder().code(GenderCodingCode.MALE).build()
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.metadata(
|
||||||
|
MvhMetadata.builder().modelProjectConsent(
|
||||||
|
ModelProjectConsent.builder().provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().date(Date()).type(consentProvision)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.episodesOfCare(
|
||||||
|
listOf(
|
||||||
|
MtbEpisodeOfCare.builder().id("1")
|
||||||
|
.patient(Reference.builder().id("TEST_12345678").build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MtbFileProvider : ArgProvider(
|
||||||
|
// Has file consent => consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.PERMIT),
|
||||||
|
ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, true)
|
||||||
|
),
|
||||||
|
// File consent rejected => no consent given
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ConsentProvision.DENY),
|
||||||
|
ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false)
|
||||||
|
),
|
||||||
|
// policy REIDENTIFICATION has no effect on ConsentEvaluation
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ModelProjectConsentPurpose.REIDENTIFICATION, ConsentProvision.DENY),
|
||||||
|
ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false)
|
||||||
|
), Arguments.of(
|
||||||
|
buildMtb(ModelProjectConsentPurpose.REIDENTIFICATION, ConsentProvision.PERMIT),
|
||||||
|
ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false)
|
||||||
|
),
|
||||||
|
// policy CASE_IDENTIFICATION has no effect on ConsentEvaluation
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(ModelProjectConsentPurpose.CASE_IDENTIFICATION, ConsentProvision.DENY),
|
||||||
|
ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false)
|
||||||
|
), Arguments.of(
|
||||||
|
buildMtb(ModelProjectConsentPurpose.CASE_IDENTIFICATION, ConsentProvision.PERMIT),
|
||||||
|
ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun buildMtb(consentProvision: ConsentProvision): Mtb {
|
||||||
|
return buildMtb(ModelProjectConsentPurpose.SEQUENCING, consentProvision)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildMtb(
|
||||||
|
policy: ModelProjectConsentPurpose,
|
||||||
|
consentProvision: ConsentProvision
|
||||||
|
): Mtb {
|
||||||
|
return Mtb.builder()
|
||||||
|
.patient(
|
||||||
|
Patient.builder().id("TEST_12345678")
|
||||||
|
.birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))).gender(
|
||||||
|
GenderCoding.builder().code(GenderCodingCode.MALE).build()
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.metadata(
|
||||||
|
MvhMetadata.builder().modelProjectConsent(
|
||||||
|
ModelProjectConsent.builder().provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().date(Date()).type(consentProvision)
|
||||||
|
.purpose(policy).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.episodesOfCare(
|
||||||
|
listOf(
|
||||||
|
MtbEpisodeOfCare.builder().id("1")
|
||||||
|
.patient(Reference.builder().id("TEST_12345678").build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -17,4 +17,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package dev.dnpm.etl.processor
|
package dev.dnpm.etl.processor
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.extension.ExtensionContext
|
||||||
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
|
import org.junit.jupiter.params.provider.ArgumentsProvider
|
||||||
|
import java.util.stream.Stream
|
||||||
|
|
||||||
|
open class ArgProvider(vararg val data: Arguments) : ArgumentsProvider {
|
||||||
|
override fun provideArguments(
|
||||||
|
context: ExtensionContext?
|
||||||
|
): Stream<out Arguments> = Stream.of(*data)
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
package dev.dnpm.etl.processor.input
|
package dev.dnpm.etl.processor.input
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import dev.dnpm.etl.processor.consent.ConsentEvaluation
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
import dev.dnpm.etl.processor.consent.ConsentEvaluator
|
||||||
|
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.*
|
||||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||||
import org.apache.kafka.common.header.internals.RecordHeader
|
import org.apache.kafka.common.header.internals.RecordHeader
|
||||||
import org.apache.kafka.common.header.internals.RecordHeaders
|
import org.apache.kafka.common.header.internals.RecordHeaders
|
||||||
@@ -33,80 +35,261 @@ 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.any
|
import org.mockito.kotlin.*
|
||||||
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)
|
||||||
class KafkaInputListenerTest {
|
class KafkaInputListenerTest {
|
||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
private lateinit var requestProcessor: RequestProcessor
|
||||||
|
private lateinit var consentEvaluator: ConsentEvaluator
|
||||||
private lateinit var objectMapper: ObjectMapper
|
private lateinit var objectMapper: ObjectMapper
|
||||||
|
|
||||||
private lateinit var kafkaInputListener: KafkaInputListener
|
private lateinit var kafkaInputListener: KafkaInputListener
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Mock requestProcessor: RequestProcessor
|
@Mock requestProcessor: RequestProcessor,
|
||||||
|
@Mock consentEvaluator: ConsentEvaluator,
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
|
this.consentEvaluator = consentEvaluator
|
||||||
this.objectMapper = ObjectMapper()
|
this.objectMapper = ObjectMapper()
|
||||||
|
|
||||||
this.kafkaInputListener = KafkaInputListener(requestProcessor, objectMapper)
|
this.kafkaInputListener = KafkaInputListener(requestProcessor, consentEvaluator, objectMapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessMtbFileRequest() {
|
fun shouldProcessMtbFileRequest() {
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentEvaluator.check(any())).thenReturn(
|
||||||
.withPatient(Patient.builder().withId("DUMMY_12345678").build())
|
ConsentEvaluation(
|
||||||
.withConsent(Consent.builder().withStatus(Consent.Status.ACTIVE).build())
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(Patient.builder().id("DUMMY_12345678").build())
|
||||||
|
.metadata(
|
||||||
|
MvhMetadata
|
||||||
|
.builder()
|
||||||
|
.modelProjectConsent(
|
||||||
|
ModelProjectConsent
|
||||||
|
.builder()
|
||||||
|
.provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().type(ConsentProvision.PERMIT)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
kafkaInputListener.onMessage(ConsumerRecord("testtopic", 0, 0, "", this.objectMapper.writeValueAsString(mtbFile)))
|
kafkaInputListener.onMessage(
|
||||||
|
ConsumerRecord(
|
||||||
|
"testtopic",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
this.objectMapper.writeValueAsString(mtbFile)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessDeleteRequest() {
|
fun shouldProcessDeleteRequest() {
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentEvaluator.check(any())).thenReturn(
|
||||||
.withPatient(Patient.builder().withId("DUMMY_12345678").build())
|
ConsentEvaluation(
|
||||||
.withConsent(Consent.builder().withStatus(Consent.Status.REJECTED).build())
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(Patient.builder().id("DUMMY_12345678").build())
|
||||||
|
.metadata(
|
||||||
|
MvhMetadata
|
||||||
|
.builder()
|
||||||
|
.modelProjectConsent(
|
||||||
|
ModelProjectConsent
|
||||||
|
.builder()
|
||||||
|
.provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().type(ConsentProvision.DENY)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
kafkaInputListener.onMessage(ConsumerRecord("testtopic", 0, 0, "", this.objectMapper.writeValueAsString(mtbFile)))
|
kafkaInputListener.onMessage(
|
||||||
|
ConsumerRecord(
|
||||||
|
"testtopic",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
this.objectMapper.writeValueAsString(mtbFile)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
verify(requestProcessor, times(1)).processDeletion(
|
||||||
|
anyValueClass(),
|
||||||
|
eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessMtbFileRequestWithExistingRequestId() {
|
fun shouldProcessMtbFileRequestWithExistingRequestId() {
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentEvaluator.check(any())).thenReturn(
|
||||||
.withPatient(Patient.builder().withId("DUMMY_12345678").build())
|
ConsentEvaluation(
|
||||||
.withConsent(Consent.builder().withStatus(Consent.Status.ACTIVE).build())
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(Patient.builder().id("DUMMY_12345678").build())
|
||||||
|
.metadata(
|
||||||
|
MvhMetadata
|
||||||
|
.builder()
|
||||||
|
.modelProjectConsent(
|
||||||
|
ModelProjectConsent
|
||||||
|
.builder()
|
||||||
|
.provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().type(ConsentProvision.PERMIT)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
||||||
kafkaInputListener.onMessage(
|
kafkaInputListener.onMessage(
|
||||||
ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty())
|
ConsumerRecord(
|
||||||
|
"testtopic",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-1L,
|
||||||
|
TimestampType.NO_TIMESTAMP_TYPE,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
"",
|
||||||
|
this.objectMapper.writeValueAsString(mtbFile),
|
||||||
|
headers,
|
||||||
|
Optional.empty()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any(), anyValueClass())
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>(), anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessDeleteRequestWithExistingRequestId() {
|
fun shouldProcessDeleteRequestWithExistingRequestId() {
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentEvaluator.check(any())).thenReturn(
|
||||||
.withPatient(Patient.builder().withId("DUMMY_12345678").build())
|
ConsentEvaluation(
|
||||||
.withConsent(Consent.builder().withStatus(Consent.Status.REJECTED).build())
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(Patient.builder().id("DUMMY_12345678").build())
|
||||||
|
.metadata(
|
||||||
|
MvhMetadata
|
||||||
|
.builder()
|
||||||
|
.modelProjectConsent(
|
||||||
|
ModelProjectConsent
|
||||||
|
.builder()
|
||||||
|
.provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().type(ConsentProvision.DENY)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
||||||
kafkaInputListener.onMessage(
|
kafkaInputListener.onMessage(
|
||||||
ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty())
|
ConsumerRecord(
|
||||||
|
"testtopic",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-1L,
|
||||||
|
TimestampType.NO_TIMESTAMP_TYPE,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
"",
|
||||||
|
this.objectMapper.writeValueAsString(mtbFile),
|
||||||
|
headers,
|
||||||
|
Optional.empty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
verify(requestProcessor, times(1)).processDeletion(
|
||||||
|
anyValueClass(), anyValueClass(), eq(
|
||||||
|
TtpConsentStatus.UNKNOWN_CHECK_FILE
|
||||||
|
)
|
||||||
)
|
)
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@Test
|
||||||
|
fun shouldNotProcessDnpmV2Request() {
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(Patient.builder().id("DUMMY_12345678").build())
|
||||||
|
.metadata(
|
||||||
|
MvhMetadata
|
||||||
|
.builder()
|
||||||
|
.modelProjectConsent(
|
||||||
|
ModelProjectConsent
|
||||||
|
.builder()
|
||||||
|
.provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().type(ConsentProvision.DENY)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val headers = RecordHeaders(
|
||||||
|
listOf(
|
||||||
|
RecordHeader("requestId", UUID.randomUUID().toString().toByteArray()),
|
||||||
|
RecordHeader("contentType", CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
kafkaInputListener.onMessage(
|
||||||
|
ConsumerRecord(
|
||||||
|
"testtopic",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-1L,
|
||||||
|
TimestampType.NO_TIMESTAMP_TYPE,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
"",
|
||||||
|
this.objectMapper.writeValueAsString(mtbFile),
|
||||||
|
headers,
|
||||||
|
Optional.empty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
verify(requestProcessor, times(0)).processDeletion(
|
||||||
|
anyValueClass(), anyValueClass(), eq(
|
||||||
|
TtpConsentStatus.UNKNOWN_CHECK_FILE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@@ -20,23 +20,34 @@
|
|||||||
package dev.dnpm.etl.processor.input
|
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 dev.dnpm.etl.processor.ArgProvider
|
||||||
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
|
import dev.dnpm.etl.processor.consent.ConsentEvaluation
|
||||||
|
import dev.dnpm.etl.processor.consent.ConsentEvaluator
|
||||||
|
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.*
|
||||||
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.Arguments
|
||||||
|
import org.junit.jupiter.params.provider.ArgumentsSource
|
||||||
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.springframework.http.MediaType
|
import org.mockito.kotlin.whenever
|
||||||
|
import org.springframework.core.io.ClassPathResource
|
||||||
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
|
||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class MtbFileRestControllerTest {
|
class MtbFileRestControllerTest {
|
||||||
@@ -44,47 +55,78 @@ class MtbFileRestControllerTest {
|
|||||||
private val objectMapper = ObjectMapper()
|
private val objectMapper = ObjectMapper()
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class BwhcRequests {
|
inner class RequestsForDnpmDataModel21 {
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
private lateinit var requestProcessor: RequestProcessor
|
||||||
|
private lateinit var consentEvaluator: ConsentEvaluator
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Mock requestProcessor: RequestProcessor
|
@Mock requestProcessor: RequestProcessor,
|
||||||
|
@Mock consentEvaluator: ConsentEvaluator
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
val controller = MtbFileRestController(requestProcessor)
|
this.consentEvaluator = consentEvaluator
|
||||||
|
val controller = MtbFileRestController(
|
||||||
|
requestProcessor,
|
||||||
|
consentEvaluator
|
||||||
|
)
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequest() {
|
fun shouldRespondPostRequest() {
|
||||||
mockMvc.post("/mtbfile") {
|
whenever(consentEvaluator.check(any())).thenReturn(
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE))
|
ConsentEvaluation(
|
||||||
contentType = MediaType.APPLICATION_JSON
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mtbFileContent =
|
||||||
|
ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes().toString(Charsets.UTF_8)
|
||||||
|
|
||||||
|
mockMvc.post("/mtb") {
|
||||||
|
content = mtbFileContent
|
||||||
|
contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
isAccepted()
|
isAccepted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
fun shouldProcessPostRequestWithRejectedConsent() {
|
@ArgumentsSource(Dnpm21MtbFile::class)
|
||||||
|
fun shouldProcessPostRequest(mtb: Mtb, broadConsent: TtpConsentStatus, shouldProcess: String) {
|
||||||
|
whenever(consentEvaluator.check(any<Mtb>())).thenReturn(
|
||||||
|
ConsentEvaluation(
|
||||||
|
broadConsent,
|
||||||
|
shouldProcess == "process"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
mockMvc.post("/mtbfile") {
|
mockMvc.post("/mtbfile") {
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
|
content = objectMapper.writeValueAsString(mtb)
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
isAccepted()
|
isAccepted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
if (shouldProcess == "process") {
|
||||||
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
||||||
|
} else {
|
||||||
|
verify(requestProcessor, times(1)).processDeletion(
|
||||||
|
anyValueClass(),
|
||||||
|
org.mockito.kotlin.eq(broadConsent)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -95,89 +137,90 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
verify(requestProcessor, times(1)).processDeletion(
|
||||||
}
|
anyValueClass(),
|
||||||
}
|
org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class BwhcRequestsWithAlias {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Mock requestProcessor: RequestProcessor
|
|
||||||
) {
|
|
||||||
this.requestProcessor = requestProcessor
|
|
||||||
val controller = MtbFileRestController(requestProcessor)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessPostRequest() {
|
|
||||||
mockMvc.post("/mtb") {
|
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessPostRequestWithRejectedConsent() {
|
|
||||||
mockMvc.post("/mtb") {
|
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessDeleteRequest() {
|
|
||||||
mockMvc.delete("/mtb/TEST_12345678").andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun bwhcMtbFileContent(consentStatus: Consent.Status) = MtbFile.builder()
|
|
||||||
.withPatient(
|
|
||||||
Patient.builder()
|
|
||||||
.withId("TEST_12345678")
|
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.withConsent(
|
verify(consentEvaluator, times(0)).check(any<Mtb>())
|
||||||
Consent.builder()
|
}
|
||||||
.withId("1")
|
}
|
||||||
.withStatus(consentStatus)
|
}
|
||||||
.withPatient("TEST_12345678")
|
|
||||||
.build()
|
class Dnpm21MtbFile : ArgProvider(
|
||||||
)
|
// No Metadata and no broad consent => delete
|
||||||
.withEpisode(
|
Arguments.of(
|
||||||
Episode.builder()
|
buildMtb(null),
|
||||||
.withId("1")
|
TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED,
|
||||||
.withPatient("TEST_12345678")
|
"delete"
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
),
|
||||||
.build()
|
// No Metadata and broad consent given => process
|
||||||
)
|
Arguments.of(
|
||||||
.build()
|
buildMtb(null),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
"process"
|
||||||
|
),
|
||||||
|
// No model project consent and no broad consent => delete
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(MvhMetadata.builder().modelProjectConsent(ModelProjectConsent.builder().build()).build()),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED,
|
||||||
|
"delete"
|
||||||
|
),
|
||||||
|
// No model project consent and broad consent given => process
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(MvhMetadata.builder().modelProjectConsent(ModelProjectConsent.builder().build()).build()),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
"process"
|
||||||
|
),
|
||||||
|
// Model project consent given and no broad consent => process
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(
|
||||||
|
MvhMetadata.builder().modelProjectConsent(
|
||||||
|
ModelProjectConsent.builder().provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().date(Date()).type(ConsentProvision.PERMIT)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
).build()
|
||||||
|
),
|
||||||
|
TtpConsentStatus.UNKNOWN_CHECK_FILE,
|
||||||
|
"process"
|
||||||
|
),
|
||||||
|
// Model project consent given and broad consent given => process
|
||||||
|
Arguments.of(
|
||||||
|
buildMtb(
|
||||||
|
MvhMetadata.builder().modelProjectConsent(
|
||||||
|
ModelProjectConsent.builder().provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().date(Date()).type(ConsentProvision.PERMIT)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
).build()
|
||||||
|
),
|
||||||
|
TtpConsentStatus.BROAD_CONSENT_GIVEN,
|
||||||
|
"process"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun buildMtb(metadata: MvhMetadata?): Mtb {
|
||||||
|
return Mtb.builder()
|
||||||
|
.patient(
|
||||||
|
Patient.builder().id("TEST_12345678")
|
||||||
|
.birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))).gender(
|
||||||
|
GenderCoding.builder().code(GenderCodingCode.MALE).build()
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
.metadata(metadata)
|
||||||
|
.episodesOfCare(
|
||||||
|
listOf(
|
||||||
|
MtbEpisodeOfCare.builder().id("1")
|
||||||
|
.patient(Reference.builder().id("TEST_12345678").build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -20,18 +20,20 @@
|
|||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.KafkaProperties
|
import dev.dnpm.etl.processor.config.KafkaProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
|
import org.apache.kafka.clients.producer.ProducerRecord
|
||||||
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.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.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
|
||||||
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.*
|
||||||
@@ -39,184 +41,189 @@ import org.springframework.kafka.core.KafkaTemplate
|
|||||||
import org.springframework.kafka.support.SendResult
|
import org.springframework.kafka.support.SendResult
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.CompletableFuture.completedFuture
|
import java.util.concurrent.CompletableFuture.completedFuture
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class KafkaMtbFileSenderTest {
|
class KafkaMtbFileSenderTest {
|
||||||
|
|
||||||
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
@Nested
|
||||||
|
inner class BwhcV1Record {
|
||||||
|
|
||||||
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
|
||||||
private lateinit var objectMapper: ObjectMapper
|
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
||||||
|
|
||||||
@BeforeEach
|
private lateinit var objectMapper: ObjectMapper
|
||||||
fun setup(
|
|
||||||
@Mock kafkaTemplate: KafkaTemplate<String, String>
|
|
||||||
) {
|
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
|
|
||||||
this.objectMapper = ObjectMapper()
|
@BeforeEach
|
||||||
this.kafkaTemplate = kafkaTemplate
|
fun setup(
|
||||||
|
@Mock kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
) {
|
||||||
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
this.kafkaMtbFileSender = KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper)
|
this.objectMapper = ObjectMapper()
|
||||||
}
|
this.kafkaTemplate = kafkaTemplate
|
||||||
|
|
||||||
@ParameterizedTest
|
this.kafkaMtbFileSender = KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper)
|
||||||
@MethodSource("requestWithResponseSource")
|
|
||||||
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
|
||||||
doAnswer {
|
|
||||||
if (null != testData.exception) {
|
|
||||||
throw testData.exception
|
|
||||||
}
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
|
||||||
|
|
||||||
val response = kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
|
||||||
assertThat(response.status).isEqualTo(testData.requestStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("requestWithResponseSource")
|
|
||||||
fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) {
|
|
||||||
doAnswer {
|
|
||||||
if (null != testData.exception) {
|
|
||||||
throw testData.exception
|
|
||||||
}
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
|
||||||
|
|
||||||
val response = kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
assertThat(response.status).isEqualTo(testData.requestStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldSendMtbFileRequestWithCorrectKeyAndBody() {
|
|
||||||
doAnswer {
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
|
||||||
|
|
||||||
kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
|
||||||
|
|
||||||
val captor = argumentCaptor<String>()
|
|
||||||
verify(kafkaTemplate, times(1)).send(anyString(), captor.capture(), captor.capture())
|
|
||||||
assertThat(captor.firstValue).isNotNull
|
|
||||||
assertThat(captor.firstValue).isEqualTo("{\"pid\": \"PID\"}")
|
|
||||||
assertThat(captor.secondValue).isNotNull
|
|
||||||
assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData(TEST_REQUEST_ID, Consent.Status.ACTIVE)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldSendDeleteRequestWithCorrectKeyAndBody() {
|
|
||||||
doAnswer {
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
|
||||||
|
|
||||||
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
|
|
||||||
val captor = argumentCaptor<String>()
|
|
||||||
verify(kafkaTemplate, times(1)).send(anyString(), captor.capture(), captor.capture())
|
|
||||||
assertThat(captor.firstValue).isNotNull
|
|
||||||
assertThat(captor.firstValue).isEqualTo("{\"pid\": \"PID\"}")
|
|
||||||
assertThat(captor.secondValue).isNotNull
|
|
||||||
assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData(TEST_REQUEST_ID, Consent.Status.REJECTED)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("requestWithResponseSource")
|
|
||||||
fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) {
|
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
|
||||||
this.kafkaMtbFileSender = KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper)
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
if (null != testData.exception) {
|
|
||||||
throw testData.exception
|
|
||||||
}
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
|
||||||
|
|
||||||
kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
|
||||||
|
|
||||||
val expectedCount = when (testData.exception) {
|
|
||||||
// OK - No Retry
|
|
||||||
null -> times(1)
|
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> times(3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(kafkaTemplate, expectedCount).send(anyString(), anyString(), anyString())
|
@ParameterizedTest
|
||||||
}
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
|
fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) {
|
||||||
|
doAnswer {
|
||||||
|
if (null != testData.exception) {
|
||||||
|
throw testData.exception
|
||||||
|
}
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
@ParameterizedTest
|
val response = kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
@MethodSource("requestWithResponseSource")
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
fun shouldRetryOnDeleteKafkaSendError(testData: TestData) {
|
}
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
@ParameterizedTest
|
||||||
this.kafkaMtbFileSender = KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper)
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
|
fun shouldRetryOnDeleteKafkaSendError(testData: TestData) {
|
||||||
doAnswer {
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
if (null != testData.exception) {
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
throw testData.exception
|
this.kafkaMtbFileSender = KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper)
|
||||||
}
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
doAnswer {
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
if (null != testData.exception) {
|
||||||
|
throw testData.exception
|
||||||
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
}
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
val expectedCount = when (testData.exception) {
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
// OK - No Retry
|
|
||||||
null -> times(1)
|
kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> times(3)
|
val expectedCount = when (testData.exception) {
|
||||||
|
// OK - No Retry
|
||||||
|
null -> times(1)
|
||||||
|
// Request failed - Retry max 3 times
|
||||||
|
else -> times(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class DnpmV2Record {
|
||||||
|
|
||||||
|
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
|
||||||
|
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
||||||
|
|
||||||
|
private lateinit var objectMapper: ObjectMapper
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup(
|
||||||
|
@Mock kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
) {
|
||||||
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
|
this.objectMapper = ObjectMapper()
|
||||||
|
this.kafkaTemplate = kafkaTemplate
|
||||||
|
|
||||||
|
this.kafkaMtbFileSender = KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
|
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
||||||
|
doAnswer {
|
||||||
|
if (null != testData.exception) {
|
||||||
|
throw testData.exception
|
||||||
|
}
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
|
val response = kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
||||||
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldSendMtbFileRequestWithCorrectKeyAndHeaderAndBody() {
|
||||||
|
doAnswer {
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
||||||
|
|
||||||
|
val captor = argumentCaptor<ProducerRecord<String, String>>()
|
||||||
|
verify(kafkaTemplate, times(1)).send(captor.capture())
|
||||||
|
assertThat(captor.firstValue.key()).isNotNull
|
||||||
|
assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}")
|
||||||
|
assertThat(captor.firstValue.headers().headers("contentType")).isNotNull
|
||||||
|
assertThat(captor.firstValue.headers().headers("contentType")?.firstOrNull()?.value()).isEqualTo(CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
|
||||||
|
assertThat(captor.firstValue.value()).isNotNull
|
||||||
|
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(dnmpV2kafkaRecordData(TEST_REQUEST_ID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
|
fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) {
|
||||||
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
|
this.kafkaMtbFileSender = KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper)
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
if (null != testData.exception) {
|
||||||
|
throw testData.exception
|
||||||
|
}
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
||||||
|
|
||||||
|
val expectedCount = when (testData.exception) {
|
||||||
|
// OK - No Retry
|
||||||
|
null -> times(1)
|
||||||
|
// Request failed - Retry max 3 times
|
||||||
|
else -> times(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(kafkaTemplate, expectedCount).send(anyString(), anyString(), anyString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
val TEST_REQUEST_ID = RequestId("TestId")
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
||||||
|
|
||||||
fun mtbFile(consentStatus: Consent.Status): MtbFile {
|
fun dnpmV2MtbFile(): Mtb {
|
||||||
return if (consentStatus == Consent.Status.ACTIVE) {
|
return Mtb().apply {
|
||||||
MtbFile.builder()
|
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
||||||
.withPatient(
|
this.id = "PID"
|
||||||
Patient.builder()
|
this.birthDate = Date.from(Instant.now())
|
||||||
.withId("PID")
|
this.gender = GenderCoding().apply {
|
||||||
.withBirthDate("2000-08-08")
|
this.code = GenderCodingCode.MALE
|
||||||
.withGender(Patient.Gender.MALE)
|
}
|
||||||
.build()
|
}
|
||||||
)
|
this.episodesOfCare = listOf(
|
||||||
.withConsent(
|
MtbEpisodeOfCare().apply {
|
||||||
Consent.builder()
|
this.id = "1"
|
||||||
.withId("1")
|
this.patient = Reference().apply {
|
||||||
.withStatus(consentStatus)
|
this.id = "PID"
|
||||||
.withPatient("PID")
|
}
|
||||||
.build()
|
this.period = PeriodDate().apply {
|
||||||
)
|
this.start = Date.from(Instant.now())
|
||||||
.withEpisode(
|
}
|
||||||
Episode.builder()
|
}
|
||||||
.withId("1")
|
)
|
||||||
.withPatient("PID")
|
}
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
MtbFile.builder()
|
|
||||||
.withConsent(
|
|
||||||
Consent.builder()
|
|
||||||
.withStatus(consentStatus)
|
|
||||||
.withPatient("PID")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun kafkaRecordData(requestId: RequestId, consentStatus: Consent.Status): KafkaMtbFileSender.Data {
|
fun dnmpV2kafkaRecordData(requestId: RequestId): MtbRequest {
|
||||||
return KafkaMtbFileSender.Data(requestId, mtbFile(consentStatus))
|
return DnpmV2MtbFileRequest(requestId, dnpmV2MtbFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null)
|
data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null)
|
||||||
@@ -231,4 +238,4 @@ class KafkaMtbFileSenderTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,312 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
|
||||||
import org.junit.jupiter.params.provider.Arguments
|
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
|
||||||
import org.springframework.http.HttpMethod
|
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
|
||||||
import org.springframework.web.client.RestTemplate
|
|
||||||
|
|
||||||
class RestBwhcMtbFileSenderTest {
|
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
|
||||||
|
|
||||||
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("deleteRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.DELETE))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/Patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("mtbFileRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.POST))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("mtbFileRequestWithResponseSource")
|
|
||||||
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
|
|
||||||
val expectedCount = when (requestWithResponse.httpStatus) {
|
|
||||||
// OK - No Retry
|
|
||||||
HttpStatus.OK, HttpStatus.CREATED -> ExpectedCount.max(1)
|
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> ExpectedCount.max(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(expectedCount, method(HttpMethod.POST))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("deleteRequestWithResponseSource")
|
|
||||||
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
|
|
||||||
val expectedCount = when (requestWithResponse.httpStatus) {
|
|
||||||
// OK - No Retry
|
|
||||||
HttpStatus.OK, HttpStatus.CREATED -> ExpectedCount.max(1)
|
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> ExpectedCount.max(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(expectedCount, method(HttpMethod.DELETE))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/Patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
data class RequestWithResponse(
|
|
||||||
val httpStatus: HttpStatus,
|
|
||||||
val body: String,
|
|
||||||
val response: MtbFileSender.Response
|
|
||||||
)
|
|
||||||
|
|
||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
|
||||||
|
|
||||||
val mtbFile: MtbFile = MtbFile.builder()
|
|
||||||
.withPatient(
|
|
||||||
Patient.builder()
|
|
||||||
.withId("PID")
|
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withConsent(
|
|
||||||
Consent.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
|
||||||
.withPatient("PID")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withEpisode(
|
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("PID")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthetic http responses with related request status
|
|
||||||
* Also see: https://ibmi-intra.cs.uni-tuebingen.de/display/ZPM/bwHC+REST+API
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun mtbFileRequestWithResponseSource(): Set<RequestWithResponse> {
|
|
||||||
return setOf(
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.OK,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.INFO),
|
|
||||||
MtbFileSender.Response(
|
|
||||||
RequestStatus.SUCCESS,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.INFO)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.CREATED,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.WARNING),
|
|
||||||
MtbFileSender.Response(
|
|
||||||
RequestStatus.WARNING,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.WARNING)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.ERROR),
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR))
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.FATAL),
|
|
||||||
MtbFileSender.Response(
|
|
||||||
RequestStatus.ERROR,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.FATAL)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// Some more errors not mentioned in documentation
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
ERROR_RESPONSE_BODY,
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
ERROR_RESPONSE_BODY,
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthetic http responses with related request status
|
|
||||||
* Also see: https://ibmi-intra.cs.uni-tuebingen.de/display/ZPM/bwHC+REST+API
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun deleteRequestWithResponseSource(): Set<RequestWithResponse> {
|
|
||||||
return setOf(
|
|
||||||
RequestWithResponse(HttpStatus.OK, "", MtbFileSender.Response(RequestStatus.SUCCESS)),
|
|
||||||
// Some more errors not mentioned in documentation
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
"what????",
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
"what????",
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun responseBodyWithMaxSeverity(severity: ReportService.Severity): String {
|
|
||||||
return when (severity) {
|
|
||||||
ReportService.Severity.INFO -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.WARNING -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.ERROR -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" },
|
|
||||||
{ "severity": "error", "message": "Error Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.FATAL -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" },
|
|
||||||
{ "severity": "error", "message": "Error Message" },
|
|
||||||
{ "severity": "fatal", "message": "Fatal Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@@ -21,7 +21,7 @@ package dev.dnpm.etl.processor.output
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
@@ -29,136 +29,136 @@ import dev.dnpm.etl.processor.config.AppConfiguration
|
|||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.output.RestBwhcMtbFileSenderTest.Companion
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
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.Nested
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.retry.backoff.NoBackOffPolicy
|
import org.springframework.retry.backoff.NoBackOffPolicy
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
import org.springframework.test.web.client.ExpectedCount
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
import org.springframework.test.web.client.MockRestServiceServer
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class RestDipMtbFileSenderTest {
|
class RestDipMtbFileSenderTest {
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
@Nested
|
||||||
|
inner class DnpmV2ContentRequest {
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
|
|
||||||
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
private lateinit var restMtbFileSender: RestMtbFileSender
|
||||||
|
|
||||||
@BeforeEach
|
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
||||||
fun setup() {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
val restTemplate = RestTemplate()
|
||||||
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null)
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
@MethodSource("deleteRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.DELETE))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("mtbFileRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.POST))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("mtbFileRequestWithResponseSource")
|
|
||||||
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = AppConfiguration().retryTemplate(AppConfigProperties("http://localhost:9000"))
|
|
||||||
retryTemplate.setBackOffPolicy(NoBackOffPolicy())
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
|
|
||||||
val expectedCount = when (requestWithResponse.httpStatus) {
|
|
||||||
// OK - No Retry
|
|
||||||
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(
|
|
||||||
1
|
|
||||||
)
|
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> ExpectedCount.max(3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mockRestServiceServer
|
@ParameterizedTest
|
||||||
.expect(expectedCount, method(HttpMethod.POST))
|
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
fun shouldReturnExpectedResponseForDnpmV2MtbFilePost(requestWithResponse: RequestWithResponse) {
|
||||||
.andRespond {
|
this.mockRestServiceServer
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
.expect(method(HttpMethod.POST))
|
||||||
}
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
||||||
|
.andExpect(header(HttpHeaders.CONTENT_TYPE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE))
|
||||||
|
.andRespond {
|
||||||
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("deleteRequestWithResponseSource")
|
|
||||||
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = AppConfiguration().retryTemplate(AppConfigProperties("http://localhost:9000"))
|
|
||||||
retryTemplate.setBackOffPolicy(NoBackOffPolicy())
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
|
|
||||||
val expectedCount = when (requestWithResponse.httpStatus) {
|
|
||||||
// OK - No Retry
|
|
||||||
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(
|
|
||||||
1
|
|
||||||
)
|
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> ExpectedCount.max(3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mockRestServiceServer
|
}
|
||||||
.expect(expectedCount, method(HttpMethod.DELETE))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
@Nested
|
||||||
.andRespond {
|
inner class DeleteRequest {
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
|
|
||||||
|
private lateinit var restMtbFileSender: RestMtbFileSender
|
||||||
|
|
||||||
|
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
val restTemplate = RestTemplate()
|
||||||
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null)
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
|
|
||||||
|
this.restMtbFileSender =
|
||||||
|
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource")
|
||||||
|
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
||||||
|
this.mockRestServiceServer
|
||||||
|
.expect(method(HttpMethod.DELETE))
|
||||||
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
||||||
|
.andRespond {
|
||||||
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource")
|
||||||
|
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
||||||
|
val restTemplate = RestTemplate()
|
||||||
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null)
|
||||||
|
val retryTemplate = AppConfiguration().retryTemplate(AppConfigProperties())
|
||||||
|
retryTemplate.setBackOffPolicy(NoBackOffPolicy())
|
||||||
|
|
||||||
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
|
this.restMtbFileSender =
|
||||||
|
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
|
|
||||||
|
val expectedCount = when (requestWithResponse.httpStatus) {
|
||||||
|
// OK - No Retry
|
||||||
|
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(
|
||||||
|
1
|
||||||
|
)
|
||||||
|
// Request failed - Retry max 3 times
|
||||||
|
else -> ExpectedCount.max(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
this.mockRestServiceServer
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
.expect(expectedCount, method(HttpMethod.DELETE))
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
||||||
|
.andRespond {
|
||||||
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -171,29 +171,28 @@ class RestDipMtbFileSenderTest {
|
|||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
val TEST_REQUEST_ID = RequestId("TestId")
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
||||||
|
|
||||||
val mtbFile: MtbFile = MtbFile.builder()
|
fun dnpmV2MtbFile(): Mtb {
|
||||||
.withPatient(
|
return Mtb().apply {
|
||||||
Patient.builder()
|
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
||||||
.withId("PID")
|
this.id = "PID"
|
||||||
.withBirthDate("2000-08-08")
|
this.birthDate = Date.from(Instant.now())
|
||||||
.withGender(Patient.Gender.MALE)
|
this.gender = GenderCoding().apply {
|
||||||
.build()
|
this.code = GenderCodingCode.MALE
|
||||||
)
|
}
|
||||||
.withConsent(
|
}
|
||||||
Consent.builder()
|
this.episodesOfCare = listOf(
|
||||||
.withId("1")
|
MtbEpisodeOfCare().apply {
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
this.id = "1"
|
||||||
.withPatient("PID")
|
this.patient = Reference().apply {
|
||||||
.build()
|
this.id = "PID"
|
||||||
)
|
}
|
||||||
.withEpisode(
|
this.period = PeriodDate().apply {
|
||||||
Episode.builder()
|
this.start = Date.from(Instant.now())
|
||||||
.withId("1")
|
}
|
||||||
.withPatient("PID")
|
}
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
)
|
||||||
.build()
|
}
|
||||||
)
|
}
|
||||||
.build()
|
|
||||||
|
|
||||||
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
||||||
|
|
||||||
@@ -311,4 +310,4 @@ class RestDipMtbFileSenderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -19,9 +19,18 @@
|
|||||||
|
|
||||||
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 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.MtbFileConsentService
|
||||||
|
import dev.dnpm.etl.processor.services.ConsentProcessor
|
||||||
|
import dev.dnpm.etl.processor.services.ConsentProcessorTest
|
||||||
|
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.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
@@ -31,168 +40,161 @@ import org.mockito.kotlin.anyValueClass
|
|||||||
import org.mockito.kotlin.doAnswer
|
import org.mockito.kotlin.doAnswer
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.core.io.ClassPathResource
|
import org.springframework.core.io.ClassPathResource
|
||||||
|
import java.time.Instant
|
||||||
const val FAKE_MTB_FILE_PATH = "fake_MTBFile.json"
|
import java.util.*
|
||||||
const val CLEAN_PATIENT_ID = "5dad2f0b-49c6-47d8-a952-7b9e9e0f7549"
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class ExtensionsTest {
|
class ExtensionsTest {
|
||||||
|
fun getObjectMapper(): ObjectMapper {
|
||||||
private fun fakeMtbFile(): MtbFile {
|
return JacksonConfig().objectMapper()
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
|
||||||
return ObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MtbFile.serialized(): String {
|
@Nested
|
||||||
return ObjectMapper().writeValueAsString(this)
|
inner class UsingDnpmV2Datamodel {
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json"
|
||||||
fun shouldNotContainCleanPatientId(@Mock pseudonymizeService: PseudonymizeService) {
|
val CLEAN_PATIENT_ID = "644bae7a-56f6-4ee8-b02f-c532e65af5b1"
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
val mtbFile = fakeMtbFile()
|
private fun fakeMtbFile(): Mtb {
|
||||||
|
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
return getObjectMapper().readValue(mtbFile, Mtb::class.java)
|
||||||
|
|
||||||
assertThat(mtbFile.patient.id).isEqualTo("PSEUDO-ID")
|
|
||||||
assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotContainAnyUuidAfterRehashingOfIds(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
"TESTDOMAIN"
|
|
||||||
}.whenever(pseudonymizeService).prefix()
|
|
||||||
|
|
||||||
val mtbFile = fakeMtbFile()
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
|
||||||
|
|
||||||
val pattern = "\"[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())
|
|
||||||
|
|
||||||
assertThrows<IllegalStateException> {
|
|
||||||
matcher.find()
|
|
||||||
matcher.group()
|
|
||||||
}.also {
|
|
||||||
assertThat(it.message).isEqualTo("No match found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Mtb.serialized(): String {
|
||||||
|
return getObjectMapper().writeValueAsString(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldNotContainCleanPatientId(@Mock pseudonymizeService: PseudonymizeService) {
|
||||||
|
doAnswer {
|
||||||
|
it.arguments[0]
|
||||||
|
"PSEUDO-ID"
|
||||||
|
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
||||||
|
|
||||||
|
val mtbFile = fakeMtbFile()
|
||||||
|
mtbFile.ensureMetaDataIsInitialized()
|
||||||
|
addConsentData(mtbFile)
|
||||||
|
|
||||||
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
|
|
||||||
|
assertThat(mtbFile.patient.id).isEqualTo("PSEUDO-ID")
|
||||||
|
assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addConsentData(mtbFile: Mtb) {
|
||||||
|
val gIcsConfigProperties = GIcsConfigProperties("", "", "")
|
||||||
|
val appConfigProperties = AppConfigProperties(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(),
|
||||||
|
MtbFileConsentService()
|
||||||
|
).embedBroadConsentResources(mtbFile, bundle)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
|
||||||
|
doAnswer {
|
||||||
|
it.arguments[0]
|
||||||
|
"PSEUDO-ID"
|
||||||
|
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
"TESTDOMAIN"
|
||||||
|
}.whenever(pseudonymizeService).prefix()
|
||||||
|
|
||||||
|
val mtbFile = Mtb().apply {
|
||||||
|
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
||||||
|
this.id = "PID"
|
||||||
|
this.birthDate = Date.from(Instant.now())
|
||||||
|
this.gender = GenderCoding().apply {
|
||||||
|
this.code = GenderCodingCode.MALE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.episodesOfCare = listOf(
|
||||||
|
MtbEpisodeOfCare().apply {
|
||||||
|
this.id = "1"
|
||||||
|
this.patient = Reference().apply {
|
||||||
|
this.id = "PID"
|
||||||
|
}
|
||||||
|
this.period = PeriodDate().apply {
|
||||||
|
this.start = Date.from(Instant.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
|
mtbFile.anonymizeContentWith(pseudonymizeService)
|
||||||
|
|
||||||
|
assertThat(mtbFile.episodesOfCare).hasSize(1)
|
||||||
|
assertThat(mtbFile.episodesOfCare.map { it.id }).isNotNull
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldNotContainAnyUuidAfterRehashingOfIds(@Mock pseudonymizeService: PseudonymizeService) {
|
||||||
|
doAnswer {
|
||||||
|
it.arguments[0]
|
||||||
|
"PSEUDO-ID"
|
||||||
|
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
"TESTDOMAIN"
|
||||||
|
}.whenever(pseudonymizeService).prefix()
|
||||||
|
|
||||||
|
val mtbFile = fakeMtbFile()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* replace hex values with random long, so our test does not match false positives
|
||||||
|
*/
|
||||||
|
mtbFile.ngsReports.forEach { report ->
|
||||||
|
report.results.simpleVariants.forEach { simpleVariant ->
|
||||||
|
simpleVariant.externalIds.forEach { extIdValue ->
|
||||||
|
extIdValue.value =
|
||||||
|
Math.random().toLong().toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mtbFile.ngsReports.forEach { report ->
|
||||||
|
report.results.rnaFusions.forEach { simpleVariant ->
|
||||||
|
simpleVariant.externalIds.forEach { extIdValue ->
|
||||||
|
extIdValue.value =
|
||||||
|
Math.random().toLong().toString()
|
||||||
|
}
|
||||||
|
simpleVariant.fusionPartner3Prime?.transcriptId?.value =
|
||||||
|
Math.random().toLong().toString()
|
||||||
|
simpleVariant.fusionPartner5Prime?.transcriptId?.value =
|
||||||
|
Math.random().toLong().toString()
|
||||||
|
simpleVariant.externalIds?.forEach { it ->
|
||||||
|
it?.value = Math.random().toLong().toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
|
mtbFile.anonymizeContentWith(pseudonymizeService)
|
||||||
|
|
||||||
|
val pattern =
|
||||||
|
"\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex()
|
||||||
|
.toPattern()
|
||||||
|
val input = mtbFile.serialized()
|
||||||
|
val matcher = pattern.matcher(input)
|
||||||
|
|
||||||
|
assertThrows<IllegalStateException> {
|
||||||
|
matcher.find()
|
||||||
|
val posSt = "check at pos: " + matcher.start().toString() + ", " + matcher.end()
|
||||||
|
println(posSt + " with " + matcher.group())
|
||||||
|
}.also {
|
||||||
|
assertThat(it.message).isEqualTo("No match found")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Test
|
|
||||||
fun shouldRehashIdsWithPrefix(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
"TESTDOMAIN"
|
|
||||||
}.whenever(pseudonymizeService).prefix()
|
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
|
||||||
.withPatient(
|
|
||||||
Patient.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withConsent(
|
|
||||||
Consent.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
|
||||||
.withPatient("123")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withEpisode(
|
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("1")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
|
||||||
|
|
||||||
|
|
||||||
assertThat(mtbFile.episode.id)
|
|
||||||
// TESTDOMAIN<sha256(TESTDOMAIN-1)[0-41]>
|
|
||||||
.isEqualTo("TESTDOMAIN44e20a53bbbf9f3ae39626d05df7014dcd77d6098")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
"TESTDOMAIN"
|
|
||||||
}.whenever(pseudonymizeService).prefix()
|
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
|
||||||
.withPatient(
|
|
||||||
Patient.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withConsent(
|
|
||||||
Consent.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
|
||||||
.withPatient("123")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withEpisode(
|
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("1")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withClaims(null)
|
|
||||||
.withDiagnoses(null)
|
|
||||||
.withCarePlans(null)
|
|
||||||
.withClaimResponses(null)
|
|
||||||
.withEcogStatus(null)
|
|
||||||
.withFamilyMemberDiagnoses(null)
|
|
||||||
.withGeneticCounsellingRequests(null)
|
|
||||||
.withHistologyReevaluationRequests(null)
|
|
||||||
.withHistologyReports(null)
|
|
||||||
.withLastGuidelineTherapies(null)
|
|
||||||
.withMolecularPathologyFindings(null)
|
|
||||||
.withMolecularTherapies(null)
|
|
||||||
.withNgsReports(null)
|
|
||||||
.withPreviousGuidelineTherapies(null)
|
|
||||||
.withRebiopsyRequests(null)
|
|
||||||
.withRecommendations(null)
|
|
||||||
.withResponses(null)
|
|
||||||
.withStudyInclusionRequests(null)
|
|
||||||
.withSpecimens(null)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
|
||||||
|
|
||||||
|
|
||||||
assertThat(mtbFile.episode.id).isNotNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.pseudonym
|
package dev.dnpm.etl.processor.pseudonym
|
||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
|
||||||
import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties
|
import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
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
|
||||||
@@ -29,31 +29,26 @@ import org.mockito.Mock
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.doAnswer
|
import org.mockito.kotlin.doAnswer
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class PseudonymizeServiceTest {
|
class PseudonymizeServiceTest {
|
||||||
|
|
||||||
private val mtbFile = MtbFile.builder()
|
private val mtbFile = Mtb.builder()
|
||||||
.withPatient(
|
.patient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("123")
|
.id("123")
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withConsent(
|
.episodesOfCare(
|
||||||
Consent.builder()
|
listOf(
|
||||||
.withId("1")
|
MtbEpisodeOfCare.builder()
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
.id("1")
|
||||||
.withPatient("123")
|
.patient(Reference.builder().id("123").build())
|
||||||
.build()
|
.period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build())
|
||||||
)
|
.build()
|
||||||
.withEpisode(
|
)
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("123")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -71,8 +66,8 @@ class PseudonymizeServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sanitizeFileName(@Mock generator: GpasPseudonymGenerator) {
|
fun sanitizeFileName() {
|
||||||
val result= GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>")
|
val result = GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>")
|
||||||
|
|
||||||
assertThat(result).isEqualTo("l___a_bs_1_2_3_")
|
assertThat(result).isEqualTo("l___a_bs_1_2_3_")
|
||||||
}
|
}
|
||||||
@@ -90,4 +85,16 @@ class PseudonymizeServiceTest {
|
|||||||
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
|
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@Test
|
||||||
|
fun shouldReturnDifferentValues() {
|
||||||
|
val ag = AnonymizingGenerator()
|
||||||
|
|
||||||
|
val tans = HashSet<String>()
|
||||||
|
|
||||||
|
(1..1000).forEach { i ->
|
||||||
|
val tan = ag.generateGenomDeTan("12345")
|
||||||
|
assertThat(tan).hasSize(64)
|
||||||
|
assertThat(tans.add(tan)).`as`("never the same result!").isTrue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,232 @@
|
|||||||
|
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.ConsentDomain
|
||||||
|
import dev.dnpm.etl.processor.consent.GicsConsentService
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.MvhSubmissionType
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Patient
|
||||||
|
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.junit.jupiter.params.provider.ValueSource
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.doAnswer
|
||||||
|
import org.mockito.kotlin.eq
|
||||||
|
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("https://gics.example.com")
|
||||||
|
val jacksonConfig = JacksonConfig()
|
||||||
|
this.objectMapper = jacksonConfig.objectMapper()
|
||||||
|
this.fhirContext = JacksonConfig.fhirContext()
|
||||||
|
this.gicsConsentService = gicsConsentService
|
||||||
|
this.appConfigProperties = AppConfigProperties(emptyList())
|
||||||
|
this.consentProcessor =
|
||||||
|
ConsentProcessor(
|
||||||
|
appConfigProperties,
|
||||||
|
gIcsConfigProperties,
|
||||||
|
objectMapper,
|
||||||
|
fhirContext,
|
||||||
|
gicsConsentService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun consentOk() {
|
||||||
|
assertThat(consentProcessor.toString()).isNotNull
|
||||||
|
// prep gICS response
|
||||||
|
doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService)
|
||||||
|
.getConsent(any(), any(), eq(ConsentDomain.BROAD_CONSENT))
|
||||||
|
|
||||||
|
doAnswer { Bundle() }.whenever(gicsConsentService)
|
||||||
|
.getConsent(any(), any(), eq(ConsentDomain.MODELLVORHABEN_64E))
|
||||||
|
|
||||||
|
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(26)
|
||||||
|
}
|
||||||
|
|
||||||
|
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-08-15T00: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-08-15T00: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-08-15T00: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-08-15T00: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-08-15T00: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-08-15T00: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-08-15T00: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-08-15T00:00:00+02:00,NULL,date is after end",
|
||||||
|
"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-08-15T00:00:00+02:00,DENY,provision is denied",
|
||||||
|
"unknownCode,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,NULL,code does not exist - therefore expect NULL",
|
||||||
|
"2.16.840.1.113883.3.1937.777.24.5.3.8,XXXX,2025-08-15T00:00:00+02:00,NULL,system not found - therefore expect NULL",
|
||||||
|
)
|
||||||
|
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!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getProvisionTypeOnEmptyConsent(
|
||||||
|
) {
|
||||||
|
val emptyResources = Bundle().addEntry(Bundle.BundleEntryComponent().setResource(Consent()))
|
||||||
|
|
||||||
|
val requestDate = Date.from(OffsetDateTime.parse("2025-08-15T00:00:00+02:00").toInstant())
|
||||||
|
|
||||||
|
val result: Consent.ConsentProvisionType =
|
||||||
|
consentProcessor.getProvisionTypeByPolicyCode(
|
||||||
|
emptyResources,
|
||||||
|
"anyCode",
|
||||||
|
"anySystem",
|
||||||
|
requestDate
|
||||||
|
)
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(result).`as`("empty consent resource - expect NULL")
|
||||||
|
.isEqualTo(Consent.ConsentProvisionType.NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(booleans = [true, false])
|
||||||
|
fun mvSubmissionTypeIsSet(isTestSubmission: Boolean) {
|
||||||
|
appConfigProperties.genomDeTestSubmission = isTestSubmission
|
||||||
|
val fixture =
|
||||||
|
ConsentProcessor(
|
||||||
|
appConfigProperties,
|
||||||
|
gIcsConfigProperties,
|
||||||
|
objectMapper,
|
||||||
|
fhirContext,
|
||||||
|
gicsConsentService
|
||||||
|
)
|
||||||
|
|
||||||
|
doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService)
|
||||||
|
.getConsent(any(), any(), eq(ConsentDomain.BROAD_CONSENT))
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
Bundle().addEntry(
|
||||||
|
Bundle.BundleEntryComponent().setResource(getDummyGenomDeConsent())
|
||||||
|
)
|
||||||
|
}.whenever(gicsConsentService)
|
||||||
|
.getConsent(any(), any(), eq(ConsentDomain.MODELLVORHABEN_64E))
|
||||||
|
|
||||||
|
val inputMtb = Mtb.builder()
|
||||||
|
.patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build()
|
||||||
|
val checkResult = fixture.consentGatedCheckAndTryEmbedding(inputMtb)
|
||||||
|
assertThat(checkResult).isNotNull
|
||||||
|
|
||||||
|
if (isTestSubmission)
|
||||||
|
assertThat(inputMtb.metadata.type).isEqualTo(MvhSubmissionType.TEST)
|
||||||
|
else {
|
||||||
|
assertThat(inputMtb.metadata.type).isEqualTo(MvhSubmissionType.INITIAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -20,15 +20,21 @@
|
|||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import dev.dnpm.etl.processor.Fingerprint
|
||||||
import dev.dnpm.etl.processor.*
|
import dev.dnpm.etl.processor.PatientId
|
||||||
|
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.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
|
||||||
|
import dev.dnpm.etl.processor.output.DeleteRequest
|
||||||
|
import dev.dnpm.etl.processor.output.DnpmV2MtbFileRequest
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||||
|
import dev.dnpm.etl.processor.randomRequestId
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
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
|
||||||
@@ -42,6 +48,7 @@ import org.mockito.kotlin.argumentCaptor
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
@@ -53,7 +60,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
|
||||||
@@ -62,14 +69,16 @@ 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
|
||||||
this.sender = sender
|
this.sender = sender
|
||||||
this.requestService = requestService
|
this.requestService = requestService
|
||||||
this.applicationEventPublisher = applicationEventPublisher
|
this.applicationEventPublisher = applicationEventPublisher
|
||||||
this.appConfigProperties = AppConfigProperties(null)
|
this.appConfigProperties = AppConfigProperties()
|
||||||
|
this.consentProcessor = consentProcessor
|
||||||
|
|
||||||
val objectMapper = ObjectMapper()
|
val objectMapper = ObjectMapper()
|
||||||
|
|
||||||
@@ -80,7 +89,8 @@ class RequestProcessorTest {
|
|||||||
requestService,
|
requestService,
|
||||||
objectMapper,
|
objectMapper,
|
||||||
applicationEventPublisher,
|
applicationEventPublisher,
|
||||||
appConfigProperties
|
appConfigProperties,
|
||||||
|
consentProcessor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +102,7 @@ class RequestProcessorTest {
|
|||||||
randomRequestId(),
|
randomRequestId(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
PatientPseudonym("TEST_12345678901"),
|
||||||
PatientId("P1"),
|
PatientId("P1"),
|
||||||
Fingerprint("zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga"),
|
Fingerprint("6vkiti5bk6ikwifpajpt7cygmd3dvw54d6lwfhzlynb3pqtzferq"),
|
||||||
RequestType.MTB_FILE,
|
RequestType.MTB_FILE,
|
||||||
RequestStatus.SUCCESS,
|
RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-08-08T02:00:00Z")
|
Instant.parse("2023-08-08T02:00:00Z")
|
||||||
@@ -109,29 +119,24 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<Mtb>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true)
|
||||||
.withPatient(
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("1")
|
.id("123")
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withConsent(
|
.episodesOfCare(
|
||||||
Consent.builder()
|
listOf(
|
||||||
.withId("1")
|
MtbEpisodeOfCare.builder()
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
.id("1")
|
||||||
.withPatient("123")
|
.patient(Reference.builder().id("123").build())
|
||||||
.build()
|
.period(PeriodDate.builder().start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))).build())
|
||||||
)
|
.build()
|
||||||
.withEpisode(
|
)
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("1")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -151,7 +156,7 @@ class RequestProcessorTest {
|
|||||||
randomRequestId(),
|
randomRequestId(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
PatientPseudonym("TEST_12345678901"),
|
||||||
PatientId("P1"),
|
PatientId("P1"),
|
||||||
Fingerprint("zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga"),
|
Fingerprint("4gcjwtjjtcczybsljxepdfpkaeusvd7g3vogfqpmphyffyzfx7dq"),
|
||||||
RequestType.MTB_FILE,
|
RequestType.MTB_FILE,
|
||||||
RequestStatus.SUCCESS,
|
RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-08-08T02:00:00Z")
|
Instant.parse("2023-08-08T02:00:00Z")
|
||||||
@@ -168,29 +173,24 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<Mtb>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true)
|
||||||
.withPatient(
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("1")
|
.id("123")
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withConsent(
|
.episodesOfCare(
|
||||||
Consent.builder()
|
listOf(
|
||||||
.withId("1")
|
MtbEpisodeOfCare.builder()
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
.id("1")
|
||||||
.withPatient("123")
|
.patient(Reference.builder().id("123").build())
|
||||||
.build()
|
.period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build())
|
||||||
)
|
.build()
|
||||||
.withEpisode(
|
)
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("1")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
}.whenever(sender).send(any<DnpmV2MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
@@ -231,29 +231,24 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<Mtb>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true)
|
||||||
.withPatient(
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("1")
|
.id("123")
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withConsent(
|
.episodesOfCare(
|
||||||
Consent.builder()
|
listOf(
|
||||||
.withId("1")
|
MtbEpisodeOfCare.builder()
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
.id("1")
|
||||||
.withPatient("123")
|
.patient(Reference.builder().id("123").build())
|
||||||
.build()
|
.period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build())
|
||||||
)
|
.build()
|
||||||
.withEpisode(
|
)
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("1")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -286,7 +281,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
}.whenever(sender).send(any<DnpmV2MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
@@ -294,29 +289,36 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<Mtb>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true)
|
||||||
.withPatient(
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("1")
|
.id("123")
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withConsent(
|
.metadata(
|
||||||
Consent.builder()
|
MvhMetadata
|
||||||
.withId("1")
|
.builder()
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
.modelProjectConsent(
|
||||||
.withPatient("123")
|
ModelProjectConsent
|
||||||
|
.builder()
|
||||||
|
.provisions(
|
||||||
|
listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build())
|
||||||
|
).build()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withEpisode(
|
.episodesOfCare(
|
||||||
Episode.builder()
|
listOf(
|
||||||
.withId("1")
|
MtbEpisodeOfCare.builder()
|
||||||
.withPatient("1")
|
.id("1")
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
.patient(Reference.builder().id("123").build())
|
||||||
.build()
|
.period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -336,9 +338,12 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
||||||
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(
|
||||||
|
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())
|
||||||
@@ -354,9 +359,12 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(
|
||||||
|
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())
|
||||||
@@ -372,9 +380,12 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(
|
||||||
|
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())
|
||||||
@@ -386,7 +397,10 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
|
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
|
||||||
doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(
|
||||||
|
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())
|
||||||
@@ -404,33 +418,28 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<Mtb>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
}.whenever(sender).send(any<DnpmV2MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true)
|
||||||
.withPatient(
|
|
||||||
|
val mtbFile = Mtb.builder()
|
||||||
|
.patient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("1")
|
.id("123")
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withConsent(
|
.episodesOfCare(
|
||||||
Consent.builder()
|
listOf(
|
||||||
.withId("1")
|
MtbEpisodeOfCare.builder()
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
.id("1")
|
||||||
.withPatient("123")
|
.patient(Reference.builder().id("123").build())
|
||||||
.build()
|
.period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build())
|
||||||
)
|
.build()
|
||||||
.withEpisode(
|
)
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("1")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -446,4 +455,4 @@ class RequestProcessorTest {
|
|||||||
val TEST_PATIENT_ID = PatientId("TEST_12345678901")
|
val TEST_PATIENT_ID = PatientId("TEST_12345678901")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -19,14 +19,14 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import dev.dnpm.etl.processor.config.JacksonConfig
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
import de.ukw.ccc.bwhc.dto.Diagnosis
|
|
||||||
import de.ukw.ccc.bwhc.dto.Icd10
|
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
|
||||||
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 org.hl7.fhir.instance.model.api.IBaseResource
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
class TransformationServiceTest {
|
class TransformationServiceTest {
|
||||||
|
|
||||||
@@ -35,61 +35,92 @@ class TransformationServiceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
this.service = TransformationService(
|
this.service = TransformationService(
|
||||||
ObjectMapper(), listOf(
|
JacksonConfig().objectMapper(), listOf(
|
||||||
Transformation.of("consent.status") from Consent.Status.ACTIVE to Consent.Status.REJECTED,
|
Transformation.of("diagnoses[*].code.version") from "2013" to "2014",
|
||||||
Transformation.of("diagnoses[*].icd10.version") from "2013" to "2014",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldTransformMtbFile() {
|
fun shouldTransformMtbFile() {
|
||||||
val mtbFile = MtbFile.builder().withDiagnoses(
|
val mtbFile = Mtb.builder().diagnoses(
|
||||||
listOf(
|
listOf(
|
||||||
Diagnosis.builder().withId("1234").withIcd10(Icd10("F79.9").also {
|
MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.9").version("2013").build()).build()
|
||||||
it.version = "2013"
|
|
||||||
}).build()
|
|
||||||
)
|
)
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
val actual = this.service.transform(mtbFile)
|
val actual = this.service.transform(mtbFile)
|
||||||
|
|
||||||
assertThat(actual).isNotNull
|
assertThat(actual).isNotNull
|
||||||
assertThat(actual.diagnoses[0].icd10.version).isEqualTo("2014")
|
assertThat(actual.diagnoses[0].code.version).isEqualTo("2014")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldOnlyTransformGivenValues() {
|
fun shouldOnlyTransformGivenValues() {
|
||||||
val mtbFile = MtbFile.builder().withDiagnoses(
|
val mtbFile = Mtb.builder().diagnoses(
|
||||||
listOf(
|
listOf(
|
||||||
Diagnosis.builder().withId("1234").withIcd10(Icd10("F79.9").also {
|
MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.9").version("2013").build()).build(),
|
||||||
it.version = "2013"
|
MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.8").version("2019").build()).build()
|
||||||
}).build(),
|
|
||||||
Diagnosis.builder().withId("5678").withIcd10(Icd10("F79.8").also {
|
|
||||||
it.version = "2019"
|
|
||||||
}).build()
|
|
||||||
)
|
)
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
val actual = this.service.transform(mtbFile)
|
val actual = this.service.transform(mtbFile)
|
||||||
|
|
||||||
assertThat(actual).isNotNull
|
assertThat(actual).isNotNull
|
||||||
assertThat(actual.diagnoses[0].icd10.code).isEqualTo("F79.9")
|
assertThat(actual.diagnoses[0].code.code).isEqualTo("F79.9")
|
||||||
assertThat(actual.diagnoses[0].icd10.version).isEqualTo("2014")
|
assertThat(actual.diagnoses[0].code.version).isEqualTo("2014")
|
||||||
assertThat(actual.diagnoses[1].icd10.code).isEqualTo("F79.8")
|
assertThat(actual.diagnoses[1].code.code).isEqualTo("F79.8")
|
||||||
assertThat(actual.diagnoses[1].icd10.version).isEqualTo("2019")
|
assertThat(actual.diagnoses[1].code.version).isEqualTo("2019")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldTransformMtbFileWithConsentEnum() {
|
fun shouldTransformConsentValues() {
|
||||||
val mtbFile = MtbFile.builder().withConsent(
|
val mtbFile = Mtb.builder().diagnoses(
|
||||||
Consent("123", "456", Consent.Status.ACTIVE)
|
listOf(
|
||||||
|
MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.9").version("2013").build()).build(),
|
||||||
|
MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.8").version("2019").build()).build()
|
||||||
|
)
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
val actual = this.service.transform(mtbFile)
|
val actual = this.service.transform(mtbFile)
|
||||||
|
|
||||||
assertThat(actual.consent).isNotNull
|
assertThat(actual).isNotNull
|
||||||
assertThat(actual.consent.status).isEqualTo(Consent.Status.REJECTED)
|
assertThat(actual.diagnoses[0].code.code).isEqualTo("F79.9")
|
||||||
|
assertThat(actual.diagnoses[0].code.version).isEqualTo("2014")
|
||||||
|
assertThat(actual.diagnoses[1].code.code).isEqualTo("F79.8")
|
||||||
|
assertThat(actual.diagnoses[1].code.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-08-15T00:00:00.00Z")))
|
||||||
|
.version("1").provisions(
|
||||||
|
listOf(
|
||||||
|
Provision.builder().type(ConsentProvision.PERMIT)
|
||||||
|
.purpose(ModelProjectConsentPurpose.SEQUENCING)
|
||||||
|
.date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))).build(),
|
||||||
|
Provision.builder().type(ConsentProvision.PERMIT)
|
||||||
|
.purpose(ModelProjectConsentPurpose.REIDENTIFICATION)
|
||||||
|
.date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))).build(),
|
||||||
|
Provision.builder().type(ConsentProvision.DENY)
|
||||||
|
.purpose(ModelProjectConsentPurpose.CASE_IDENTIFICATION)
|
||||||
|
.date(Date.from(Instant.parse("2025-08-15T00: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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
3101
src/test/resources/fake_broadConsent_gics_response_permit.json
Normal file
3101
src/test/resources/fake_broadConsent_gics_response_permit.json
Normal file
File diff suppressed because it is too large
Load Diff
333
src/test/resources/fake_mv64e-gics-response_deny.json
Normal file
333
src/test/resources/fake_mv64e-gics-response_deny.json
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
333
src/test/resources/fake_mv64e-gics-response_permit.json
Normal file
333
src/test/resources/fake_mv64e-gics-response_permit.json
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2509
src/test/resources/mv64e-mtb-fake-patient.json
Normal file
2509
src/test/resources/mv64e-mtb-fake-patient.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user