diff options
Diffstat (limited to 'src/main/kotlin/dev/dnpm')
47 files changed, 2055 insertions, 2073 deletions
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/Exceptions.kt b/src/main/kotlin/dev/dnpm/etl/processor/Exceptions.kt index 32d0954..1c590fc 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/Exceptions.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/Exceptions.kt @@ -19,4 +19,4 @@ package dev.dnpm.etl.processor -class NotFoundException : RuntimeException()
\ No newline at end of file +class NotFoundException : RuntimeException() diff --git a/src/main/kotlin/dev/dnpm/etl/processor/ServletInitializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/ServletInitializer.kt index 2618b09..e35cddf 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/ServletInitializer.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/ServletInitializer.kt @@ -23,9 +23,6 @@ import org.springframework.boot.builder.SpringApplicationBuilder import org.springframework.boot.web.servlet.support.SpringBootServletInitializer class ServletInitializer : SpringBootServletInitializer() { - - override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder { - return application.sources(EtlProcessorApplication::class.java) - } - + override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder = + application.sources(EtlProcessorApplication::class.java) } 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 57ab06c..8786c34 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -28,7 +28,7 @@ data class AppConfigProperties( var transformations: List<TransformationProperties> = listOf(), var maxRetryAttempts: Int = 3, var duplicationDetection: Boolean = true, - var genomDeTestSubmission: Boolean = false + var genomDeTestSubmission: Boolean = false, ) { companion object { const val NAME = "app" @@ -49,8 +49,7 @@ data class PseudonymizeConfigProperties( data class GPasConfigProperties( val uri: String?, val soapEndpoint: String?, - @get:DeprecatedConfigurationProperty(since = "0.12") - val pidDomain: String?, + @get:DeprecatedConfigurationProperty(since = "0.12") val pidDomain: String?, val patientDomain: String = pidDomain ?: "etl-processor", val genomDeTanDomain: String = "ccdn", val username: String?, @@ -63,7 +62,7 @@ data class GPasConfigProperties( @ConfigurationProperties(ConsentConfigProperties.NAME) data class ConsentConfigProperties( - var service: ConsentService = ConsentService.NONE + var service: ConsentService = ConsentService.NONE, ) { companion object { const val NAME = "app.consent" @@ -72,60 +71,33 @@ data class ConsentConfigProperties( @ConfigurationProperties(GIcsConfigProperties.NAME) data class GIcsConfigProperties( - /** - * Base URL to gICS System - * - */ + /** Base URL to gICS System */ val uri: String?, val username: String? = null, val password: String? = null, - /** * gICS specific system - * **/ + * * + */ val personIdentifierSystem: String = "https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID", - - /** - * Domain of broad consent resources - **/ + /** Domain of broad consent resources */ val broadConsentDomainName: String = "MII", - - /** - * Domain of Modelvorhaben 64e consent resources - **/ + /** Domain of Modelvorhaben 64e consent resources */ val genomDeConsentDomainName: String = "GenomDE_MV", - - /** - * Value to expect in case of positiv consent - */ + /** 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 - */ + /** 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", - - /** - * Consent Policy uri for MII Broad Consent Version - */ + /** Consent Policy uri for MII Broad Consent Version */ val broadConsentPolicyUri: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790", - - /** - * Value to expect in case of positiv consent - */ + /** 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" + /** 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" @@ -136,7 +108,7 @@ data class GIcsConfigProperties( data class RestTargetProperties( val uri: String?, val username: String?, - val password: String? + val password: String?, ) { companion object { const val NAME = "app.rest" @@ -149,7 +121,7 @@ data class KafkaProperties( val outputTopic: String = "etl-processor", val outputResponseTopic: String = "${outputTopic}_response", val groupId: String = "${outputTopic}_group", - val servers: String = "" + val servers: String = "", ) { companion object { const val NAME = "app.kafka" @@ -162,7 +134,7 @@ data class SecurityConfigProperties( val adminPassword: String?, val enableTokens: Boolean = false, val enableOidc: Boolean = false, - val defaultNewUserRole: Role = Role.USER + val defaultNewUserRole: Role = Role.USER, ) { companion object { const val NAME = "app.security" @@ -171,16 +143,16 @@ data class SecurityConfigProperties( enum class PseudonymGenerator { BUILDIN, - GPAS + GPAS, } enum class ConsentService { NONE, - GICS + GICS, } data class TransformationProperties( val path: String, val from: String, - val to: String + val to: String, ) 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 de302fd..36b0a75 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -30,6 +30,8 @@ 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 kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration import org.apache.cxf.jaxws.JaxWsProxyFactoryBean import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.AnyNestedCondition @@ -53,258 +55,251 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.web.client.HttpClientErrorException import org.springframework.web.client.RestTemplate import reactor.core.publisher.Sinks -import kotlin.time.Duration.Companion.seconds -import kotlin.time.toJavaDuration - @Configuration @EnableConfigurationProperties( - value = [ - AppConfigProperties::class, - PseudonymizeConfigProperties::class, - GPasConfigProperties::class, - ConsentConfigProperties::class, - GIcsConfigProperties::class - ] + value = + [ + AppConfigProperties::class, + PseudonymizeConfigProperties::class, + GPasConfigProperties::class, + ConsentConfigProperties::class, + GIcsConfigProperties::class, + ] ) @EnableScheduling class AppConfiguration { - private val logger = LoggerFactory.getLogger(AppConfiguration::class.java) + private val logger = LoggerFactory.getLogger(AppConfiguration::class.java) - @Bean - fun restTemplate(): RestTemplate { - return RestTemplate() - } + @Bean + fun restTemplate(): RestTemplate { + return RestTemplate() + } - @Bean - fun appFhirConfig(): AppFhirConfig { - return AppFhirConfig() - } + @Bean + fun appFhirConfig(): AppFhirConfig { + return AppFhirConfig() + } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) - @Bean - fun gpasSoapProxyFactoryBean(gpasConfigProperties: GPasConfigProperties): JaxWsProxyFactoryBean { - val proxyFactory = JaxWsProxyFactoryBean() - proxyFactory.serviceClass = GpasSoapService::class.java - proxyFactory.address = gpasConfigProperties.soapEndpoint - return proxyFactory - } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) + @Bean + fun gpasSoapProxyFactoryBean(gpasConfigProperties: GPasConfigProperties): JaxWsProxyFactoryBean { + val proxyFactory = JaxWsProxyFactoryBean() + proxyFactory.serviceClass = GpasSoapService::class.java + proxyFactory.address = gpasConfigProperties.soapEndpoint + return proxyFactory + } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) - @Bean - fun gpasSoapProxy(gpasConfigProperties: GPasConfigProperties): GpasSoapService { - return gpasSoapProxyFactoryBean(gpasConfigProperties).create() as GpasSoapService - } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) + @Bean + fun gpasSoapProxy(gpasConfigProperties: GPasConfigProperties): GpasSoapService { + return gpasSoapProxyFactoryBean(gpasConfigProperties).create() as GpasSoapService + } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) - @Bean - fun gpasSoapPseudonymGenerator( - configProperties: GPasConfigProperties, - retryTemplate: RetryTemplate, - gpasSoapService: GpasSoapService, - appFhirConfig: AppFhirConfig - ): Generator { - logger.info("Selected 'GpasSoapPseudonym Generator'") - return GpasSoapPseudonymGenerator(configProperties, retryTemplate, gpasSoapService, appFhirConfig) - } - - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @ConditionalOnProperty(value = ["app.pseudonymize.gpas.uri"]) - @Bean - fun gpasPseudonymGenerator( - configProperties: GPasConfigProperties, - retryTemplate: RetryTemplate, - restTemplate: RestTemplate, - appFhirConfig: AppFhirConfig - ): Generator { - logger.info("Selected 'GpasPseudonym Generator'") - return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig) - } - - @ConditionalOnProperty( - value = ["app.pseudonymize.generator"], - havingValue = "BUILDIN", - matchIfMissing = true + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) + @Bean + fun gpasSoapPseudonymGenerator( + configProperties: GPasConfigProperties, + retryTemplate: RetryTemplate, + gpasSoapService: GpasSoapService, + appFhirConfig: AppFhirConfig, + ): Generator { + logger.info("Selected 'GpasSoapPseudonym Generator'") + return GpasSoapPseudonymGenerator( + configProperties, + retryTemplate, + gpasSoapService, + appFhirConfig, ) - @Bean - fun buildinPseudonymGenerator(): Generator { - logger.info("Selected 'BUILDIN Pseudonym Generator'") - return AnonymizingGenerator() - } + } - @Bean - fun pseudonymizeService( - generator: Generator, - pseudonymizeConfigProperties: PseudonymizeConfigProperties - ): PseudonymizeService { - return PseudonymizeService(generator, pseudonymizeConfigProperties) - } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @ConditionalOnProperty(value = ["app.pseudonymize.gpas.uri"]) + @Bean + fun gpasPseudonymGenerator( + configProperties: GPasConfigProperties, + retryTemplate: RetryTemplate, + restTemplate: RestTemplate, + appFhirConfig: AppFhirConfig, + ): Generator { + logger.info("Selected 'GpasPseudonym Generator'") + return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig) + } - @Bean - fun reportService(): ReportService { - return ReportService(getObjectMapper()) - } + @ConditionalOnProperty( + value = ["app.pseudonymize.generator"], + havingValue = "BUILDIN", + matchIfMissing = true, + ) + @Bean + fun buildinPseudonymGenerator(): Generator { + logger.info("Selected 'BUILDIN Pseudonym Generator'") + return AnonymizingGenerator() + } - @Bean - fun getObjectMapper(): ObjectMapper { - return JacksonConfig().objectMapper() - } + @Bean + fun pseudonymizeService( + generator: Generator, + pseudonymizeConfigProperties: PseudonymizeConfigProperties, + ): PseudonymizeService { + return PseudonymizeService(generator, pseudonymizeConfigProperties) + } - @Bean - fun transformationService( - configProperties: AppConfigProperties - ): TransformationService { - logger.info("Apply ${configProperties.transformations.size} transformation rules") - return TransformationService(getObjectMapper(), configProperties.transformations.map { - Transformation.of(it.path) from it.from to it.to - }) - } + @Bean + fun reportService(): ReportService { + return ReportService(getObjectMapper()) + } - @Bean - fun retryTemplate(configProperties: AppConfigProperties): RetryTemplate { - return RetryTemplateBuilder() - .notRetryOn(IllegalArgumentException::class.java) - .notRetryOn(HttpClientErrorException.BadRequest::class.java) - .notRetryOn(HttpClientErrorException.UnprocessableEntity::class.java) - .exponentialBackoff(2.seconds.toJavaDuration(), 1.25, 5.seconds.toJavaDuration()) - .customPolicy(SimpleRetryPolicy(configProperties.maxRetryAttempts)) - .withListener(object : RetryListener { - override fun <T : Any, E : Throwable> onError( - context: RetryContext, - callback: RetryCallback<T, E>, - throwable: Throwable - ) { - logger.warn( - "Error occured: {}. Retrying {}", - throwable.message, - context.retryCount - ) - } - }) - .build() - } + @Bean + fun getObjectMapper(): ObjectMapper { + return JacksonConfig().objectMapper() + } - @ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true") - @Bean - fun tokenService( - userDetailsManager: InMemoryUserDetailsManager, - passwordEncoder: PasswordEncoder, - tokenRepository: TokenRepository - ): TokenService { - return TokenService(userDetailsManager, passwordEncoder, tokenRepository) - } + @Bean + fun transformationService(configProperties: AppConfigProperties): TransformationService { + logger.info("Apply ${configProperties.transformations.size} transformation rules") + return TransformationService( + getObjectMapper(), + configProperties.transformations.map { Transformation.of(it.path) from it.from to it.to }, + ) + } - @Bean - fun statisticsUpdateProducer(): Sinks.Many<Any> { - return Sinks.many().multicast().directBestEffort() - } + @Bean + fun retryTemplate(configProperties: AppConfigProperties): RetryTemplate { + return RetryTemplateBuilder() + .notRetryOn(IllegalArgumentException::class.java) + .notRetryOn(HttpClientErrorException.BadRequest::class.java) + .notRetryOn(HttpClientErrorException.UnprocessableEntity::class.java) + .exponentialBackoff(2.seconds.toJavaDuration(), 1.25, 5.seconds.toJavaDuration()) + .customPolicy(SimpleRetryPolicy(configProperties.maxRetryAttempts)) + .withListener( + object : RetryListener { + override fun <T : Any, E : Throwable> onError( + context: RetryContext, + callback: RetryCallback<T, E>, + throwable: Throwable, + ) { + logger.warn("Error occured: {}. Retrying {}", throwable.message, context.retryCount) + } + } + ) + .build() + } - @Bean - fun connectionCheckUpdateProducer(): Sinks.Many<ConnectionCheckResult> { - return Sinks.many().multicast().onBackpressureBuffer() - } + @ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true") + @Bean + fun tokenService( + userDetailsManager: InMemoryUserDetailsManager, + passwordEncoder: PasswordEncoder, + tokenRepository: TokenRepository, + ): TokenService { + return TokenService(userDetailsManager, passwordEncoder, tokenRepository) + } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @Bean - fun gPasConnectionCheckService( - restTemplate: RestTemplate, - gPasConfigProperties: GPasConfigProperties, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return GPasConnectionCheckService( - restTemplate, - gPasConfigProperties, - connectionCheckUpdateProducer - ) - } + @Bean + fun statisticsUpdateProducer(): Sinks.Many<Any> { + return Sinks.many().multicast().directBestEffort() + } - @ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS") - @ConditionalOnMissingBean - @Bean - fun gPasConnectionCheckServiceOnDeprecatedProperty( - restTemplate: RestTemplate, - gPasConfigProperties: GPasConfigProperties, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return GPasConnectionCheckService( - restTemplate, - gPasConfigProperties, - connectionCheckUpdateProducer - ) - } + @Bean + fun connectionCheckUpdateProducer(): Sinks.Many<ConnectionCheckResult> { + return Sinks.many().multicast().onBackpressureBuffer() + } - @Bean - fun jdbcConfiguration(): AbstractJdbcConfiguration { - return AppJdbcConfiguration() - } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @Bean + fun gPasConnectionCheckService( + restTemplate: RestTemplate, + gPasConfigProperties: GPasConfigProperties, + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService { + return GPasConnectionCheckService( + restTemplate, + gPasConfigProperties, + connectionCheckUpdateProducer, + ) + } - @Conditional(GicsEnabledCondition::class) - @Bean - fun gicsConsentService( - gIcsConfigProperties: GIcsConfigProperties, - retryTemplate: RetryTemplate, - restTemplate: RestTemplate, - appFhirConfig: AppFhirConfig - ): IConsentService { - return GicsConsentService( - gIcsConfigProperties, - retryTemplate, - restTemplate, - appFhirConfig - ) - } + @ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS") + @ConditionalOnMissingBean + @Bean + fun gPasConnectionCheckServiceOnDeprecatedProperty( + restTemplate: RestTemplate, + gPasConfigProperties: GPasConfigProperties, + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService { + return GPasConnectionCheckService( + restTemplate, + gPasConfigProperties, + connectionCheckUpdateProducer, + ) + } - @Conditional(GicsEnabledCondition::class) - @Bean - fun consentProcessor( - configProperties: AppConfigProperties, - gIcsConfigProperties: GIcsConfigProperties, - getObjectMapper: ObjectMapper, - appFhirConfig: AppFhirConfig, - gicsConsentService: IConsentService - ): ConsentProcessor { - return ConsentProcessor( - configProperties, - gIcsConfigProperties, - getObjectMapper, - appFhirConfig.fhirContext(), - gicsConsentService - ) - } + @Bean + fun jdbcConfiguration(): AbstractJdbcConfiguration { + return AppJdbcConfiguration() + } - @Conditional(GicsEnabledCondition::class) - @Bean - fun gIcsConnectionCheckService( - restTemplate: RestTemplate, - gIcsConfigProperties: GIcsConfigProperties, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return GIcsConnectionCheckService( - restTemplate, - gIcsConfigProperties, - connectionCheckUpdateProducer - ) - } + @Conditional(GicsEnabledCondition::class) + @Bean + fun gicsConsentService( + gIcsConfigProperties: GIcsConfigProperties, + retryTemplate: RetryTemplate, + restTemplate: RestTemplate, + appFhirConfig: AppFhirConfig, + ): IConsentService { + return GicsConsentService(gIcsConfigProperties, retryTemplate, restTemplate, appFhirConfig) + } - @Bean - @ConditionalOnMissingBean - fun iGetConsentService(): IConsentService { - return MtbFileConsentService() - } + @Conditional(GicsEnabledCondition::class) + @Bean + fun consentProcessor( + configProperties: AppConfigProperties, + gIcsConfigProperties: GIcsConfigProperties, + getObjectMapper: ObjectMapper, + appFhirConfig: AppFhirConfig, + gicsConsentService: IConsentService, + ): ConsentProcessor { + return ConsentProcessor( + configProperties, + gIcsConfigProperties, + getObjectMapper, + appFhirConfig.fhirContext(), + gicsConsentService, + ) + } + @Conditional(GicsEnabledCondition::class) + @Bean + fun gIcsConnectionCheckService( + restTemplate: RestTemplate, + gIcsConfigProperties: GIcsConfigProperties, + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService { + return GIcsConnectionCheckService( + restTemplate, + gIcsConfigProperties, + connectionCheckUpdateProducer, + ) + } + + @Bean + @ConditionalOnMissingBean + fun iGetConsentService(): IConsentService { + return MtbFileConsentService() + } } class GicsEnabledCondition : AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) { - @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics") - @ConditionalOnProperty(name = ["app.consent.gics.uri"]) - class OnGicsServiceSelected { - // Just for Condition - } - + @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics") + @ConditionalOnProperty(name = ["app.consent.gics.uri"]) + class OnGicsServiceSelected { + // 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 index 2b5ff8f..052822e 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt @@ -4,13 +4,9 @@ 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 + @Bean fun fhirContext(): FhirContext = fhirCtx +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt index 898982c..769faf3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt @@ -7,19 +7,13 @@ import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration @Configuration class AppJdbcConfiguration : AbstractJdbcConfiguration() { - override fun userConverters(): MutableList<*> { - return mutableListOf(StringToFingerprintConverter(), FingerprintToStringConverter()) - } + override fun userConverters(): MutableList<*> = mutableListOf(StringToFingerprintConverter(), FingerprintToStringConverter()) } class StringToFingerprintConverter : Converter<String, Fingerprint> { - override fun convert(source: String): Fingerprint { - return Fingerprint(source) - } + override fun convert(source: String): Fingerprint = Fingerprint(source) } class FingerprintToStringConverter : Converter<Fingerprint, String> { - override fun convert(source: Fingerprint): String { - return source.value - } -}
\ No newline at end of file + override fun convert(source: Fingerprint): String = source.value +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt index 6551713..2f89dea 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt @@ -45,14 +45,11 @@ import org.springframework.retry.support.RetryTemplate import reactor.core.publisher.Sinks @Configuration -@EnableConfigurationProperties( - value = [KafkaProperties::class] -) +@EnableConfigurationProperties(value = [KafkaProperties::class]) @ConditionalOnProperty(value = ["app.kafka.servers"]) @ConditionalOnMissingBean(MtbFileSender::class) @Order(-5) class AppKafkaConfiguration { - private val logger = LoggerFactory.getLogger(AppKafkaConfiguration::class.java) @Bean @@ -60,7 +57,7 @@ class AppKafkaConfiguration { kafkaTemplate: KafkaTemplate<String, String>, kafkaProperties: KafkaProperties, retryTemplate: RetryTemplate, - objectMapper: ObjectMapper + objectMapper: ObjectMapper, ): MtbFileSender { logger.info("Selected 'KafkaMtbFileSender'") return KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper) @@ -70,7 +67,7 @@ class AppKafkaConfiguration { fun kafkaResponseListenerContainer( consumerFactory: ConsumerFactory<String, String>, kafkaProperties: KafkaProperties, - kafkaResponseProcessor: KafkaResponseProcessor + kafkaResponseProcessor: KafkaResponseProcessor, ): KafkaMessageListenerContainer<String, String> { val containerProperties = ContainerProperties(kafkaProperties.outputResponseTopic) containerProperties.messageListener = kafkaResponseProcessor @@ -80,17 +77,15 @@ class AppKafkaConfiguration { @Bean fun kafkaResponseProcessor( applicationEventPublisher: ApplicationEventPublisher, - objectMapper: ObjectMapper - ): KafkaResponseProcessor { - return KafkaResponseProcessor(applicationEventPublisher, objectMapper) - } + objectMapper: ObjectMapper, + ): KafkaResponseProcessor = KafkaResponseProcessor(applicationEventPublisher, objectMapper) @Bean @ConditionalOnProperty(value = ["app.kafka.input-topic"]) fun kafkaInputListenerContainer( consumerFactory: ConsumerFactory<String, String>, kafkaProperties: KafkaProperties, - kafkaInputListener: KafkaInputListener + kafkaInputListener: KafkaInputListener, ): KafkaMessageListenerContainer<String, String> { val containerProperties = ContainerProperties(kafkaProperties.inputTopic) containerProperties.messageListener = kafkaInputListener @@ -102,17 +97,16 @@ class AppKafkaConfiguration { fun kafkaInputListener( requestProcessor: RequestProcessor, objectMapper: ObjectMapper, - consentEvaluator: ConsentEvaluator - ): KafkaInputListener { - return KafkaInputListener(requestProcessor, consentEvaluator, objectMapper) - } + consentEvaluator: ConsentEvaluator, + ): KafkaInputListener = KafkaInputListener(requestProcessor, consentEvaluator, objectMapper) @Bean fun kafkaConnectionCheckService( consumerFactory: ConsumerFactory<String, String>, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return KafkaConnectionCheckService(consumerFactory.createConsumer(), connectionCheckUpdateProducer) - } - + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService = + KafkaConnectionCheckService( + consumerFactory.createConsumer(), + connectionCheckUpdateProducer, + ) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppRestConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppRestConfiguration.kt index 62c25bc..565209e 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppRestConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppRestConfiguration.kt @@ -37,16 +37,11 @@ import org.springframework.web.client.RestTemplate import reactor.core.publisher.Sinks @Configuration -@EnableConfigurationProperties( - value = [ - RestTargetProperties::class - ] -) +@EnableConfigurationProperties(value = [RestTargetProperties::class]) @ConditionalOnProperty(value = ["app.rest.uri"]) @ConditionalOnMissingBean(MtbFileSender::class) @Order(-10) class AppRestConfiguration { - private val logger = LoggerFactory.getLogger(AppRestConfiguration::class.java) @Bean @@ -64,10 +59,11 @@ class AppRestConfiguration { fun restConnectionCheckService( restTemplate: RestTemplate, restTargetProperties: RestTargetProperties, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return RestConnectionCheckService(restTemplate, restTargetProperties, connectionCheckUpdateProducer) - } - + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService = + RestConnectionCheckService( + restTemplate, + restTargetProperties, + connectionCheckUpdateProducer, + ) } - diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt index b2bd044..d44303b 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt @@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.config import dev.dnpm.etl.processor.security.UserRole import dev.dnpm.etl.processor.security.UserRoleRepository import dev.dnpm.etl.processor.security.UserRoleService +import java.util.* import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.EnableConfigurationProperties @@ -41,158 +42,146 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.security.web.SecurityFilterChain -import java.util.* - private const val LOGIN_PATH = "/login" @Configuration -@EnableConfigurationProperties( - value = [ - SecurityConfigProperties::class - ] -) +@EnableConfigurationProperties(value = [SecurityConfigProperties::class]) @ConditionalOnProperty(value = ["app.security.admin-user"]) @EnableWebSecurity -class AppSecurityConfiguration( - private val securityConfigProperties: SecurityConfigProperties -) { +class AppSecurityConfiguration(private val securityConfigProperties: SecurityConfigProperties) { - private val logger = LoggerFactory.getLogger(AppSecurityConfiguration::class.java) + private val logger = LoggerFactory.getLogger(AppSecurityConfiguration::class.java) - @Bean - fun userDetailsService(passwordEncoder: PasswordEncoder): InMemoryUserDetailsManager { - val adminUser = if (securityConfigProperties.adminUser.isNullOrBlank()) { - logger.warn("Using random Admin User: admin") - "admin" + @Bean + fun userDetailsService(passwordEncoder: PasswordEncoder): InMemoryUserDetailsManager { + val adminUser = + if (securityConfigProperties.adminUser.isNullOrBlank()) { + logger.warn("Using random Admin User: admin") + "admin" } else { - securityConfigProperties.adminUser + securityConfigProperties.adminUser } - val adminPassword = if (securityConfigProperties.adminPassword.isNullOrBlank()) { - val random = UUID.randomUUID().toString() - logger.warn("Using random Admin Passwort: {}", random) - passwordEncoder.encode(random) + val adminPassword = + if (securityConfigProperties.adminPassword.isNullOrBlank()) { + val random = UUID.randomUUID().toString() + logger.warn("Using random Admin Passwort: {}", random) + passwordEncoder.encode(random) } else { - securityConfigProperties.adminPassword + securityConfigProperties.adminPassword } - val user: UserDetails = User.withUsername(adminUser) - .password(adminPassword) - .roles("ADMIN") - .build() - - return InMemoryUserDetailsManager(user) - } - - @Bean - @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") - fun filterChainOidc( - http: HttpSecurity, - passwordEncoder: PasswordEncoder, - userRoleRepository: UserRoleRepository, - sessionRegistry: SessionRegistry - ): SecurityFilterChain { - http { - authorizeHttpRequests { - authorize("/configs/**", hasRole("ADMIN")) - authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER")) - authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER")) - authorize("/report/**", hasAnyRole("ADMIN", "USER")) - authorize("*.css", permitAll) - authorize("*.ico", permitAll) - authorize("*.jpeg", permitAll) - authorize("*.js", permitAll) - authorize("*.svg", permitAll) - authorize("*.css", permitAll) - authorize("/login/**", permitAll) - authorize(anyRequest, permitAll) - } - httpBasic { - realmName = "ETL-Processor" - } - formLogin { - loginPage = LOGIN_PATH - } - oauth2Login { - loginPage = LOGIN_PATH - } - sessionManagement { - sessionConcurrency { - maximumSessions = 1 - expiredUrl = "$LOGIN_PATH?expired" - } - sessionFixation { - newSession() - } - } - csrf { disable() } + val user: UserDetails = + User.withUsername(adminUser).password(adminPassword).roles("ADMIN").build() + + return InMemoryUserDetailsManager(user) + } + + @Bean + @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") + fun filterChainOidc( + http: HttpSecurity, + passwordEncoder: PasswordEncoder, + userRoleRepository: UserRoleRepository, + sessionRegistry: SessionRegistry, + ): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/configs/**", hasRole("ADMIN")) + authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER")) + authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER")) + authorize("/report/**", hasAnyRole("ADMIN", "USER")) + authorize("*.css", permitAll) + authorize("*.ico", permitAll) + authorize("*.jpeg", permitAll) + authorize("*.js", permitAll) + authorize("*.svg", permitAll) + authorize("*.css", permitAll) + authorize("/login/**", permitAll) + authorize(anyRequest, permitAll) + } + httpBasic { realmName = "ETL-Processor" } + formLogin { loginPage = LOGIN_PATH } + oauth2Login { loginPage = LOGIN_PATH } + sessionManagement { + sessionConcurrency { + maximumSessions = 1 + expiredUrl = "$LOGIN_PATH?expired" } - return http.build() + sessionFixation { newSession() } + } + csrf { disable() } } - - @Bean - @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") - fun grantedAuthoritiesMapper( - userRoleRepository: UserRoleRepository, - appSecurityConfigProperties: SecurityConfigProperties - ): GrantedAuthoritiesMapper { - return GrantedAuthoritiesMapper { grantedAuthority -> - grantedAuthority.filterIsInstance<OidcUserAuthority>() - .onEach { - val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername) - if (userRole.isEmpty) { - userRoleRepository.save( - UserRole( - null, - it.userInfo.preferredUsername, - appSecurityConfigProperties.defaultNewUserRole - ) - ) - } - } - .map { - val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername) - SimpleGrantedAuthority("ROLE_${userRole.get().role.toString().uppercase()}") - } - } - } - - @Bean - @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "false", matchIfMissing = true) - fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain { - http { - authorizeHttpRequests { - authorize("/configs/**", hasRole("ADMIN")) - authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN")) - authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN")) - authorize("/report/**", hasRole("ADMIN")) - authorize(anyRequest, permitAll) + return http.build() + } + + @Bean + @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") + fun grantedAuthoritiesMapper( + userRoleRepository: UserRoleRepository, + appSecurityConfigProperties: SecurityConfigProperties, + ): GrantedAuthoritiesMapper { + return GrantedAuthoritiesMapper { grantedAuthority -> + grantedAuthority + .filterIsInstance<OidcUserAuthority>() + .onEach { + val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername) + if (userRole.isEmpty) { + userRoleRepository.save( + UserRole( + null, + it.userInfo.preferredUsername, + appSecurityConfigProperties.defaultNewUserRole, + ) + ) } - httpBasic { - realmName = "ETL-Processor" - } - formLogin { - loginPage = LOGIN_PATH - } - csrf { disable() } - } - return http.build() + } + .map { + val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername) + SimpleGrantedAuthority("ROLE_${userRole.get().role.toString().uppercase()}") + } } - - @Bean - fun sessionRegistry(): SessionRegistry { - return SessionRegistryImpl() - } - - @Bean - fun passwordEncoder(): PasswordEncoder { - return PasswordEncoderFactories.createDelegatingPasswordEncoder() - } - - @Bean - @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") - fun userRoleService(userRoleRepository: UserRoleRepository, sessionRegistry: SessionRegistry): UserRoleService { - return UserRoleService(userRoleRepository, sessionRegistry) + } + + @Bean + @ConditionalOnProperty( + value = ["app.security.enable-oidc"], + havingValue = "false", + matchIfMissing = true, + ) + fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/configs/**", hasRole("ADMIN")) + authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN")) + authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN")) + authorize("/report/**", hasRole("ADMIN")) + authorize(anyRequest, permitAll) + } + httpBasic { realmName = "ETL-Processor" } + formLogin { loginPage = LOGIN_PATH } + csrf { disable() } } + return http.build() + } + + @Bean + fun sessionRegistry(): SessionRegistry { + return SessionRegistryImpl() + } + + @Bean + fun passwordEncoder(): PasswordEncoder { + return PasswordEncoderFactories.createDelegatingPasswordEncoder() + } + + @Bean + @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") + fun userRoleService( + userRoleRepository: UserRoleRepository, + sessionRegistry: SessionRegistry, + ): UserRoleService { + return UserRoleService(userRoleRepository, sessionRegistry) + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt index 5469b1b..48163a1 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt @@ -1,18 +1,19 @@ 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 { - + 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 index 812ce44..b4f29a4 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt @@ -7,9 +7,11 @@ import org.hl7.fhir.r4.model.Consent class ConsentResourceSerializer : JsonSerializer<Consent>() { override fun serialize( - value: Consent, gen: JsonGenerator, serializers: SerializerProvider + 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 index 2ae0dd3..45a3d93 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt @@ -1,6 +1,5 @@ package dev.dnpm.etl.processor.config - import com.fasterxml.jackson.databind.module.SimpleModule import org.hl7.fhir.r4.model.Consent @@ -9,4 +8,4 @@ class FhirResourceModule : SimpleModule() { 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 index 282f69e..2480de8 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt @@ -2,33 +2,27 @@ package dev.dnpm.etl.processor.config import ca.uhn.fhir.context.FhirContext import com.fasterxml.jackson.annotation.JsonInclude -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.jdk8.Jdk8Module import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration @Configuration class JacksonConfig { - companion object { var fhirContext: FhirContext = FhirContext.forR4() - @JvmStatic - fun fhirContext(): FhirContext { - return fhirContext - } + @JvmStatic fun fhirContext(): FhirContext = fhirContext } @Bean - fun objectMapper(): ObjectMapper = ObjectMapper().registerModule(FhirResourceModule()) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .registerModule( - JavaTimeModule() - ) - .registerModule( - Jdk8Module() - ) - .setSerializationInclusion(JsonInclude.Include.NON_NULL) + fun objectMapper(): ObjectMapper = + ObjectMapper() + .registerModule(FhirResourceModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .registerModule(JavaTimeModule()) + .registerModule(Jdk8Module()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt b/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt index 195346d..58f647f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt @@ -24,38 +24,35 @@ import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose import dev.pcvolkmer.mv64e.mtb.Mtb import org.springframework.stereotype.Service -/** - * Evaluates consent using provided consent service and file based consent information - */ +/** Evaluates consent using provided consent service and file based consent information */ @Service class ConsentEvaluator( - private val consentService: IConsentService + private val consentService: IConsentService, ) { fun check(mtbFile: Mtb): ConsentEvaluation { val ttpConsentStatus = consentService.getTtpBroadConsentStatus(mtbFile.patient.id) - val consentGiven = ttpConsentStatus == TtpConsentStatus.BROAD_CONSENT_GIVEN - || ttpConsentStatus == TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT + val consentGiven = + ttpConsentStatus == TtpConsentStatus.BROAD_CONSENT_GIVEN || + ttpConsentStatus == TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT || // Aktuell nur Modellvorhaben Consent im File - || ttpConsentStatus == TtpConsentStatus.UNKNOWN_CHECK_FILE && mtbFile.metadata?.modelProjectConsent?.provisions?.any { - it.purpose == ModelProjectConsentPurpose.SEQUENCING - && it.type == ConsentProvision.PERMIT + ttpConsentStatus == TtpConsentStatus.UNKNOWN_CHECK_FILE && + mtbFile.metadata?.modelProjectConsent?.provisions?.any { + it.purpose == ModelProjectConsentPurpose.SEQUENCING && + it.type == ConsentProvision.PERMIT } == true return ConsentEvaluation(ttpConsentStatus, consentGiven) } } -data class ConsentEvaluation(private val ttpConsentStatus: TtpConsentStatus, private val consentGiven: Boolean) { - /** - * Checks if any required consent is present - */ - fun hasConsent(): Boolean { - return consentGiven - } +data class ConsentEvaluation( + private val ttpConsentStatus: TtpConsentStatus, + private val consentGiven: Boolean, +) { + /** Checks if any required consent is present */ + fun hasConsent(): Boolean = consentGiven - /** - * Returns the consent status - */ + /** Returns the consent status */ fun getStatus(): TtpConsentStatus { if (ttpConsentStatus == TtpConsentStatus.UNKNOWN_CHECK_FILE) { // in case ttp check is disabled - we propagate rejected status anyway 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 d53eb7e..2f6b2bb 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt @@ -36,7 +36,7 @@ import java.nio.charset.Charset class KafkaInputListener( private val requestProcessor: RequestProcessor, private val consentEvaluator: ConsentEvaluator, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) : MessageListener<String, String> { private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java) @@ -45,29 +45,40 @@ class KafkaInputListener( MediaType.APPLICATION_JSON_VALUE -> handleDnpmV2Message(record) CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE -> handleDnpmV2Message(record) else -> { - /* ignore other messages */ + // ignore other messages } } } private fun guessMimeType(record: ConsumerRecord<String, String>): String? { - if (record.headers().headers("contentType").toList().isEmpty()) { + if (record + .headers() + .headers("contentType") + .toList() + .isEmpty() + ) { // Fallback if no contentType set (old behavior) return MediaType.APPLICATION_JSON_VALUE } - return record.headers().headers("contentType")?.firstOrNull()?.value()?.toString(Charset.forName("UTF-8")) + return record + .headers() + .headers("contentType") + ?.firstOrNull() + ?.value() + ?.toString(Charset.forName("UTF-8")) } private fun handleDnpmV2Message(record: ConsumerRecord<String, String>) { val mtbFile = objectMapper.readValue(record.value(), Mtb::class.java) val patientId = PatientId(mtbFile.patient.id) val firstRequestIdHeader = record.headers().headers("requestId")?.firstOrNull() - val requestId = if (null != firstRequestIdHeader) { - RequestId(String(firstRequestIdHeader.value())) - } else { - RequestId("") - } + val requestId = + if (null != firstRequestIdHeader) { + RequestId(String(firstRequestIdHeader.value())) + } else { + RequestId("") + } if (consentEvaluator.check(mtbFile).hasConsent()) { logger.debug("Accepted MTB File for processing") @@ -81,13 +92,8 @@ class KafkaInputListener( if (requestId.isBlank()) { requestProcessor.processDeletion(patientId, TtpConsentStatus.UNKNOWN_CHECK_FILE) } else { - requestProcessor.processDeletion( - patientId, - requestId, - TtpConsentStatus.UNKNOWN_CHECK_FILE - ) + 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 e154536..5a43242 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt @@ -34,34 +34,36 @@ import org.springframework.web.bind.annotation.* @RequestMapping(path = ["mtbfile", "mtb"]) class MtbFileRestController( private val requestProcessor: RequestProcessor, - private val consentEvaluator: ConsentEvaluator + private val consentEvaluator: ConsentEvaluator, ) { - private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) + private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) - @GetMapping - fun info(): ResponseEntity<String> { - return ResponseEntity.ok("Test") - } - - @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE]) - fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> { - val consentEvaluation = consentEvaluator.check(mtbFile) - if (consentEvaluation.hasConsent()) { - logger.debug("Accepted MTB File (DNPM V2) for processing") - requestProcessor.processMtbFile(mtbFile) - } else { - logger.debug("Accepted MTB File (DNPM V2) and process deletion") - val patientId = PatientId(mtbFile.patient.id) - requestProcessor.processDeletion(patientId, consentEvaluation.getStatus()) - } - return ResponseEntity.accepted().build() - } + @GetMapping + fun info(): ResponseEntity<String> { + return ResponseEntity.ok("Test") + } - @DeleteMapping(path = ["{patientId}"]) - fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> { - logger.debug("Accepted patient ID to process deletion") - requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE) - return ResponseEntity.accepted().build() + @PostMapping( + consumes = + [MediaType.APPLICATION_JSON_VALUE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] + ) + fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> { + val consentEvaluation = consentEvaluator.check(mtbFile) + if (consentEvaluation.hasConsent()) { + logger.debug("Accepted MTB File (DNPM V2) for processing") + requestProcessor.processMtbFile(mtbFile) + } else { + logger.debug("Accepted MTB File (DNPM V2) and process deletion") + val patientId = PatientId(mtbFile.patient.id) + requestProcessor.processDeletion(patientId, consentEvaluation.getStatus()) } + return ResponseEntity.accepted().build() + } + @DeleteMapping(path = ["{patientId}"]) + fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> { + logger.debug("Accepted patient ID to process deletion") + 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 6e97865..085d0a3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt @@ -17,13 +17,15 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - 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 +import java.time.Instant +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration import org.apache.kafka.clients.consumer.Consumer import org.apache.kafka.common.errors.TimeoutException import org.slf4j.LoggerFactory @@ -33,254 +35,288 @@ import org.springframework.scheduling.annotation.Scheduled import org.springframework.web.client.RestTemplate import org.springframework.web.util.UriComponentsBuilder import reactor.core.publisher.Sinks -import java.time.Instant -import kotlin.time.Duration.Companion.seconds -import kotlin.time.toJavaDuration fun interface ConnectionCheckService { - fun connectionAvailable(): ConnectionCheckResult - + fun connectionAvailable(): ConnectionCheckResult } interface OutputConnectionCheckService : ConnectionCheckService sealed class ConnectionCheckResult { - abstract val available: Boolean + abstract val available: Boolean - abstract val timestamp: Instant + abstract val timestamp: Instant - abstract val lastChange: Instant + abstract val lastChange: Instant - data class KafkaConnectionCheckResult( - override val available: Boolean, - override val timestamp: Instant, - override val lastChange: Instant - ) : ConnectionCheckResult() + data class KafkaConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant, + ) : ConnectionCheckResult() - data class RestConnectionCheckResult( - override val available: Boolean, - override val timestamp: Instant, - override val lastChange: Instant - ) : ConnectionCheckResult() + data class RestConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant, + ) : ConnectionCheckResult() - data class GPasConnectionCheckResult( - override val available: Boolean, - override val timestamp: Instant, - override val lastChange: Instant - ) : ConnectionCheckResult() + data class GPasConnectionCheckResult( + override val available: Boolean, + 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() + data class GIcsConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant, + ) : ConnectionCheckResult() } class KafkaConnectionCheckService( private val consumer: Consumer<String, String>, @param:Qualifier("connectionCheckUpdateProducer") - private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, ) : OutputConnectionCheckService { - private val logger = LoggerFactory.getLogger(javaClass) - private var result = ConnectionCheckResult.KafkaConnectionCheckResult(false, Instant.now(), Instant.now()) - - @PostConstruct - @Scheduled(cron = "0 * * * * *") - fun check() { - result = try { - val available = null != consumer.listTopics(5.seconds.toJavaDuration()) - ConnectionCheckResult.KafkaConnectionCheckResult( - available, - Instant.now(), - if (result.available == available) { result.lastChange } else { Instant.now() } - ) + private val logger = LoggerFactory.getLogger(javaClass) + private var result = + ConnectionCheckResult.KafkaConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = + try { + val available = null != consumer.listTopics(5.seconds.toJavaDuration()) + ConnectionCheckResult.KafkaConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { + result.lastChange + } else { + Instant.now() + }, + ) } catch (ex: TimeoutException) { - logger.error("Connection-Timeout error: {}", ex.message) - ConnectionCheckResult.KafkaConnectionCheckResult( - false, - Instant.now(), - if (!result.available) { result.lastChange } else { Instant.now() } - ) + logger.error("Connection-Timeout error: {}", ex.message) + ConnectionCheckResult.KafkaConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { + result.lastChange + } else { + Instant.now() + }, + ) } - connectionCheckUpdateProducer.emitNext( - result, - Sinks.EmitFailureHandler.FAIL_FAST - ) - } - - override fun connectionAvailable(): ConnectionCheckResult.KafkaConnectionCheckResult { - return this.result - } + connectionCheckUpdateProducer.emitNext(result, Sinks.EmitFailureHandler.FAIL_FAST) + } + override fun connectionAvailable(): ConnectionCheckResult.KafkaConnectionCheckResult { + return this.result + } } class RestConnectionCheckService( private val restTemplate: RestTemplate, private val restTargetProperties: RestTargetProperties, @param:Qualifier("connectionCheckUpdateProducer") - private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, ) : OutputConnectionCheckService { - private val logger = LoggerFactory.getLogger(javaClass) - private var result = ConnectionCheckResult.RestConnectionCheckResult(false, Instant.now(), Instant.now()) - - @PostConstruct - @Scheduled(cron = "0 * * * * *") - fun check() { - result = try { - val statusCode = restTemplate.getForEntity( - UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString()) - .pathSegment("mtb") - .pathSegment("kaplan-meier") - .pathSegment("config") - .toUriString(), - String::class.java - ).statusCode - val available = statusCode == HttpStatus.OK - - if (available.not()) { - logger.error("Invalid response code {}, expected HTTP status 200", statusCode) - } - ConnectionCheckResult.RestConnectionCheckResult( - available, - Instant.now(), - if (result.available == available) { result.lastChange } else { Instant.now() } - ) + private val logger = LoggerFactory.getLogger(javaClass) + private var result = + ConnectionCheckResult.RestConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = + try { + val statusCode = + restTemplate + .getForEntity( + UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString()) + .pathSegment("mtb") + .pathSegment("kaplan-meier") + .pathSegment("config") + .toUriString(), + String::class.java, + ) + .statusCode + val available = statusCode == HttpStatus.OK + + if (available.not()) { + logger.error("Invalid response code {}, expected HTTP status 200", statusCode) + } + ConnectionCheckResult.RestConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { + result.lastChange + } else { + Instant.now() + }, + ) } catch (ex: Exception) { - logger.error("Connection-Check error: {}", ex.message) - ConnectionCheckResult.RestConnectionCheckResult( - false, - Instant.now(), - if (!result.available) { result.lastChange } else { Instant.now() } - ) + logger.error("Connection-Check error: {}", ex.message) + ConnectionCheckResult.RestConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { + result.lastChange + } else { + Instant.now() + }, + ) } - connectionCheckUpdateProducer.emitNext( - result, - Sinks.EmitFailureHandler.FAIL_FAST - ) - } - - override fun connectionAvailable(): ConnectionCheckResult.RestConnectionCheckResult { - return this.result - } + connectionCheckUpdateProducer.emitNext(result, Sinks.EmitFailureHandler.FAIL_FAST) + } + + override fun connectionAvailable(): ConnectionCheckResult.RestConnectionCheckResult { + return this.result + } } class GPasConnectionCheckService( private val restTemplate: RestTemplate, private val gPasConfigProperties: GPasConfigProperties, @param:Qualifier("connectionCheckUpdateProducer") - private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, ) : ConnectionCheckService { - private val logger = LoggerFactory.getLogger(javaClass) - private var result = ConnectionCheckResult.GPasConnectionCheckResult(false, Instant.now(), Instant.now()) - - @PostConstruct - @Scheduled(cron = "0 * * * * *") - fun check() { - result = try { - val uri = UriComponentsBuilder.fromUriString( - gPasConfigProperties.uri.toString()).path("/metadata").build().toUri() - - val headers = HttpHeaders() - headers.contentType = MediaType.APPLICATION_JSON - if (!gPasConfigProperties.username.isNullOrBlank() && !gPasConfigProperties.password.isNullOrBlank()) { - headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password) - } - - val statusCode = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity<Void>(headers), - Void::class.java - ).statusCode - val available = statusCode == HttpStatus.OK - - if (available.not()) { - logger.error("Invalid response code {}, expected HTTP status 200", statusCode) - } - ConnectionCheckResult.GPasConnectionCheckResult( - available, - Instant.now(), - if (result.available == available) { result.lastChange } else { Instant.now() } - ) + private val logger = LoggerFactory.getLogger(javaClass) + private var result = + ConnectionCheckResult.GPasConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = + try { + val uri = + UriComponentsBuilder.fromUriString(gPasConfigProperties.uri.toString()) + .path("/metadata") + .build() + .toUri() + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + if ( + !gPasConfigProperties.username.isNullOrBlank() && + !gPasConfigProperties.password.isNullOrBlank() + ) { + headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password) + } + + val statusCode = + restTemplate + .exchange(uri, HttpMethod.GET, HttpEntity<Void>(headers), Void::class.java) + .statusCode + val available = statusCode == HttpStatus.OK + + if (available.not()) { + logger.error("Invalid response code {}, expected HTTP status 200", statusCode) + } + ConnectionCheckResult.GPasConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { + result.lastChange + } else { + Instant.now() + }, + ) } catch (ex: Exception) { - logger.error("Connection-Check error: {}", ex.message) - ConnectionCheckResult.GPasConnectionCheckResult( - false, - Instant.now(), - if (!result.available) { result.lastChange } else { Instant.now() } - ) + logger.error("Connection-Check error: {}", ex.message) + ConnectionCheckResult.GPasConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { + result.lastChange + } else { + Instant.now() + }, + ) } - connectionCheckUpdateProducer.emitNext( - result, - Sinks.EmitFailureHandler.FAIL_FAST - ) - } - - override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult { - return this.result - } + connectionCheckUpdateProducer.emitNext(result, Sinks.EmitFailureHandler.FAIL_FAST) + } + + override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult { + return this.result + } } class GIcsConnectionCheckService( private val restTemplate: RestTemplate, private val gIcsConfigProperties: GIcsConfigProperties, @param:Qualifier("connectionCheckUpdateProducer") - private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, ) : ConnectionCheckService { - private val logger = LoggerFactory.getLogger(javaClass) - 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 statusCode = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity<Void>(headers), - Void::class.java - ).statusCode - val available = statusCode == HttpStatus.OK - - if (available.not()) { - logger.error("Invalid response code {}, expected HTTP status 200", statusCode) - } - ConnectionCheckResult.GIcsConnectionCheckResult( - available, - Instant.now(), - if (result.available == available) { result.lastChange } else { Instant.now() } - ) + private val logger = LoggerFactory.getLogger(javaClass) + 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 statusCode = + restTemplate + .exchange(uri, HttpMethod.GET, HttpEntity<Void>(headers), Void::class.java) + .statusCode + val available = statusCode == HttpStatus.OK + + if (available.not()) { + logger.error("Invalid response code {}, expected HTTP status 200", statusCode) + } + ConnectionCheckResult.GIcsConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { + result.lastChange + } else { + Instant.now() + }, + ) } catch (ex: Exception) { - logger.error("Connection-Check error: {}", ex.message) - ConnectionCheckResult.GIcsConnectionCheckResult( - false, - Instant.now(), - if (!result.available) { result.lastChange } else { Instant.now() } - ) + logger.error("Connection-Check error: {}", ex.message) + 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 - } + connectionCheckUpdateProducer.emitNext(result, Sinks.EmitFailureHandler.FAIL_FAST) + } + + override fun connectionAvailable(): ConnectionCheckResult.GIcsConnectionCheckResult { + return this.result + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt index dd5c44a..c54aa7a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt @@ -30,61 +30,53 @@ import dev.dnpm.etl.processor.monitoring.ReportService.Issue import dev.dnpm.etl.processor.monitoring.ReportService.Severity import java.util.* -class ReportService( - private val objectMapper: ObjectMapper -) { +class ReportService(private val objectMapper: ObjectMapper) { - fun deserialize(dataQualityReport: String?): List<Issue> { - if (dataQualityReport.isNullOrBlank()) { - return listOf() - } - return try { - objectMapper - .readValue(dataQualityReport, DataQualityReport::class.java) - .issues - .sortedBy { it.severity } - } catch (e: Exception) { - val otherIssue = - Issue(Severity.ERROR, "Not parsable data quality report '$dataQualityReport'") - return when (e) { - is JsonMappingException -> listOf(otherIssue) - is JsonParseException -> listOf(otherIssue) - else -> throw e - } - } + fun deserialize(dataQualityReport: String?): List<Issue> { + if (dataQualityReport.isNullOrBlank()) { + return listOf() } + return try { + objectMapper.readValue(dataQualityReport, DataQualityReport::class.java).issues.sortedBy { + it.severity + } + } catch (e: Exception) { + val otherIssue = + Issue(Severity.ERROR, "Not parsable data quality report '$dataQualityReport'") + return when (e) { + is JsonMappingException -> listOf(otherIssue) + is JsonParseException -> listOf(otherIssue) + else -> throw e + } + } + } + @JsonIgnoreProperties(ignoreUnknown = true) + private data class DataQualityReport( + @param:JsonProperty(value = "issues") val issues: List<Issue> + ) - @JsonIgnoreProperties(ignoreUnknown = true) - private data class DataQualityReport( - @param:JsonProperty(value = "issues") - val issues: List<Issue> - ) - - @JsonIgnoreProperties(ignoreUnknown = true) - data class Issue( - @param:JsonProperty(value = "severity") - val severity: Severity, - @param:JsonProperty(value = "message") - @param:JsonAlias("details") - val message: String, - @param:JsonProperty(value = "path") - val path: Optional<String> = Optional.empty() - ) + @JsonIgnoreProperties(ignoreUnknown = true) + data class Issue( + @param:JsonProperty(value = "severity") val severity: Severity, + @param:JsonProperty(value = "message") @param:JsonAlias("details") val message: String, + @param:JsonProperty(value = "path") val path: Optional<String> = Optional.empty(), + ) - enum class Severity(@JsonValue val value: String) { - FATAL("fatal"), - ERROR("error"), - WARNING("warning"), - INFO("info") - } + enum class Severity(@JsonValue val value: String) { + FATAL("fatal"), + ERROR("error"), + WARNING("warning"), + INFO("info"), + } } fun List<Issue>.asRequestStatus(): RequestStatus { - val severity = this.minOfOrNull { it.severity } - return when (severity) { - Severity.FATAL, Severity.ERROR -> RequestStatus.ERROR - Severity.WARNING -> RequestStatus.WARNING - else -> RequestStatus.SUCCESS - } -}
\ No newline at end of file + val severity = this.minOfOrNull { it.severity } + return when (severity) { + Severity.FATAL, + Severity.ERROR -> RequestStatus.ERROR + Severity.WARNING -> RequestStatus.WARNING + else -> RequestStatus.SUCCESS + } +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt index f2509dd..71731f1 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt @@ -20,6 +20,9 @@ package dev.dnpm.etl.processor.monitoring import dev.dnpm.etl.processor.* +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* import org.springframework.data.annotation.Id import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable @@ -29,9 +32,6 @@ import org.springframework.data.relational.core.mapping.Embedded import org.springframework.data.relational.core.mapping.Table import org.springframework.data.repository.CrudRepository import org.springframework.data.repository.PagingAndSortingRepository -import java.time.Instant -import java.time.temporal.ChronoUnit -import java.util.* @Table("request") data class Request( @@ -39,46 +39,38 @@ data class Request( val uuid: RequestId = randomRequestId(), val patientPseudonym: PatientPseudonym, val pid: PatientId, - @Column("fingerprint") - val fingerprint: Fingerprint, + @Column("fingerprint") val fingerprint: Fingerprint, val type: RequestType, var status: RequestStatus, var processedAt: Instant = Instant.now(), - @Embedded.Nullable var report: Report? = null + @Embedded.Nullable var report: Report? = null, ) { - constructor( - uuid: RequestId, - patientPseudonym: PatientPseudonym, - pid: PatientId, - fingerprint: Fingerprint, - type: RequestType, - status: RequestStatus - ) : - this(null, uuid, patientPseudonym, pid, fingerprint, type, status, Instant.now()) - - constructor( - uuid: RequestId, - patientPseudonym: PatientPseudonym, - pid: PatientId, - fingerprint: Fingerprint, - type: RequestType, - status: RequestStatus, - processedAt: Instant - ) : - this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt) - - fun isPendingUnknown(): Boolean { - return this.status == RequestStatus.UNKNOWN && this.processedAt.isBefore( - Instant.now().minus(10, ChronoUnit.MINUTES) - ) - } + constructor( + uuid: RequestId, + patientPseudonym: PatientPseudonym, + pid: PatientId, + fingerprint: Fingerprint, + type: RequestType, + status: RequestStatus, + ) : this(null, uuid, patientPseudonym, pid, fingerprint, type, status, Instant.now()) + + constructor( + uuid: RequestId, + patientPseudonym: PatientPseudonym, + pid: PatientId, + fingerprint: Fingerprint, + type: RequestType, + status: RequestStatus, + processedAt: Instant, + ) : this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt) + + fun isPendingUnknown(): Boolean { + return this.status == RequestStatus.UNKNOWN && + this.processedAt.isBefore(Instant.now().minus(10, ChronoUnit.MINUTES)) + } } -@JvmRecord -data class Report( - val description: String, - val dataQualityReport: String = "" -) +@JvmRecord data class Report(val description: String, val dataQualityReport: String = "") @JvmRecord data class CountedState( @@ -86,34 +78,41 @@ data class CountedState( val status: RequestStatus, ) -interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRepository<Request, Long> { - - fun findAllByPatientPseudonymOrderByProcessedAtDesc(patientId: PatientPseudonym): List<Request> - - fun findByUuidEquals(uuid: RequestId): Optional<Request> - - fun findRequestByPatientPseudonym(patientPseudonym: PatientPseudonym, pageable: Pageable): Page<Request> - - @Query("SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;") - fun countStates(): List<CountedState> - - @Query( - "SELECT count(*) AS count, status FROM (" + - "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " + - "WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " + - ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;" - ) - fun findPatientUniqueStates(): List<CountedState> - - @Query("SELECT count(*) AS count, status FROM request WHERE type = 'DELETE' GROUP BY status ORDER BY status, count DESC;") - fun countDeleteStates(): List<CountedState> - - @Query( - "SELECT count(*) AS count, status FROM (" + - "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " + - "WHERE type = 'DELETE'" + - ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;" - ) - fun findPatientUniqueDeleteStates(): List<CountedState> - +interface RequestRepository : + CrudRepository<Request, Long>, PagingAndSortingRepository<Request, Long> { + + fun findAllByPatientPseudonymOrderByProcessedAtDesc(patientId: PatientPseudonym): List<Request> + + fun findByUuidEquals(uuid: RequestId): Optional<Request> + + fun findRequestByPatientPseudonym( + patientPseudonym: PatientPseudonym, + pageable: Pageable, + ): Page<Request> + + @Query( + "SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;" + ) + fun countStates(): List<CountedState> + + @Query( + "SELECT count(*) AS count, status FROM (" + + "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " + + "WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " + + ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;" + ) + fun findPatientUniqueStates(): List<CountedState> + + @Query( + "SELECT count(*) AS count, status FROM request WHERE type = 'DELETE' GROUP BY status ORDER BY status, count DESC;" + ) + fun countDeleteStates(): List<CountedState> + + @Query( + "SELECT count(*) AS count, status FROM (" + + "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " + + "WHERE type = 'DELETE'" + + ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;" + ) + fun findPatientUniqueDeleteStates(): List<CountedState> } 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 0c8adb1..5487a05 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt @@ -19,11 +19,13 @@ package dev.dnpm.etl.processor.monitoring -enum class RequestStatus(val value: String) { +enum class RequestStatus( + val value: String, +) { SUCCESS("success"), WARNING("warning"), ERROR("error"), UNKNOWN("unknown"), DUPLICATION("duplication"), - NO_CONSENT("no-consent") -}
\ No newline at end of file + NO_CONSENT("no-consent"), +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestType.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestType.kt index cb43d7f..ef7f1e3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestType.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestType.kt @@ -19,7 +19,9 @@ package dev.dnpm.etl.processor.monitoring -enum class RequestType(val value: String) { +enum class RequestType( + val value: String, +) { MTB_FILE("mtb_file"), DELETE("delete"), -}
\ 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 ef46c0a..71e4a78 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt @@ -34,9 +34,8 @@ class KafkaMtbFileSender( private val kafkaTemplate: KafkaTemplate<String, String>, private val kafkaProperties: KafkaProperties, private val retryTemplate: RetryTemplate, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) : MtbFileSender { - private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java) override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response { @@ -50,11 +49,13 @@ class KafkaMtbFileSender( ) record.headers().add("requestId", request.requestId.value.toByteArray()) when (request) { - is DnpmV2MtbFileRequest -> record.headers() - .add( - "contentType", - CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray() - ) + is DnpmV2MtbFileRequest -> + record + .headers() + .add( + "contentType", + CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray(), + ) } val result = kafkaTemplate.send(record) @@ -72,9 +73,7 @@ class KafkaMtbFileSender( } override fun send(request: DeleteRequest): MtbFileSender.Response { - val dummyMtbFile = Mtb.builder() - .metadata(MvhMetadata()) - .build() + val dummyMtbFile = Mtb.builder().metadata(MvhMetadata()).build() return try { return retryTemplate.execute<MtbFileSender.Response, Exception> { @@ -83,11 +82,8 @@ class KafkaMtbFileSender( kafkaProperties.outputTopic, key(request), objectMapper.writeValueAsString( - DnpmV2MtbFileRequest( - request.requestId, - dummyMtbFile - ) - ) + DnpmV2MtbFileRequest(request.requestId, dummyMtbFile), + ), ) record.headers().add("requestId", request.requestId.value.toByteArray()) val result = kafkaTemplate.send(record) @@ -104,15 +100,14 @@ class KafkaMtbFileSender( } } - override fun endpoint(): String { - return "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})" - } + override fun endpoint(): String = + "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})" - private fun key(request: MtbRequest): String { - return when (request) { + private fun key(request: MtbRequest): String = + when (request) { is DnpmV2MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}" is DeleteRequest -> "{\"pid\": \"${request.patientId.value}\"}" - else -> throw IllegalArgumentException("Unsupported request type: ${request::class.simpleName}") + else -> + throw IllegalArgumentException("Unsupported request type: ${request::class.simpleName}") } - } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt index 285ce07..c81b572 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt @@ -29,18 +29,18 @@ interface MtbFileSender { fun endpoint(): String - data class Response(val status: RequestStatus, val body: String = "") + data class Response( + val status: RequestStatus, + val body: String = "", + ) } -fun Int.asRequestStatus(): RequestStatus { - return when (this) { +fun Int.asRequestStatus(): RequestStatus = + when (this) { 200 -> RequestStatus.SUCCESS 201 -> RequestStatus.WARNING - in 400 .. 999 -> RequestStatus.ERROR - else -> RequestStatus.UNKNOWN + in 400..999 -> RequestStatus.ERROR + else -> RequestStatus.UNKNOWN } -} -fun HttpStatusCode.asRequestStatus(): RequestStatus { - return this.value().asRequestStatus() -} +fun HttpStatusCode.asRequestStatus(): RequestStatus = this.value().asRequestStatus() diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt index 7512200..b228c4c 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt @@ -36,14 +36,12 @@ sealed interface MtbFileRequest<out T> : MtbRequest { data class DnpmV2MtbFileRequest( override val requestId: RequestId, - override val content: Mtb + override val content: Mtb, ) : MtbFileRequest<Mtb> { - override fun patientPseudonym(): PatientPseudonym { - return PatientPseudonym(content.patient.id) - } + override fun patientPseudonym(): PatientPseudonym = PatientPseudonym(content.patient.id) } data class DeleteRequest( override val requestId: RequestId, - val patientId: PatientPseudonym + val patientId: PatientPseudonym, ) : MtbRequest diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSender.kt index 1e6a5a7..5aad133 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSender.kt @@ -30,26 +30,22 @@ class RestDipMtbFileSender( restTemplate: RestTemplate, private val restTargetProperties: RestTargetProperties, retryTemplate: RetryTemplate, - reportService: ReportService + reportService: ReportService, ) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) { - - override fun sendUrl(): String { - return UriComponentsBuilder + override fun sendUrl(): String = + UriComponentsBuilder .fromUriString(restTargetProperties.uri.toString()) .pathSegment("mtb") .pathSegment("etl") .pathSegment("patient-record") .toUriString() - } - override fun deleteUrl(patientId: PatientPseudonym): String { - return UriComponentsBuilder + override fun deleteUrl(patientId: PatientPseudonym): String = + UriComponentsBuilder .fromUriString(restTargetProperties.uri.toString()) .pathSegment("mtb") .pathSegment("etl") .pathSegment("patient") .pathSegment(patientId.value) .toUriString() - } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt index ec6ff85..4120d4a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt @@ -38,9 +38,8 @@ abstract class RestMtbFileSender( private val restTemplate: RestTemplate, private val restTargetProperties: RestTargetProperties, private val retryTemplate: RetryTemplate, - private val reportService: ReportService + private val reportService: ReportService, ) : MtbFileSender { - private val logger = LoggerFactory.getLogger(RestMtbFileSender::class.java) abstract fun sendUrl(): String @@ -52,27 +51,29 @@ abstract class RestMtbFileSender( return retryTemplate.execute<MtbFileSender.Response, Exception> { val headers = getHttpHeaders(request) val entityReq = HttpEntity(request.content, headers) - val response = restTemplate.postForEntity( - sendUrl(), - entityReq, - String::class.java - ) + val response = restTemplate.postForEntity(sendUrl(), entityReq, String::class.java) if (!response.statusCode.is2xxSuccessful) { logger.warn("Error sending to remote system: {}", response.body) return@execute MtbFileSender.Response( reportService.deserialize(response.body).asRequestStatus(), - "Status-Code: ${response.statusCode.value()}" + "Status-Code: ${response.statusCode.value()}", ) } logger.debug("Sent file via RestMtbFileSender") - return@execute MtbFileSender.Response(reportService.deserialize(response.body).asRequestStatus(), response.body.orEmpty()) + return@execute MtbFileSender.Response( + reportService.deserialize(response.body).asRequestStatus(), + response.body.orEmpty(), + ) } } catch (e: IllegalArgumentException) { logger.error("Not a valid URI to export to: '{}'", restTargetProperties.uri!!) } catch (e: RestClientResponseException) { logger.info(restTargetProperties.uri!!.toString()) logger.error("Request data not accepted by remote system", e) - return MtbFileSender.Response(reportService.deserialize(e.responseBodyAsString).asRequestStatus(), e.responseBodyAsString) + return MtbFileSender.Response( + reportService.deserialize(e.responseBodyAsString).asRequestStatus(), + e.responseBodyAsString, + ) } return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung") } @@ -82,11 +83,7 @@ abstract class RestMtbFileSender( return retryTemplate.execute<MtbFileSender.Response, Exception> { val headers = getHttpHeaders(request) val entityReq = HttpEntity(null, headers) - restTemplate.delete( - deleteUrl(request.patientId), - entityReq, - String::class.java - ) + restTemplate.delete(deleteUrl(request.patientId), entityReq, String::class.java) logger.debug("Sent file via RestMtbFileSender") return@execute MtbFileSender.Response(RequestStatus.SUCCESS) } @@ -99,18 +96,17 @@ abstract class RestMtbFileSender( return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung") } - override fun endpoint(): String { - return this.restTargetProperties.uri.orEmpty() - } + override fun endpoint(): String = this.restTargetProperties.uri.orEmpty() private fun getHttpHeaders(request: MtbRequest): HttpHeaders { val username = restTargetProperties.username val password = restTargetProperties.password val headers = HttpHeaders() - headers.contentType = when (request) { - is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON - else -> MediaType.APPLICATION_JSON - } + headers.contentType = + when (request) { + is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON + else -> MediaType.APPLICATION_JSON + } if (username.isNullOrBlank() || password.isNullOrBlank()) { return headers @@ -119,5 +115,4 @@ abstract class RestMtbFileSender( headers.setBasicAuth(username, password) return headers } - } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt index dcb438f..90d867f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt @@ -24,24 +24,17 @@ import org.apache.commons.codec.digest.DigestUtils import java.security.SecureRandom class AnonymizingGenerator : Generator { - companion object fun getSecureRandom() : SecureRandom { - return SecureRandom() - } + companion object - override fun generate(id: String): String { - return Base32().encodeAsString(DigestUtils.sha256(id)) - .substring(0..41) - .lowercase() - } + fun getSecureRandom(): SecureRandom = SecureRandom() + + override fun generate(id: String): String = Base32().encodeAsString(DigestUtils.sha256(id)).substring(0..41).lowercase() @OptIn(ExperimentalStdlibApi::class) override fun generateGenomDeTan(id: String): String { - val bytes = ByteArray(64 / 2) getSecureRandom().nextBytes(bytes) return bytes.joinToString("") { "%02x".format(it) } - } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt index 8215d23..089736c 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt @@ -8,19 +8,15 @@ class GpasSoapPseudonymGenerator( private val gpasCfg: GPasConfigProperties, private val retryTemplate: RetryTemplate, private val gpasSoapService: GpasSoapService, - private val appFhirConfig: AppFhirConfig + private val appFhirConfig: AppFhirConfig, ) : Generator { - - override fun generate(id: String): String { - return retryTemplate.execute<String, Exception> { + override fun generate(id: String): String = + retryTemplate.execute<String, Exception> { gpasSoapService.getOrCreatePseudonymFor(id, gpasCfg.patientDomain) } - } - override fun generateGenomDeTan(id: String): String { - return retryTemplate.execute<String, Exception> { + override fun generateGenomDeTan(id: String): String = + retryTemplate.execute<String, Exception> { gpasSoapService.createPseudonymsFor(id, gpasCfg.genomDeTanDomain, 1).first() } - } } - diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt index 0909924..f1121b8 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt @@ -8,15 +8,14 @@ import jakarta.xml.bind.annotation.XmlElementWrapper @WebService( name = "PSNManagerBeanService", - targetNamespace ="http://psn.ttp.ganimed.icmvc.emau.org/" + targetNamespace = "http://psn.ttp.ganimed.icmvc.emau.org/", ) interface GpasSoapService { - @WebMethod(operationName = "getOrCreatePseudonymFor") @WebResult(name = "psn") fun getOrCreatePseudonymFor( @WebParam(name = "value") value: String, - @WebParam(name = "domainName") domainName: String + @WebParam(name = "domainName") domainName: String, ): String @WebMethod(operationName = "createPseudonymsFor") @@ -25,7 +24,6 @@ interface GpasSoapService { fun createPseudonymsFor( @WebParam(name = "value") value: String, @WebParam(name = "domainName") domainName: String, - @WebParam(name = "number") minNumber: Int + @WebParam(name = "number") minNumber: Int, ): List<String> - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt index 96225a9..77ab87d 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt @@ -25,22 +25,16 @@ import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties class PseudonymizeService( private val generator: Generator, - private val configProperties: PseudonymizeConfigProperties + private val configProperties: PseudonymizeConfigProperties, ) { - - fun patientPseudonym(patientId: PatientId): PatientPseudonym { - return when (generator) { + fun patientPseudonym(patientId: PatientId): PatientPseudonym = + when (generator) { is GpasPseudonymGenerator -> PatientPseudonym(generator.generate(patientId.value)) - else -> PatientPseudonym("${configProperties.prefix}_${generator.generate(patientId.value)}") + else -> + PatientPseudonym("${configProperties.prefix}_${generator.generate(patientId.value)}") } - } - - fun genomDeTan(patientId: PatientId): String { - return generator.generateGenomDeTan(patientId.value) - } - fun prefix(): String { - return configProperties.prefix - } + fun genomDeTan(patientId: PatientId): String = generator.generateGenomDeTan(patientId.value) -}
\ No newline at end of file + fun prefix(): String = configProperties.prefix +} 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 8721cbe..48ac58a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt @@ -25,282 +25,246 @@ 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 - * - * @since 0.11.0 +/** + * Replaces patient ID with generated patient pseudonym * * @param pseudonymizeService The pseudonymizeService to be used * @return The MTB file containing patient pseudonymes + * @since 0.11.0 */ infix fun Mtb.pseudonymizeWith(pseudonymizeService: PseudonymizeService) { - val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value - - this.episodesOfCare?.forEach { it.patient?.id = patientPseudonym } - this.carePlans?.forEach { - it.patient.id = patientPseudonym - it.rebiopsyRequests?.forEach { it.patient?.id = patientPseudonym } - it.histologyReevaluationRequests?.forEach { it.patient?.id = patientPseudonym } - it.medicationRecommendations?.forEach { it.patient?.id = patientPseudonym } - it.studyEnrollmentRecommendations?.forEach { it.patient?.id = patientPseudonym } - it.procedureRecommendations?.forEach { it.patient?.id = patientPseudonym } - it.geneticCounselingRecommendation?.patient?.id = patientPseudonym - } - this.diagnoses?.forEach { it.patient?.id = patientPseudonym } - this.guidelineTherapies?.forEach { it.patient?.id = patientPseudonym } - this.guidelineProcedures?.forEach { it.patient?.id = patientPseudonym } - this.patient.id = patientPseudonym - this.claims?.forEach { it.patient?.id = patientPseudonym } - this.claimResponses?.forEach { it.patient?.id = patientPseudonym } - this.diagnoses?.forEach { it.patient?.id = patientPseudonym } - this.familyMemberHistories?.forEach { it.patient?.id = patientPseudonym } - this.histologyReports?.forEach { - it.patient.id = patientPseudonym - it.results.tumorMorphology?.patient?.id = patientPseudonym - it.results.tumorCellContent?.patient?.id = patientPseudonym - } - this.ngsReports?.forEach { - it.patient?.id = patientPseudonym - it.results?.simpleVariants?.forEach { it.patient?.id = patientPseudonym } - it.results?.copyNumberVariants?.forEach { it.patient?.id = patientPseudonym } - it.results?.dnaFusions?.forEach { it.patient?.id = patientPseudonym } - it.results?.rnaFusions?.forEach { it.patient?.id = patientPseudonym } - it.results?.tumorCellContent?.patient?.id = patientPseudonym - it.results?.brcaness?.patient?.id = patientPseudonym - it.results?.tmb?.patient?.id = patientPseudonym - it.results?.hrdScore?.patient?.id = patientPseudonym - } - this.ihcReports?.forEach { - it.patient?.id = patientPseudonym - it.results?.msiMmr?.forEach { it.patient?.id = patientPseudonym } - it.results?.proteinExpression?.forEach { it.patient?.id = patientPseudonym } - } - this.responses?.forEach { it.patient?.id = patientPseudonym } - this.specimens?.forEach { it.patient?.id = patientPseudonym } - this.priorDiagnosticReports?.forEach { it.patient?.id = patientPseudonym } - this.performanceStatus?.forEach { it.patient?.id = patientPseudonym } - this.systemicTherapies?.forEach { - it.history?.forEach { - it.patient?.id = patientPseudonym - } - } - this.followUps?.forEach { - it.patient?.id = patientPseudonym - } - - this.msiFindings?.forEach { it -> it.patient.id = patientPseudonym } - - this.metadata?.researchConsents?.forEach { it -> - val entry = it ?: return@forEach - if (entry.contains("patient")) { - // here we expect only a patient reference any other data like display - // need to be removed, since may contain unsecure data - entry.remove("patient") - entry["patient"] = mapOf("reference" to "Patient/$patientPseudonym") - } + val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value + + this.episodesOfCare?.forEach { it.patient?.id = patientPseudonym } + this.carePlans?.forEach { + it.patient.id = patientPseudonym + it.rebiopsyRequests?.forEach { it.patient?.id = patientPseudonym } + it.histologyReevaluationRequests?.forEach { it.patient?.id = patientPseudonym } + it.medicationRecommendations?.forEach { it.patient?.id = patientPseudonym } + it.studyEnrollmentRecommendations?.forEach { it.patient?.id = patientPseudonym } + it.procedureRecommendations?.forEach { it.patient?.id = patientPseudonym } + it.geneticCounselingRecommendation?.patient?.id = patientPseudonym + } + this.diagnoses?.forEach { it.patient?.id = patientPseudonym } + this.guidelineTherapies?.forEach { it.patient?.id = patientPseudonym } + this.guidelineProcedures?.forEach { it.patient?.id = patientPseudonym } + this.patient.id = patientPseudonym + this.claims?.forEach { it.patient?.id = patientPseudonym } + this.claimResponses?.forEach { it.patient?.id = patientPseudonym } + this.diagnoses?.forEach { it.patient?.id = patientPseudonym } + this.familyMemberHistories?.forEach { it.patient?.id = patientPseudonym } + this.histologyReports?.forEach { + it.patient.id = patientPseudonym + it.results.tumorMorphology?.patient?.id = patientPseudonym + it.results.tumorCellContent?.patient?.id = patientPseudonym + } + this.ngsReports?.forEach { + it.patient?.id = patientPseudonym + it.results?.simpleVariants?.forEach { it.patient?.id = patientPseudonym } + it.results?.copyNumberVariants?.forEach { it.patient?.id = patientPseudonym } + it.results?.dnaFusions?.forEach { it.patient?.id = patientPseudonym } + it.results?.rnaFusions?.forEach { it.patient?.id = patientPseudonym } + it.results?.tumorCellContent?.patient?.id = patientPseudonym + it.results?.brcaness?.patient?.id = patientPseudonym + it.results?.tmb?.patient?.id = patientPseudonym + it.results?.hrdScore?.patient?.id = patientPseudonym + } + this.ihcReports?.forEach { + it.patient?.id = patientPseudonym + it.results?.msiMmr?.forEach { it.patient?.id = patientPseudonym } + it.results?.proteinExpression?.forEach { it.patient?.id = patientPseudonym } + } + this.responses?.forEach { it.patient?.id = patientPseudonym } + this.specimens?.forEach { it.patient?.id = patientPseudonym } + this.priorDiagnosticReports?.forEach { it.patient?.id = patientPseudonym } + this.performanceStatus?.forEach { it.patient?.id = patientPseudonym } + this.systemicTherapies?.forEach { it.history?.forEach { it.patient?.id = patientPseudonym } } + this.followUps?.forEach { it.patient?.id = patientPseudonym } + + this.msiFindings?.forEach { it -> it.patient.id = patientPseudonym } + + this.metadata?.researchConsents?.forEach { it -> + val entry = it ?: return@forEach + if (entry.contains("patient")) { + // here we expect only a patient reference any other data like display + // need to be removed, since may contain unsecure data + entry.remove("patient") + entry["patient"] = mapOf("reference" to "Patient/$patientPseudonym") } + } } /** * Creates new hash of content IDs with given prefix except for patient IDs * - * @since 0.11.0 - * * @param pseudonymizeService The pseudonymizeService to be used * @return The MTB file containing rehashed content IDs + * @since 0.11.0 */ infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) { - val prefix = pseudonymizeService.prefix() - - fun anonymize(id: String): String { - val hash = DigestUtils.sha256Hex("$prefix-$id").substring(0, 41).lowercase() - return "$prefix$hash" - } - - this.episodesOfCare?.forEach { - it?.apply { id = id?.let(::anonymize) } - it.diagnoses?.forEach { it -> - it?.id = it.id?.let(::anonymize) - } - } - - this.carePlans?.onEach { carePlan -> - carePlan?.apply { - this.id = id?.let { anonymize(it) } - - this.geneticCounselingRecommendation?.apply { - this.id = this.id?.let(::anonymize) - } - this.rebiopsyRequests?.forEach { it -> - it.id = it.id?.let(::anonymize) - it.tumorEntity?.id = it.tumorEntity?.id?.let(::anonymize) - } - this.histologyReevaluationRequests?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.specimen?.id = it.specimen?.id?.let(::anonymize) - } - - this.medicationRecommendations?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.supportingVariants?.forEach { it -> - it.variant?.id = it.variant?.id?.let(::anonymize) - } - it.reason?.id = it.reason?.id?.let(::anonymize) - } - this.reason?.id = this.reason?.id?.let(::anonymize) - this.studyEnrollmentRecommendations?.forEach { it -> - it?.reason?.id = it.reason?.id?.let(::anonymize) - } - this.procedureRecommendations?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.supportingVariants?.forEach { it -> - it.variant?.id = it.variant?.id?.let(::anonymize) - } - - it.reason?.id = it.reason?.id?.let(::anonymize) - - } - this.studyEnrollmentRecommendations?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.supportingVariants.forEach { it -> - it.variant?.id = it?.variant?.id?.let(::anonymize) - } - } - } - } - - - this.responses?.forEach { it -> - - it?.id = it.id?.let(::anonymize) - it?.therapy?.id = it.therapy?.id?.let(::anonymize) - - } - - this.diagnoses?.forEach { it -> - + val prefix = pseudonymizeService.prefix() + + fun anonymize(id: String): String { + val hash = DigestUtils.sha256Hex("$prefix-$id").substring(0, 41).lowercase() + return "$prefix$hash" + } + + this.episodesOfCare?.forEach { + it?.apply { id = id?.let(::anonymize) } + it.diagnoses?.forEach { it -> it?.id = it.id?.let(::anonymize) } + } + + this.carePlans?.onEach { carePlan -> + carePlan?.apply { + this.id = id?.let { anonymize(it) } + + this.geneticCounselingRecommendation?.apply { this.id = this.id?.let(::anonymize) } + this.rebiopsyRequests?.forEach { it -> + it.id = it.id?.let(::anonymize) + it.tumorEntity?.id = it.tumorEntity?.id?.let(::anonymize) + } + this.histologyReevaluationRequests?.forEach { it -> it.id = it?.id?.let(::anonymize) - it.histology?.forEach { it -> it.id = it?.id?.let(::anonymize) } - } - - this.ngsReports?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.results?.tumorCellContent?.id = it.results.tumorCellContent?.id?.let(::anonymize) - it.results?.tumorCellContent?.specimen?.id = - it.results?.tumorCellContent?.specimen?.id?.let(::anonymize) - it.results?.rnaFusions?.forEach { it -> - it?.id = it.id?.let(::anonymize) - } - it.results?.simpleVariants?.forEach { it -> - it?.id = it.id?.let(::anonymize) - it?.transcriptId?.value = it.transcriptId?.value?.let(::anonymize) - } - it.results?.tmb?.id = it.results?.tmb?.id?.let(::anonymize) - it.results?.tmb?.specimen?.id = it.results?.tmb?.specimen?.id?.let(::anonymize) - - it.results?.brcaness?.id = it.results?.brcaness?.id?.let(::anonymize) - it.results?.brcaness?.specimen?.id = it.results?.brcaness?.specimen?.id?.let(::anonymize) - it.results?.copyNumberVariants?.forEach { it -> it?.id = it.id?.let(::anonymize) } - it.results?.hrdScore?.id = it.results?.hrdScore?.id?.let(::anonymize) - it.results?.hrdScore?.specimen?.id = it.results?.hrdScore?.specimen?.id?.let(::anonymize) - it.results?.rnaSeqs?.forEach { it -> it?.id = it.id?.let(::anonymize) } - it.results?.dnaFusions?.forEach { it -> it?.id = it.id?.let(::anonymize) } - it.specimen?.id = it?.specimen?.id?.let(::anonymize) - - } - - this.histologyReports?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.results?.tumorCellContent?.id = it.results?.tumorCellContent?.id?.let(::anonymize) - it.results?.tumorCellContent?.specimen?.id = - it.results?.tumorCellContent?.specimen?.id?.let(::anonymize) - - it.results?.tumorMorphology?.id = it.results?.tumorMorphology?.id?.let(::anonymize) - it.results?.tumorMorphology?.specimen?.id = - it.results?.tumorMorphology?.specimen?.id?.let(::anonymize) it.specimen?.id = it.specimen?.id?.let(::anonymize) + } - } - this.claimResponses?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.claim?.id = it.claim?.id?.let(::anonymize) - } - this.claims?.forEach { it -> - - it.id = it?.id?.let(::anonymize) - it.recommendation?.id = it.recommendation?.id?.let(::anonymize) - - } - this.familyMemberHistories?.forEach { it -> it.id = it?.id?.let(::anonymize) } - this.guidelineProcedures?.forEach { it -> + this.medicationRecommendations?.forEach { it -> it.id = it?.id?.let(::anonymize) + it.supportingVariants?.forEach { it -> it.variant?.id = it.variant?.id?.let(::anonymize) } it.reason?.id = it.reason?.id?.let(::anonymize) - it.basedOn?.id = it.basedOn?.id?.let(::anonymize) - - } - - this.guidelineTherapies?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.reason?.id = it.reason?.id?.let(::anonymize) - it.basedOn?.id = it.basedOn?.id?.let(::anonymize) - } - this.ihcReports?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.specimen?.id = it.specimen?.id?.let(::anonymize) - it.results?.proteinExpression?.forEach { it -> it?.id = it.id.let(::anonymize) } - } - - this.msiFindings?.forEach { it -> - + } + this.reason?.id = this.reason?.id?.let(::anonymize) + this.studyEnrollmentRecommendations?.forEach { it -> + it?.reason?.id = it.reason?.id?.let(::anonymize) + } + this.procedureRecommendations?.forEach { it -> it.id = it?.id?.let(::anonymize) - it.specimen?.id = it.specimen?.id?.let(::anonymize) - } - - this.performanceStatus?.forEach { it -> it.id = it?.id?.let(::anonymize) } - - this.priorDiagnosticReports?.forEach { it -> + it.supportingVariants?.forEach { it -> it.variant?.id = it.variant?.id?.let(::anonymize) } + it.reason?.id = it.reason?.id?.let(::anonymize) + } + this.studyEnrollmentRecommendations?.forEach { it -> it.id = it?.id?.let(::anonymize) - it.specimen?.id = it.specimen?.id?.let(::anonymize) + it.supportingVariants.forEach { it -> it.variant?.id = it?.variant?.id?.let(::anonymize) } + } } - - this.specimens?.forEach { it -> - - it.id = it?.id?.let(::anonymize) - it.diagnosis?.id = it.diagnosis?.id?.let(::anonymize) - + } + + this.responses?.forEach { it -> + it?.id = it.id?.let(::anonymize) + it?.therapy?.id = it.therapy?.id?.let(::anonymize) + } + + this.diagnoses?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.histology?.forEach { it -> it.id = it?.id?.let(::anonymize) } + } + + this.ngsReports?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.results?.tumorCellContent?.id = it.results.tumorCellContent?.id?.let(::anonymize) + it.results?.tumorCellContent?.specimen?.id = + it.results?.tumorCellContent?.specimen?.id?.let(::anonymize) + it.results?.rnaFusions?.forEach { it -> it?.id = it.id?.let(::anonymize) } + it.results?.simpleVariants?.forEach { it -> + it?.id = it.id?.let(::anonymize) + it?.transcriptId?.value = it.transcriptId?.value?.let(::anonymize) } - - this.systemicTherapies?.forEach { it -> - - it.history?.forEach { it -> - - it.id = it?.id?.let(::anonymize) - it.reason?.id = it.reason?.id?.let(::anonymize) - it.basedOn?.id = it.basedOn?.id?.let(::anonymize) - } - + it.results?.tmb?.id = it.results?.tmb?.id?.let(::anonymize) + it.results?.tmb?.specimen?.id = it.results?.tmb?.specimen?.id?.let(::anonymize) + + it.results?.brcaness?.id = it.results?.brcaness?.id?.let(::anonymize) + it.results?.brcaness?.specimen?.id = it.results?.brcaness?.specimen?.id?.let(::anonymize) + it.results?.copyNumberVariants?.forEach { it -> it?.id = it.id?.let(::anonymize) } + it.results?.hrdScore?.id = it.results?.hrdScore?.id?.let(::anonymize) + it.results?.hrdScore?.specimen?.id = it.results?.hrdScore?.specimen?.id?.let(::anonymize) + it.results?.rnaSeqs?.forEach { it -> it?.id = it.id?.let(::anonymize) } + it.results?.dnaFusions?.forEach { it -> it?.id = it.id?.let(::anonymize) } + it.specimen?.id = it?.specimen?.id?.let(::anonymize) + } + + this.histologyReports?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.results?.tumorCellContent?.id = it.results?.tumorCellContent?.id?.let(::anonymize) + it.results?.tumorCellContent?.specimen?.id = + it.results?.tumorCellContent?.specimen?.id?.let(::anonymize) + + it.results?.tumorMorphology?.id = it.results?.tumorMorphology?.id?.let(::anonymize) + it.results?.tumorMorphology?.specimen?.id = + it.results?.tumorMorphology?.specimen?.id?.let(::anonymize) + it.specimen?.id = it.specimen?.id?.let(::anonymize) + } + this.claimResponses?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.claim?.id = it.claim?.id?.let(::anonymize) + } + this.claims?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.recommendation?.id = it.recommendation?.id?.let(::anonymize) + } + this.familyMemberHistories?.forEach { it -> it.id = it?.id?.let(::anonymize) } + this.guidelineProcedures?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.reason?.id = it.reason?.id?.let(::anonymize) + it.basedOn?.id = it.basedOn?.id?.let(::anonymize) + } + + this.guidelineTherapies?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.reason?.id = it.reason?.id?.let(::anonymize) + it.basedOn?.id = it.basedOn?.id?.let(::anonymize) + } + this.ihcReports?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.specimen?.id = it.specimen?.id?.let(::anonymize) + it.results?.proteinExpression?.forEach { it -> it?.id = it.id.let(::anonymize) } + } + + this.msiFindings?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.specimen?.id = it.specimen?.id?.let(::anonymize) + } + + this.performanceStatus?.forEach { it -> it.id = it?.id?.let(::anonymize) } + + this.priorDiagnosticReports?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.specimen?.id = it.specimen?.id?.let(::anonymize) + } + + this.specimens?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.diagnosis?.id = it.diagnosis?.id?.let(::anonymize) + } + + this.systemicTherapies?.forEach { it -> + it.history?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.reason?.id = it.reason?.id?.let(::anonymize) + it.basedOn?.id = it.basedOn?.id?.let(::anonymize) } + } } 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() - } + // 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() + } } infix fun Mtb.addGenomDeTan(pseudonymizeService: PseudonymizeService) { - this.metadata?.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id)) + this.metadata?.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id)) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt index 44b04e8..fdaa7d2 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt @@ -20,6 +20,8 @@ package dev.dnpm.etl.processor.security import jakarta.annotation.PostConstruct +import java.time.Instant +import java.util.* import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table import org.springframework.data.repository.CrudRepository @@ -27,57 +29,50 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.security.core.userdetails.User import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.provisioning.InMemoryUserDetailsManager -import java.time.Instant -import java.util.* class TokenService( private val userDetailsManager: InMemoryUserDetailsManager, private val passwordEncoder: PasswordEncoder, - private val tokenRepository: TokenRepository + private val tokenRepository: TokenRepository, ) { - @PostConstruct - fun setup() { - tokenRepository.findAll().forEach { - userDetailsManager.createUser( - User.withUsername(it.username) - .password(it.password) - .roles("MTBFILE") - .build() - ) - } + @PostConstruct + fun setup() { + tokenRepository.findAll().forEach { + userDetailsManager.createUser( + User.withUsername(it.username).password(it.password).roles("MTBFILE").build() + ) } + } - fun addToken(name: String): Result<String> { - val username = name.lowercase().replace("""[^a-z0-9]""".toRegex(), "") - if (userDetailsManager.userExists(username)) { - return Result.failure(RuntimeException("Cannot use token name")) - } + fun addToken(name: String): Result<String> { + val username = name.lowercase().replace("""[^a-z0-9]""".toRegex(), "") + if (userDetailsManager.userExists(username)) { + return Result.failure(RuntimeException("Cannot use token name")) + } - val password = Base64.getEncoder().encodeToString(UUID.randomUUID().toString().encodeToByteArray()) - val encodedPassword = passwordEncoder.encode(password).toString() + val password = + Base64.getEncoder().encodeToString(UUID.randomUUID().toString().encodeToByteArray()) + val encodedPassword = passwordEncoder.encode(password).toString() - userDetailsManager.createUser( - User.withUsername(username) - .password(encodedPassword) - .roles("MTBFILE") - .build() - ) + userDetailsManager.createUser( + User.withUsername(username).password(encodedPassword).roles("MTBFILE").build() + ) - tokenRepository.save(Token(name = name, username = username, password = encodedPassword)) + tokenRepository.save(Token(name = name, username = username, password = encodedPassword)) - return Result.success("$username:$password") - } + return Result.success("$username:$password") + } - fun deleteToken(id: Long) { - val token = tokenRepository.findByIdOrNull(id) ?: return - userDetailsManager.deleteUser(token.username) - tokenRepository.delete(token) - } + fun deleteToken(id: Long) { + val token = tokenRepository.findByIdOrNull(id) ?: return + userDetailsManager.deleteUser(token.username) + tokenRepository.delete(token) + } - fun findAll(): List<Token> { - return tokenRepository.findAll().toList() - } + fun findAll(): List<Token> { + return tokenRepository.findAll().toList() + } } @Table("token") @@ -86,7 +81,7 @@ data class Token( val name: String, val username: String, val password: String, - val createdAt: Instant = Instant.now() + val createdAt: Instant = Instant.now(), ) -interface TokenRepository : CrudRepository<Token, Long>
\ No newline at end of file +interface TokenRepository : CrudRepository<Token, Long> diff --git a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRole.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRole.kt index a1d45c8..bfe966a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRole.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRole.kt @@ -20,26 +20,21 @@ package dev.dnpm.etl.processor.security +import java.util.* import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table import org.springframework.data.repository.CrudRepository -import java.util.* @Table("user_role") -data class UserRole( - @Id val id: Long? = null, - val username: String, - var role: Role = Role.GUEST -) +data class UserRole(@Id val id: Long? = null, val username: String, var role: Role = Role.GUEST) enum class Role(val value: String) { - GUEST("guest"), - USER("user"), - ADMIN("admin") + GUEST("guest"), + USER("user"), + ADMIN("admin"), } interface UserRoleRepository : CrudRepository<UserRole, Long> { - fun findByUsername(username: String): Optional<UserRole> - -}
\ No newline at end of file + fun findByUsername(username: String): Optional<UserRole> +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt index 174f8a9..bf46b84 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt @@ -25,9 +25,12 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser class UserRoleService( private val userRoleRepository: UserRoleRepository, - private val sessionRegistry: SessionRegistry + private val sessionRegistry: SessionRegistry, ) { - fun updateUserRole(id: Long, role: Role) { + fun updateUserRole( + id: Long, + role: Role, + ) { val userRole = userRoleRepository.findByIdOrNull(id) ?: return userRole.role = role userRoleRepository.save(userRole) @@ -40,19 +43,13 @@ class UserRoleService( expireSessionFor(userRole.username) } - fun findAll(): List<UserRole> { - return userRoleRepository.findAll().toList() - } + fun findAll(): List<UserRole> = userRoleRepository.findAll().toList() private fun expireSessionFor(username: String) { sessionRegistry.allPrincipals .filterIsInstance<OidcUser>() .filter { it.preferredUsername == username } - .flatMap { - sessionRegistry.getAllSessions(it, true) - } - .onEach { - it.expireNow() - } + .flatMap { sessionRegistry.getAllSessions(it, true) } + .onEach { it.expireNow() } } -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt index b420d1f..8437962 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt @@ -11,6 +11,10 @@ import dev.dnpm.etl.processor.consent.IConsentService import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized import dev.pcvolkmer.mv64e.mtb.* +import java.io.IOException +import java.time.Clock +import java.time.Instant +import java.util.* import org.apache.commons.lang3.NotImplementedException import org.hl7.fhir.r4.model.* import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent @@ -19,10 +23,6 @@ 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( @@ -30,248 +30,271 @@ class ConsentProcessor( private val gIcsConfigProperties: GIcsConfigProperties, private val objectMapper: ObjectMapper, private val fhirContext: FhirContext, - private val consentService: IConsentService + private val consentService: IConsentService, ) { - private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor") - - /** - * In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked. - * - * Logic: - * * <c>true</c> IF consent check is disabled. - * * <c>true</c> IF broad consent (BC) has been given. - * * <c>true</c> BC has been asked AND declined but genomDe consent has been consented. - * * ELSE <c>false</c> is returned. - * - * @param mtbFile File v2 (will be enriched with consent data) - * @return true if consent is given - * - */ - fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean { - if (consentService is MtbFileConsentService) { - // consent check is disabled - return true - } - - mtbFile.ensureMetaDataIsInitialized() - - val personIdentifierValue = mtbFile.patient.id - val requestDate = Date.from(Instant.now(Clock.systemUTC())) + private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor") + + /** + * In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked. + * + * Logic: + * * <c>true</c> IF consent check is disabled. + * * <c>true</c> IF broad consent (BC) has been given. + * * <c>true</c> BC has been asked AND declined but genomDe consent has been consented. + * * ELSE <c>false</c> is returned. + * + * @param mtbFile File v2 (will be enriched with consent data) + * @return true if consent is given + */ + fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean { + if (consentService is MtbFileConsentService) { + // consent check is disabled + return true + } - // 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 + mtbFile.ensureMetaDataIsInitialized() - /* - * broad consent - */ - val broadConsent = consentService.getConsent( - personIdentifierValue, requestDate, ConsentDomain.BROAD_CONSENT - ) - val broadConsentHasBeenAsked = broadConsent.entry.isNotEmpty() + val personIdentifierValue = mtbFile.patient.id + val requestDate = Date.from(Instant.now(Clock.systemUTC())) - // fast exit - if patient has not been asked, we can skip and exit - if (!broadConsentHasBeenAsked) return false + // 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 - val genomeDeConsent = consentService.getConsent( - personIdentifierValue, requestDate, ConsentDomain.MODELLVORHABEN_64E + /* + * broad consent + */ + val broadConsent = + consentService.getConsent(personIdentifierValue, requestDate, ConsentDomain.BROAD_CONSENT) + val broadConsentHasBeenAsked = broadConsent.entry.isNotEmpty() + + // fast exit - if patient has not been asked, we can skip and exit + if (!broadConsentHasBeenAsked) return false + + val genomeDeConsent = + consentService.getConsent( + personIdentifierValue, + requestDate, + ConsentDomain.MODELLVORHABEN_64E, ) - addGenomeDbProvisions(mtbFile, genomeDeConsent) + addGenomeDbProvisions(mtbFile, genomeDeConsent) - if (genomeDeConsent.entry.isNotEmpty()) setGenomDeSubmissionType(mtbFile) + if (genomeDeConsent.entry.isNotEmpty()) setGenomDeSubmissionType(mtbFile) - embedBroadConsentResources(mtbFile, broadConsent) + embedBroadConsentResources(mtbFile, broadConsent) - val broadConsentStatus = getProvisionTypeByPolicyCode( - broadConsent, requestDate, ConsentDomain.BROAD_CONSENT - ) - - val genomDeSequencingStatus = getProvisionTypeByPolicyCode( - genomeDeConsent, requestDate, ConsentDomain.MODELLVORHABEN_64E - ) + val broadConsentStatus = + getProvisionTypeByPolicyCode(broadConsent, requestDate, ConsentDomain.BROAD_CONSENT) - if (Consent.ConsentProvisionType.NULL == broadConsentStatus) { - // bc not asked - return false - } - if (Consent.ConsentProvisionType.PERMIT == broadConsentStatus || Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus) return true + val genomDeSequencingStatus = + getProvisionTypeByPolicyCode(genomeDeConsent, requestDate, ConsentDomain.MODELLVORHABEN_64E) - return false + if (Consent.ConsentProvisionType.NULL == broadConsentStatus) { + // bc not asked + return false } - - fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) { - for (entry in broadConsent.entry) { - val resource = entry.resource - if (resource is Consent) { - // since jackson convertValue does not work here, - // we need another step to back to string, before we convert to object map - val asJsonString = fhirContext.newJsonParser().encodeResourceToString(resource) - try { - val mapOfJson: HashMap<String?, Any?>? = - objectMapper.readValue<HashMap<String?, Any?>?>( - asJsonString, object : TypeReference<HashMap<String?, Any?>?>() {}) - mtbFile.metadata.researchConsents.add(mapOfJson) - } catch (e: JsonProcessingException) { - throw RuntimeException(e) - } - } + if ( + Consent.ConsentProvisionType.PERMIT == broadConsentStatus || + Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus + ) + return true + + return false + } + + fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) { + for (entry in broadConsent.entry) { + val resource = entry.resource + if (resource is Consent) { + // since jackson convertValue does not work here, + // we need another step to back to string, before we convert to object map + val asJsonString = fhirContext.newJsonParser().encodeResourceToString(resource) + try { + val mapOfJson: HashMap<String?, Any?>? = + objectMapper.readValue<HashMap<String?, Any?>?>( + asJsonString, + object : TypeReference<HashMap<String?, Any?>?>() {}, + ) + mtbFile.metadata.researchConsents.add(mapOfJson) + } catch (e: JsonProcessingException) { + throw RuntimeException(e) } + } } - - fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) { - for (entry in consentGnomeDe.entry) { - val resource = entry.resource - if (resource !is Consent) { - continue - } - - // We expect only one provision in collection, therefore get first or none - val provisions = resource.provision.provision - if (provisions.isEmpty()) { - continue - } - - val provisionComponent: ProvisionComponent = provisions.first() - val provisionCode = getProvisionCode(provisionComponent) - if (provisionCode != null) { - try { - val modelProjectConsentPurpose = - ModelProjectConsentPurpose.forValue(provisionCode) - - if (ModelProjectConsentPurpose.SEQUENCING == modelProjectConsentPurpose) { - // CONVENTION: wrapping date is date of SEQUENCING consent - mtbFile.metadata.modelProjectConsent.date = resource.dateTime - } - - val provision = Provision.builder() - .type(ConsentProvision.valueOf(provisionComponent.type.name)) - .date(provisionComponent.period.start) - .purpose(modelProjectConsentPurpose).build() - - mtbFile.metadata.modelProjectConsent.provisions.add(provision) - } catch (ioe: IOException) { - logger.error( - "Provision code '$provisionCode' is unknown and cannot be mapped.", - ioe.toString() - ) - } - } - - if (mtbFile.metadata.modelProjectConsent.provisions.isNotEmpty()) { - mtbFile.metadata.modelProjectConsent.version = - gIcsConfigProperties.genomeDeConsentVersion - } + } + + fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) { + for (entry in consentGnomeDe.entry) { + val resource = entry.resource + if (resource !is Consent) { + continue + } + + // We expect only one provision in collection, therefore get first or none + val provisions = resource.provision.provision + if (provisions.isEmpty()) { + continue + } + + val provisionComponent: ProvisionComponent = provisions.first() + val provisionCode = getProvisionCode(provisionComponent) + if (provisionCode != null) { + try { + val modelProjectConsentPurpose = ModelProjectConsentPurpose.forValue(provisionCode) + + if (ModelProjectConsentPurpose.SEQUENCING == modelProjectConsentPurpose) { + // CONVENTION: wrapping date is date of SEQUENCING consent + mtbFile.metadata.modelProjectConsent.date = resource.dateTime + } + + val provision = + Provision.builder() + .type(ConsentProvision.valueOf(provisionComponent.type.name)) + .date(provisionComponent.period.start) + .purpose(modelProjectConsentPurpose) + .build() + + mtbFile.metadata.modelProjectConsent.provisions.add(provision) + } catch (ioe: IOException) { + logger.error( + "Provision code '$provisionCode' is unknown and cannot be mapped.", + ioe.toString(), + ) } - } + } - private fun getProvisionCode(provisionComponent: ProvisionComponent): String? { - var provisionCode: String? = null - if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) { - val codableConcept: CodeableConcept = provisionComponent.code.first() - if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) { - provisionCode = codableConcept.coding.first().code - } - } - return provisionCode + if (mtbFile.metadata.modelProjectConsent.provisions.isNotEmpty()) { + mtbFile.metadata.modelProjectConsent.version = gIcsConfigProperties.genomeDeConsentVersion + } } - - private fun setGenomDeSubmissionType(mtbFile: Mtb) { - if (appConfigProperties.genomDeTestSubmission) { - mtbFile.metadata.type = MvhSubmissionType.TEST - logger.info("genomeDe submission mit TEST") - } else { - mtbFile.metadata.type = when (mtbFile.metadata.type) { - null -> MvhSubmissionType.INITIAL - else -> mtbFile.metadata.type - } - } + } + + private fun getProvisionCode(provisionComponent: ProvisionComponent): String? { + var provisionCode: String? = null + if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) { + val codableConcept: CodeableConcept = provisionComponent.code.first() + if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) { + provisionCode = codableConcept.coding.first().code + } } - - /** - * @param consentBundle consent resource - * @param requestDate date which must be within validation period of provision - * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found. - */ - fun getProvisionTypeByPolicyCode( - consentBundle: Bundle, requestDate: Date?, consentDomain: ConsentDomain - ): Consent.ConsentProvisionType { - val code: String? - val system: String? - if (ConsentDomain.BROAD_CONSENT == consentDomain) { - code = gIcsConfigProperties.broadConsentPolicyCode - system = gIcsConfigProperties.broadConsentPolicySystem - } else if (ConsentDomain.MODELLVORHABEN_64E == consentDomain) { - code = gIcsConfigProperties.genomeDePolicyCode - system = gIcsConfigProperties.genomeDePolicySystem - } else { - throw NotImplementedException("unknown consent domain " + consentDomain.name) - } - - val provisionTypeByPolicyCode = getProvisionTypeByPolicyCode( - consentBundle, code, system, requestDate - ) - return provisionTypeByPolicyCode + return provisionCode + } + + private fun setGenomDeSubmissionType(mtbFile: Mtb) { + if (appConfigProperties.genomDeTestSubmission) { + mtbFile.metadata.type = MvhSubmissionType.TEST + logger.info("genomeDe submission mit TEST") + } else { + mtbFile.metadata.type = + when (mtbFile.metadata.type) { + null -> MvhSubmissionType.INITIAL + else -> mtbFile.metadata.type + } + } + } + + /** + * @param consentBundle consent resource + * @param requestDate date which must be within validation period of provision + * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if + * none is found. + */ + fun getProvisionTypeByPolicyCode( + consentBundle: Bundle, + requestDate: Date?, + consentDomain: ConsentDomain, + ): Consent.ConsentProvisionType { + val code: String? + val system: String? + if (ConsentDomain.BROAD_CONSENT == consentDomain) { + code = gIcsConfigProperties.broadConsentPolicyCode + system = gIcsConfigProperties.broadConsentPolicySystem + } else if (ConsentDomain.MODELLVORHABEN_64E == consentDomain) { + code = gIcsConfigProperties.genomeDePolicyCode + system = gIcsConfigProperties.genomeDePolicySystem + } else { + throw NotImplementedException("unknown consent domain " + consentDomain.name) } - /** - * @param consentBundle consent resource - * @param targetCode policyRule and provision code value - * @param targetSystem policyRule and provision system value - * @param requestDate date which must be within validation period of provision - * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found. - */ - fun getProvisionTypeByPolicyCode( - consentBundle: Bundle, targetCode: String?, targetSystem: String?, requestDate: Date? - ): Consent.ConsentProvisionType { - val entriesOfInterest = consentBundle.entry.filter { entry -> - val isConsentResource = - entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent - val consentIsActive = (entry.resource as Consent).status == ConsentState.ACTIVE - - val provisions = (entry.resource as Consent).provision.provision - - val isValidCoding = checkProvisionExist( - targetCode, targetSystem, provisions - ) - - isConsentResource && consentIsActive && isValidCoding && isRequestDateInRange(requestDate, (entry.resource as Consent).provision.period) - }.map { entry: BundleEntryComponent -> - val consent = (entry.getResource() as Consent) - consent.provision.provision.filter { subProvision -> - isRequestDateInRange(requestDate, subProvision.period) - // search coding entries of current provision for code and system - subProvision.code.map { c -> c.coding }.flatten().any { code -> - targetCode.equals(code.code) && targetSystem.equals(code.system) - } - }.map { subProvision -> - subProvision + val provisionTypeByPolicyCode = + getProvisionTypeByPolicyCode(consentBundle, code, system, requestDate) + return provisionTypeByPolicyCode + } + + /** + * @param consentBundle consent resource + * @param targetCode policyRule and provision code value + * @param targetSystem policyRule and provision system value + * @param requestDate date which must be within validation period of provision + * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if + * none is found. + */ + fun getProvisionTypeByPolicyCode( + consentBundle: Bundle, + targetCode: String?, + targetSystem: String?, + requestDate: Date?, + ): Consent.ConsentProvisionType { + val entriesOfInterest = + consentBundle.entry + .filter { entry -> + val isConsentResource = + entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent + val consentIsActive = (entry.resource as Consent).status == ConsentState.ACTIVE + + val provisions = (entry.resource as Consent).provision.provision + + val isValidCoding = checkProvisionExist(targetCode, targetSystem, provisions) + + isConsentResource && + consentIsActive && + isValidCoding && + isRequestDateInRange(requestDate, (entry.resource as Consent).provision.period) } - }.flatten() + .map { entry: BundleEntryComponent -> + val consent = (entry.getResource() as Consent) + consent.provision.provision + .filter { subProvision -> + isRequestDateInRange(requestDate, subProvision.period) + // search coding entries of current provision for code and system + subProvision.code + .map { c -> c.coding } + .flatten() + .any { code -> + targetCode.equals(code.code) && targetSystem.equals(code.system) + } + } + .map { subProvision -> subProvision } + } + .flatten() - if (entriesOfInterest.isNotEmpty()) { - return entriesOfInterest.first().type - } - return Consent.ConsentProvisionType.NULL + if (entriesOfInterest.isNotEmpty()) { + return entriesOfInterest.first().type } - - fun checkProvisionExist( - researchAllowedPolicyOid: String?, - researchAllowedPolicySystem: String?, - provisions: Collection<ProvisionComponent> - ): Boolean { - return provisions.any { provision -> - provision.code.any { codeableConcept -> codeableConcept.coding.any { it.system == researchAllowedPolicySystem && it.code == researchAllowedPolicyOid } } + return Consent.ConsentProvisionType.NULL + } + + fun checkProvisionExist( + researchAllowedPolicyOid: String?, + researchAllowedPolicySystem: String?, + provisions: Collection<ProvisionComponent>, + ): Boolean { + return provisions.any { provision -> + provision.code.any { codeableConcept -> + codeableConcept.coding.any { + it.system == researchAllowedPolicySystem && it.code == researchAllowedPolicyOid } + } } + } - fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean { - val isRequestDateAfterOrEqualStart = provPeriod.start.compareTo(requestDate) - val isRequestDateBeforeOrEqualEnd = provPeriod.end.compareTo(requestDate) - return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0 - } - + fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean { + val isRequestDateAfterOrEqualStart = provPeriod.start.compareTo(requestDate) + val isRequestDateBeforeOrEqualEnd = provPeriod.end.compareTo(requestDate) + return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0 + } } 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 07d8a8d..4721f75 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -38,14 +38,14 @@ import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith import dev.pcvolkmer.mv64e.mtb.ConsentProvision import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose import dev.pcvolkmer.mv64e.mtb.Mtb +import java.time.Instant +import java.util.* 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.time.Instant -import java.util.* @Service class RequestProcessor( @@ -56,163 +56,178 @@ class RequestProcessor( private val objectMapper: ObjectMapper, private val applicationEventPublisher: ApplicationEventPublisher, private val appConfigProperties: AppConfigProperties, - private val consentProcessor: ConsentProcessor? + private val consentProcessor: ConsentProcessor?, ) { - private var logger: Logger = LoggerFactory.getLogger("RequestProcessor") - - fun processMtbFile(mtbFile: Mtb) { - processMtbFile(mtbFile, randomRequestId()) + private var logger: Logger = LoggerFactory.getLogger("RequestProcessor") + + fun processMtbFile(mtbFile: Mtb) { + processMtbFile(mtbFile, randomRequestId()) + } + + fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { + val pid = PatientId(extractPatientIdentifier(mtbFile)) + + val isConsentOk = + consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || + consentProcessor == null + if (isConsentOk) { + if (isGenomDeConsented(mtbFile)) { + mtbFile addGenomDeTan pseudonymizeService + } + mtbFile pseudonymizeWith pseudonymizeService + mtbFile anonymizeContentWith pseudonymizeService + val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) + saveAndSend(request, pid) + } else { + logger.warn("consent check failed file will not be processed further!") + applicationEventPublisher.publishEvent( + ResponseEvent(requestId, Instant.now(), RequestStatus.NO_CONSENT) + ) } + } - - fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { - val pid = PatientId(extractPatientIdentifier(mtbFile)) - - val isConsentOk = - consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null - if (isConsentOk) { - if (isGenomDeConsented(mtbFile)) { - mtbFile addGenomDeTan pseudonymizeService - } - mtbFile pseudonymizeWith pseudonymizeService - mtbFile anonymizeContentWith pseudonymizeService - val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) - saveAndSend(request, pid) - } else { - logger.warn("consent check failed file will not be processed further!") - applicationEventPublisher.publishEvent( - ResponseEvent( - requestId, Instant.now(), RequestStatus.NO_CONSENT - ) - ) - } - } - - private fun isGenomDeConsented(mtbFile: Mtb): Boolean { - val isModelProjectConsented = mtbFile.metadata?.modelProjectConsent?.provisions?.any { p -> - p.purpose == ModelProjectConsentPurpose.SEQUENCING && p.type == ConsentProvision.PERMIT + private fun isGenomDeConsented(mtbFile: Mtb): Boolean { + val isModelProjectConsented = + mtbFile.metadata?.modelProjectConsent?.provisions?.any { p -> + p.purpose == ModelProjectConsentPurpose.SEQUENCING && p.type == ConsentProvision.PERMIT } == true - return isModelProjectConsented - } - - private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) { - requestService.save( - Request( - request.requestId, - request.patientPseudonym(), - pid, - fingerprint(request), - RequestType.MTB_FILE, - RequestStatus.UNKNOWN - ) + return isModelProjectConsented + } + + private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) { + requestService.save( + Request( + request.requestId, + request.patientPseudonym(), + pid, + fingerprint(request), + RequestType.MTB_FILE, + RequestStatus.UNKNOWN, ) + ) - if (appConfigProperties.duplicationDetection && isDuplication(request)) { - applicationEventPublisher.publishEvent( - ResponseEvent( - request.requestId, Instant.now(), RequestStatus.DUPLICATION - ) - ) - return - } - - val responseStatus = sender.send(request) - - applicationEventPublisher.publishEvent( - ResponseEvent( - request.requestId, - Instant.now(), - responseStatus.status, - when (responseStatus.status) { - RequestStatus.ERROR, RequestStatus.WARNING -> Optional.of(responseStatus.body) - else -> Optional.empty() - } - ) - ) + if (appConfigProperties.duplicationDetection && isDuplication(request)) { + applicationEventPublisher.publishEvent( + ResponseEvent(request.requestId, Instant.now(), RequestStatus.DUPLICATION) + ) + return } - private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean { - val patientPseudonym = when (pseudonymizedMtbFileRequest) { - is DnpmV2MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id) - } - - val lastMtbFileRequestForPatient = - requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) - val isLastRequestDeletion = - requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) - - return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint( - pseudonymizedMtbFileRequest + val responseStatus = sender.send(request) + + applicationEventPublisher.publishEvent( + ResponseEvent( + request.requestId, + Instant.now(), + responseStatus.status, + when (responseStatus.status) { + RequestStatus.ERROR, + RequestStatus.WARNING -> Optional.of(responseStatus.body) + else -> Optional.empty() + }, ) - } - - fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) { - processDeletion(patientId, randomRequestId(), isConsented) - } - - 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, - patientPseudonym, - patientId, - fingerprint(patientPseudonym.value), - RequestType.DELETE, - requestStatus - ) - ) - - val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym)) - - applicationEventPublisher.publishEvent( - ResponseEvent( - requestId, Instant.now(), responseStatus.status, when (responseStatus.status) { - RequestStatus.WARNING, RequestStatus.ERROR -> Optional.of(responseStatus.body) - else -> Optional.empty() - } - ) - ) - - } catch (_: Exception) { - requestService.save( - Request( - uuid = requestId, - patientPseudonym = emptyPatientPseudonym(), - pid = patientId, - fingerprint = Fingerprint.empty(), - status = RequestStatus.ERROR, - type = RequestType.DELETE, - report = Report("Fehler bei der Pseudonymisierung") - ) - ) + ) + } + + private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean { + val patientPseudonym = + when (pseudonymizedMtbFileRequest) { + is DnpmV2MtbFileRequest -> + PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id) } - } - private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint { - return when (request) { - is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content)) - } + val lastMtbFileRequestForPatient = + requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) + val isLastRequestDeletion = + requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) + + return null != lastMtbFileRequestForPatient && + !isLastRequestDeletion && + lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest) + } + + fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) { + processDeletion(patientId, randomRequestId(), isConsented) + } + + 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, + patientPseudonym, + patientId, + fingerprint(patientPseudonym.value), + RequestType.DELETE, + requestStatus, + ) + ) + + val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym)) + + applicationEventPublisher.publishEvent( + ResponseEvent( + requestId, + Instant.now(), + responseStatus.status, + when (responseStatus.status) { + RequestStatus.WARNING, + RequestStatus.ERROR -> Optional.of(responseStatus.body) + else -> Optional.empty() + }, + ) + ) + } catch (_: Exception) { + requestService.save( + Request( + uuid = requestId, + patientPseudonym = emptyPatientPseudonym(), + pid = patientId, + fingerprint = Fingerprint.empty(), + status = RequestStatus.ERROR, + type = RequestType.DELETE, + report = Report("Fehler bei der Pseudonymisierung"), + ) + ) } + } - private fun fingerprint(s: String): Fingerprint { - return Fingerprint( - Base32().encodeAsString(DigestUtils.sha256(s)).replace("=", "").lowercase() - ) + private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint { + return when (request) { + is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content)) } - + } + + private fun fingerprint(s: String): Fingerprint { + return Fingerprint( + 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/RequestService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt index 757b353..e7cb95f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt @@ -22,55 +22,63 @@ package dev.dnpm.etl.processor.services import dev.dnpm.etl.processor.PatientPseudonym import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.monitoring.* +import java.util.* import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service -import java.util.* @Service -class RequestService( - private val requestRepository: RequestRepository -) { +class RequestService(private val requestRepository: RequestRepository) { - fun save(request: Request) = requestRepository.save(request) + fun save(request: Request) = requestRepository.save(request) - fun findAll(): Iterable<Request> = requestRepository.findAll() + fun findAll(): Iterable<Request> = requestRepository.findAll() - fun findAll(pageable: Pageable): Page<Request> = requestRepository.findAll(pageable) + fun findAll(pageable: Pageable): Page<Request> = requestRepository.findAll(pageable) - fun findByUuid(uuid: RequestId): Optional<Request> = - requestRepository.findByUuidEquals(uuid) + fun findByUuid(uuid: RequestId): Optional<Request> = requestRepository.findByUuidEquals(uuid) - fun findRequestByPatientId(patientPseudonym: PatientPseudonym, pageable: Pageable): Page<Request> = requestRepository.findRequestByPatientPseudonym(patientPseudonym, pageable) + fun findRequestByPatientId( + patientPseudonym: PatientPseudonym, + pageable: Pageable, + ): Page<Request> = requestRepository.findRequestByPatientPseudonym(patientPseudonym, pageable) - fun allRequestsByPatientPseudonym(patientPseudonym: PatientPseudonym) = requestRepository - .findAllByPatientPseudonymOrderByProcessedAtDesc(patientPseudonym) + fun allRequestsByPatientPseudonym(patientPseudonym: PatientPseudonym) = + requestRepository.findAllByPatientPseudonymOrderByProcessedAtDesc(patientPseudonym) - fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: PatientPseudonym) = - Companion.lastMtbFileRequestForPatientPseudonym(allRequestsByPatientPseudonym(patientPseudonym)) + fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: PatientPseudonym) = + Companion.lastMtbFileRequestForPatientPseudonym( + allRequestsByPatientPseudonym(patientPseudonym) + ) - fun isLastRequestWithKnownStatusDeletion(patientPseudonym: PatientPseudonym) = - Companion.isLastRequestWithKnownStatusDeletion(allRequestsByPatientPseudonym(patientPseudonym)) + fun isLastRequestWithKnownStatusDeletion(patientPseudonym: PatientPseudonym) = + Companion.isLastRequestWithKnownStatusDeletion( + allRequestsByPatientPseudonym(patientPseudonym) + ) - fun countStates(): Iterable<CountedState> = requestRepository.countStates() + fun countStates(): Iterable<CountedState> = requestRepository.countStates() - fun countDeleteStates(): Iterable<CountedState> = requestRepository.countDeleteStates() + fun countDeleteStates(): Iterable<CountedState> = requestRepository.countDeleteStates() - fun findPatientUniqueStates(): List<CountedState> = requestRepository.findPatientUniqueStates() + fun findPatientUniqueStates(): List<CountedState> = requestRepository.findPatientUniqueStates() - fun findPatientUniqueDeleteStates(): List<CountedState> = requestRepository.findPatientUniqueDeleteStates() + fun findPatientUniqueDeleteStates(): List<CountedState> = + requestRepository.findPatientUniqueDeleteStates() - companion object { + companion object { - fun lastMtbFileRequestForPatientPseudonym(allRequests: List<Request>) = allRequests + fun lastMtbFileRequestForPatientPseudonym(allRequests: List<Request>) = + allRequests .filter { it.type == RequestType.MTB_FILE } .sortedByDescending { it.processedAt } - .firstOrNull { it.status == RequestStatus.SUCCESS || it.status == RequestStatus.WARNING } + .firstOrNull { + it.status == RequestStatus.SUCCESS || it.status == RequestStatus.WARNING + } - fun isLastRequestWithKnownStatusDeletion(allRequests: List<Request>) = allRequests + fun isLastRequestWithKnownStatusDeletion(allRequests: List<Request>) = + allRequests .filter { it.status != RequestStatus.UNKNOWN } - .maxByOrNull { it.processedAt }?.type == RequestType.DELETE - - } - -}
\ No newline at end of file + .maxByOrNull { it.processedAt } + ?.type == RequestType.DELETE + } +} 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 fb82647..190cefe 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt @@ -22,79 +22,76 @@ package dev.dnpm.etl.processor.services import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.monitoring.Report import dev.dnpm.etl.processor.monitoring.RequestStatus +import java.time.Instant +import java.util.* import org.slf4j.LoggerFactory import org.springframework.context.event.EventListener import org.springframework.stereotype.Service import reactor.core.publisher.Sinks -import java.time.Instant -import java.util.* @Service class ResponseProcessor( private val requestService: RequestService, - private val statisticsUpdateProducer: Sinks.Many<Any> + private val statisticsUpdateProducer: Sinks.Many<Any>, ) { - private val logger = LoggerFactory.getLogger(ResponseProcessor::class.java) + private val logger = LoggerFactory.getLogger(ResponseProcessor::class.java) - @EventListener(classes = [ResponseEvent::class]) - fun handleResponseEvent(event: ResponseEvent) { - requestService.findByUuid(event.requestUuid).ifPresentOrElse({ - it.processedAt = event.timestamp - it.status = event.status + @EventListener(classes = [ResponseEvent::class]) + fun handleResponseEvent(event: ResponseEvent) { + requestService + .findByUuid(event.requestUuid) + .ifPresentOrElse( + { + it.processedAt = event.timestamp + it.status = event.status - when (event.status) { + when (event.status) { RequestStatus.SUCCESS -> { - it.report = Report( - "Keine Probleme erkannt", - ) + it.report = + Report( + "Keine Probleme erkannt", + ) } RequestStatus.WARNING -> { - it.report = Report( - "Warnungen über mangelhafte Daten", - event.body.orElse("") - ) + it.report = Report("Warnungen über mangelhafte Daten", event.body.orElse("")) } RequestStatus.ERROR -> { - it.report = Report( - "Fehler bei der Datenübertragung oder Inhalt nicht verarbeitbar", - event.body.orElse("") - ) + it.report = + Report( + "Fehler bei der Datenübertragung oder Inhalt nicht verarbeitbar", + event.body.orElse(""), + ) } RequestStatus.DUPLICATION -> { - it.report = Report( - "Duplikat erkannt" - ) + it.report = Report("Duplikat erkannt") } RequestStatus.NO_CONSENT -> { - it.report = Report( - "Einwilligung Status fehlt, widerrufen oder ungeklärt." - ) + it.report = Report("Einwilligung Status fehlt, widerrufen oder ungeklärt.") } else -> { - logger.error("Cannot process response: Unknown response!") - return@ifPresentOrElse + logger.error("Cannot process response: Unknown response!") + return@ifPresentOrElse } - } - - requestService.save(it) + } - statisticsUpdateProducer.emitNext("", Sinks.EmitFailureHandler.FAIL_FAST) - }, { - logger.error("Response for unknown request '${event.requestUuid}'!") - }) - } + requestService.save(it) + statisticsUpdateProducer.emitNext("", Sinks.EmitFailureHandler.FAIL_FAST) + }, + { logger.error("Response for unknown request '${event.requestUuid}'!") }, + ) + } } data class ResponseEvent( val requestUuid: RequestId, val timestamp: Instant, val status: RequestStatus, - val body: Optional<String> = Optional.empty() -)
\ No newline at end of file + val body: Optional<String> = Optional.empty(), +) diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/TransformationService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/TransformationService.kt index 8f1081e..df8ac3d 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/TransformationService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/TransformationService.kt @@ -24,8 +24,10 @@ import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.PathNotFoundException import dev.pcvolkmer.mv64e.mtb.Mtb -class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) { - +class TransformationService( + private val objectMapper: ObjectMapper, + private val transformations: List<Transformation>, +) { fun transform(mtbFile: Mtb): Mtb { val json = transform(objectMapper.writeValueAsString(mtbFile)) return objectMapper.readValue(json, Mtb::class.java) @@ -41,12 +43,24 @@ class TransformationService(private val objectMapper: ObjectMapper, private val val before = transformation.path.substringBeforeLast(".") val last = transformation.path.substringAfterLast(".") - val existingValue = if (transformation.existingValue is Number) transformation.existingValue else transformation.existingValue.toString() - val newValue = if (transformation.newValue is Number) transformation.newValue else transformation.newValue.toString() - - jsonPath.set("$.$before.[?]$last", newValue, { - it.item(HashMap::class.java)[last] == existingValue - }) + val existingValue = + if (transformation.existingValue is Number) { + transformation.existingValue + } else { + transformation.existingValue.toString() + } + val newValue = + if (transformation.newValue is Number) { + transformation.newValue + } else { + transformation.newValue.toString() + } + + jsonPath.set( + "$.$before.[?]$last", + newValue, + { it.item(HashMap::class.java)[last] == existingValue }, + ) } catch (e: PathNotFoundException) { // Ignore } @@ -57,35 +71,30 @@ class TransformationService(private val objectMapper: ObjectMapper, private val return json } - fun getTransformations(): List<Transformation> { - return this.transformations - } - + fun getTransformations(): List<Transformation> = this.transformations } -class Transformation private constructor(val path: String) { - - lateinit var existingValue: Any - private set - lateinit var newValue: Any - private set - - infix fun from(value: Any): Transformation { - this.existingValue = value - return this - } +class Transformation + private constructor( + val path: String, + ) { + lateinit var existingValue: Any + private set - infix fun to(value: Any): Transformation { - this.newValue = value - return this - } + lateinit var newValue: Any + private set - companion object { + infix fun from(value: Any): Transformation { + this.existingValue = value + return this + } - fun of(path: String): Transformation { - return Transformation(path) + infix fun to(value: Any): Transformation { + this.newValue = value + return this } + companion object { + fun of(path: String): Transformation = Transformation(path) + } } - -} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt index fd6a9b4..e70f1e7 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt @@ -26,56 +26,64 @@ import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.output.asRequestStatus import dev.dnpm.etl.processor.services.ResponseEvent +import java.time.Instant +import java.util.* import org.apache.kafka.clients.consumer.ConsumerRecord import org.slf4j.LoggerFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.kafka.listener.MessageListener -import java.time.Instant -import java.util.* class KafkaResponseProcessor( private val eventPublisher: ApplicationEventPublisher, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) : MessageListener<String, String> { - private val logger = LoggerFactory.getLogger(KafkaResponseProcessor::class.java) + private val logger = LoggerFactory.getLogger(KafkaResponseProcessor::class.java) - override fun onMessage(data: ConsumerRecord<String, String>) { - try { - Optional.of(objectMapper.readValue(data.value(), ResponseBody::class.java)) + override fun onMessage(data: ConsumerRecord<String, String>) { + try { + Optional.of(objectMapper.readValue(data.value(), ResponseBody::class.java)) } catch (e: Exception) { - logger.error("Cannot process Kafka response", e) - Optional.empty() - }.ifPresentOrElse({ responseBody -> - val event = ResponseEvent( - RequestId(responseBody.requestId), - Instant.ofEpochMilli(data.timestamp()), - responseBody.statusCode.asRequestStatus(), - when (responseBody.statusCode.asRequestStatus()) { - RequestStatus.SUCCESS -> { - Optional.empty() - } - - RequestStatus.WARNING, RequestStatus.ERROR -> { - Optional.of(objectMapper.writeValueAsString(responseBody.statusBody)) - } + logger.error("Cannot process Kafka response", e) + Optional.empty() + } + .ifPresentOrElse( + { responseBody -> + val event = + ResponseEvent( + RequestId(responseBody.requestId), + Instant.ofEpochMilli(data.timestamp()), + responseBody.statusCode.asRequestStatus(), + when (responseBody.statusCode.asRequestStatus()) { + RequestStatus.SUCCESS -> { + Optional.empty() + } - else -> { - logger.error("Kafka response: Unknown response code '{}'!", responseBody.statusCode) - Optional.empty() - } - } - ) - eventPublisher.publishEvent(event) - }, { - logger.error("No requestId in Kafka response") - }) - } + RequestStatus.WARNING, + RequestStatus.ERROR -> { + Optional.of(objectMapper.writeValueAsString(responseBody.statusBody)) + } - data class ResponseBody( - @param:JsonProperty("request_id") @param:JsonAlias("requestId") val requestId: String, - @param:JsonProperty("status_code") @param:JsonAlias("statusCode") val statusCode: Int, - @param:JsonProperty("status_body") @param:JsonAlias("statusBody") val statusBody: Map<String, Any> - ) + else -> { + logger.error( + "Kafka response: Unknown response code '{}'!", + responseBody.statusCode, + ) + Optional.empty() + } + }, + ) + eventPublisher.publishEvent(event) + }, + { logger.error("No requestId in Kafka response") }, + ) + } -}
\ No newline at end of file + data class ResponseBody( + @param:JsonProperty("request_id") @param:JsonAlias("requestId") val requestId: String, + @param:JsonProperty("status_code") @param:JsonAlias("statusCode") val statusCode: Int, + @param:JsonProperty("status_body") + @param:JsonAlias("statusBody") + val statusBody: Map<String, Any>, + ) +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/types.kt b/src/main/kotlin/dev/dnpm/etl/processor/types.kt index 90fa7cb..c7aa110 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/types.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/types.kt @@ -19,33 +19,30 @@ package dev.dnpm.etl.processor -import org.springframework.http.MediaType import java.util.* +import org.springframework.http.MediaType class Fingerprint(val value: String) { - override fun hashCode() = value.hashCode() + override fun hashCode() = value.hashCode() - override fun equals(other: Any?) = other is Fingerprint && other.value == value + override fun equals(other: Any?) = other is Fingerprint && other.value == value - companion object { - fun empty() = Fingerprint("") - } + companion object { + fun empty() = Fingerprint("") + } } @JvmInline value class RequestId(val value: String) { - fun isBlank() = value.isBlank() - + fun isBlank() = value.isBlank() } fun randomRequestId() = RequestId(UUID.randomUUID().toString()) -@JvmInline -value class PatientId(val value: String) +@JvmInline value class PatientId(val value: String) -@JvmInline -value class PatientPseudonym(val value: String) +@JvmInline value class PatientPseudonym(val value: String) fun emptyPatientPseudonym() = PatientPseudonym("") @@ -55,9 +52,9 @@ fun emptyPatientPseudonym() = PatientPseudonym("") * @since 0.11.0 */ object CustomMediaType { - val APPLICATION_VND_DNPM_V2_MTB_JSON = MediaType("application", "vnd.dnpm.v2.mtb+json") - const val APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE = "application/vnd.dnpm.v2.mtb+json" + val APPLICATION_VND_DNPM_V2_MTB_JSON = MediaType("application", "vnd.dnpm.v2.mtb+json") + const val APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE = "application/vnd.dnpm.v2.mtb+json" - val APPLICATION_VND_DNPM_V2_RD_JSON = MediaType("application", "vnd.dnpm.v2.rd+json") - const val APPLICATION_VND_DNPM_V2_RD_JSON_VALUE = "application/vnd.dnpm.v2.rd+json" + val APPLICATION_VND_DNPM_V2_RD_JSON = MediaType("application", "vnd.dnpm.v2.rd+json") + const val APPLICATION_VND_DNPM_V2_RD_JSON_VALUE = "application/vnd.dnpm.v2.rd+json" } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ApplicationControllerAdvice.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ApplicationControllerAdvice.kt index bdca57e..0dec30e 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ApplicationControllerAdvice.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ApplicationControllerAdvice.kt @@ -27,11 +27,7 @@ import org.springframework.web.bind.annotation.ResponseStatus @ControllerAdvice class ApplicationControllerAdvice { - @ExceptionHandler(NotFoundException::class) @ResponseStatus(HttpStatus.NOT_FOUND) - fun handleNotFoundException(e: NotFoundException): String { - return "errors/404" - } - -}
\ No newline at end of file + fun handleNotFoundException(e: NotFoundException): String = "errors/404" +} 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 44571d4..b77bdf9 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt @@ -23,11 +23,11 @@ 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 -import dev.dnpm.etl.processor.security.UserRole import dev.dnpm.etl.processor.security.Token import dev.dnpm.etl.processor.security.TokenService -import dev.dnpm.etl.processor.services.TransformationService +import dev.dnpm.etl.processor.security.UserRole import dev.dnpm.etl.processor.security.UserRoleService +import dev.dnpm.etl.processor.services.TransformationService import org.springframework.beans.factory.annotation.Qualifier import org.springframework.http.MediaType import org.springframework.http.codec.ServerSentEvent @@ -47,175 +47,193 @@ class ConfigController( private val mtbFileSender: MtbFileSender, private val connectionCheckServices: List<ConnectionCheckService>, private val tokenService: TokenService?, - private val userRoleService: UserRoleService? + private val userRoleService: UserRoleService?, ) { - @GetMapping - fun index(model: Model): String { - val outputConnectionAvailable = - connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().firstOrNull()?.connectionAvailable() - - 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()) - } else { - model.addAttribute("tokens", emptyList<Token>()) - } - model.addAttribute("transformations", transformationService.getTransformations()) - if (userRoleService != null) { - model.addAttribute("userRolesEnabled", true) - model.addAttribute("userRoles", userRoleService.findAll()) - } else { - model.addAttribute("userRolesEnabled", false) - model.addAttribute("userRoles", emptyList<UserRole>()) - } - return "configs" + @GetMapping + fun index(model: Model): String { + val outputConnectionAvailable = + connectionCheckServices + .filterIsInstance<OutputConnectionCheckService>() + .firstOrNull() + ?.connectionAvailable() + + 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()) + } else { + model.addAttribute("tokens", emptyList<Token>()) } - - @GetMapping(params = ["outputConnectionAvailable"]) - fun outputConnectionAvailable(model: Model): String { - val outputConnectionAvailable = - connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().first().connectionAvailable() - - model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) - model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) - model.addAttribute("outputConnectionAvailable", outputConnectionAvailable) - if (tokenService != null) { - model.addAttribute("tokensEnabled", true) - model.addAttribute("tokens", tokenService.findAll()) - } else { - model.addAttribute("tokens", listOf<Token>()) - } - - return "configs/outputConnectionAvailable" + model.addAttribute("transformations", transformationService.getTransformations()) + if (userRoleService != null) { + model.addAttribute("userRolesEnabled", true) + model.addAttribute("userRoles", userRoleService.findAll()) + } else { + model.addAttribute("userRolesEnabled", false) + model.addAttribute("userRoles", emptyList<UserRole>()) } - - @GetMapping(params = ["gPasConnectionAvailable"]) - fun gPasConnectionAvailable(model: Model): String { - val gPasConnectionAvailable = - connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable() - - model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) - model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) - model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable) - if (tokenService != null) { - model.addAttribute("tokensEnabled", true) - model.addAttribute("tokens", tokenService.findAll()) - } else { - model.addAttribute("tokens", listOf<Token>()) - } - - return "configs/gPasConnectionAvailable" + return "configs" + } + + @GetMapping(params = ["outputConnectionAvailable"]) + fun outputConnectionAvailable(model: Model): String { + val outputConnectionAvailable = + connectionCheckServices + .filterIsInstance<OutputConnectionCheckService>() + .first() + .connectionAvailable() + + model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) + model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) + model.addAttribute("outputConnectionAvailable", outputConnectionAvailable) + if (tokenService != null) { + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokens", listOf<Token>()) } - @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" + return "configs/outputConnectionAvailable" + } + + @GetMapping(params = ["gPasConnectionAvailable"]) + fun gPasConnectionAvailable(model: Model): String { + val gPasConnectionAvailable = + connectionCheckServices + .filterIsInstance<GPasConnectionCheckService>() + .firstOrNull() + ?.connectionAvailable() + + model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) + model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) + model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable) + if (tokenService != null) { + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokens", listOf<Token>()) } - @PostMapping(path = ["tokens"]) - fun addToken(@ModelAttribute("name") name: String, model: Model): String { - if (tokenService == null) { - model.addAttribute("tokensEnabled", false) - model.addAttribute("success", false) - } else { - model.addAttribute("tokensEnabled", true) - val result = tokenService.addToken(name) - result.onSuccess { - model.addAttribute("newTokenValue", it) - model.addAttribute("success", true) - } - result.onFailure { - model.addAttribute("success", false) - } - model.addAttribute("tokens", tokenService.findAll()) - } - - return "configs/tokens" + 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>()) } - @DeleteMapping(path = ["tokens/{id}"]) - fun deleteToken(@PathVariable id: Long, model: Model): String { - if (tokenService != null) { - tokenService.deleteToken(id) - - model.addAttribute("tokensEnabled", true) - model.addAttribute("tokens", tokenService.findAll()) - } else { - model.addAttribute("tokensEnabled", false) - model.addAttribute("tokens", listOf<Token>()) - } - return "configs/tokens" + return "configs/gIcsConnectionAvailable" + } + + @PostMapping(path = ["tokens"]) + fun addToken(@ModelAttribute("name") name: String, model: Model): String { + if (tokenService == null) { + model.addAttribute("tokensEnabled", false) + model.addAttribute("success", false) + } else { + model.addAttribute("tokensEnabled", true) + val result = tokenService.addToken(name) + result.onSuccess { + model.addAttribute("newTokenValue", it) + model.addAttribute("success", true) + } + result.onFailure { model.addAttribute("success", false) } + model.addAttribute("tokens", tokenService.findAll()) } - @DeleteMapping(path = ["userroles/{id}"]) - fun deleteUserRole(@PathVariable id: Long, model: Model): String { - if (userRoleService != null) { - userRoleService.deleteUserRole(id) - - model.addAttribute("userRolesEnabled", true) - model.addAttribute("userRoles", userRoleService.findAll()) - } else { - model.addAttribute("userRolesEnabled", false) - model.addAttribute("userRoles", emptyList<UserRole>()) - } - return "configs/userroles" - } + return "configs/tokens" + } - @PutMapping(path = ["userroles/{id}"]) - fun updateUserRole(@PathVariable id: Long, @ModelAttribute("role") role: Role, model: Model): String { - if (userRoleService != null) { - userRoleService.updateUserRole(id, role) - - model.addAttribute("userRolesEnabled", true) - model.addAttribute("userRoles", userRoleService.findAll()) - } else { - model.addAttribute("userRolesEnabled", false) - model.addAttribute("userRoles", emptyList<UserRole>()) - } - return "configs/userroles" - } + @DeleteMapping(path = ["tokens/{id}"]) + fun deleteToken(@PathVariable id: Long, model: Model): String { + if (tokenService != null) { + tokenService.deleteToken(id) - @GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) - @ResponseBody - fun events(): Flux<ServerSentEvent<Any>> { - return connectionCheckUpdateProducer.asFlux().map { - val event = when (it) { - 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>() - .event(event).id("none").data(it) - .build() - } + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokensEnabled", false) + model.addAttribute("tokens", listOf<Token>()) } - -}
\ No newline at end of file + return "configs/tokens" + } + + @DeleteMapping(path = ["userroles/{id}"]) + fun deleteUserRole(@PathVariable id: Long, model: Model): String { + if (userRoleService != null) { + userRoleService.deleteUserRole(id) + + model.addAttribute("userRolesEnabled", true) + model.addAttribute("userRoles", userRoleService.findAll()) + } else { + model.addAttribute("userRolesEnabled", false) + model.addAttribute("userRoles", emptyList<UserRole>()) + } + return "configs/userroles" + } + + @PutMapping(path = ["userroles/{id}"]) + fun updateUserRole( + @PathVariable id: Long, + @ModelAttribute("role") role: Role, + model: Model, + ): String { + if (userRoleService != null) { + userRoleService.updateUserRole(id, role) + + model.addAttribute("userRolesEnabled", true) + model.addAttribute("userRoles", userRoleService.findAll()) + } else { + model.addAttribute("userRolesEnabled", false) + model.addAttribute("userRoles", emptyList<UserRole>()) + } + return "configs/userroles" + } + + @GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) + @ResponseBody + fun events(): Flux<ServerSentEvent<Any>> { + return connectionCheckUpdateProducer.asFlux().map { + val event = + when (it) { + 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>().event(event).id("none").data(it).build() + } + } +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt index 54920b1..082cd20 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt @@ -37,13 +37,13 @@ import org.springframework.web.bind.annotation.RequestMapping @RequestMapping(path = ["/"]) class HomeController( private val requestService: RequestService, - private val reportService: ReportService + private val reportService: ReportService, ) { - @GetMapping fun index( - @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable, - model: Model + @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) + pageable: Pageable, + model: Model, ): String { val requests = requestService.findAll(pageable) model.addAttribute("requests", requests) @@ -54,8 +54,9 @@ class HomeController( @GetMapping(path = ["patient/{patientPseudonym}"]) fun byPatient( @PathVariable patientPseudonym: PatientPseudonym, - @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable, - model: Model + @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) + pageable: Pageable, + model: Model, ): String { val requests = requestService.findRequestByPatientId(patientPseudonym, pageable) model.addAttribute("patientPseudonym", patientPseudonym.value) @@ -65,12 +66,14 @@ class HomeController( } @GetMapping(path = ["/report/{id}"]) - fun report(@PathVariable id: RequestId, model: Model): String { + fun report( + @PathVariable id: RequestId, + model: Model, + ): String { val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException() model.addAttribute("request", request) model.addAttribute("issues", reportService.deserialize(request.report?.dataQualityReport)) return "report" } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt index 20837bb..7821bf9 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt @@ -28,20 +28,21 @@ import org.springframework.web.bind.annotation.GetMapping @Controller class LoginController( private val securityConfigProperties: SecurityConfigProperties?, - private val oAuth2ClientProperties: OAuth2ClientProperties? + private val oAuth2ClientProperties: OAuth2ClientProperties?, ) { - @GetMapping(path = ["/login"]) fun login(model: Model): String { if (securityConfigProperties?.enableOidc == true) { model.addAttribute( "oidcLogins", - oAuth2ClientProperties?.registration?.map { (key, value) -> Pair(key, value.clientName) }.orEmpty() + oAuth2ClientProperties + ?.registration + ?.map { (key, value) -> Pair(key, value.clientName) } + .orEmpty(), ) } else { model.addAttribute("oidcLogins", emptyList<Pair<String, String>>()) } return "login" } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsController.kt index adc1e2b..e48d5df 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsController.kt @@ -28,11 +28,9 @@ import java.time.Instant @Controller @RequestMapping(path = ["/statistics"]) class StatisticsController { - @GetMapping fun index(model: Model): String { model.addAttribute("now", Instant.now()) return "statistics" } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt index 8372274..c99a4a3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt @@ -41,140 +41,168 @@ import java.time.temporal.ChronoUnit class StatisticsRestController( @param:Qualifier("statisticsUpdateProducer") private val statisticsUpdateProducer: Sinks.Many<Any>, - private val requestService: RequestService + private val requestService: RequestService, ) { - @GetMapping(path = ["requeststates"]) - fun requestStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> { - val states = if (delete) { - requestService.countDeleteStates() - } else { - requestService.countStates() - } + fun requestStates( + @RequestParam(required = false, defaultValue = "false") delete: Boolean, + ): List<NameValue> { + val states = + if (delete) { + requestService.countDeleteStates() + } else { + requestService.countStates() + } return states .map { - val color = when (it.status) { - RequestStatus.ERROR -> "red" - RequestStatus.WARNING -> "darkorange" - RequestStatus.SUCCESS -> "green" - else -> "slategray" - } + val color = + when (it.status) { + RequestStatus.ERROR -> "red" + RequestStatus.WARNING -> "darkorange" + RequestStatus.SUCCESS -> "green" + else -> "slategray" + } NameValue(it.status.toString(), it.count, color) - } - .sortedByDescending { it.value } + }.sortedByDescending { it.value } } @GetMapping(path = ["requestslastmonth"]) fun requestsLastMonth( - @RequestParam( - required = false, - defaultValue = "false" - ) delete: Boolean + @RequestParam(required = false, defaultValue = "false") delete: Boolean, ): List<DateNameValues> { - val requestType = if (delete) { - RequestType.DELETE - } else { - RequestType.MTB_FILE - } + val requestType = + if (delete) { + RequestType.DELETE + } else { + RequestType.MTB_FILE + } val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("Europe/Berlin")) - val data = requestService.findAll() - .filter { it.type == requestType } - .filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) } - .groupBy { formatter.format(it.processedAt) } - .map { - val requestList = it.value - .groupBy { request -> request.status } - .map { request -> - Pair(request.key, request.value.size) - } - .toMap() - Pair( - it.key.toString(), - DateNameValues( - it.key.toString(), NameValues( - error = requestList[RequestStatus.ERROR] ?: 0, - warning = requestList[RequestStatus.WARNING] ?: 0, - success = requestList[RequestStatus.SUCCESS] ?: 0, - duplication = requestList[RequestStatus.DUPLICATION] ?: 0, - unknown = requestList[RequestStatus.UNKNOWN] ?: 0, - ) + val data = + requestService + .findAll() + .filter { it.type == requestType } + .filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) } + .groupBy { formatter.format(it.processedAt) } + .map { + val requestList = + it.value + .groupBy { request -> request.status } + .map { request -> Pair(request.key, request.value.size) } + .toMap() + Pair( + it.key.toString(), + DateNameValues( + it.key.toString(), + NameValues( + error = requestList[RequestStatus.ERROR] ?: 0, + warning = requestList[RequestStatus.WARNING] ?: 0, + success = requestList[RequestStatus.SUCCESS] ?: 0, + duplication = requestList[RequestStatus.DUPLICATION] ?: 0, + unknown = requestList[RequestStatus.UNKNOWN] ?: 0, + ), + ), ) - ) - }.toMap() + }.toMap() - return (0L..30L).map { Instant.now().minus(it, ChronoUnit.DAYS) } + return (0L..30L) + .map { Instant.now().minus(it, ChronoUnit.DAYS) } .map { formatter.format(it) } - .map { - DateNameValues(it, data[it]?.nameValues ?: NameValues()) - } + .map { DateNameValues(it, data[it]?.nameValues ?: NameValues()) } .sortedBy { it.date } } @GetMapping(path = ["requestpatientstates"]) - fun requestPatientStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> { - val states = if (delete) { - requestService.findPatientUniqueDeleteStates() - } else { - requestService.findPatientUniqueStates() - } + fun requestPatientStates( + @RequestParam(required = false, defaultValue = "false") delete: Boolean, + ): List<NameValue> { + val states = + if (delete) { + requestService.findPatientUniqueDeleteStates() + } else { + requestService.findPatientUniqueStates() + } return states.map { - val color = when (it.status) { - RequestStatus.ERROR -> "red" - RequestStatus.WARNING -> "darkorange" - RequestStatus.SUCCESS -> "green" - else -> "slategray" - } + val color = + when (it.status) { + RequestStatus.ERROR -> "red" + RequestStatus.WARNING -> "darkorange" + RequestStatus.SUCCESS -> "green" + else -> "slategray" + } NameValue(it.status.toString(), it.count, color) } } @GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) - fun updater(): Flux<ServerSentEvent<Any>> { - return statisticsUpdateProducer.asFlux().flatMap { + fun updater(): Flux<ServerSentEvent<Any>> = + statisticsUpdateProducer.asFlux().flatMap { Flux.fromIterable( listOf( - ServerSentEvent.builder<Any>() - .event("requeststates").id("none").data(this.requestStates(false)) + ServerSentEvent + .builder<Any>() + .event("requeststates") + .id("none") + .data(this.requestStates(false)) .build(), - ServerSentEvent.builder<Any>() - .event("requestslastmonth").id("none").data(this.requestsLastMonth(false)) + ServerSentEvent + .builder<Any>() + .event("requestslastmonth") + .id("none") + .data(this.requestsLastMonth(false)) .build(), - ServerSentEvent.builder<Any>() - .event("requestpatientstates").id("none").data(this.requestPatientStates(false)) + ServerSentEvent + .builder<Any>() + .event("requestpatientstates") + .id("none") + .data(this.requestPatientStates(false)) .build(), - - ServerSentEvent.builder<Any>() - .event("deleterequeststates").id("none").data(this.requestStates(true)) + ServerSentEvent + .builder<Any>() + .event("deleterequeststates") + .id("none") + .data(this.requestStates(true)) .build(), - ServerSentEvent.builder<Any>() - .event("deleterequestslastmonth").id("none").data(this.requestsLastMonth(true)) + ServerSentEvent + .builder<Any>() + .event("deleterequestslastmonth") + .id("none") + .data(this.requestsLastMonth(true)) .build(), - ServerSentEvent.builder<Any>() - .event("deleterequestpatientstates").id("none").data(this.requestPatientStates(true)) + ServerSentEvent + .builder<Any>() + .event("deleterequestpatientstates") + .id("none") + .data(this.requestPatientStates(true)) .build(), - - ServerSentEvent.builder<Any>() - .event("newrequest").id("none").data("newrequest") - .build() - ) + ServerSentEvent + .builder<Any>() + .event("newrequest") + .id("none") + .data("newrequest") + .build(), + ), ) - } - } - } -data class NameValue(val name: String, val value: Int, val color: String) +data class NameValue( + val name: String, + val value: Int, + val color: String, +) -data class DateNameValues(val date: String, val nameValues: NameValues) +data class DateNameValues( + val date: String, + val nameValues: NameValues, +) data class NameValues( val error: Int = 0, val warning: Int = 0, val success: Int = 0, val duplication: Int = 0, - val unknown: Int = 0 -)
\ No newline at end of file + val unknown: Int = 0, +) |
