/* * This file is part of ETL-Processor * * Copyright (c) 2023 Comprehensive Cancer Center Mainfranken * Copyright (c) 2023-2026 Paul-Christian Volkmer, Datenintegrationszentrum Philipps-Universität Marburg and Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper import dev.dnpm.etl.processor.consent.GicsConsentService import dev.dnpm.etl.processor.consent.GicsGetBroadConsentService import dev.dnpm.etl.processor.consent.IConsentService import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.monitoring.* import dev.dnpm.etl.processor.pseudonym.* import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.TokenService import dev.dnpm.etl.processor.services.ConsentProcessor import dev.dnpm.etl.processor.services.Transformation import dev.dnpm.etl.processor.services.TransformationService import org.apache.cxf.jaxws.JaxWsProxyFactoryBean import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.AnyNestedCondition import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.restclient.RestTemplateBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Conditional import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.ConfigurationCondition import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration import org.springframework.http.converter.StringHttpMessageConverter import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter import org.springframework.retry.RetryCallback import org.springframework.retry.RetryContext import org.springframework.retry.RetryListener import org.springframework.retry.policy.SimpleRetryPolicy import org.springframework.retry.support.RetryTemplate import org.springframework.retry.support.RetryTemplateBuilder import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.web.client.HttpClientErrorException import org.springframework.web.client.RestTemplate import reactor.core.publisher.Sinks import tools.jackson.databind.json.JsonMapper import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration @Configuration @EnableConfigurationProperties( value = [ AppConfigProperties::class, PseudonymizeConfigProperties::class, GPasConfigProperties::class, ConsentConfigProperties::class, GIcsConfigProperties::class, ] ) @EnableScheduling class AppConfiguration { private val logger = LoggerFactory.getLogger(AppConfiguration::class.java) fun stringHttpMessageConverter(): StringHttpMessageConverter { return StringHttpMessageConverter() } @Bean fun jacksonJsonHttpMapperConverter(jsonMapper: JsonMapper): JacksonJsonHttpMessageConverter { return JacksonJsonHttpMessageConverter(jsonMapper) } @Bean fun restTemplate(jsonMapper: JsonMapper): RestTemplate { return RestTemplateBuilder() .messageConverters( stringHttpMessageConverter(), jacksonJsonHttpMapperConverter(jsonMapper), ) .build() } @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 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, ) @Bean fun buildinPseudonymGenerator(): Generator { logger.info("Selected 'BUILDIN Pseudonym Generator'") return AnonymizingGenerator() } @Bean fun pseudonymizeService( generator: Generator, pseudonymizeConfigProperties: PseudonymizeConfigProperties, ): PseudonymizeService { return PseudonymizeService(generator, pseudonymizeConfigProperties) } @Bean fun reportService(jsonMapper: JsonMapper): ReportService { return ReportService(jsonMapper) } @Bean fun transformationService(jsonMapper: JsonMapper, configProperties: AppConfigProperties): TransformationService { logger.info("Apply ${configProperties.transformations.size} transformation rules") return TransformationService( jsonMapper, configProperties.transformations.map { Transformation.of(it.path) from it.from to it.to }, ) } @Bean fun retryTemplate(configProperties: AppConfigProperties): RetryTemplate { return RetryTemplateBuilder() .notRetryOn(IllegalArgumentException::class.java) .notRetryOn(HttpClientErrorException.BadRequest::class.java) .notRetryOn(HttpClientErrorException.UnprocessableContent::class.java) .exponentialBackoff(2.seconds.toJavaDuration(), 1.25, 5.seconds.toJavaDuration()) .customPolicy(SimpleRetryPolicy(configProperties.maxRetryAttempts)) .withListener( object : RetryListener { override fun onError( context: RetryContext, callback: RetryCallback, throwable: Throwable, ) { logger.warn("Error occured: {}. Retrying {}", throwable.message, context.retryCount) } } ) .build() } @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 statisticsUpdateProducer(): Sinks.Many { return Sinks.many().multicast().directBestEffort() } @Bean fun connectionCheckUpdateProducer(): Sinks.Many { return Sinks.many().multicast().onBackpressureBuffer() } @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") @Bean fun gPasConnectionCheckService( restTemplate: RestTemplate, gPasConfigProperties: GPasConfigProperties, connectionCheckUpdateProducer: Sinks.Many, ): ConnectionCheckService { return GPasConnectionCheckService( restTemplate, gPasConfigProperties, connectionCheckUpdateProducer, ) } @ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS") @ConditionalOnMissingBean @Bean fun gPasConnectionCheckServiceOnDeprecatedProperty( restTemplate: RestTemplate, gPasConfigProperties: GPasConfigProperties, connectionCheckUpdateProducer: Sinks.Many, ): ConnectionCheckService { return GPasConnectionCheckService( restTemplate, gPasConfigProperties, connectionCheckUpdateProducer, ) } @Bean fun jdbcConfiguration(): AbstractJdbcConfiguration { return AppJdbcConfiguration() } @Conditional(GicsEnabledCondition::class) @Bean fun gicsConsentService( gIcsConfigProperties: GIcsConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig, ): IConsentService { return GicsConsentService(gIcsConfigProperties, retryTemplate, restTemplate, appFhirConfig) } @Conditional(GicsGetBroadConsentEnabledCondition::class) @Bean fun gicsGetBroadConsentService( gIcsConfigProperties: GIcsConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig, ): IConsentService { return GicsGetBroadConsentService( gIcsConfigProperties, retryTemplate, restTemplate, appFhirConfig, ) } @Conditional(GicsEnabledCondition::class) @Bean fun consentProcessor( configProperties: AppConfigProperties, gIcsConfigProperties: GIcsConfigProperties, getObjectMapper: JsonMapper, 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, ): 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 } } class GicsGetBroadConsentEnabledCondition : AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) { @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics_get_bc") @ConditionalOnProperty(name = ["app.consent.gics.uri"]) class OnGicsGetBroadConsentServiceSelected { // Just for Condition } }