Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ out/
Java.gitignore
build/
bin/
.kotlin/
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ client:
tenant-id: swisscom-health-tenant-id # provided by Swisscom Health
client-id: my-client-id # Self-service on CDR website
client-secret: my-secret # Self-service on CDR website
renew-credential-at-startup: false
renew-credential: false
max-credential-age: 365d
last-credential-renewal-time: 2025-06-05T14:01:42Z
cdr-api:
host: cdr.health.swisscom.ch
retry-delay:
Expand Down
43 changes: 40 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.gradle.kotlin.dsl.withType

allprojects {
version = "3.4.2-SNAPSHOT"
}

plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
// this is necessary to avoid the plugins to be loaded multiple times in each subproject's classloader
Comment thread
rsteppac marked this conversation as resolved.
Outdated
alias(libs.plugins.jetbrains.compose) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.docker.compose) apply false
alias(libs.plugins.spring.boot) apply false
alias(libs.plugins.spring.dependency.management) apply false
alias(libs.plugins.detekt) apply false
kotlin("jvm").version(libs.versions.kotlin.lang) apply false
kotlin("plugin.spring").version(libs.versions.kotlin.lang) apply false
// https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.kotlin.configuration-properties
// KAPT is end of life, but KSP is not supported yet: https://github.com/spring-projects/spring-boot/issues/28046
kotlin("kapt").version(libs.versions.kotlin.lang) apply false

// but we actually want to run detekt in all subprojects
alias(libs.plugins.detekt)
}

subprojects {
apply {
plugin("io.gitlab.arturbosch.detekt")
Comment thread
Lesrac marked this conversation as resolved.
Outdated
}

detekt {
config.from(rootProject.files("config/detekt.yml"))
buildUponDefaultConfig = false // preconfigure defaults
allRules = true
parallel = true
}

tasks.withType<Detekt> {
reports {
xml.required.set(true)
html.required.set(false)
sarif.required.set(false)
txt.required.set(false)
}
}

project.afterEvaluate {
// https://github.com/detekt/detekt/issues/6198#issuecomment-2265183695
configurations.matching { it.name == "detekt" }.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.kotlin") {
useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())
}
}
}
}
}

// NOTE: you should to run this target or manually update `gradle/gradle-daemon-jvm.properties` if we change the Java version!
Expand Down
12 changes: 0 additions & 12 deletions cdr-client-common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
group = "com.swisscom.health.des.cdr.client.common"

plugins {
alias(libs.plugins.detekt)
kotlin("jvm").version(libs.versions.kotlin.lang)
alias(libs.plugins.kotlinx.serialization)
`maven-publish`
Expand All @@ -19,17 +18,6 @@ idea {
}
}

project.afterEvaluate {
// https://github.com/detekt/detekt/issues/6198#issuecomment-2265183695
configurations.matching { it.name == "detekt" }.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.kotlin") {
useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())
}
}
}
}

kotlin {
jvmToolchain(libs.versions.jdk.get().toInt())
compilerOptions {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.swisscom.health.des.cdr.client.common

import java.time.Duration

object Constants {
const val SHUTDOWN_DELAY_MILLIS: Long = 250

@JvmStatic
val SHUTDOWN_DELAY: Duration = Duration.ofMillis(SHUTDOWN_DELAY_MILLIS)

const val CONFIG_CHANGE_EXIT_CODE = 29
const val UNKNOWN_EXIT_CODE = 31
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,55 @@
@file:UseSerializers(InstantSerializer::class)
@file:UseSerializers(InstantSerializer::class, DurationSerializer::class, UrlSerializer::class, NioPathSerializer::class)

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

import kotlinx.serialization.Serializable
import java.time.Instant

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.net.URI
import java.net.URL
import java.nio.file.Path
import java.time.Duration
import java.time.Instant

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

object DurationSerializer : KSerializer<Duration> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.time.Duration", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Duration) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Duration = Duration.parse(decoder.decodeString())
}

object UrlSerializer : KSerializer<URL> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.net.URL", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: URL) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): URL = URL(decoder.decodeString())
}

// Need to be compatible with `com.fasterxml.jackson.databind.ext.NioPathSerializer.serialize` where a java.nio.file.Path is serialized as a URI string.
object NioPathSerializer : KSerializer<Path> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.nio.file.Path", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Path) = encoder.encodeString(value.toUri().toString())
override fun deserialize(decoder: Decoder): Path = decoder.decodeString()
.run {
runCatching {
// Try to parse as a URI first, as this is how Jackson serializes it.
Path.of(URI(this))
}
.recoverCatching {
Path.of(this)
}.getOrThrow()
}
}

class DTOs {

@Serializable
Expand Down Expand Up @@ -49,4 +80,151 @@ class DTOs {
val exitCode: Int,
)

/**
* CDR client configuration class hierarchy. It is an almost verbatim copy of `com.swisscom.health.des.cdr.client.config.CdrClientConfig`.
*
* NOTE: Deserialization with Jackson fails if constructor parameters don't have default values.
*/
@Serializable
data class CdrClientConfig(
val fileSynchronizationEnabled: Boolean,
val customer: List<Connector>,
val cdrApi: Endpoint,
val filesInProgressCacheSize: String,
val idpCredentials: IdpCredentials,
val idpEndpoint: URL,
val localFolder: Path,
val pullThreadPoolSize: Int,
val pushThreadPoolSize: Int,
val retryDelay: List<Duration>,
val scheduleDelay: Duration,
val credentialApi: Endpoint,
val fileBusyTestInterval: Duration,
val fileBusyTestTimeout: Duration,
val fileBusyTestStrategy: FileBusyTestStrategy,
) {

companion object {
@JvmStatic
val EMPTY = CdrClientConfig(
fileSynchronizationEnabled = false,
customer = emptyList(),
cdrApi = Endpoint.EMPTY,
filesInProgressCacheSize = "",
idpCredentials = IdpCredentials.EMPTY,
idpEndpoint = URL("http://localhost:8080"),
localFolder = EMPTY_PATH,
pullThreadPoolSize = 0,
pushThreadPoolSize = 0,
retryDelay = emptyList(),
scheduleDelay = Duration.ZERO,
credentialApi = Endpoint.EMPTY,
fileBusyTestInterval = Duration.ZERO,
fileBusyTestTimeout = Duration.ZERO,
fileBusyTestStrategy = FileBusyTestStrategy.NEVER_BUSY
)
}

@Serializable
data class Connector(
val connectorId: String,
val targetFolder: Path,
val sourceFolder: Path,
val contentType: String,
val sourceArchiveEnabled: Boolean,
val sourceArchiveFolder: Path,
val sourceErrorFolder: Path,
val mode: Mode,
val docTypeFolders: Map<DocumentType, DocTypeFolders>,
) {

@Serializable
data class DocTypeFolders(
val sourceFolder: Path? = null,
val targetFolder: Path,
)

}

@Serializable
data class Endpoint(
val scheme: String,
val host: String,
val port: Int,
val basePath: String,
) {
companion object {
@JvmStatic
val EMPTY = Endpoint(
scheme = "",
host = "",
port = 0,
basePath = ""
)
}
}

@Serializable
data class IdpCredentials(
val tenantId: String,
val clientId: String,
val clientSecret: String,
val scopes: List<String>,
val renewCredential: Boolean,
val maxCredentialAge: Duration,
val lastCredentialRenewalTime: Instant,
) {
companion object {
@JvmStatic
val EMPTY = IdpCredentials(
tenantId = "",
clientId = "",
clientSecret = "",
scopes = emptyList(),
renewCredential = false,
maxCredentialAge = Duration.ZERO,
lastCredentialRenewalTime = Instant.EPOCH
)
}
}

enum class DocumentType {
UNDEFINED,
CONTAINER,
CREDIT,
FORM,
HOSPITAL_MCD,
INVOICE,
NOTIFICATION;
}

enum class Mode {
TEST,
PRODUCTION,
NONE
}

enum class FileBusyTestStrategy {
/** checks for file size changes over a configurable duration. */
FILE_SIZE_CHANGED,

/**
* always flags the file as 'not busy'; use if all downstream applications
* create files in a single atomic operation (`move` on the same file system).
*/
NEVER_BUSY,

/** always flags the file as 'busy'; only useful for test scenarios. */
ALWAYS_BUSY,
}

}

companion object {

@JvmStatic
val EMPTY_PATH: Path = Path.of("")

}

}
Loading
Loading