mirror of
https://github.com/pcvolkmer/mv64e-etl-processor
synced 2025-09-13 17:02:52 +00:00
63 check consent status (#120)
Co-authored-by: Paul-Christian Volkmer <code@pcvolkmer.de>
This commit is contained in:
@@ -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
|
||||
)
|
||||
)
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
|
||||
@Bean
|
||||
fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
||||
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
||||
fun appFhirConfig(): AppFhirConfig {
|
||||
return AppFhirConfig()
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
|
||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
|
||||
@Bean
|
||||
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
|
||||
)
|
||||
@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
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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()
|
||||
)
|
||||
}
|
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -24,5 +24,6 @@ enum class RequestStatus(val value: String) {
|
||||
WARNING("warning"),
|
||||
ERROR("error"),
|
||||
UNKNOWN("unknown"),
|
||||
DUPLICATION("duplication")
|
||||
DUPLICATION("duplication"),
|
||||
NO_CONSENT("no-consent")
|
||||
}
|
@@ -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)
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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>()
|
||||
|
Reference in New Issue
Block a user