fixing gradle build

This commit is contained in:
Stefan Mogeritsch 2025-08-01 00:04:50 +02:00
parent df5919fac8
commit 4ea084bd1d
25 changed files with 280 additions and 341 deletions

View File

@ -5,14 +5,18 @@ plugins {
alias(libs.plugins.kotlin.serialization)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime")
}
}
dependencies {
// Stellt sicher, dass dieses Modul Zugriff auf die im zentralen Katalog
// definierten Bibliotheken hat.
api(projects.platform.platformDependencies)
// Kern-Abhängigkeiten für das Domänen-Modell.
// `api` wird verwendet, damit Services, die `core-domain` einbinden,
// diese Typen ebenfalls direkt nutzen können.
// Kern-Abhängigkeiten für das Domänen-Modul.
api(libs.uuid)
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)

View File

@ -2,8 +2,9 @@ package at.mocode.core.domain.event
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import java.util.UUID
import kotlin.time.Clock
import kotlin.time.Instant
/**
* Base interface for all domain events in the system.
@ -12,9 +13,9 @@ import kotlinx.datetime.Instant
interface DomainEvent {
val eventId: Uuid
val aggregateId: Uuid
val eventType: String
val eventType: java.time.Instant
val timestamp: Instant
val version: Long
val version: Int
// OPTIMIZED: Added correlation and causation IDs for distributed tracing.
/**
@ -33,8 +34,8 @@ interface DomainEvent {
*/
abstract class BaseDomainEvent(
override val aggregateId: Uuid,
override val eventType: String,
override val version: Long,
override val eventType: java.time.Instant,
override val version: Int,
override val eventId: Uuid = uuid4(),
override val timestamp: Instant = Clock.System.now(),
override val correlationId: Uuid? = null,

View File

@ -3,8 +3,8 @@ package at.mocode.core.domain.model
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.serialization.Serializable
/**
@ -49,7 +49,8 @@ data class ErrorDto(
data class ApiResponse<T>(
val data: T?,
val success: Boolean,
val errors: List<ErrorDto> = emptyList(), // OPTIMIZED: Using structured ErrorDto
val errors: List<ErrorDto> = emptyList(),
@Serializable(with = KotlinInstantSerializer::class)
val timestamp: Instant = Clock.System.now()
) {
companion object {

View File

@ -2,7 +2,7 @@ package at.mocode.core.domain.serialization
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
import kotlinx.datetime.Instant
import kotlin.time.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime

View File

@ -4,6 +4,12 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
}
dependencies {
// Abhängigkeit zum platform-Modul für zentrale Versionsverwaltung
api(projects.platform.platformDependencies)
@ -31,4 +37,5 @@ dependencies {
// Testing
testImplementation(projects.platform.platformTesting)
testImplementation(libs.bundles.testing.jvm)
testImplementation(libs.kotlin.test)
}

View File

@ -1,29 +1,24 @@
package at.mocode.core.utils.config
import at.mocode.core.utils.database.DatabaseConfig
import java.io.File
import java.net.InetAddress
import java.util.Properties
/**
* Zentrale Konfigurations-Klasse für die Anwendung.
* Hält alle Konfigurationswerte, die beim Start des Service explizit geladen werden.
* Zentrale, unveränderliche Konfigurations-Klasse für die Anwendung.
* Hält alle Konfigurationswerte, die beim Start eines Service geladen werden.
*/
class AppConfig(
val environment: AppEnvironment,
val appInfo: AppInfoConfig,
val server: ServerConfig,
val database: DatabaseConfig,
val serviceDiscovery: ServiceDiscoveryConfig,
val security: SecurityConfig,
val logging: LoggingConfig,
val rateLimit: RateLimitConfig,
val serviceDiscovery: ServiceDiscoveryConfig,
val database: DatabaseConfig
val rateLimit: RateLimitConfig
) {
companion object {
/**
* Factory-Methode, die eine AppConfig-Instanz durch das Laden von
* .properties-Dateien und Umgebungsvariablen erstellt.
* Dies ist der zentrale Einstiegspunkt, um die Konfiguration zu laden.
*/
fun load(): AppConfig {
val environment = AppEnvironment.current()
val props = loadProperties(environment)
@ -32,191 +27,148 @@ class AppConfig(
environment = environment,
appInfo = AppInfoConfig.fromProperties(props),
server = ServerConfig.fromProperties(props),
database = DatabaseConfig.fromProperties(props),
serviceDiscovery = ServiceDiscoveryConfig.fromProperties(props),
security = SecurityConfig.fromProperties(props),
logging = LoggingConfig.fromProperties(props, environment),
rateLimit = RateLimitConfig.fromProperties(props),
serviceDiscovery = ServiceDiscoveryConfig.fromProperties(props),
database = DatabaseConfig.fromProperties(props)
rateLimit = RateLimitConfig.fromProperties(props)
)
}
private fun loadProperties(environment: AppEnvironment): Properties {
val props = Properties()
// Lade Basis-Properties
loadPropertiesFile("application.properties", props)
// Lade umgebungsspezifische Properties
val envFile = "application-${environment.name.lowercase()}.properties"
loadPropertiesFile(envFile, props)
return props
}
private fun loadPropertiesFile(filename: String, props: Properties) {
val resourceStream = javaClass.classLoader.getResourceAsStream(filename)
val resourceStream = AppConfig::class.java.classLoader.getResourceAsStream(filename)
if (resourceStream != null) {
props.load(resourceStream)
resourceStream.close()
} else {
val file = File("config/$filename")
if (file.exists()) {
file.inputStream().use { props.load(it) }
}
resourceStream.use { props.load(it) }
return
}
val file = File("config/$filename")
if (file.exists()) {
file.inputStream().use { props.load(it) }
}
}
}
}
/**
* Konfiguration für Anwendungsinformationen.
*/
data class AppInfoConfig(
val name: String,
val version: String,
val description: String
) {
data class AppInfoConfig(val name: String, val version: String, val description: String) {
companion object {
fun fromProperties(props: Properties): AppInfoConfig {
return AppInfoConfig(
name = props.getProperty("app.name", "Meldestelle"),
version = props.getProperty("app.version", "1.0.0"),
description = props.getProperty("app.description", "Pferdesport Meldestelle System")
)
}
fun fromProperties(props: Properties) = AppInfoConfig(
name = props.getProperty("app.name", "Meldestelle"),
version = props.getProperty("app.version", "1.0.0"),
description = props.getProperty("app.description", "Pferdesport Meldestelle System")
)
}
}
/**
* Konfiguration für den Server.
*/
data class ServerConfig(
val port: Int,
val host: String,
val advertisedHost: String,
val workers: Int,
val cors: CorsConfig
) {
companion object {
fun fromProperties(props: Properties): ServerConfig {
val corsConfig = CorsConfig(
enabled = props.getProperty("server.cors.enabled")?.toBoolean() ?: true,
allowedOrigins = props.getProperty("server.cors.allowedOrigins")?.split(",")?.map { it.trim() }
?: listOf("*")
)
val defaultHost = try { InetAddress.getLocalHost().hostAddress } catch (_: Exception) { "127.0.0.1" }
return ServerConfig(
port = System.getenv("API_PORT")?.toIntOrNull() ?: props.getProperty("server.port", "8081").toInt(),
host = System.getenv("API_HOST") ?: props.getProperty("server.host", "0.0.0.0"),
workers = props.getProperty("server.workers")?.toIntOrNull() ?: Runtime.getRuntime()
.availableProcessors(),
cors = corsConfig
port = props.getIntProperty("server.port", "API_PORT", 8081),
host = props.getStringProperty("server.host", "API_HOST", "0.0.0.0"),
advertisedHost = props.getStringProperty("server.advertisedHost", "API_HOST_ADVERTISED", defaultHost),
workers = props.getIntProperty("server.workers", "API_WORKERS", Runtime.getRuntime().availableProcessors()),
cors = CorsConfig.fromProperties(props)
)
}
}
data class CorsConfig(val enabled: Boolean, val allowedOrigins: List<String>) {
companion object {
fun fromProperties(props: Properties) = CorsConfig(
enabled = props.getBooleanProperty("server.cors.enabled", "API_CORS_ENABLED", true),
allowedOrigins = props.getProperty("server.cors.allowedOrigins")?.split(",")?.map { it.trim() } ?: listOf("*")
)
}
}
data class CorsConfig(
val enabled: Boolean,
val allowedOrigins: List<String>
)
}
/**
* Konfiguration für die Sicherheit.
*/
data class SecurityConfig(
val jwt: JwtConfig,
val apiKey: String?
data class DatabaseConfig(
val jdbcUrl: String,
val username: String,
val password: String,
val driverClassName: String,
val maxPoolSize: Int,
val minPoolSize: Int,
val autoMigrate: Boolean
) {
companion object {
fun fromProperties(props: Properties): SecurityConfig {
val jwtConfig = JwtConfig(
secret = System.getenv("JWT_SECRET") ?: props.getProperty(
"security.jwt.secret",
"default-jwt-secret-key-please-change-in-production"
),
issuer = System.getenv("JWT_ISSUER") ?: props.getProperty("security.jwt.issuer", "meldestelle-api"),
audience = System.getenv("JWT_AUDIENCE") ?: props.getProperty(
"security.jwt.audience",
"meldestelle-clients"
),
realm = System.getenv("JWT_REALM") ?: props.getProperty("security.jwt.realm", "meldestelle"),
expirationInMinutes = props.getProperty("security.jwt.expirationInMinutes")?.toLongOrNull() ?: (60 * 24)
)
return SecurityConfig(
jwt = jwtConfig,
apiKey = System.getenv("API_KEY") ?: props.getProperty("security.apiKey")
fun fromProperties(props: Properties): DatabaseConfig {
val host = props.getStringProperty("database.host", "DB_HOST", "localhost")
val port = props.getIntProperty("database.port", "DB_PORT", 5432)
val name = props.getStringProperty("database.name", "DB_NAME", "meldestelle_db")
return DatabaseConfig(
jdbcUrl = "jdbc:postgresql://$host:$port/$name",
username = props.getStringProperty("database.username", "DB_USER", "meldestelle_user"),
password = props.getStringProperty("database.password", "DB_PASSWORD", "secure_password_change_me"),
driverClassName = "org.postgresql.Driver",
maxPoolSize = props.getIntProperty("database.maxPoolSize", "DB_MAX_POOL_SIZE", 10),
minPoolSize = props.getIntProperty("database.minPoolSize", "DB_MIN_POOL_SIZE", 5),
autoMigrate = props.getBooleanProperty("database.autoMigrate", "DB_AUTO_MIGRATE", true)
)
}
}
data class JwtConfig(
val secret: String,
val issuer: String,
val audience: String,
val realm: String,
val expirationInMinutes: Long
)
}
/**
* Konfiguration für das Logging.
*/
data class LoggingConfig(
val level: String,
val logRequests: Boolean,
val logResponses: Boolean
// ... many more detailed properties from your original file
) {
data class ServiceDiscoveryConfig(val enabled: Boolean, val consulHost: String, val consulPort: Int) {
companion object {
fun fromProperties(props: Properties, env: AppEnvironment): LoggingConfig {
return LoggingConfig(
level = props.getProperty("logging.level", if (env == AppEnvironment.PRODUCTION) "INFO" else "DEBUG"),
logRequests = props.getProperty("logging.requests")?.toBoolean() ?: true,
logResponses = props.getProperty("logging.responses")?.toBoolean() ?: (env != AppEnvironment.PRODUCTION)
// ... load other properties here
)
}
fun fromProperties(props: Properties) = ServiceDiscoveryConfig(
enabled = props.getBooleanProperty("service-discovery.enabled", "CONSUL_ENABLED", true),
consulHost = props.getStringProperty("service-discovery.consul.host", "CONSUL_HOST", "consul"),
consulPort = props.getIntProperty("service-discovery.consul.port", "CONSUL_PORT", 8500)
)
}
}
/**
* Konfiguration für Rate Limiting.
*/
data class RateLimitConfig(
val enabled: Boolean,
val globalLimit: Int,
val globalPeriodMinutes: Int
) {
data class SecurityConfig(val jwt: JwtConfig, val apiKey: String?) {
companion object {
fun fromProperties(props: Properties): RateLimitConfig {
return RateLimitConfig(
enabled = props.getProperty("ratelimit.enabled")?.toBoolean() ?: true,
globalLimit = props.getProperty("ratelimit.global.limit")?.toIntOrNull() ?: 100,
globalPeriodMinutes = props.getProperty("ratelimit.global.periodMinutes")?.toIntOrNull() ?: 1
fun fromProperties(props: Properties) = SecurityConfig(
jwt = JwtConfig.fromProperties(props),
apiKey = props.getStringProperty("security.apiKey", "API_KEY", "").ifEmpty { null }
)
}
data class JwtConfig(val secret: String, val issuer: String, val audience: String, val realm: String, val expirationInMinutes: Long) {
companion object {
fun fromProperties(props: Properties) = JwtConfig(
secret = props.getStringProperty("security.jwt.secret", "JWT_SECRET", "default-secret-please-change-in-production"),
issuer = props.getStringProperty("security.jwt.issuer", "JWT_ISSUER", "meldestelle-api"),
audience = props.getStringProperty("security.jwt.audience", "JWT_AUDIENCE", "meldestelle-clients"),
realm = props.getStringProperty("security.jwt.realm", "JWT_REALM", "meldestelle"),
expirationInMinutes = props.getLongProperty("security.jwt.expirationInMinutes", "JWT_EXPIRATION_MINUTES", 60 * 24)
)
}
}
}
/**
* Konfiguration für Service Discovery.
*/
data class ServiceDiscoveryConfig(
val enabled: Boolean,
val consulHost: String,
val consulPort: Int
) {
data class LoggingConfig(val level: String, val logRequests: Boolean, val logResponses: Boolean) {
companion object {
fun fromProperties(props: Properties): ServiceDiscoveryConfig {
return ServiceDiscoveryConfig(
enabled = props.getProperty("service-discovery.enabled")?.toBoolean() ?: true,
consulHost = System.getenv("CONSUL_HOST") ?: props.getProperty(
"service-discovery.consul.host",
"consul"
),
consulPort = System.getenv("CONSUL_PORT")?.toIntOrNull()
?: props.getProperty("service-discovery.consul.port", "8500").toInt()
)
}
fun fromProperties(props: Properties, env: AppEnvironment) = LoggingConfig(
level = props.getStringProperty("logging.level", "LOG_LEVEL", if (env.isProduction()) "INFO" else "DEBUG"),
logRequests = props.getBooleanProperty("logging.requests", "LOG_REQUESTS", true),
logResponses = props.getBooleanProperty("logging.responses", "LOG_RESPONSES", !env.isProduction())
)
}
}
data class RateLimitConfig(val enabled: Boolean, val globalLimit: Int, val globalPeriodMinutes: Int) {
companion object {
fun fromProperties(props: Properties) = RateLimitConfig(
enabled = props.getBooleanProperty("ratelimit.enabled", "RATE_LIMIT_ENABLED", true),
globalLimit = props.getIntProperty("ratelimit.global.limit", "RATE_LIMIT_GLOBAL_LIMIT", 100),
globalPeriodMinutes = props.getIntProperty("ratelimit.global.periodMinutes", "RATE_LIMIT_GLOBAL_PERIOD", 1)
)
}
}

View File

@ -1,48 +1,26 @@
package at.mocode.core.utils.config
/**
* Aufzählung der verschiedenen Anwendungsumgebungen.
*/
import org.slf4j.LoggerFactory
enum class AppEnvironment {
DEVELOPMENT, // Lokale Entwicklungsumgebung
TEST, // Testumgebung (CI/CD, Integrationstests)
STAGING, // Vorabproduktionsumgebung
PRODUCTION; // Produktionsumgebung
DEVELOPMENT,
TEST,
STAGING,
PRODUCTION;
fun isProduction() = this == PRODUCTION
companion object {
/**
* Ermittelt die aktuelle Umgebung basierend auf der APP_ENV Umgebungsvariable.
*
* @return Die aktuelle Umgebung (Standardmäßig DEVELOPMENT wenn nicht definiert)
*/
private val logger = LoggerFactory.getLogger(AppEnvironment::class.java)
fun current(): AppEnvironment {
val envName = System.getenv("APP_ENV")?.uppercase() ?: "DEVELOPMENT"
return try {
valueOf(envName)
} catch (_: IllegalArgumentException) {
println("Warnung: Unbekannte Umgebung '$envName', verwende DEVELOPMENT")
logger.warn("Unknown environment '{}', falling back to DEVELOPMENT.", envName)
DEVELOPMENT
}
}
/**
* Prüft, ob die aktuelle Umgebung die Entwicklungsumgebung ist.
*/
fun isDevelopment() = current() == DEVELOPMENT
/**
* Prüft, ob die aktuelle Umgebung die Testumgebung ist.
*/
fun isTest() = current() == TEST
/**
* Prüft, ob die aktuelle Umgebung die Staging-Umgebung ist.
*/
fun isStaging() = current() == STAGING
/**
* Prüft, ob die aktuelle Umgebung die Produktionsumgebung ist.
*/
fun isProduction() = current() == PRODUCTION
}
}

View File

@ -0,0 +1,39 @@
package at.mocode.core.utils.config
import java.util.Properties
/**
* Liest eine String-Property, wobei eine Umgebungsvariable Vorrang hat.
*
* @param key Der Schlüssel in der '.properties-Datei'.
* @param envVar Der Name der Umgebungsvariable.
* @param default Der Standardwert, falls weder Property noch Env-Var existieren.
* @return Der geladene Konfigurationswert.
*/
fun Properties.getStringProperty(key: String, envVar: String, default: String): String {
return System.getenv(envVar) ?: this.getProperty(key, default)
}
/**
* Liest eine Integer-Property, wobei eine Umgebungsvariable Vorrang hat.
*/
fun Properties.getIntProperty(key: String, envVar: String, default: Int): Int {
val value = System.getenv(envVar) ?: this.getProperty(key)
return value?.toIntOrNull() ?: default
}
/**
* Liest eine Boolean-Property, wobei eine Umgebungsvariable Vorrang hat.
*/
fun Properties.getBooleanProperty(key: String, envVar: String, default: Boolean): Boolean {
val value = System.getenv(envVar) ?: this.getProperty(key)
return value?.toBoolean() ?: default
}
/**
* Liest eine Long-Property, wobei eine Umgebungsvariable Vorrang hat.
*/
fun Properties.getLongProperty(key: String, envVar: String, default: Long): Long {
val value = System.getenv(envVar) ?: this.getProperty(key)
return value?.toLongOrNull() ?: default
}

View File

@ -1,55 +0,0 @@
package at.mocode.core.utils.database
import java.util.Properties
/**
* Konfiguration für die Datenbankverbindung.
* Diese Klasse ist ein reiner Datenhalter (Value Object). Die Logik zum Laden
* der Werte ist in der companion object Factory-Methode gekapselt.
*/
data class DatabaseConfig(
val jdbcUrl: String,
val username: String,
val password: String,
val driverClassName: String = "org.postgresql.Driver",
val maxPoolSize: Int = 10,
val minPoolSize: Int = 5,
val autoMigrate: Boolean = true // Flag to enable/disable Flyway migrations
) {
companion object {
/**
* Erstellt eine Datenbank-Konfiguration aus Umgebungsvariablen und Properties.
* Die Priorität ist: Umgebungsvariablen > Properties > Standardwerte.
*/
fun fromProperties(props: Properties): DatabaseConfig {
val host = System.getenv("DB_HOST") ?: props.getProperty("database.host", "localhost")
val port = System.getenv("DB_PORT") ?: props.getProperty("database.port", "5432")
val database = System.getenv("DB_NAME") ?: props.getProperty("database.name", "meldestelle_db")
val username = System.getenv("DB_USER") ?: props.getProperty("database.username", "meldestelle_user")
val password =
System.getenv("DB_PASSWORD") ?: props.getProperty("database.password", "secure_password_change_me")
val maxPoolSize = System.getenv("DB_MAX_POOL_SIZE")?.toIntOrNull()
?: props.getProperty("database.maxPoolSize")?.toIntOrNull()
?: 10
val minPoolSize = System.getenv("DB_MIN_POOL_SIZE")?.toIntOrNull()
?: props.getProperty("database.minPoolSize")?.toIntOrNull()
?: 5
val autoMigrate = System.getenv("DB_AUTO_MIGRATE")?.toBoolean()
?: props.getProperty("database.autoMigrate")?.toBoolean()
?: true
return DatabaseConfig(
jdbcUrl = "jdbc:postgresql://$host:$port/$database",
username = username,
password = password,
driverClassName = "org.postgresql.Driver",
maxPoolSize = maxPoolSize,
minPoolSize = minPoolSize,
autoMigrate = autoMigrate
)
}
}
}

View File

@ -1,33 +1,29 @@
package at.mocode.core.utils.database
import at.mocode.core.utils.config.DatabaseConfig
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import kotlinx.coroutines.Dispatchers
import org.flywaydb.core.Flyway
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.slf4j.LoggerFactory
/**
* Factory-Klasse für die Datenbankverbindung.
* Erstellt und konfiguriert eine Datenbankverbindung inklusive Connection Pool
* und führt bei der Initialisierung die notwendigen Migrationen aus.
*
* @property config Die Datenbankkonfiguration, die für diese Instanz verwendet werden soll.
*/
class DatabaseFactory(private val config: DatabaseConfig) {
private companion object {
private val logger = LoggerFactory.getLogger(DatabaseFactory::class.java)
}
private var dataSource: HikariDataSource? = null
private var database: Database? = null
/**
* Initialisiert die Datenbankverbindung. Muss vor der ersten Verwendung aufgerufen werden.
* Konfiguriert den Connection Pool und führt Flyway-Migrationen aus.
*/
fun connect() {
if (dataSource != null) {
logger.warn("Database already connected. Closing existing connection before creating a new one.")
close()
}
logger.info("Initializing database connection to ${config.jdbcUrl}")
val hikariConfig = createHikariConfig()
val ds = HikariDataSource(hikariConfig)
dataSource = ds
@ -38,28 +34,16 @@ class DatabaseFactory(private val config: DatabaseConfig) {
}
}
/**
* Schließt die Datenbankverbindung und den Connection Pool.
*/
fun close() {
dataSource?.close()
dataSource = null
database = null
logger.info("Database connection closed.")
}
/**
* Führt eine Datenbankoperation in einer neuen, suspendierenden Transaktion aus.
* Dies ist die primäre Methode, um mit der Datenbank zu interagieren.
*
* @param block Der Code, der in der Transaktion ausgeführt werden soll.
* @return Das Ergebnis der Transaktion.
*/
suspend fun <T> dbQuery(block: suspend () -> T): T {
// Wir stellen sicher, dass die dbQuery-Funktion nur auf einer verbundenen Datenbank läuft.
if (database == null) {
throw IllegalStateException("Database has not been connected. Call connect() first.")
}
return newSuspendedTransaction(Dispatchers.IO, db = database) {
val db = database ?: throw IllegalStateException("Database has not been connected. Call connect() first.")
return newSuspendedTransaction(Dispatchers.IO, db = db) {
block()
}
}
@ -74,29 +58,27 @@ class DatabaseFactory(private val config: DatabaseConfig) {
minimumIdle = config.minPoolSize
isAutoCommit = false
transactionIsolation = "TRANSACTION_READ_COMMITTED"
connectionTestQuery = "SELECT 1"
validationTimeout = 5000 // 5 seconds
connectionTimeout = 30000 // 30 seconds
idleTimeout = 600000 // 10 minutes
maxLifetime = 1800000 // 30 minutes
leakDetectionThreshold = 60000 // 1 minute
poolName = "MeldestelleDbPool-${config.jdbcUrl.substringAfterLast('/')}" // Eindeutiger Pool-Name
validationTimeout = 5000
connectionTimeout = 30000
idleTimeout = 600000
maxLifetime = 1800000
leakDetectionThreshold = 60000
poolName = "MeldestelleDbPool"
}
}
private fun runFlyway(dataSource: HikariDataSource) {
println("Starte Flyway-Migrationen für Schema: ${dataSource.jdbcUrl}")
logger.info("Starting Flyway migrations...")
try {
Flyway.configure()
val count = Flyway.configure()
.dataSource(dataSource)
.locations("classpath:db/migration")
.load()
.migrate()
println("Flyway-Migrationen erfolgreich abgeschlossen.")
.migrationsExecuted
logger.info("Flyway migrations completed successfully. Applied $count migrations.")
} catch (e: Exception) {
println("FEHLER: Flyway-Migration fehlgeschlagen! Details: ${e.message}")
// Wir werfen den Fehler weiter, damit die Anwendung beim Start fehlschlägt.
// Das ist wichtig, um Inkonsistenzen zu vermeiden.
logger.error("Flyway migration failed!", e)
throw IllegalStateException("Flyway migration failed", e)
}
}

View File

@ -3,31 +3,32 @@ package at.mocode.core.utils.discovery
import at.mocode.core.utils.config.AppConfig
import com.orbitz.consul.Consul
import com.orbitz.consul.model.agent.ImmutableRegistration
// KORREKTUR: Expliziter Import für die `Registration`-Klasse, die den `RegCheck` enthält.
import com.orbitz.consul.model.agent.Registration
import java.net.InetAddress
import org.slf4j.LoggerFactory
import java.util.*
/**
* Repräsentiert die Registrierung eines einzelnen Service-Exemplars bei Consul.
* Diese Klasse kümmert sich um den Lebenszyklus (Registrierung, Deregistrierung).
*/
class ServiceRegistration internal constructor(
private val consul: Consul,
private val registration: ImmutableRegistration
) {
private companion object {
private val logger = LoggerFactory.getLogger(ServiceRegistration::class.java)
}
private var isRegistered = false
fun register() {
if (isRegistered) return
try {
// Der `register`-Aufruf ist korrekt, da das `registration`-Objekt
// bereits außerhalb vollständig und korrekt gebaut wurde.
consul.agentClient().register(registration)
isRegistered = true
println("Service '${registration.name()}' mit ID '${registration.id()}' erfolgreich bei Consul registriert.")
logger.info(
"Service '{}' with ID '{}' successfully registered with Consul.",
registration.name(),
registration.id()
)
} catch (e: Exception) {
println("FEHLER: Service-Registrierung bei Consul fehlgeschlagen: ${e.message}")
logger.error("Failed to register service '{}' with Consul.", registration.name(), e)
throw IllegalStateException("Could not register service with Consul", e)
}
}
@ -35,63 +36,65 @@ class ServiceRegistration internal constructor(
fun deregister() {
if (!isRegistered) return
try {
// Der `deregister`-Aufruf ist korrekt. Er erwartet die Service-ID als einfachen String.
consul.agentClient().deregister(registration.id())
isRegistered = false
println("Service '${registration.name()}' mit ID '${registration.id()}' erfolgreich bei Consul deregistriert.")
logger.info(
"Service '{}' with ID '{}' successfully deregistered from Consul.",
registration.name(),
registration.id()
)
} catch (e: Exception) {
println("FEHLER: Service-Deregistrierung bei Consul fehlgeschlagen: ${e.message}")
logger.error("Failed to deregister service '{}' from Consul.", registration.id(), e)
}
}
}
/**
* Zentraler Registrar, der beim Anwendungsstart Services registriert.
* Diese Klasse wird einmalig mit der Gesamt-AppConfig initialisiert.
*/
class ServiceRegistrar(private val appConfig: AppConfig) {
private companion object {
private val logger = LoggerFactory.getLogger(ServiceRegistrar::class.java)
}
private val consul: Consul by lazy {
val consulConfig = appConfig.serviceDiscovery
logger.info("Connecting to Consul at {}:{}", consulConfig.consulHost, consulConfig.consulPort)
Consul.builder()
.withUrl("http://${consulConfig.consulHost}:${consulConfig.consulPort}")
.build()
}
/**
* Erstellt und registriert einen Service basierend auf der App-Konfiguration.
* @return Eine ServiceRegistration-Instanz zur Verwaltung des Lebenszyklus.
*/
fun registerCurrentService(): ServiceRegistration {
val serviceName = appConfig.appInfo.name
val servicePort = appConfig.server.port
val serviceId = "$serviceName-${UUID.randomUUID()}"
val hostAddress = InetAddress.getLocalHost().hostAddress
val hostAddress = appConfig.server.advertisedHost
// KORREKTUR: Der Health Check MUSS über die statische Factory-Methode `http`
// der `Registration.RegCheck`-Klasse erstellt werden. Dies war die Hauptfehlerquelle.
val healthCheck = Registration.RegCheck.http(
"http://$hostAddress:$servicePort/health", // Standard-Health-Check-Pfad
10L, // Intervall in Sekunden
5L // Timeout in Sekunden
"http://$hostAddress:$servicePort/health",
10L,
5L
)
// ========= FINALE KORREKTUR =========
// Wir erstellen die Liste und die Map VORHER mit expliziten Typen,
// um dem Compiler bei der Typinferenz zu helfen.
val serviceTags: List<String> = listOf("env:${appConfig.environment.name.lowercase()}")
val serviceMeta: Map<String, String> = mapOf("version" to appConfig.appInfo.version)
val registration = ImmutableRegistration.builder()
.id(serviceId)
.name(serviceName)
.address(hostAddress)
.port(servicePort)
.check(healthCheck)
.tags(listOf("env:${appConfig.environment.name.lowercase()}"))
.meta(mapOf("version" to appConfig.appInfo.version))
.tags(serviceTags) // Verwenden der explizit typisierten Variablen
.meta(serviceMeta) // Verwenden der explizit typisierten Variablen
.build()
val serviceRegistration = ServiceRegistration(consul, registration)
serviceRegistration.register()
// Fügt einen Shutdown-Hook hinzu, um den Service beim Beenden sauber zu deregistrieren
Runtime.getRuntime().addShutdownHook(Thread {
println("Shutdown-Hook: Deregistriere Service ${serviceId}...")
logger.info("Shutdown hook triggered: Deregistering service '{}'...", serviceId)
serviceRegistration.deregister()
})

View File

@ -4,7 +4,7 @@ import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
// KORREKTUR: Der Import wurde von java.math.BigDecimal auf die korrekte Bibliothek geändert.
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Instant
import kotlin.time.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
@ -14,6 +14,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.time.ExperimentalTime
object BigDecimalSerializer : KSerializer<BigDecimal> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)
@ -27,6 +28,7 @@ object UuidSerializer : KSerializer<Uuid> {
override fun deserialize(decoder: Decoder): Uuid = uuidFrom(decoder.decodeString())
}
@OptIn(ExperimentalTime::class)
object KotlinInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())

View File

@ -1,9 +1,10 @@
package at.mocode.core.utils.validation
import kotlinx.datetime.LocalDate
import kotlinx.datetime.Clock
import kotlin.time.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import kotlin.time.ExperimentalTime
/**
* Common validation utilities
@ -92,6 +93,7 @@ object ValidationUtils {
/**
* Validates birth date
*/
@OptIn(ExperimentalTime::class)
fun validateBirthDate(birthDate: LocalDate?, fieldName: String = "geburtsdatum"): ValidationError? {
if (birthDate == null) return null
@ -116,6 +118,7 @@ object ValidationUtils {
/**
* Validates year value
*/
@OptIn(ExperimentalTime::class)
fun validateYear(year: Int?, fieldName: String, minYear: Int = 1900): ValidationError? {
if (year == null) return null

View File

@ -2,9 +2,8 @@ package at.mocode.core.utils.database
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
import org.junit.*
import org.junit.jupiter.api.Assertions.assertEquals
/**
* Comprehensive database connectivity and operations test.

View File

@ -33,9 +33,9 @@ lettuce = "6.3.1.RELEASE"
# --- Service Discovery & Monitoring ---
consulClient = "1.5.3"
micrometer = "1.12.2"
micrometerTracing = "1.2.5" # NEU
zipkin = "2.24.4" # NEU (Verwendet eine neuere, kompatible Version)
zipkinReporter = "2.16.4" # NEU
micrometerTracing = "1.2.5"
zipkin = "3.0.5"
zipkinReporter = "2.16.4"
# --- Authentication ---
auth0Jwt = "4.4.0"
@ -134,10 +134,10 @@ lettuce-core = { module = "io.lettuce:lettuce-core", version.ref = "lettuce" }
consul-client = { module = "com.orbitz.consul:consul-client", version.ref = "consulClient" }
micrometer-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" }
micrometer-tracing-bridge-brave = { module = "io.micrometer:micrometer-tracing-bridge-brave", version.ref = "micrometerTracing" } # NEU
zipkin-reporter-brave = { module = "io.zipkin.reporter2:zipkin-reporter-brave", version.ref = "zipkinReporter" } # NEU
zipkin-sender-okhttp3 = { module = "io.zipkin.reporter2:zipkin-sender-okhttp3", version.ref = "zipkinReporter" } # NEU
zipkin-server = { module = "io.zipkin:zipkin-server", version.ref = "zipkin" } # NEU
zipkin-autoconfigure-ui = { module = "io.zipkin:zipkin-autoconfigure-ui", version.ref = "zipkin" } # NEU
zipkin-reporter-brave = { module = "io.zipkin.reporter2:zipkin-reporter-brave", version.ref = "zipkinReporter" }
zipkin-sender-okhttp3 = { module = "io.zipkin.reporter2:zipkin-sender-okhttp3", version.ref = "zipkinReporter" }
zipkin-server = { module = "io.zipkin:zipkin-server", version.ref = "zipkin" }
zipkin-autoconfigure-ui = { module = "io.zipkin:zipkin-autoconfigure-ui", version.ref = "zipkin" }
# --- Authentication ---
auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = "auth0Jwt" }

View File

@ -7,6 +7,11 @@ plugins {
alias(libs.plugins.spring.dependencyManagement)
}
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.getByName("bootJar") {
enabled = false
}
dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
api(platform(projects.platform.platformBom))
@ -20,4 +25,6 @@ dependencies {
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
testImplementation(projects.platform.platformTesting)
testImplementation(libs.bundles.testing.jvm)
testImplementation(libs.kotlin.test)
}

View File

@ -2,6 +2,8 @@ package at.mocode.infrastructure.eventstore.redis
import at.mocode.core.domain.event.DomainEvent
import at.mocode.infrastructure.eventstore.api.EventSerializer
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import org.slf4j.LoggerFactory
import org.springframework.data.domain.Range
import org.springframework.data.redis.connection.stream.*
@ -9,8 +11,6 @@ import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.scheduling.annotation.Scheduled
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
/**
* Consumer for Redis Streams that processes events using consumer groups.

View File

@ -5,6 +5,8 @@ import at.mocode.core.domain.event.DomainEvent
import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
@ -18,8 +20,6 @@ import java.time.Instant
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Integration tests for Redis Event Store.
@ -285,7 +285,7 @@ class RedisEventStoreIntegrationTest {
override val eventId: UUID = UUID.randomUUID(),
override val timestamp: Instant = Instant.now(),
override val aggregateId: UUID,
override val version: Long,
override val version: Int,
val name: String
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
@ -293,7 +293,7 @@ class RedisEventStoreIntegrationTest {
override val eventId: UUID = UUID.randomUUID(),
override val timestamp: Instant = Instant.now(),
override val aggregateId: UUID,
override val version: Long,
override val version: Int,
val name: String
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
}

View File

@ -519,7 +519,7 @@ class RedisEventStoreTest {
override val eventId: UUID = UUID.randomUUID(),
override val timestamp: Instant = Instant.now(),
override val aggregateId: UUID,
override val version: Long,
override val version: UUID,
val name: String
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
@ -527,7 +527,7 @@ class RedisEventStoreTest {
override val eventId: UUID = UUID.randomUUID(),
override val timestamp: Instant = Instant.now(),
override val aggregateId: UUID,
override val version: Long,
override val version: UUID,
val name: String
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
}

View File

@ -231,7 +231,7 @@ class RedisIntegrationTest {
override val eventId: UUID = UUID.randomUUID(),
override val timestamp: Instant = Instant.now(),
override val aggregateId: UUID,
override val version: Long,
override val version: UUID,
val name: String
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
@ -239,7 +239,7 @@ class RedisIntegrationTest {
override val eventId: UUID = UUID.randomUUID(),
override val timestamp: Instant = Instant.now(),
override val aggregateId: UUID,
override val version: Long,
override val version: UUID,
val name: String
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
}

View File

@ -7,6 +7,11 @@ plugins {
alias(libs.plugins.spring.dependencyManagement)
}
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.getByName("bootJar") {
enabled = false
}
dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
implementation(platform(projects.platform.platformBom))

View File

@ -7,6 +7,11 @@ plugins {
alias(libs.plugins.spring.dependencyManagement)
}
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.getByName("bootJar") {
enabled = false
}
dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
api(platform(projects.platform.platformBom))

View File

@ -7,6 +7,12 @@ plugins {
alias(libs.plugins.spring.dependencyManagement)
}
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.getByName("bootJar") {
enabled = false
}
dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
implementation(platform(projects.platform.platformBom))

View File

@ -23,9 +23,7 @@ dependencies {
implementation(libs.spring.boot.starter.actuator)
// Abhängigkeiten für den Zipkin-Server und seine UI.
// OPTIMIERUNG: Versionen werden jetzt zentral über libs.versions.toml verwaltet.
implementation(libs.zipkin.server)
implementation(libs.zipkin.autoconfigure.ui)
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
testImplementation(projects.platform.platformTesting)

View File

@ -1,4 +1,5 @@
/*plugins {
/*
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
@ -34,7 +35,9 @@ kotlin {
}
}
}
}*/
}
*/
// Dieses Modul bündelt alle für JVM-Tests notwendigen Abhängigkeiten.
// Jedes Modul, das Tests enthält, sollte dieses Modul mit `testImplementation` einbinden.
@ -46,12 +49,11 @@ dependencies {
// Importiert die zentrale BOM für konsistente Versionen.
api(platform(projects.platform.platformBom))
// OPTIMIERUNG: Verwendung von Bundles, um die Konfiguration zu vereinfachen.
// Diese Bundles sind in `libs.versions.toml` definiert.
api(libs.bundles.testing.jvm)
api(libs.bundles.testcontainers)
// Einzelne Test-Abhängigkeiten, die nicht in den Haupt-Bundles enthalten sind.
// Stellt Spring Boot Test-Abhängigkeiten und die H2-Datenbank für Tests bereit.
api(libs.spring.boot.starter.test)
api(libs.h2.driver) // H2 wird oft für In-Memory-Tests benötigt.
api(libs.h2.driver)
}