From 05fe7c2091cf7ca45c2cfcec8ec506c43417f3e6 Mon Sep 17 00:00:00 2001
From: Paul-Christian Volkmer
Date: Fri, 5 Dec 2025 11:59:04 +0100
Subject: fix: do not serialize null values (#216)
For outgoing HTTP requests, null values
have been serialized as `"name": null`.
Since this causes problems in DNPM:DIP,
data parts with null values must not be
serialized.---
.../processor/output/RestDipMtbFileSenderTest.kt | 146 +++++++++++++++++++++
.../dnpm/etl/processor/config/AppConfiguration.kt | 29 +++-
.../dnpm/etl/processor/output/RestMtbFileSender.kt | 6 +-
3 files changed, 172 insertions(+), 9 deletions(-)
create mode 100644 src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt
(limited to 'src')
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt
new file mode 100644
index 0000000..f6f6a08
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt
@@ -0,0 +1,146 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2023 Comprehensive Cancer Center Mainfranken
+ * Copyright (c) 2023-2025 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.output
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import dev.dnpm.etl.processor.RequestId
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppRestConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.config.RestTargetProperties
+import dev.dnpm.etl.processor.consent.ConsentEvaluator
+import dev.dnpm.etl.processor.monitoring.ReportService
+import dev.dnpm.etl.processor.monitoring.RequestStatus
+import dev.pcvolkmer.mv64e.mtb.*
+import org.assertj.core.api.Assertions.assertThat
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.Matchers.containsString
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.http.HttpMethod
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.retry.policy.SimpleRetryPolicy
+import org.springframework.retry.support.RetryTemplateBuilder
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.bean.override.mockito.MockitoBean
+import org.springframework.test.web.client.MockRestServiceServer
+import org.springframework.test.web.client.match.MockRestRequestMatchers.*
+import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
+import org.springframework.web.client.RestTemplate
+import java.time.Instant
+import java.util.*
+
+@SpringBootTest
+@MockitoBean(types = [ReportService::class])
+@ContextConfiguration(
+ classes =
+ [
+ AppConfiguration::class,
+ AppSecurityConfiguration::class,
+ AppRestConfiguration::class,
+ ConsentEvaluator::class,
+ ],
+)
+@TestPropertySource(
+ properties = ["app.rest.uri=http://localhost:9000", "app.max-retry-attempts=5"],
+)
+class RestDipMtbFileSenderTest {
+
+ @Nested
+ inner class DnpmV2ContentRequest {
+
+ private lateinit var mockRestServiceServer: MockRestServiceServer
+
+ private lateinit var restMtbFileSender: RestMtbFileSender
+
+ private var reportService =
+ ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
+
+ @BeforeEach
+ fun setup(
+ @Autowired restTemplate: RestTemplate
+ ) {
+ val restTemplate = restTemplate
+ val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null)
+ val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
+
+ this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
+
+ this.restMtbFileSender =
+ RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
+ }
+
+ @Test
+ fun shouldNotSendJsonNullValues() {
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.POST))
+ .andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
+ .andExpect(
+ content().string(not(containsString("null")))
+ )
+ .andRespond {
+ withStatus(HttpStatus.OK)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(
+ """
+ {
+ "patient": "PID",
+ "issues": [
+ { "severity": "info", "message": "Info Message" }
+ ]
+ }
+ """
+ )
+ .createResponse(it)
+ }
+
+ val response = restMtbFileSender.send(DnpmV2MtbFileRequest(RequestId("TEST1234"), dnpmV2MtbFile()))
+ assertThat(response.status).isEqualTo(RequestStatus.SUCCESS)
+ }
+ }
+
+ companion object {
+ fun dnpmV2MtbFile(): Mtb {
+ return Mtb().apply {
+ this.patient =
+ Patient().apply {
+ this.id = "PID"
+ this.birthDate = Date.from(Instant.now())
+ this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE }
+ }
+ this.episodesOfCare =
+ listOf(
+ MtbEpisodeOfCare().apply {
+ this.id = "1"
+ this.patient = Reference().apply { this.id = "PID" }
+ this.period = PeriodDate().apply { this.start = Date.from(Instant.now()) }
+ }
+ )
+ }
+ }
+ }
+}
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 35585cb..f21c09b 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
@@ -1,7 +1,8 @@
/*
* This file is part of ETL-Processor
*
- * Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ * Copyright (c) 2023 Comprehensive Cancer Center Mainfranken
+ * Copyright (c) 2023-2025 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
@@ -31,21 +32,20 @@ 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.AllNestedConditions
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.web.client.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.data.relational.core.sql.NestedCondition
+import org.springframework.http.converter.StringHttpMessageConverter
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.retry.RetryCallback
import org.springframework.retry.RetryContext
import org.springframework.retry.RetryListener
@@ -58,6 +58,8 @@ 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(
@@ -75,9 +77,22 @@ class AppConfiguration {
private val logger = LoggerFactory.getLogger(AppConfiguration::class.java)
+ fun stringHttpMessageConverter(): StringHttpMessageConverter {
+ return StringHttpMessageConverter()
+ }
+
+ @Bean
+ fun mappingJacksonHttpMessageConverter(objectMapper: ObjectMapper): MappingJackson2HttpMessageConverter {
+ val converter = MappingJackson2HttpMessageConverter()
+ converter.setObjectMapper(objectMapper)
+ return converter
+ }
+
@Bean
- fun restTemplate(): RestTemplate {
- return RestTemplate()
+ fun restTemplate(objectMapper: ObjectMapper): RestTemplate {
+ return RestTemplateBuilder()
+ .messageConverters(stringHttpMessageConverter(), mappingJacksonHttpMessageConverter(objectMapper))
+ .build()
}
@Bean
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 4120d4a..9c22ec0 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
@@ -1,7 +1,8 @@
/*
* This file is part of ETL-Processor
*
- * Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ * Copyright (c) 2023 Comprehensive Cancer Center Mainfranken
+ * Copyright (c) 2023-2025 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
@@ -28,6 +29,7 @@ import dev.dnpm.etl.processor.monitoring.asRequestStatus
import org.slf4j.LoggerFactory
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.retry.support.RetryTemplate
import org.springframework.web.client.RestClientException
@@ -51,7 +53,7 @@ abstract class RestMtbFileSender(
return retryTemplate.execute {
val headers = getHttpHeaders(request)
val entityReq = HttpEntity(request.content, headers)
- val response = restTemplate.postForEntity(sendUrl(), entityReq, String::class.java)
+ val response = restTemplate.exchange(sendUrl(), HttpMethod.POST, entityReq, String::class.java)
if (!response.statusCode.is2xxSuccessful) {
logger.warn("Error sending to remote system: {}", response.body)
return@execute MtbFileSender.Response(
--
cgit v1.2.3