From c8f6e6efc812cc12d17c2af1cc24a9318180a8fe Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Mon, 15 Jul 2024 10:27:51 +0200 Subject: refactor: add types for patient id and pseudonym --- .../dnpm/etl/processor/input/KafkaInputListener.kt | 6 ++++-- .../etl/processor/input/MtbFileRestController.kt | 6 ++++-- .../dev/dnpm/etl/processor/monitoring/Request.kt | 20 ++++++++---------- .../etl/processor/output/KafkaMtbFileSender.kt | 4 ++-- .../dev/dnpm/etl/processor/output/MtbFileSender.kt | 3 ++- .../etl/processor/pseudonym/PseudonymizeService.kt | 8 +++++--- .../dev/dnpm/etl/processor/pseudonym/extensions.kt | 3 ++- .../etl/processor/services/RequestProcessor.kt | 24 ++++++++++++---------- .../dnpm/etl/processor/services/RequestService.kt | 9 ++++---- src/main/kotlin/dev/dnpm/etl/processor/types.kt | 10 ++++++++- .../dev/dnpm/etl/processor/web/HomeController.kt | 3 ++- 11 files changed, 57 insertions(+), 39 deletions(-) (limited to 'src/main/kotlin/dev/dnpm/etl/processor') diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt index b72b1fd..2aff8cb 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt @@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.input import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.MtbFile +import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.services.RequestProcessor import org.apache.kafka.clients.consumer.ConsumerRecord @@ -36,6 +37,7 @@ class KafkaInputListener( override fun onMessage(data: ConsumerRecord) { val mtbFile = objectMapper.readValue(data.value(), MtbFile::class.java) + val patientId = PatientId(mtbFile.patient.id) val firstRequestIdHeader = data.headers().headers("requestId")?.firstOrNull() val requestId = if (null != firstRequestIdHeader) { RequestId(String(firstRequestIdHeader.value())) @@ -53,9 +55,9 @@ class KafkaInputListener( } else { logger.debug("Accepted MTB File and process deletion") if (requestId.isBlank()) { - requestProcessor.processDeletion(mtbFile.patient.id) + requestProcessor.processDeletion(patientId) } else { - requestProcessor.processDeletion(mtbFile.patient.id, requestId) + requestProcessor.processDeletion(patientId, requestId) } } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt index 8259288..9e282c2 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt @@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.input import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.MtbFile +import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.services.RequestProcessor import org.slf4j.LoggerFactory import org.springframework.http.ResponseEntity @@ -46,7 +47,8 @@ class MtbFileRestController( requestProcessor.processMtbFile(mtbFile) } else { logger.debug("Accepted MTB File and process deletion") - requestProcessor.processDeletion(mtbFile.patient.id) + val patientId = PatientId(mtbFile.patient.id) + requestProcessor.processDeletion(patientId) } return ResponseEntity.accepted().build() } @@ -54,7 +56,7 @@ class MtbFileRestController( @DeleteMapping(path = ["{patientId}"]) fun deleteData(@PathVariable patientId: String): ResponseEntity { logger.debug("Accepted patient ID to process deletion") - requestProcessor.processDeletion(patientId) + requestProcessor.processDeletion(PatientId(patientId)) return ResponseEntity.accepted().build() } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt index 9efae4c..d26d222 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt @@ -19,9 +19,7 @@ package dev.dnpm.etl.processor.monitoring -import dev.dnpm.etl.processor.Fingerprint -import dev.dnpm.etl.processor.randomRequestId -import dev.dnpm.etl.processor.RequestId +import dev.dnpm.etl.processor.* import org.springframework.data.annotation.Id import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable @@ -38,8 +36,8 @@ import java.util.* data class Request( @Id val id: Long? = null, val uuid: RequestId = randomRequestId(), - val patientId: String, - val pid: String, + val patientId: PatientPseudonym, + val pid: PatientId, @Column("fingerprint") val fingerprint: Fingerprint, val type: RequestType, @@ -49,8 +47,8 @@ data class Request( ) { constructor( uuid: RequestId, - patientId: String, - pid: String, + patientId: PatientPseudonym, + pid: PatientId, fingerprint: Fingerprint, type: RequestType, status: RequestStatus @@ -59,8 +57,8 @@ data class Request( constructor( uuid: RequestId, - patientId: String, - pid: String, + patientId: PatientPseudonym, + pid: PatientId, fingerprint: Fingerprint, type: RequestType, status: RequestStatus, @@ -83,11 +81,11 @@ data class CountedState( interface RequestRepository : CrudRepository, PagingAndSortingRepository { - fun findAllByPatientIdOrderByProcessedAtDesc(patientId: String): List + fun findAllByPatientIdOrderByProcessedAtDesc(patientId: PatientPseudonym): List fun findByUuidEquals(uuid: RequestId): Optional - fun findRequestByPatientId(patientId: String, pageable: Pageable): Page + fun findRequestByPatientId(patientId: PatientPseudonym, pageable: Pageable): Page @Query("SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;") fun countStates(): List diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt index 7b777e8..4838689 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt @@ -63,7 +63,7 @@ class KafkaMtbFileSender( val dummyMtbFile = MtbFile.builder() .withConsent( Consent.builder() - .withPatient(request.patientId) + .withPatient(request.patientId.value) .withStatus(Consent.Status.REJECTED) .build() ) @@ -99,7 +99,7 @@ class KafkaMtbFileSender( } private fun key(request: MtbFileSender.DeleteRequest): String { - return "{\"pid\": \"${request.patientId}\"}" + return "{\"pid\": \"${request.patientId.value}\"}" } data class Data(val requestId: RequestId, val content: MtbFile) diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt index 2670f2e..8d994c5 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt @@ -20,6 +20,7 @@ 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 org.springframework.http.HttpStatusCode @@ -35,7 +36,7 @@ interface MtbFileSender { data class MtbFileRequest(val requestId: RequestId, val mtbFile: MtbFile) - data class DeleteRequest(val requestId: RequestId, val patientId: String) + data class DeleteRequest(val requestId: RequestId, val patientId: PatientPseudonym) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt index d18cd2c..e80f6ec 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt @@ -19,6 +19,8 @@ package dev.dnpm.etl.processor.pseudonym +import dev.dnpm.etl.processor.PatientId +import dev.dnpm.etl.processor.PatientPseudonym import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties class PseudonymizeService( @@ -26,10 +28,10 @@ class PseudonymizeService( private val configProperties: PseudonymizeConfigProperties ) { - fun patientPseudonym(patientId: String): String { + fun patientPseudonym(patientId: PatientId): PatientPseudonym { return when (generator) { - is GpasPseudonymGenerator -> generator.generate(patientId) - else -> "${configProperties.prefix}_${generator.generate(patientId)}" + is GpasPseudonymGenerator -> PatientPseudonym(generator.generate(patientId.value)) + else -> PatientPseudonym("${configProperties.prefix}_${generator.generate(patientId.value)}") } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt index ef25787..bf645f6 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt @@ -20,6 +20,7 @@ package dev.dnpm.etl.processor.pseudonym import de.ukw.ccc.bwhc.dto.MtbFile +import dev.dnpm.etl.processor.PatientId import org.apache.commons.codec.digest.DigestUtils /** Replaces patient ID with generated patient pseudonym @@ -29,7 +30,7 @@ import org.apache.commons.codec.digest.DigestUtils * @return The MTB file containing patient pseudonymes */ infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) { - val patientPseudonym = pseudonymizeService.patientPseudonym(this.patient.id) + val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value this.episode?.patient = patientPseudonym this.carePlans?.forEach { it.patient = patientPseudonym } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt index 94598ae..2cbfd2f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -21,9 +21,7 @@ package dev.dnpm.etl.processor.services import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.MtbFile -import dev.dnpm.etl.processor.Fingerprint -import dev.dnpm.etl.processor.randomRequestId -import dev.dnpm.etl.processor.RequestId +import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.config.AppConfigProperties import dev.dnpm.etl.processor.monitoring.Report import dev.dnpm.etl.processor.monitoring.Request @@ -56,17 +54,19 @@ class RequestProcessor( } fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) { - val pid = mtbFile.patient.id + val pid = PatientId(mtbFile.patient.id) mtbFile pseudonymizeWith pseudonymizeService mtbFile anonymizeContentWith pseudonymizeService val request = MtbFileSender.MtbFileRequest(requestId, transformationService.transform(mtbFile)) + val patientPseudonym = PatientPseudonym(request.mtbFile.patient.id) + requestService.save( Request( requestId, - request.mtbFile.patient.id, + patientPseudonym, pid, fingerprint(request.mtbFile), RequestType.MTB_FILE, @@ -101,20 +101,22 @@ class RequestProcessor( } private fun isDuplication(pseudonymizedMtbFile: MtbFile): Boolean { + val patientPseudonym = PatientPseudonym(pseudonymizedMtbFile.patient.id) + val lastMtbFileRequestForPatient = - requestService.lastMtbFileRequestForPatientPseudonym(pseudonymizedMtbFile.patient.id) - val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(pseudonymizedMtbFile.patient.id) + requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) + val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFile) } - fun processDeletion(patientId: String) { + fun processDeletion(patientId: PatientId) { processDeletion(patientId, randomRequestId()) } - fun processDeletion(patientId: String, requestId: RequestId) { + fun processDeletion(patientId: PatientId, requestId: RequestId) { try { val patientPseudonym = pseudonymizeService.patientPseudonym(patientId) @@ -123,7 +125,7 @@ class RequestProcessor( requestId, patientPseudonym, patientId, - fingerprint(patientPseudonym), + fingerprint(patientPseudonym.value), RequestType.DELETE, RequestStatus.UNKNOWN ) @@ -147,7 +149,7 @@ class RequestProcessor( requestService.save( Request( uuid = requestId, - patientId = "???", + patientId = emptyPatientPseudonym(), pid = patientId, fingerprint = Fingerprint.empty(), status = RequestStatus.ERROR, diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt index a2e8de3..826b060 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt @@ -19,6 +19,7 @@ package dev.dnpm.etl.processor.services +import dev.dnpm.etl.processor.PatientPseudonym import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.monitoring.* import org.springframework.data.domain.Page @@ -40,15 +41,15 @@ class RequestService( fun findByUuid(uuid: RequestId): Optional = requestRepository.findByUuidEquals(uuid) - fun findRequestByPatientId(patientId: String, pageable: Pageable): Page = requestRepository.findRequestByPatientId(patientId, pageable) + fun findRequestByPatientId(patientId: PatientPseudonym, pageable: Pageable): Page = requestRepository.findRequestByPatientId(patientId, pageable) - fun allRequestsByPatientPseudonym(patientPseudonym: String) = requestRepository + fun allRequestsByPatientPseudonym(patientPseudonym: PatientPseudonym) = requestRepository .findAllByPatientIdOrderByProcessedAtDesc(patientPseudonym) - fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: String) = + fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: PatientPseudonym) = Companion.lastMtbFileRequestForPatientPseudonym(allRequestsByPatientPseudonym(patientPseudonym)) - fun isLastRequestWithKnownStatusDeletion(patientPseudonym: String) = + fun isLastRequestWithKnownStatusDeletion(patientPseudonym: PatientPseudonym) = Companion.isLastRequestWithKnownStatusDeletion(allRequestsByPatientPseudonym(patientPseudonym)) fun countStates(): Iterable = requestRepository.countStates() diff --git a/src/main/kotlin/dev/dnpm/etl/processor/types.kt b/src/main/kotlin/dev/dnpm/etl/processor/types.kt index b41a550..b2f13ef 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/types.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/types.kt @@ -38,4 +38,12 @@ value class RequestId(val value: String) { } -fun randomRequestId() = RequestId(UUID.randomUUID().toString()) \ No newline at end of file +fun randomRequestId() = RequestId(UUID.randomUUID().toString()) + +@JvmInline +value class PatientId(val value: String) + +@JvmInline +value class PatientPseudonym(val value: String) + +fun emptyPatientPseudonym() = PatientPseudonym("") \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt index ac003d3..43f2f1d 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt @@ -20,6 +20,7 @@ package dev.dnpm.etl.processor.web import dev.dnpm.etl.processor.NotFoundException +import dev.dnpm.etl.processor.PatientPseudonym import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.monitoring.ReportService import dev.dnpm.etl.processor.services.RequestService @@ -56,7 +57,7 @@ class HomeController( @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable, model: Model ): String { - val requests = requestService.findRequestByPatientId(patientId, pageable) + val requests = requestService.findRequestByPatientId(PatientPseudonym(patientId), pageable) model.addAttribute("patientId", patientId) model.addAttribute("requests", requests) -- cgit v1.2.3