diff options
Diffstat (limited to 'src/main/kotlin/dev')
11 files changed, 148 insertions, 46 deletions
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt index e5db63e..ee33114 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -48,6 +48,7 @@ data class PseudonymizeConfigProperties( @ConfigurationProperties(GPasConfigProperties.NAME) data class GPasConfigProperties( val uri: String?, + val soapEndpoint: String?, @get:DeprecatedConfigurationProperty(since = "0.12") val pidDomain: String?, val patientDomain: String = pidDomain ?: "etl-processor", 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 b4fad3e..de302fd 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -20,19 +20,17 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper -import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.consent.GicsConsentService 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.AnonymizingGenerator -import dev.dnpm.etl.processor.pseudonym.Generator -import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator -import dev.dnpm.etl.processor.pseudonym.PseudonymizeService +import dev.dnpm.etl.processor.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 @@ -85,6 +83,37 @@ class AppConfiguration { } @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, 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 bf8b8bd..6e97865 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt @@ -80,7 +80,7 @@ sealed class ConnectionCheckResult { class KafkaConnectionCheckService( private val consumer: Consumer<String, String>, - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> ) : OutputConnectionCheckService { @@ -120,7 +120,7 @@ class KafkaConnectionCheckService( class RestConnectionCheckService( private val restTemplate: RestTemplate, private val restTargetProperties: RestTargetProperties, - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> ) : OutputConnectionCheckService { @@ -171,7 +171,7 @@ class RestConnectionCheckService( class GPasConnectionCheckService( private val restTemplate: RestTemplate, private val gPasConfigProperties: GPasConfigProperties, - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> ) : ConnectionCheckService { @@ -229,7 +229,7 @@ class GPasConnectionCheckService( class GIcsConnectionCheckService( private val restTemplate: RestTemplate, private val gIcsConfigProperties: GIcsConfigProperties, - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> ) : ConnectionCheckService { 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 36c9705..f2509dd 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt @@ -30,6 +30,7 @@ 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") @@ -65,6 +66,12 @@ data class Request( 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 @@ -90,19 +97,23 @@ interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRep @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;") + @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;") + @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> -}
\ No newline at end of file +} 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 0537cbb..dcb438f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt @@ -35,7 +35,7 @@ class AnonymizingGenerator : Generator { } @OptIn(ExperimentalStdlibApi::class) - override fun generateGenomDeTan(id: String?): String { + override fun generateGenomDeTan(id: String): String { val bytes = ByteArray(64 / 2) getSecureRandom().nextBytes(bytes) diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt new file mode 100644 index 0000000..8215d23 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt @@ -0,0 +1,26 @@ +package dev.dnpm.etl.processor.pseudonym + +import dev.dnpm.etl.processor.config.AppFhirConfig +import dev.dnpm.etl.processor.config.GPasConfigProperties +import org.springframework.retry.support.RetryTemplate + +class GpasSoapPseudonymGenerator( + private val gpasCfg: GPasConfigProperties, + private val retryTemplate: RetryTemplate, + private val gpasSoapService: GpasSoapService, + private val appFhirConfig: AppFhirConfig +) : Generator { + + override fun generate(id: String): String { + return retryTemplate.execute<String, Exception> { + gpasSoapService.getOrCreatePseudonymFor(id, gpasCfg.patientDomain) + } + } + + override fun generateGenomDeTan(id: String): String { + return 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 new file mode 100644 index 0000000..0909924 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt @@ -0,0 +1,31 @@ +package dev.dnpm.etl.processor.pseudonym + +import jakarta.jws.WebMethod +import jakarta.jws.WebParam +import jakarta.jws.WebResult +import jakarta.jws.WebService +import jakarta.xml.bind.annotation.XmlElementWrapper + +@WebService( + name = "PSNManagerBeanService", + 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 + ): String + + @WebMethod(operationName = "createPseudonymsFor") + @WebResult(name = "psn") + @XmlElementWrapper(name = "return") + fun createPseudonymsFor( + @WebParam(name = "value") value: String, + @WebParam(name = "domainName") domainName: String, + @WebParam(name = "number") minNumber: Int + ): List<String> + +}
\ 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 2ed21eb..b420d1f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt @@ -14,7 +14,6 @@ import dev.pcvolkmer.mv64e.mtb.* import org.apache.commons.lang3.NotImplementedException import org.hl7.fhir.r4.model.* import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent -import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Consent.ConsentState import org.hl7.fhir.r4.model.Consent.ProvisionComponent import org.slf4j.Logger @@ -137,15 +136,7 @@ class ConsentProcessor( } val provisionComponent: ProvisionComponent = provisions.first() - - 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 - } - } - + val provisionCode = getProvisionCode(provisionComponent) if (provisionCode != null) { try { val modelProjectConsentPurpose = @@ -177,6 +168,17 @@ class ConsentProcessor( } } + 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 + } + private fun setGenomDeSubmissionType(mtbFile: Mtb) { if (appConfigProperties.genomDeTestSubmission) { mtbFile.metadata.type = MvhSubmissionType.TEST @@ -230,17 +232,21 @@ class ConsentProcessor( entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent val consentIsActive = (entry.resource as Consent).status == ConsentState.ACTIVE - isConsentResource && consentIsActive && checkCoding( - targetCode, targetSystem, (entry.resource as Consent).policyRule.coding - ) && isRequestDateInRange(requestDate, (entry.resource as Consent).provision.period) + 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().firstOrNull { code -> + subProvision.code.map { c -> c.coding }.flatten().any { code -> targetCode.equals(code.code) && targetSystem.equals(code.system) - } != null + } }.map { subProvision -> subProvision } @@ -252,16 +258,14 @@ class ConsentProcessor( return Consent.ConsentProvisionType.NULL } - fun checkCoding( + fun checkProvisionExist( researchAllowedPolicyOid: String?, researchAllowedPolicySystem: String?, - policyRules: Collection<Coding> + provisions: Collection<ProvisionComponent> ): Boolean { - return policyRules.find { code -> - researchAllowedPolicySystem.equals(code.getSystem()) && (researchAllowedPolicyOid.equals( - code.getCode() - )) - } != null + return provisions.any { provision -> + provision.code.any { codeableConcept -> codeableConcept.coding.any { it.system == researchAllowedPolicySystem && it.code == researchAllowedPolicyOid } } + } } fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean { 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 12e824d..fd6a9b4 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 @@ -73,9 +73,9 @@ class KafkaResponseProcessor( } data class ResponseBody( - @JsonProperty("request_id") @JsonAlias("requestId") val requestId: String, - @JsonProperty("status_code") @JsonAlias("statusCode") val statusCode: Int, - @JsonProperty("status_body") @JsonAlias("statusBody") val statusBody: Map<String, Any> + @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> ) }
\ No newline at end of file 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 ea89e98..44571d4 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt @@ -40,7 +40,7 @@ import reactor.core.publisher.Sinks @Controller @RequestMapping(path = ["configs"]) class ConfigController( - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, private val transformationService: TransformationService, private val pseudonymGenerator: Generator, 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 c034cb4..8372274 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt @@ -39,7 +39,7 @@ import java.time.temporal.ChronoUnit @RestController @RequestMapping(path = ["/statistics"]) class StatisticsRestController( - @Qualifier("statisticsUpdateProducer") + @param:Qualifier("statisticsUpdateProducer") private val statisticsUpdateProducer: Sinks.Many<Any>, private val requestService: RequestService ) { |
