Skip to content

Commit cd6f4c0

Browse files
author
Ralf Steppacher
committed
#41312 first config property (sync enabled/disabled) can be manipulated with the UI and persisted through the cdr service.
To update the single property I implemented a generic way to update any property that is flagged as updatable (by a marker interface) and applied the new mechanism to the existing client credential auto-renewal option. The test coverage has suffered. Will need to be brought up again...
1 parent fd7f15e commit cd6f4c0

54 files changed

Lines changed: 2110 additions & 949 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ out/
3838
Java.gitignore
3939
build/
4040
bin/
41+
.kotlin/

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,9 @@ client:
147147
tenant-id: swisscom-health-tenant-id # provided by Swisscom Health
148148
client-id: my-client-id # Self-service on CDR website
149149
client-secret: my-secret # Self-service on CDR website
150-
renew-credential-at-startup: false
150+
renew-credential: false
151+
max-credential-age: 365d
152+
last-credential-renewal-time: 2025-06-05T14:01:42Z
151153
cdr-api:
152154
host: cdr.health.swisscom.ch
153155
retry-delay:

build.gradle.kts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,59 @@
1+
import io.gitlab.arturbosch.detekt.Detekt
2+
import org.gradle.kotlin.dsl.withType
3+
14
allprojects {
25
version = "3.4.2-SNAPSHOT"
36
}
47

58
plugins {
6-
// this is necessary to avoid the plugins to be loaded multiple times
7-
// in each subproject's classloader
9+
// this is necessary to avoid the plugins to be loaded multiple times in each subproject's classloader
810
alias(libs.plugins.jetbrains.compose) apply false
911
alias(libs.plugins.compose.compiler) apply false
1012
alias(libs.plugins.kotlin.multiplatform) apply false
1113
alias(libs.plugins.docker.compose) apply false
1214
alias(libs.plugins.spring.boot) apply false
1315
alias(libs.plugins.spring.dependency.management) apply false
14-
alias(libs.plugins.detekt) apply false
1516
kotlin("jvm").version(libs.versions.kotlin.lang) apply false
1617
kotlin("plugin.spring").version(libs.versions.kotlin.lang) apply false
1718
// https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.kotlin.configuration-properties
1819
// KAPT is end of life, but KSP is not supported yet: https://github.com/spring-projects/spring-boot/issues/28046
1920
kotlin("kapt").version(libs.versions.kotlin.lang) apply false
21+
22+
// but we actually want to run detekt in all subprojects
23+
alias(libs.plugins.detekt)
24+
}
25+
26+
subprojects {
27+
apply {
28+
plugin("io.gitlab.arturbosch.detekt")
29+
}
30+
31+
detekt {
32+
config.from(rootProject.files("config/detekt.yml"))
33+
buildUponDefaultConfig = false // preconfigure defaults
34+
allRules = true
35+
parallel = true
36+
}
37+
38+
tasks.withType<Detekt> {
39+
reports {
40+
xml.required.set(true)
41+
html.required.set(false)
42+
sarif.required.set(false)
43+
txt.required.set(false)
44+
}
45+
}
46+
47+
project.afterEvaluate {
48+
// https://github.com/detekt/detekt/issues/6198#issuecomment-2265183695
49+
configurations.matching { it.name == "detekt" }.all {
50+
resolutionStrategy.eachDependency {
51+
if (requested.group == "org.jetbrains.kotlin") {
52+
useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())
53+
}
54+
}
55+
}
56+
}
2057
}
2158

2259
// NOTE: you should to run this target or manually update `gradle/gradle-daemon-jvm.properties` if we change the Java version!

cdr-client-common/build.gradle.kts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
group = "com.swisscom.health.des.cdr.client.common"
22

33
plugins {
4-
alias(libs.plugins.detekt)
54
kotlin("jvm").version(libs.versions.kotlin.lang)
65
alias(libs.plugins.kotlinx.serialization)
76
`maven-publish`
@@ -19,17 +18,6 @@ idea {
1918
}
2019
}
2120

22-
project.afterEvaluate {
23-
// https://github.com/detekt/detekt/issues/6198#issuecomment-2265183695
24-
configurations.matching { it.name == "detekt" }.all {
25-
resolutionStrategy.eachDependency {
26-
if (requested.group == "org.jetbrains.kotlin") {
27-
useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())
28-
}
29-
}
30-
}
31-
}
32-
3321
kotlin {
3422
jvmToolchain(libs.versions.jdk.get().toInt())
3523
compilerOptions {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.swisscom.health.des.cdr.client.common
2+
3+
import java.time.Duration
4+
5+
object Constants {
6+
const val SHUTDOWN_DELAY_MILLIS: Long = 250
7+
8+
@JvmStatic
9+
val SHUTDOWN_DELAY: Duration = Duration.ofMillis(SHUTDOWN_DELAY_MILLIS)
10+
11+
const val CONFIG_CHANGE_EXIT_CODE = 29
12+
const val UNKNOWN_EXIT_CODE = 31
13+
}

cdr-client-common/src/main/kotlin/com/swisscom/health/des/cdr/client/common/DTOs.kt

Lines changed: 182 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,55 @@
1-
@file:UseSerializers(InstantSerializer::class)
1+
@file:UseSerializers(InstantSerializer::class, DurationSerializer::class, UrlSerializer::class, NioPathSerializer::class)
22

33
package com.swisscom.health.des.cdr.client.common
44

5-
import kotlinx.serialization.Serializable
6-
import java.time.Instant
7-
85
import kotlinx.serialization.KSerializer
6+
import kotlinx.serialization.Serializable
97
import kotlinx.serialization.UseSerializers
108
import kotlinx.serialization.descriptors.PrimitiveKind
119
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
1210
import kotlinx.serialization.descriptors.SerialDescriptor
1311
import kotlinx.serialization.encoding.Decoder
1412
import kotlinx.serialization.encoding.Encoder
13+
import java.net.URI
14+
import java.net.URL
15+
import java.nio.file.Path
16+
import java.time.Duration
17+
import java.time.Instant
1518

1619
object InstantSerializer : KSerializer<Instant> {
1720
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.time.Instant", PrimitiveKind.STRING)
1821
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
1922
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
2023
}
2124

25+
object DurationSerializer : KSerializer<Duration> {
26+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.time.Duration", PrimitiveKind.STRING)
27+
override fun serialize(encoder: Encoder, value: Duration) = encoder.encodeString(value.toString())
28+
override fun deserialize(decoder: Decoder): Duration = Duration.parse(decoder.decodeString())
29+
}
30+
31+
object UrlSerializer : KSerializer<URL> {
32+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.net.URL", PrimitiveKind.STRING)
33+
override fun serialize(encoder: Encoder, value: URL) = encoder.encodeString(value.toString())
34+
override fun deserialize(decoder: Decoder): URL = URL(decoder.decodeString())
35+
}
36+
37+
// Need to be compatible with `com.fasterxml.jackson.databind.ext.NioPathSerializer.serialize` where a java.nio.file.Path is serialized as a URI string.
38+
object NioPathSerializer : KSerializer<Path> {
39+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.nio.file.Path", PrimitiveKind.STRING)
40+
override fun serialize(encoder: Encoder, value: Path) = encoder.encodeString(value.toUri().toString())
41+
override fun deserialize(decoder: Decoder): Path = decoder.decodeString()
42+
.run {
43+
runCatching {
44+
// Try to parse as a URI first, as this is how Jackson serializes it.
45+
Path.of(URI(this))
46+
}
47+
.recoverCatching {
48+
Path.of(this)
49+
}.getOrThrow()
50+
}
51+
}
52+
2253
class DTOs {
2354

2455
@Serializable
@@ -49,4 +80,151 @@ class DTOs {
4980
val exitCode: Int,
5081
)
5182

83+
/**
84+
* CDR client configuration class hierarchy. It is an almost verbatim copy of `com.swisscom.health.des.cdr.client.config.CdrClientConfig`.
85+
*
86+
* NOTE: Deserialization with Jackson fails if constructor parameters don't have default values.
87+
*/
88+
@Serializable
89+
data class CdrClientConfig(
90+
val fileSynchronizationEnabled: Boolean,
91+
val customer: List<Connector>,
92+
val cdrApi: Endpoint,
93+
val filesInProgressCacheSize: String,
94+
val idpCredentials: IdpCredentials,
95+
val idpEndpoint: URL,
96+
val localFolder: Path,
97+
val pullThreadPoolSize: Int,
98+
val pushThreadPoolSize: Int,
99+
val retryDelay: List<Duration>,
100+
val scheduleDelay: Duration,
101+
val credentialApi: Endpoint,
102+
val fileBusyTestInterval: Duration,
103+
val fileBusyTestTimeout: Duration,
104+
val fileBusyTestStrategy: FileBusyTestStrategy,
105+
) {
106+
107+
companion object {
108+
@JvmStatic
109+
val EMPTY = CdrClientConfig(
110+
fileSynchronizationEnabled = false,
111+
customer = emptyList(),
112+
cdrApi = Endpoint.EMPTY,
113+
filesInProgressCacheSize = "",
114+
idpCredentials = IdpCredentials.EMPTY,
115+
idpEndpoint = URL("http://localhost:8080"),
116+
localFolder = EMPTY_PATH,
117+
pullThreadPoolSize = 0,
118+
pushThreadPoolSize = 0,
119+
retryDelay = emptyList(),
120+
scheduleDelay = Duration.ZERO,
121+
credentialApi = Endpoint.EMPTY,
122+
fileBusyTestInterval = Duration.ZERO,
123+
fileBusyTestTimeout = Duration.ZERO,
124+
fileBusyTestStrategy = FileBusyTestStrategy.NEVER_BUSY
125+
)
126+
}
127+
128+
@Serializable
129+
data class Connector(
130+
val connectorId: String,
131+
val targetFolder: Path,
132+
val sourceFolder: Path,
133+
val contentType: String,
134+
val sourceArchiveEnabled: Boolean,
135+
val sourceArchiveFolder: Path,
136+
val sourceErrorFolder: Path,
137+
val mode: Mode,
138+
val docTypeFolders: Map<DocumentType, DocTypeFolders>,
139+
) {
140+
141+
@Serializable
142+
data class DocTypeFolders(
143+
val sourceFolder: Path? = null,
144+
val targetFolder: Path,
145+
)
146+
147+
}
148+
149+
@Serializable
150+
data class Endpoint(
151+
val scheme: String,
152+
val host: String,
153+
val port: Int,
154+
val basePath: String,
155+
) {
156+
companion object {
157+
@JvmStatic
158+
val EMPTY = Endpoint(
159+
scheme = "",
160+
host = "",
161+
port = 0,
162+
basePath = ""
163+
)
164+
}
165+
}
166+
167+
@Serializable
168+
data class IdpCredentials(
169+
val tenantId: String,
170+
val clientId: String,
171+
val clientSecret: String,
172+
val scopes: List<String>,
173+
val renewCredential: Boolean,
174+
val maxCredentialAge: Duration,
175+
val lastCredentialRenewalTime: Instant,
176+
) {
177+
companion object {
178+
@JvmStatic
179+
val EMPTY = IdpCredentials(
180+
tenantId = "",
181+
clientId = "",
182+
clientSecret = "",
183+
scopes = emptyList(),
184+
renewCredential = false,
185+
maxCredentialAge = Duration.ZERO,
186+
lastCredentialRenewalTime = Instant.EPOCH
187+
)
188+
}
189+
}
190+
191+
enum class DocumentType {
192+
UNDEFINED,
193+
CONTAINER,
194+
CREDIT,
195+
FORM,
196+
HOSPITAL_MCD,
197+
INVOICE,
198+
NOTIFICATION;
199+
}
200+
201+
enum class Mode {
202+
TEST,
203+
PRODUCTION,
204+
NONE
205+
}
206+
207+
enum class FileBusyTestStrategy {
208+
/** checks for file size changes over a configurable duration. */
209+
FILE_SIZE_CHANGED,
210+
211+
/**
212+
* always flags the file as 'not busy'; use if all downstream applications
213+
* create files in a single atomic operation (`move` on the same file system).
214+
*/
215+
NEVER_BUSY,
216+
217+
/** always flags the file as 'busy'; only useful for test scenarios. */
218+
ALWAYS_BUSY,
219+
}
220+
221+
}
222+
223+
companion object {
224+
225+
@JvmStatic
226+
val EMPTY_PATH: Path = Path.of("")
227+
228+
}
229+
52230
}

0 commit comments

Comments
 (0)