summaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
authorjlidke2025-07-22 20:02:15 +0200
committerGitHub2025-07-22 20:02:15 +0200
commit199511e567884bb703277c276b782e54e528f744 (patch)
tree42c19612ae161f98a252a13f4bd8234d7bae0527 /src/main/kotlin
parent1319be8b3f5fbb3c4800dc9b942fc1982ac928d3 (diff)
63 check consent status (#120)
Co-authored-by: Paul-Christian Volkmer <code@pcvolkmer.de>
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt76
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt141
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt16
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt18
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt15
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt27
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt9
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt33
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt60
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt18
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt32
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt282
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt74
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt6
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt28
17 files changed, 791 insertions, 59 deletions
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
index 331c8b5..a2ea032 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -27,7 +27,8 @@ data class AppConfigProperties(
var bwhcUri: String?,
var transformations: List<TransformationProperties> = listOf(),
var maxRetryAttempts: Int = 3,
- var duplicationDetection: Boolean = true
+ var duplicationDetection: Boolean = true,
+ var genomDeTestSubmission: Boolean = true
) {
companion object {
const val NAME = "app"
@@ -56,6 +57,72 @@ data class GPasConfigProperties(
}
}
+@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?,
+ val password: String?,
+
+ /**
+ * 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)
data class RestTargetProperties(
val uri: String?,
@@ -99,8 +166,13 @@ enum class PseudonymGenerator {
GPAS
}
+enum class ConsentService {
+ NONE,
+ GICS
+}
+
data class TransformationProperties(
val path: String,
val from: String,
val to: String
-) \ No newline at end of file
+)
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
index c8f3fba..8f90947 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
@@ -20,24 +20,28 @@
package dev.dnpm.etl.processor.config
import com.fasterxml.jackson.databind.ObjectMapper
-import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
-import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
-import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
-import dev.dnpm.etl.processor.monitoring.ReportService
+import dev.dnpm.etl.processor.consent.ConsentByMtbFile
+import dev.dnpm.etl.processor.consent.GicsConsentService
+import dev.dnpm.etl.processor.consent.IGetConsent
+import dev.dnpm.etl.processor.monitoring.*
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
import dev.dnpm.etl.processor.security.TokenRepository
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.TransformationService
import org.slf4j.LoggerFactory
+import org.springframework.boot.autoconfigure.condition.AnyNestedCondition
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Conditional
import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.ConfigurationCondition
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
import org.springframework.retry.RetryCallback
import org.springframework.retry.RetryContext
@@ -60,7 +64,9 @@ import kotlin.time.toJavaDuration
value = [
AppConfigProperties::class,
PseudonymizeConfigProperties::class,
- GPasConfigProperties::class
+ GPasConfigProperties::class,
+ ConsentConfigProperties::class,
+ GIcsConfigProperties::class
]
)
@EnableScheduling
@@ -73,13 +79,27 @@ class AppConfiguration {
return RestTemplate()
}
+ @Bean
+ fun appFhirConfig(): AppFhirConfig {
+ return AppFhirConfig()
+ }
+
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
@Bean
- fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
- return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
+ fun gpasPseudonymGenerator(
+ configProperties: GPasConfigProperties,
+ retryTemplate: RetryTemplate,
+ restTemplate: RestTemplate,
+ appFhirConfig: AppFhirConfig
+ ): Generator {
+ return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig)
}
- @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
+ @ConditionalOnProperty(
+ value = ["app.pseudonymize.generator"],
+ havingValue = "BUILDIN",
+ matchIfMissing = true
+ )
@Bean
fun buildinPseudonymGenerator(): Generator {
return AnonymizingGenerator()
@@ -94,17 +114,21 @@ class AppConfiguration {
}
@Bean
- fun reportService(objectMapper: ObjectMapper): ReportService {
- return ReportService(objectMapper)
+ fun reportService(): ReportService {
+ return ReportService(getObjectMapper())
+ }
+
+ @Bean
+ fun getObjectMapper(): ObjectMapper {
+ return JacksonConfig().objectMapper()
}
@Bean
fun transformationService(
- objectMapper: ObjectMapper,
configProperties: AppConfigProperties
): TransformationService {
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
})
}
@@ -123,7 +147,11 @@ class AppConfiguration {
callback: RetryCallback<T, E>,
throwable: Throwable
) {
- logger.warn("Error occured: {}. Retrying {}", throwable.message, context.retryCount)
+ logger.warn(
+ "Error occured: {}. Retrying {}",
+ throwable.message,
+ context.retryCount
+ )
}
})
.build()
@@ -131,7 +159,11 @@ class AppConfiguration {
@ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true")
@Bean
- fun tokenService(userDetailsManager: InMemoryUserDetailsManager, passwordEncoder: PasswordEncoder, tokenRepository: TokenRepository): TokenService {
+ fun tokenService(
+ userDetailsManager: InMemoryUserDetailsManager,
+ passwordEncoder: PasswordEncoder,
+ tokenRepository: TokenRepository
+ ): TokenService {
return TokenService(userDetailsManager, passwordEncoder, tokenRepository)
}
@@ -152,7 +184,11 @@ class AppConfiguration {
gPasConfigProperties: GPasConfigProperties,
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
): ConnectionCheckService {
- return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
+ return GPasConnectionCheckService(
+ restTemplate,
+ gPasConfigProperties,
+ connectionCheckUpdateProducer
+ )
}
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
@@ -163,12 +199,85 @@ class AppConfiguration {
gPasConfigProperties: GPasConfigProperties,
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
): ConnectionCheckService {
- return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
+ return GPasConnectionCheckService(
+ restTemplate,
+ gPasConfigProperties,
+ connectionCheckUpdateProducer
+ )
}
@Bean
fun jdbcConfiguration(): AbstractJdbcConfiguration {
return AppJdbcConfiguration()
}
+
+ @Conditional(GicsEnabledCondition::class)
+ @Bean
+ fun gicsConsentService(
+ gIcsConfigProperties: GIcsConfigProperties,
+ retryTemplate: RetryTemplate,
+ restTemplate: RestTemplate,
+ appFhirConfig: AppFhirConfig
+ ): IGetConsent {
+ return GicsConsentService(
+ gIcsConfigProperties,
+ retryTemplate,
+ restTemplate,
+ appFhirConfig
+ )
+ }
+
+ @Conditional(GicsEnabledCondition::class)
+ @Bean
+ fun consentProcessor(
+ configProperties: AppConfigProperties,
+ gIcsConfigProperties: GIcsConfigProperties,
+ getObjectMapper: ObjectMapper,
+ appFhirConfig: AppFhirConfig,
+ gicsConsentService: IGetConsent
+ ): 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(): IGetConsent {
+ return ConsentByMtbFile()
+ }
+
}
+class GicsEnabledCondition :
+ AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) {
+
+ @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics")
+ class OnGicsServiceSelected {
+ // Just for Condition
+ }
+
+ @ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
+ class OnGicsEnabled {
+ // Just for Condition
+ }
+
+}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt
new file mode 100644
index 0000000..2b5ff8f
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt
@@ -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
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt
new file mode 100644
index 0000000..5469b1b
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt
@@ -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
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt
new file mode 100644
index 0000000..812ce44
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt
@@ -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)
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt
new file mode 100644
index 0000000..2ae0dd3
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt
@@ -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())
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt
new file mode 100644
index 0000000..fb03d66
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt
@@ -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()
+ )
+}
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 e797390..415a68f 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt
@@ -25,6 +25,7 @@ import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.PatientId
import dev.dnpm.etl.processor.RequestId
+import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.slf4j.LoggerFactory
@@ -76,9 +77,13 @@ class KafkaInputListener(
} else {
logger.debug("Accepted MTB File and process deletion")
if (requestId.isBlank()) {
- requestProcessor.processDeletion(patientId)
+ requestProcessor.processDeletion(patientId, TtpConsentStatus.UNKNOWN_CHECK_FILE)
} else {
- requestProcessor.processDeletion(patientId, requestId)
+ requestProcessor.processDeletion(
+ patientId,
+ requestId,
+ TtpConsentStatus.UNKNOWN_CHECK_FILE
+ )
}
}
}
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 e67a380..44c74e3 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt
@@ -23,6 +23,8 @@ import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.PatientId
+import dev.dnpm.etl.processor.consent.IGetConsent
+import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor
import dev.pcvolkmer.mv64e.mtb.Mtb
import org.slf4j.LoggerFactory
@@ -33,7 +35,7 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping(path = ["mtbfile", "mtb"])
class MtbFileRestController(
- private val requestProcessor: RequestProcessor,
+ private val requestProcessor: RequestProcessor, private val iGetConsent: IGetConsent
) {
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
@@ -43,20 +45,39 @@ class MtbFileRestController(
return ResponseEntity.ok("Test")
}
- @PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] )
+ @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
- if (mtbFile.consent.status == Consent.Status.ACTIVE) {
+ val consentStatusBooleanPair = checkConsentStatus(mtbFile)
+ val ttpConsentStatus = consentStatusBooleanPair.first
+ val isConsentOK = consentStatusBooleanPair.second
+ if (isConsentOK) {
logger.debug("Accepted MTB File (bwHC V1) for processing")
requestProcessor.processMtbFile(mtbFile)
} else {
+
logger.debug("Accepted MTB File (bwHC V1) and process deletion")
val patientId = PatientId(mtbFile.patient.id)
- requestProcessor.processDeletion(patientId)
+ requestProcessor.processDeletion(patientId, ttpConsentStatus)
}
return ResponseEntity.accepted().build()
}
- @PostMapping( consumes = [ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] )
+ private fun checkConsentStatus(mtbFile: MtbFile): Pair<TtpConsentStatus, Boolean> {
+ var ttpConsentStatus = iGetConsent.getTtpBroadConsentStatus(mtbFile.patient.id)
+
+ val isConsentOK =
+ (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.ACTIVE) ||
+ ttpConsentStatus.equals(
+ TtpConsentStatus.BROAD_CONSENT_GIVEN
+ )
+ if (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.REJECTED) {
+ // in case ttp check is disabled - we propagate rejected status anyway
+ ttpConsentStatus = TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED
+ }
+ return Pair(ttpConsentStatus, isConsentOK)
+ }
+
+ @PostMapping(consumes = [CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE])
fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> {
logger.debug("Accepted MTB File (DNPM V2) for processing")
requestProcessor.processMtbFile(mtbFile)
@@ -66,7 +87,7 @@ class MtbFileRestController(
@DeleteMapping(path = ["{patientId}"])
fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> {
logger.debug("Accepted patient ID to process deletion")
- requestProcessor.processDeletion(PatientId(patientId))
+ requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE)
return ResponseEntity.accepted().build()
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
index b845e21..fe02b69 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
@@ -20,6 +20,7 @@
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.RestTargetProperties
import jakarta.annotation.PostConstruct
@@ -68,6 +69,12 @@ sealed class ConnectionCheckResult {
override val timestamp: Instant,
override val lastChange: Instant
) : ConnectionCheckResult()
+
+ data class GIcsConnectionCheckResult(
+ override val available: Boolean,
+ override val timestamp: Instant,
+ override val lastChange: Instant
+ ) : ConnectionCheckResult()
}
class KafkaConnectionCheckService(
@@ -207,4 +214,57 @@ class GPasConnectionCheckService(
override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult {
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
+ }
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
index 8c19e86..0c8adb1 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
@@ -24,5 +24,6 @@ enum class RequestStatus(val value: String) {
WARNING("warning"),
ERROR("error"),
UNKNOWN("unknown"),
- DUPLICATION("duplication")
+ DUPLICATION("duplication"),
+ NO_CONSENT("no-consent")
} \ No newline at end of file
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 c00b2fd..1f2743e 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt
@@ -44,13 +44,20 @@ class KafkaMtbFileSender(
return try {
return retryTemplate.execute<MtbFileSender.Response, Exception> {
val record =
- ProducerRecord(kafkaProperties.outputTopic, key(request), objectMapper.writeValueAsString(request))
+ ProducerRecord(
+ kafkaProperties.outputTopic,
+ key(request),
+ objectMapper.writeValueAsString(request)
+ )
when (request) {
is BwhcV1MtbFileRequest -> record.headers()
.add("contentType", MediaType.APPLICATION_JSON_VALUE.toByteArray())
is DnpmV2MtbFileRequest -> record.headers()
- .add("contentType", CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
+ .add(
+ "contentType",
+ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray()
+ )
}
val result = kafkaTemplate.send(record)
@@ -84,7 +91,12 @@ class KafkaMtbFileSender(
kafkaProperties.outputTopic,
key(request),
// Always use old BwhcV1FileRequest with Consent REJECT
- objectMapper.writeValueAsString(BwhcV1MtbFileRequest(request.requestId, dummyMtbFile))
+ objectMapper.writeValueAsString(
+ BwhcV1MtbFileRequest(
+ request.requestId,
+ dummyMtbFile
+ )
+ )
)
val result = kafkaTemplate.send(record)
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 8d5a2cc..77f3399 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
@@ -21,7 +21,9 @@ package dev.dnpm.etl.processor.pseudonym
import de.ukw.ccc.bwhc.dto.MtbFile
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
/** Replaces patient ID with generated patient pseudonym
@@ -289,6 +291,16 @@ infix fun Mtb.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
this.followUps?.forEach {
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")
+ }
+ }
}
/**
@@ -317,3 +329,23 @@ infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
// TODO all other properties
}
+
+fun Mtb.ensureMetaDataIsInitialized() {
+ // init metadata if necessary
+ if (this.metadata == null) {
+ val mvhMetadata = MvhMetadata.builder().build()
+ this.metadata = mvhMetadata
+ }
+ if (this.metadata.researchConsents == null) {
+ this.metadata.researchConsents = mutableListOf()
+ }
+ if (this.metadata.modelProjectConsent == null) {
+ this.metadata.modelProjectConsent = ModelProjectConsent()
+ this.metadata.modelProjectConsent.provisions = mutableListOf()
+ } else
+ if (this.metadata.modelProjectConsent.provisions != null) {
+ // make sure list can be changed
+ this.metadata.modelProjectConsent.provisions =
+ this.metadata.modelProjectConsent.provisions.toMutableList()
+ }
+}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
new file mode 100644
index 0000000..3841641
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
@@ -0,0 +1,282 @@
+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.ConsentByMtbFile
+import dev.dnpm.etl.processor.consent.ConsentDomain
+import dev.dnpm.etl.processor.consent.IGetConsent
+import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized
+import dev.pcvolkmer.mv64e.mtb.*
+import org.apache.commons.lang3.NotImplementedException
+import org.hl7.fhir.r4.model.*
+import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent
+import org.hl7.fhir.r4.model.Coding
+import org.hl7.fhir.r4.model.Consent.ConsentState
+import org.hl7.fhir.r4.model.Consent.ProvisionComponent
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Service
+import java.io.IOException
+import java.time.Clock
+import java.time.Instant
+import java.util.*
+
+@Service
+class ConsentProcessor(
+ private val appConfigProperties: AppConfigProperties,
+ private val gIcsConfigProperties: GIcsConfigProperties,
+ private val objectMapper: ObjectMapper,
+ private val fhirContext: FhirContext,
+ private val consentService: IGetConsent
+) {
+ private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor")
+
+ /**
+ * In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked.
+ *
+ * Logik:
+ * * <c>true</c> IF consent check is disabled.
+ * * <c>true</c> IF broad consent (BC) has been given.
+ * * <c>true</c> BC has been asked AND declined but genomDe consent has been consented.
+ * * ELSE <c>false</c> is returned.
+ *
+ * @param mtbFile File v2 (will be enriched with consent data)
+ * @return true if consent is given
+ *
+ */
+ fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean {
+ if (consentService is ConsentByMtbFile) {
+ // 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.BroadConsent
+ )
+ val broadConsentHasBeenAsked = !broadConsent.entry.isEmpty()
+
+ // fast exit - if patient has not been asked, we can skip and exit
+ if (!broadConsentHasBeenAsked) return false
+
+ val genomeDeConsent = consentService.getConsent(
+ personIdentifierValue, requestDate, ConsentDomain.Modelvorhaben64e
+ )
+
+ addGenomeDbProvisions(mtbFile, genomeDeConsent)
+
+ if (!genomeDeConsent.entry.isEmpty()) setGenomDeSubmissionType(mtbFile)
+
+ embedBroadConsentResources(mtbFile, broadConsent)
+
+ val broadConsentStatus = getProvisionTypeByPolicyCode(
+ broadConsent, requestDate, ConsentDomain.BroadConsent
+ )
+
+ val genomDeSequencingStatus = getProvisionTypeByPolicyCode(
+ genomeDeConsent, requestDate, ConsentDomain.Modelvorhaben64e
+ )
+
+ if (Consent.ConsentProvisionType.NULL == broadConsentStatus) {
+ // bc not asked
+ return false
+ }
+ if (Consent.ConsentProvisionType.PERMIT == broadConsentStatus || Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus) return true
+
+ return false
+ }
+
+ fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) {
+ for (entry in broadConsent.getEntry()) {
+ val resource = entry.getResource()
+ if (resource is Consent) {
+ // since jackson convertValue does not work here,
+ // we need another step to back to string, before we convert to object map
+ val asJsonString = fhirContext.newJsonParser().encodeResourceToString(resource)
+ try {
+ val mapOfJson: HashMap<String?, Any?>? =
+ objectMapper.readValue<HashMap<String?, Any?>?>(
+ asJsonString, object : TypeReference<HashMap<String?, Any?>?>() {})
+ mtbFile.metadata.researchConsents.add(mapOfJson)
+ } catch (e: JsonProcessingException) {
+ throw RuntimeException(e)
+ }
+ }
+ }
+ }
+
+ fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) {
+ for (entry in consentGnomeDe.getEntry()) {
+ val resource = entry.getResource()
+ if (resource !is Consent) {
+ continue
+ }
+
+ // We expect only one provision in collection, therefore get first or none
+ val provisions = resource.getProvision().getProvision()
+ if (provisions.isEmpty()) {
+ continue
+ }
+
+ val provisionComponent: ProvisionComponent = provisions.first()
+
+ var provisionCode: String? = null
+ if (provisionComponent.getCode() != null && !provisionComponent.getCode().isEmpty()) {
+ val codableConcept: CodeableConcept = provisionComponent.getCode().first()
+ if (codableConcept.getCoding() != null && !codableConcept.getCoding().isEmpty()) {
+ provisionCode = codableConcept.getCoding().first().getCode()
+ }
+ }
+
+ if (provisionCode != null) {
+ try {
+ val modelProjectConsentPurpose =
+ ModelProjectConsentPurpose.forValue(provisionCode)
+
+ if (ModelProjectConsentPurpose.SEQUENCING == modelProjectConsentPurpose) {
+ // CONVENTION: wrapping date is date of SEQUENCING consent
+ mtbFile.metadata.modelProjectConsent.date = resource.getDateTime()
+ }
+
+ val provision = Provision.builder()
+ .type(ConsentProvision.valueOf(provisionComponent.getType().name))
+ .date(provisionComponent.getPeriod().getStart())
+ .purpose(modelProjectConsentPurpose).build()
+
+ mtbFile.metadata.modelProjectConsent.provisions.add(provision)
+ } catch (ioe: IOException) {
+ logger.error(
+ "Provision code '$provisionCode' is unknown and cannot be mapped.",
+ ioe.toString()
+ )
+ }
+ }
+
+ if (!mtbFile.metadata.modelProjectConsent.provisions.isEmpty()) {
+ mtbFile.metadata.modelProjectConsent.version =
+ gIcsConfigProperties.genomeDeConsentVersion
+ }
+ }
+ }
+
+ /**
+ * fixme: currently we do not have information about submission type
+ */
+ private fun setGenomDeSubmissionType(mtbFile: Mtb) {
+ if (appConfigProperties.genomDeTestSubmission) {
+
+ // fixme: remove INITIAL and uncomment when data model is updated
+ mtbFile.metadata.type = MvhSubmissionType.INITIAL
+ // mtbFile.metadata.type = MvhSubmissionType.Test
+
+ logger.info("genomeDe submission mit TEST")
+
+ } else {
+ mtbFile.metadata.type = MvhSubmissionType.INITIAL
+ }
+ }
+
+ /**
+ * @param consentBundle consent resource
+ * @param requestDate date which must be within validation period of provision
+ * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found.
+ */
+ fun getProvisionTypeByPolicyCode(
+ consentBundle: Bundle, requestDate: Date?, consentDomain: ConsentDomain
+ ): Consent.ConsentProvisionType {
+ val code: String?
+ val system: String?
+ if (ConsentDomain.BroadConsent == consentDomain) {
+ code = gIcsConfigProperties.broadConsentPolicyCode
+ system = gIcsConfigProperties.broadConsentPolicySystem
+ } else if (ConsentDomain.Modelvorhaben64e == consentDomain) {
+ code = gIcsConfigProperties.genomeDePolicyCode
+ system = gIcsConfigProperties.genomeDePolicySystem
+ } else {
+ throw NotImplementedException("unknown consent domain " + consentDomain.name)
+ }
+
+ val provisionTypeByPolicyCode = getProvisionTypeByPolicyCode(
+ consentBundle, code, system, requestDate
+ )
+ return provisionTypeByPolicyCode
+ }
+
+ /**
+ * @param consentBundle consent resource
+ * @param policyAndProvisionCode policyRule and provision code value
+ * @param policyAndProvisionSystem policyRule and provision system value
+ * @param requestDate date which must be within validation period of provision
+ * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found.
+ */
+ fun getProvisionTypeByPolicyCode(
+ consentBundle: Bundle,
+ policyAndProvisionCode: String?,
+ policyAndProvisionSystem: String?,
+ requestDate: Date?
+ ): Consent.ConsentProvisionType {
+ val entriesOfInterest = consentBundle.entry.filter { entry ->
+ entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent && (entry.resource as Consent).status == ConsentState.ACTIVE && checkCoding(
+ policyAndProvisionCode,
+ policyAndProvisionSystem,
+ (entry.resource as Consent).policyRule.codingFirstRep
+ ) && isIsRequestDateInRange(
+ requestDate, (entry.resource as Consent).provision.period
+ )
+ }.map { consentWithTargetPolicy: BundleEntryComponent ->
+ val provision = (consentWithTargetPolicy.getResource() as Consent).getProvision()
+ val provisionComponentByCode =
+ provision.getProvision().stream().filter { prov: ProvisionComponent? ->
+ checkCoding(
+ policyAndProvisionCode,
+ policyAndProvisionSystem,
+ prov!!.getCodeFirstRep().getCodingFirstRep()
+ ) && isIsRequestDateInRange(
+ requestDate, prov.getPeriod()
+ )
+ }.findFirst()
+ if (provisionComponentByCode.isPresent) {
+ // actual provision we search for
+ return@map provisionComponentByCode.get().getType()
+ } else {
+ if (provision.type != null) return provision.type
+
+ }
+ return Consent.ConsentProvisionType.NULL
+ }.firstOrNull()
+
+ if (entriesOfInterest == null) return Consent.ConsentProvisionType.NULL
+ return entriesOfInterest
+ }
+
+ fun checkCoding(
+ researchAllowedPolicyOid: String?, researchAllowedPolicySystem: String?, coding: Coding
+ ): Boolean {
+ return coding.getSystem() == researchAllowedPolicySystem && (coding.getCode() == researchAllowedPolicyOid)
+ }
+
+ fun isIsRequestDateInRange(requestdate: Date?, provPeriod: Period): Boolean {
+ val isRequestDateAfterOrEqualStart = provPeriod.getStart().compareTo(requestdate)
+ val isRequestDateBeforeOrEqualEnd = provPeriod.getEnd().compareTo(requestdate)
+ return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0
+ }
+
+} \ No newline at end of file
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 f25452e..bb226c0 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.*
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.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
@@ -34,8 +35,11 @@ import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
import dev.pcvolkmer.mv64e.mtb.Mtb
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.digest.DigestUtils
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
+import java.lang.RuntimeException
import java.time.Instant
import java.util.*
@@ -47,9 +51,11 @@ class RequestProcessor(
private val requestService: RequestService,
private val objectMapper: ObjectMapper,
private val applicationEventPublisher: ApplicationEventPublisher,
- private val appConfigProperties: AppConfigProperties
+ private val appConfigProperties: AppConfigProperties,
+ private val consentProcessor: ConsentProcessor?
) {
+ private var logger: Logger = LoggerFactory.getLogger("RequestProcessor")
fun processMtbFile(mtbFile: MtbFile) {
processMtbFile(mtbFile, randomRequestId())
}
@@ -66,12 +72,25 @@ class RequestProcessor(
processMtbFile(mtbFile, randomRequestId())
}
+
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
- val pid = PatientId(mtbFile.patient.id)
- mtbFile pseudonymizeWith pseudonymizeService
- mtbFile anonymizeContentWith pseudonymizeService
- val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
- saveAndSend(request, pid)
+ val pid = PatientId(extractPatientIdentifier(mtbFile))
+
+ val isConsentOk = consentProcessor != null &&
+ consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null
+ if (isConsentOk) {
+ 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
+ )
+ )
+ }
}
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
@@ -89,9 +108,7 @@ class RequestProcessor(
if (appConfigProperties.duplicationDetection && isDuplication(request)) {
applicationEventPublisher.publishEvent(
ResponseEvent(
- request.requestId,
- Instant.now(),
- RequestStatus.DUPLICATION
+ request.requestId, Instant.now(), RequestStatus.DUPLICATION
)
)
return
@@ -120,21 +137,31 @@ class RequestProcessor(
val lastMtbFileRequestForPatient =
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
- val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
+ val isLastRequestDeletion =
+ requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
- return null != lastMtbFileRequestForPatient
- && !isLastRequestDeletion
- && lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest)
+ return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint(
+ pseudonymizedMtbFileRequest
+ )
}
- fun processDeletion(patientId: PatientId) {
- processDeletion(patientId, randomRequestId())
+ fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) {
+ processDeletion(patientId, randomRequestId(), isConsented)
}
- fun processDeletion(patientId: PatientId, requestId: RequestId) {
+ fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) {
try {
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(
Request(
requestId,
@@ -142,7 +169,7 @@ class RequestProcessor(
patientId,
fingerprint(patientPseudonym.value),
RequestType.DELETE,
- RequestStatus.UNKNOWN
+ requestStatus
)
)
@@ -150,17 +177,14 @@ class RequestProcessor(
applicationEventPublisher.publishEvent(
ResponseEvent(
- requestId,
- Instant.now(),
- responseStatus.status,
- when (responseStatus.status) {
+ requestId, Instant.now(), responseStatus.status, when (responseStatus.status) {
RequestStatus.WARNING, RequestStatus.ERROR -> Optional.of(responseStatus.body)
else -> Optional.empty()
}
)
)
- } catch (e: Exception) {
+ } catch (_: Exception) {
requestService.save(
Request(
uuid = requestId,
@@ -184,10 +208,10 @@ class RequestProcessor(
private fun fingerprint(s: String): Fingerprint {
return Fingerprint(
- Base32().encodeAsString(DigestUtils.sha256(s))
- .replace("=", "")
- .lowercase()
+ Base32().encodeAsString(DigestUtils.sha256(s)).replace("=", "").lowercase()
)
}
}
+
+private fun extractPatientIdentifier(mtbFile: Mtb): String = mtbFile.patient.id
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
index ecb2ec7..fb82647 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
@@ -70,6 +70,12 @@ class ResponseProcessor(
)
}
+ RequestStatus.NO_CONSENT -> {
+ it.report = Report(
+ "Einwilligung Status fehlt, widerrufen oder ungeklärt."
+ )
+ }
+
else -> {
logger.error("Cannot process response: Unknown response!")
return@ifPresentOrElse
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
index 25ec7cc..ea89e98 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
@@ -19,10 +19,7 @@
package dev.dnpm.etl.processor.web
-import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
-import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
-import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
-import dev.dnpm.etl.processor.monitoring.OutputConnectionCheckService
+import dev.dnpm.etl.processor.monitoring.*
import dev.dnpm.etl.processor.output.MtbFileSender
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.security.Role
@@ -61,11 +58,15 @@ class ConfigController(
val gPasConnectionAvailable =
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
+ val gIcsConnectionAvailable =
+ connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable()
+
model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName)
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
model.addAttribute("outputConnectionAvailable", outputConnectionAvailable)
model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable)
+ model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable)
model.addAttribute("tokensEnabled", tokenService != null)
if (tokenService != null) {
model.addAttribute("tokens", tokenService.findAll())
@@ -119,6 +120,24 @@ class ConfigController(
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"])
fun addToken(@ModelAttribute("name") name: String, model: Model): String {
if (tokenService == null) {
@@ -190,6 +209,7 @@ class ConfigController(
is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check"
is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check"
is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check"
+ is ConnectionCheckResult.GIcsConnectionCheckResult -> "gics-connection-check"
}
ServerSentEvent.builder<Any>()