feat(zns-importer): add ZNSImportService with tests and REST controller
- Created `ZnsImportService` to handle uploading, parsing, and persisting ZNS data from legacy `.zip` files. - Introduced corresponding test cases in `ZnsImportServiceTest` for handling edge cases including imports and updates. - Added REST controller `ZnsImportController` for initiating import jobs and retrieving their status. - Defined `ZnsImportResult` data structure for reporting results of import operations. - Established database configuration specific to ZNS importer for development profile. - Updated utility libraries with `FixedWidthLineReader` for fixed-width string parsing. - Refactored architecture by placing parser logic in `core:zns-parser` for reuse across backend and Compose Desktop app. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.znsParser)
|
||||
implementation(projects.backend.services.clubs.clubsDomain)
|
||||
implementation(projects.backend.services.persons.personsDomain)
|
||||
implementation(projects.backend.services.horses.horsesDomain)
|
||||
implementation(projects.backend.services.officials.officialsDomain)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package at.mocode.zns.importer
|
||||
|
||||
/**
|
||||
* Ergebnis eines ZNS-Imports.
|
||||
*
|
||||
* Enthält Zähler für importierte, aktualisierte und übersprungene Datensätze
|
||||
* sowie eine Liste von Fehlern/Warnungen, die während des Imports aufgetreten sind.
|
||||
*/
|
||||
data class ZnsImportResult(
|
||||
val vereineImportiert: Int = 0,
|
||||
val vereineAktualisiert: Int = 0,
|
||||
val reiterImportiert: Int = 0,
|
||||
val reiterAktualisiert: Int = 0,
|
||||
val pferdeImportiert: Int = 0,
|
||||
val pferdeAktualisiert: Int = 0,
|
||||
val richterImportiert: Int = 0,
|
||||
val richterAktualisiert: Int = 0,
|
||||
val fehler: List<String> = emptyList(),
|
||||
val warnungen: List<String> = emptyList()
|
||||
) {
|
||||
val gesamtImportiert: Int
|
||||
get() = vereineImportiert + reiterImportiert + pferdeImportiert + richterImportiert
|
||||
|
||||
val gesamtAktualisiert: Int
|
||||
get() = vereineAktualisiert + reiterAktualisiert + pferdeAktualisiert + richterAktualisiert
|
||||
|
||||
val hatFehler: Boolean
|
||||
get() = fehler.isNotEmpty()
|
||||
|
||||
fun zusammenfassung(): String =
|
||||
"ZNS-Import abgeschlossen: " +
|
||||
"$gesamtImportiert neu importiert, $gesamtAktualisiert aktualisiert. " +
|
||||
"Vereine: ${vereineImportiert}N/${vereineAktualisiert}U, " +
|
||||
"Reiter: ${reiterImportiert}N/${reiterAktualisiert}U, " +
|
||||
"Pferde: ${pferdeImportiert}N/${pferdeAktualisiert}U, " +
|
||||
"Richter: ${richterImportiert}N/${richterAktualisiert}U. " +
|
||||
if (hatFehler) "${fehler.size} Fehler aufgetreten." else "Keine Fehler."
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.zns.importer
|
||||
|
||||
import at.mocode.clubs.domain.repository.VereinRepository
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.officials.domain.repository.FunktionaerRepository
|
||||
import at.mocode.persons.domain.repository.ReiterRepository
|
||||
import at.mocode.zns.parser.ZnsLegacyParsers
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
/**
|
||||
* Orchestrator für den ZNS-Stammdaten-Import.
|
||||
*
|
||||
* Nimmt eine ZNS-ZIP-Datei entgegen, entpackt sie in-memory, liest die
|
||||
* enthaltenen .DAT-Dateien mit CP850-Encoding und persistiert die geparsten
|
||||
* Domänenobjekte über die jeweiligen Repositories (Upsert-Logik).
|
||||
*
|
||||
* Die Verarbeitungsreihenfolge ist fix:
|
||||
* 1. VEREIN01.DAT → DomVerein (via VereinRepository)
|
||||
* 2. LIZENZ01.DAT → DomReiter (via ReiterRepository)
|
||||
* 3. PFERDE01.DAT → DomPferd (via HorseRepository)
|
||||
* 4. RICHT01.DAT → DomFunktionaer (via FunktionaerRepository)
|
||||
*
|
||||
* Dieser Service hat **keine** Spring-Abhängigkeit und kann daher sowohl
|
||||
* im Backend (REST-Upload) als auch in der Compose Desktop App (Offline-Import)
|
||||
* verwendet werden.
|
||||
*
|
||||
* @param vereinRepository Repository für Vereine.
|
||||
* @param reiterRepository Repository für Reiter/Personen.
|
||||
* @param horseRepository Repository für Pferde.
|
||||
* @param funktionaerRepository Repository für Funktionäre/Richter.
|
||||
*/
|
||||
class ZnsImportService(
|
||||
private val vereinRepository: VereinRepository,
|
||||
private val reiterRepository: ReiterRepository,
|
||||
private val horseRepository: HorseRepository,
|
||||
private val funktionaerRepository: FunktionaerRepository
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val CP850 = Charset.forName("Cp850")
|
||||
|
||||
private const val FILE_VEREIN = "VEREIN01.DAT"
|
||||
private const val FILE_LIZENZ = "LIZENZ01.DAT"
|
||||
private const val FILE_PFERDE = "PFERDE01.DAT"
|
||||
private const val FILE_RICHT = "RICHT01.DAT"
|
||||
}
|
||||
|
||||
/**
|
||||
* Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
|
||||
*
|
||||
* @param zipInputStream Der InputStream der ZIP-Datei.
|
||||
* @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern.
|
||||
*/
|
||||
suspend fun importiereZip(zipInputStream: InputStream): ZnsImportResult {
|
||||
val dateien = mutableMapOf<String, List<String>>()
|
||||
ZipInputStream(zipInputStream).use { zip ->
|
||||
var entry = zip.nextEntry
|
||||
while (entry != null) {
|
||||
val name = entry.name.uppercase().substringAfterLast("/")
|
||||
if (name in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
|
||||
dateien[name] = zip.readBytes().toString(CP850).lines()
|
||||
}
|
||||
zip.closeEntry()
|
||||
entry = zip.nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
val fehler = mutableListOf<String>()
|
||||
val warnungen = mutableListOf<String>()
|
||||
|
||||
val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler)
|
||||
val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen)
|
||||
val (pferdeNeu, pferdeUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
|
||||
val (richterNeu, richterUpd) = importiereRichter(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
|
||||
|
||||
return ZnsImportResult(
|
||||
vereineImportiert = vereineNeu,
|
||||
vereineAktualisiert = vereineUpd,
|
||||
reiterImportiert = reiterNeu,
|
||||
reiterAktualisiert = reiterUpd,
|
||||
pferdeImportiert = pferdeNeu,
|
||||
pferdeAktualisiert = pferdeUpd,
|
||||
richterImportiert = richterNeu,
|
||||
richterAktualisiert = richterUpd,
|
||||
fehler = fehler,
|
||||
warnungen = warnungen
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Hilfsmethoden
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private suspend fun importiereVereine(
|
||||
zeilen: List<String>,
|
||||
fehler: MutableList<String>
|
||||
): Pair<Int, Int> {
|
||||
var neu = 0
|
||||
var aktualisiert = 0
|
||||
zeilen.forEachIndexed { index, zeile ->
|
||||
runCatching {
|
||||
val verein = ZnsLegacyParsers.parseVerein(zeile) ?: return@forEachIndexed
|
||||
val vorhanden = vereinRepository.findByVereinsNummer(verein.vereinsNummer)
|
||||
if (vorhanden == null) {
|
||||
vereinRepository.save(verein)
|
||||
neu++
|
||||
} else {
|
||||
vereinRepository.save(
|
||||
vorhanden.copy(
|
||||
name = verein.name,
|
||||
kurzname = verein.kurzname,
|
||||
bundesland = verein.bundesland,
|
||||
ort = verein.ort,
|
||||
plz = verein.plz,
|
||||
strasse = verein.strasse,
|
||||
oepsRegionNummer = verein.oepsRegionNummer,
|
||||
istAktiv = verein.istAktiv,
|
||||
datenQuelle = verein.datenQuelle
|
||||
).withUpdatedTimestamp()
|
||||
)
|
||||
aktualisiert++
|
||||
}
|
||||
}.onFailure { e ->
|
||||
fehler.add("$FILE_VEREIN Zeile ${index + 1}: ${e.message}")
|
||||
}
|
||||
}
|
||||
return Pair(neu, aktualisiert)
|
||||
}
|
||||
|
||||
private suspend fun importiereReiter(
|
||||
zeilen: List<String>,
|
||||
fehler: MutableList<String>,
|
||||
warnungen: MutableList<String>
|
||||
): Pair<Int, Int> {
|
||||
var neu = 0
|
||||
var aktualisiert = 0
|
||||
zeilen.forEachIndexed { index, zeile ->
|
||||
runCatching {
|
||||
val reiter = ZnsLegacyParsers.parseLizenz(zeile) ?: return@forEachIndexed
|
||||
val vorhanden = reiterRepository.findBySatznummer(reiter.satznummer)
|
||||
if (vorhanden == null) {
|
||||
reiterRepository.save(reiter)
|
||||
neu++
|
||||
} else {
|
||||
reiterRepository.save(
|
||||
vorhanden.copy(
|
||||
vorname = reiter.vorname,
|
||||
nachname = reiter.nachname,
|
||||
vereinsName = reiter.vereinsName,
|
||||
nation = reiter.nation,
|
||||
lizenzKlasse = reiter.lizenzKlasse,
|
||||
istAktiv = reiter.istAktiv,
|
||||
datenQuelle = reiter.datenQuelle
|
||||
).withUpdatedTimestamp()
|
||||
)
|
||||
aktualisiert++
|
||||
}
|
||||
}.onFailure { e ->
|
||||
fehler.add("$FILE_LIZENZ Zeile ${index + 1}: ${e.message}")
|
||||
}
|
||||
}
|
||||
return Pair(neu, aktualisiert)
|
||||
}
|
||||
|
||||
private suspend fun importierePferde(
|
||||
zeilen: List<String>,
|
||||
fehler: MutableList<String>
|
||||
): Pair<Int, Int> {
|
||||
var neu = 0
|
||||
var aktualisiert = 0
|
||||
zeilen.forEachIndexed { index, zeile ->
|
||||
runCatching {
|
||||
val pferd = ZnsLegacyParsers.parsePferd(zeile) ?: return@forEachIndexed
|
||||
val vorhanden = pferd.lebensnummer
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { horseRepository.findByLebensnummer(it) }
|
||||
if (vorhanden == null) {
|
||||
horseRepository.save(pferd)
|
||||
neu++
|
||||
} else {
|
||||
horseRepository.save(
|
||||
vorhanden.copy(
|
||||
pferdeName = pferd.pferdeName,
|
||||
geschlecht = pferd.geschlecht,
|
||||
geburtsdatum = pferd.geburtsdatum,
|
||||
rasse = pferd.rasse,
|
||||
lebensnummer = pferd.lebensnummer,
|
||||
oepsNummer = pferd.oepsNummer,
|
||||
istAktiv = pferd.istAktiv,
|
||||
datenQuelle = pferd.datenQuelle
|
||||
).withUpdatedTimestamp()
|
||||
)
|
||||
aktualisiert++
|
||||
}
|
||||
}.onFailure { e ->
|
||||
fehler.add("$FILE_PFERDE Zeile ${index + 1}: ${e.message}")
|
||||
}
|
||||
}
|
||||
return Pair(neu, aktualisiert)
|
||||
}
|
||||
|
||||
private suspend fun importiereRichter(
|
||||
zeilen: List<String>,
|
||||
fehler: MutableList<String>,
|
||||
warnungen: MutableList<String>
|
||||
): Pair<Int, Int> {
|
||||
var neu = 0
|
||||
var aktualisiert = 0
|
||||
zeilen.forEachIndexed { index, zeile ->
|
||||
runCatching {
|
||||
val richter = ZnsLegacyParsers.parseRichter(zeile) ?: return@forEachIndexed
|
||||
val richterNummer = richter.richterNummer ?: run {
|
||||
warnungen.add("$FILE_RICHT Zeile ${index + 1}: Keine RichterNummer – übersprungen.")
|
||||
return@forEachIndexed
|
||||
}
|
||||
val vorhanden = funktionaerRepository.findByRichterNummer(richterNummer)
|
||||
if (vorhanden == null) {
|
||||
funktionaerRepository.save(richter)
|
||||
neu++
|
||||
} else {
|
||||
funktionaerRepository.save(
|
||||
vorhanden.copy(
|
||||
vorname = richter.vorname,
|
||||
nachname = richter.nachname,
|
||||
vereinsNummer = richter.vereinsNummer,
|
||||
richterNummer = richter.richterNummer,
|
||||
istAktiv = richter.istAktiv,
|
||||
datenQuelle = richter.datenQuelle
|
||||
).withUpdatedTimestamp()
|
||||
)
|
||||
aktualisiert++
|
||||
}
|
||||
}.onFailure { e ->
|
||||
fehler.add("$FILE_RICHT Zeile ${index + 1}: ${e.message}")
|
||||
}
|
||||
}
|
||||
return Pair(neu, aktualisiert)
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
package at.mocode.zns.importer
|
||||
|
||||
import at.mocode.clubs.domain.model.DomVerein
|
||||
import at.mocode.clubs.domain.repository.VereinRepository
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.officials.domain.model.DomFunktionaer
|
||||
import at.mocode.officials.domain.repository.FunktionaerRepository
|
||||
import at.mocode.persons.domain.model.DomReiter
|
||||
import at.mocode.persons.domain.repository.ReiterRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
@OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
class ZnsImportServiceTest {
|
||||
|
||||
private val vereinRepository = mockk<VereinRepository>()
|
||||
private val reiterRepository = mockk<ReiterRepository>()
|
||||
private val horseRepository = mockk<HorseRepository>()
|
||||
private val funktionaerRepository = mockk<FunktionaerRepository>()
|
||||
|
||||
private lateinit var service: ZnsImportService
|
||||
|
||||
private val cp850 = Charset.forName("Cp850")
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
service = ZnsImportService(vereinRepository, reiterRepository, horseRepository, funktionaerRepository)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hilfsmethoden
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Erstellt eine in-memory ZIP-Datei mit den übergebenen Einträgen. */
|
||||
private fun buildZip(vararg entries: Pair<String, String>): ByteArrayInputStream {
|
||||
val baos = ByteArrayOutputStream()
|
||||
ZipOutputStream(baos).use { zip ->
|
||||
for ((name, content) in entries) {
|
||||
zip.putNextEntry(ZipEntry(name))
|
||||
zip.write(content.toByteArray(cp850))
|
||||
zip.closeEntry()
|
||||
}
|
||||
}
|
||||
return ByteArrayInputStream(baos.toByteArray())
|
||||
}
|
||||
|
||||
/** Erzeugt eine gültige VEREIN01.DAT-Zeile (mind. 30 Zeichen). */
|
||||
private fun vereinZeile(
|
||||
nummer: String = "0001",
|
||||
name: String = "Testverein Wien"
|
||||
): String {
|
||||
// Stelle 1-4: Vereinsnummer, Stelle 5-34: Name (30 Zeichen)
|
||||
return nummer.padEnd(4) + name.padEnd(30)
|
||||
}
|
||||
|
||||
/** Erzeugt eine gültige LIZENZ01.DAT-Zeile (mind. 200 Zeichen). */
|
||||
private fun lizenzZeile(
|
||||
satznummer: String = "123456",
|
||||
nachname: String = "Mustermann",
|
||||
vorname: String = "Max"
|
||||
): String {
|
||||
// Stelle 1-6: Satznummer, 7-56: Nachname (50), 57-81: Vorname (25)
|
||||
return satznummer.padEnd(6) +
|
||||
nachname.padEnd(50) +
|
||||
vorname.padEnd(25) +
|
||||
" ".repeat(200) // Rest auffüllen
|
||||
}
|
||||
|
||||
/** Erzeugt eine gültige PFERDE01.DAT-Zeile (mind. 211 Zeichen). */
|
||||
private fun pferdeZeile(
|
||||
name: String = "Blitz",
|
||||
lebensnummer: String = "AT123456"
|
||||
): String {
|
||||
// Stelle 1-4: Kopfnummer, 5-34: Name (30), 35-43: Lebensnummer (9), 44: Geschlecht, 45-48: Geburtsjahr, 202-211: Satznummer
|
||||
val base = "0001" +
|
||||
name.padEnd(30) +
|
||||
lebensnummer.padEnd(9) +
|
||||
"W" + // Geschlecht: Wallach
|
||||
"2015" + // Geburtsjahr
|
||||
" ".repeat(157) // Auffüllen bis Stelle 201
|
||||
return base + "SAT0000001".padEnd(10) // Satznummer ab Stelle 202
|
||||
}
|
||||
|
||||
/** Erzeugt eine gültige RICHT01.DAT-Zeile (mind. 83 Zeichen). */
|
||||
private fun richterZeile(
|
||||
satznummer: String = "R00001",
|
||||
name: String = "Huber, Anna"
|
||||
): String {
|
||||
// Stelle 1: Typ, 2-7: Satznummer (6), 8-82: Name (75)
|
||||
return "R" + satznummer.padEnd(6) + name.padEnd(75)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `importiereZip - neue Vereine werden gespeichert`() = runTest {
|
||||
val zip = buildZip("VEREIN01.DAT" to vereinZeile())
|
||||
|
||||
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
|
||||
coEvery { vereinRepository.save(any()) } answers { firstArg() }
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.vereineImportiert).isEqualTo(1)
|
||||
assertThat(result.vereineAktualisiert).isEqualTo(0)
|
||||
assertThat(result.fehler).isEmpty()
|
||||
coVerify(exactly = 1) { vereinRepository.save(any<DomVerein>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importiereZip - bestehende Vereine werden aktualisiert`() = runTest {
|
||||
val zip = buildZip("VEREIN01.DAT" to vereinZeile(name = "Neuer Name"))
|
||||
val vorhanden = DomVerein(vereinsNummer = "0001", name = "Alter Name")
|
||||
|
||||
coEvery { vereinRepository.findByVereinsNummer("0001") } returns vorhanden
|
||||
coEvery { vereinRepository.save(any()) } answers { firstArg() }
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.vereineImportiert).isEqualTo(0)
|
||||
assertThat(result.vereineAktualisiert).isEqualTo(1)
|
||||
coVerify(exactly = 1) { vereinRepository.save(match { it.name == "Neuer Name" }) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importiereZip - neue Reiter werden gespeichert`() = runTest {
|
||||
val zip = buildZip("LIZENZ01.DAT" to lizenzZeile())
|
||||
|
||||
coEvery { reiterRepository.findBySatznummer(any()) } returns null
|
||||
coEvery { reiterRepository.save(any()) } answers { firstArg() }
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.reiterImportiert).isEqualTo(1)
|
||||
assertThat(result.reiterAktualisiert).isEqualTo(0)
|
||||
assertThat(result.fehler).isEmpty()
|
||||
coVerify(exactly = 1) { reiterRepository.save(any<DomReiter>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importiereZip - neue Pferde werden gespeichert`() = runTest {
|
||||
val zip = buildZip("PFERDE01.DAT" to pferdeZeile())
|
||||
|
||||
coEvery { horseRepository.findByLebensnummer(any()) } returns null
|
||||
coEvery { horseRepository.save(any()) } answers { firstArg() }
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.pferdeImportiert).isEqualTo(1)
|
||||
assertThat(result.pferdeAktualisiert).isEqualTo(0)
|
||||
assertThat(result.fehler).isEmpty()
|
||||
coVerify(exactly = 1) { horseRepository.save(any<DomPferd>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importiereZip - neue Richter werden gespeichert`() = runTest {
|
||||
val zip = buildZip("RICHT01.DAT" to richterZeile())
|
||||
|
||||
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
|
||||
coEvery { funktionaerRepository.save(any()) } answers { firstArg() }
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.richterImportiert).isEqualTo(1)
|
||||
assertThat(result.richterAktualisiert).isEqualTo(0)
|
||||
assertThat(result.fehler).isEmpty()
|
||||
coVerify(exactly = 1) { funktionaerRepository.save(any<DomFunktionaer>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importiereZip - vollstaendiger Import aller vier Dateien`() = runTest {
|
||||
val zip = buildZip(
|
||||
"VEREIN01.DAT" to vereinZeile(),
|
||||
"LIZENZ01.DAT" to lizenzZeile(),
|
||||
"PFERDE01.DAT" to pferdeZeile(),
|
||||
"RICHT01.DAT" to richterZeile()
|
||||
)
|
||||
|
||||
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
|
||||
coEvery { vereinRepository.save(any()) } answers { firstArg() }
|
||||
coEvery { reiterRepository.findBySatznummer(any()) } returns null
|
||||
coEvery { reiterRepository.save(any()) } answers { firstArg() }
|
||||
coEvery { horseRepository.findByLebensnummer(any()) } returns null
|
||||
coEvery { horseRepository.save(any()) } answers { firstArg() }
|
||||
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
|
||||
coEvery { funktionaerRepository.save(any()) } answers { firstArg() }
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.gesamtImportiert).isEqualTo(4)
|
||||
assertThat(result.gesamtAktualisiert).isEqualTo(0)
|
||||
assertThat(result.fehler).isEmpty()
|
||||
assertThat(result.hatFehler).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importiereZip - leere ZIP ergibt Null-Ergebnis ohne Fehler`() = runTest {
|
||||
val zip = buildZip()
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.gesamtImportiert).isEqualTo(0)
|
||||
assertThat(result.gesamtAktualisiert).isEqualTo(0)
|
||||
assertThat(result.fehler).isEmpty()
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.horses.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import org.jetbrains.exposed.v1.core.*
|
||||
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import kotlin.time.Clock
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toJavaUuid
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
/**
|
||||
* Exposed-basierte Implementierung des HorseRepository (v1-API).
|
||||
*/
|
||||
class HorseRepositoryImpl : HorseRepository {
|
||||
|
||||
override suspend fun findById(id: Uuid): DomPferd? = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.id eq id.toJavaUuid() }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByLebensnummer(lebensnummer: String): DomPferd? = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByPassNummer(passNummer: String): DomPferd? = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> = transaction {
|
||||
val pattern = "%$searchTerm%"
|
||||
HorseTable.selectAll().where { HorseTable.pferdeName like pattern }
|
||||
.limit(limit)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> = transaction {
|
||||
HorseTable.selectAll().where {
|
||||
(HorseTable.besitzerId eq ownerId.toJavaUuid()).let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> =
|
||||
transaction {
|
||||
HorseTable.selectAll().where {
|
||||
(HorseTable.verantwortlichePersonId eq responsiblePersonId.toJavaUuid()).let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByGeschlecht(
|
||||
geschlecht: PferdeGeschlechtE,
|
||||
activeOnly: Boolean,
|
||||
limit: Int
|
||||
): List<DomPferd> = transaction {
|
||||
HorseTable.selectAll().where {
|
||||
(HorseTable.geschlecht eq geschlecht).let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.limit(limit).map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> = transaction {
|
||||
HorseTable.selectAll().where {
|
||||
(HorseTable.rasse eq rasse).let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.limit(limit).map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = transaction {
|
||||
HorseTable.selectAll()
|
||||
.map { rowToDomPferd(it) }
|
||||
.filter { it.geburtsdatum?.year == birthYear && (!activeOnly || it.istAktiv) }
|
||||
}
|
||||
|
||||
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> =
|
||||
transaction {
|
||||
HorseTable.selectAll()
|
||||
.map { rowToDomPferd(it) }
|
||||
.filter {
|
||||
val year = it.geburtsdatum?.year
|
||||
year != null && year in fromYear..toYear && (!activeOnly || it.istAktiv)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(limit: Int): List<DomPferd> = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.istAktiv eq true }
|
||||
.limit(limit)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> = transaction {
|
||||
HorseTable.selectAll().where {
|
||||
HorseTable.oepsNummer.isNotNull().let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> = transaction {
|
||||
HorseTable.selectAll().where {
|
||||
HorseTable.feiNummer.isNotNull().let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun save(horse: DomPferd): DomPferd = transaction {
|
||||
val existing = HorseTable.selectAll()
|
||||
.where { HorseTable.id eq horse.pferdId.toJavaUuid() }
|
||||
.singleOrNull()
|
||||
if (existing == null) {
|
||||
HorseTable.insert { applyToStatement(it, horse) }
|
||||
} else {
|
||||
HorseTable.update({ HorseTable.id eq horse.pferdId.toJavaUuid() }) {
|
||||
applyToStatement(it, horse.copy(updatedAt = Clock.System.now()))
|
||||
}
|
||||
}
|
||||
horse
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = transaction {
|
||||
HorseTable.deleteWhere { HorseTable.id eq id.toJavaUuid() } > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByLebensnummer(lebensnummer: String): Boolean = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByChipNummer(chipNummer: String): Boolean = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByPassNummer(passNummer: String): Boolean = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long = transaction {
|
||||
HorseTable.selectAll().where { HorseTable.istAktiv eq true }.count()
|
||||
}
|
||||
|
||||
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long = transaction {
|
||||
HorseTable.selectAll().where {
|
||||
(HorseTable.besitzerId eq ownerId.toJavaUuid()).let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.count()
|
||||
}
|
||||
|
||||
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = transaction {
|
||||
HorseTable.selectAll().where {
|
||||
HorseTable.oepsNummer.isNotNull().let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.count()
|
||||
}
|
||||
|
||||
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = transaction {
|
||||
HorseTable.selectAll().where {
|
||||
HorseTable.feiNummer.isNotNull().let {
|
||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
||||
}
|
||||
}.count()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hilfsmethoden
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private fun rowToDomPferd(row: ResultRow): DomPferd = DomPferd(
|
||||
pferdId = row[HorseTable.id].value.toKotlinUuid(),
|
||||
pferdeName = row[HorseTable.pferdeName],
|
||||
geschlecht = row[HorseTable.geschlecht],
|
||||
geburtsdatum = row[HorseTable.geburtsdatum],
|
||||
rasse = row[HorseTable.rasse],
|
||||
farbe = row[HorseTable.farbe],
|
||||
besitzerId = row[HorseTable.besitzerId]?.toKotlinUuid(),
|
||||
verantwortlichePersonId = row[HorseTable.verantwortlichePersonId]?.toKotlinUuid(),
|
||||
zuechterName = row[HorseTable.zuechterName],
|
||||
zuchtbuchNummer = row[HorseTable.zuchtbuchNummer],
|
||||
lebensnummer = row[HorseTable.lebensnummer],
|
||||
chipNummer = row[HorseTable.chipNummer],
|
||||
passNummer = row[HorseTable.passNummer],
|
||||
oepsNummer = row[HorseTable.oepsNummer],
|
||||
feiNummer = row[HorseTable.feiNummer],
|
||||
vaterName = row[HorseTable.vaterName],
|
||||
mutterName = row[HorseTable.mutterName],
|
||||
mutterVaterName = row[HorseTable.mutterVaterName],
|
||||
stockmass = row[HorseTable.stockmass],
|
||||
istAktiv = row[HorseTable.istAktiv],
|
||||
bemerkungen = row[HorseTable.bemerkungen],
|
||||
datenQuelle = row[HorseTable.datenQuelle],
|
||||
createdAt = row[HorseTable.createdAt],
|
||||
updatedAt = row[HorseTable.updatedAt]
|
||||
)
|
||||
|
||||
private fun applyToStatement(statement: UpdateBuilder<*>, horse: DomPferd) {
|
||||
statement[HorseTable.id] = horse.pferdId.toJavaUuid()
|
||||
statement[HorseTable.pferdeName] = horse.pferdeName
|
||||
statement[HorseTable.geschlecht] = horse.geschlecht
|
||||
statement[HorseTable.geburtsdatum] = horse.geburtsdatum
|
||||
statement[HorseTable.rasse] = horse.rasse
|
||||
statement[HorseTable.farbe] = horse.farbe
|
||||
statement[HorseTable.besitzerId] = horse.besitzerId?.toJavaUuid()
|
||||
statement[HorseTable.verantwortlichePersonId] = horse.verantwortlichePersonId?.toJavaUuid()
|
||||
statement[HorseTable.zuechterName] = horse.zuechterName
|
||||
statement[HorseTable.zuchtbuchNummer] = horse.zuchtbuchNummer
|
||||
statement[HorseTable.lebensnummer] = horse.lebensnummer
|
||||
statement[HorseTable.chipNummer] = horse.chipNummer
|
||||
statement[HorseTable.passNummer] = horse.passNummer
|
||||
statement[HorseTable.oepsNummer] = horse.oepsNummer
|
||||
statement[HorseTable.feiNummer] = horse.feiNummer
|
||||
statement[HorseTable.vaterName] = horse.vaterName
|
||||
statement[HorseTable.mutterName] = horse.mutterName
|
||||
statement[HorseTable.mutterVaterName] = horse.mutterVaterName
|
||||
statement[HorseTable.stockmass] = horse.stockmass
|
||||
statement[HorseTable.istAktiv] = horse.istAktiv
|
||||
statement[HorseTable.bemerkungen] = horse.bemerkungen
|
||||
statement[HorseTable.datenQuelle] = horse.datenQuelle
|
||||
statement[HorseTable.createdAt] = horse.createdAt
|
||||
statement[HorseTable.updatedAt] = horse.updatedAt
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.zns.import.service.ZnsImportServiceApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.backend.infrastructure.znsImporter)
|
||||
implementation(projects.backend.services.clubs.clubsDomain)
|
||||
implementation(projects.backend.services.clubs.clubsInfrastructure)
|
||||
implementation(projects.backend.services.persons.personsDomain)
|
||||
implementation(projects.backend.services.persons.personsInfrastructure)
|
||||
implementation(projects.backend.services.horses.horsesDomain)
|
||||
implementation(projects.backend.services.horses.horsesInfrastructure)
|
||||
implementation(projects.backend.services.officials.officialsDomain)
|
||||
implementation(projects.backend.services.officials.officialsInfrastructure)
|
||||
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
implementation(libs.spring.boot.starter.actuator)
|
||||
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.migration.jdbc)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
implementation(libs.hikari.cp)
|
||||
|
||||
runtimeOnly(libs.postgresql.driver)
|
||||
testRuntimeOnly(libs.h2.driver)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
testImplementation(libs.logback.classic)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package at.mocode.zns.import.service
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
|
||||
@SpringBootApplication
|
||||
@ComponentScan(
|
||||
basePackages = [
|
||||
"at.mocode.zns.import.service",
|
||||
"at.mocode.clubs.infrastructure",
|
||||
"at.mocode.persons.infrastructure",
|
||||
"at.mocode.horses.infrastructure",
|
||||
"at.mocode.officials.infrastructure"
|
||||
]
|
||||
)
|
||||
class ZnsImportServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<ZnsImportServiceApplication>(*args)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package at.mocode.zns.import.service.api
|
||||
|
||||
import at.mocode.zns.import.service.job.ImportJob
|
||||
import at.mocode.zns.import.service.job.ImportJobRegistry
|
||||
import at.mocode.zns.import.service.job.ZnsImportOrchestrator
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
data class ImportStartResponse(val jobId: String)
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/import/zns")
|
||||
class ZnsImportController(
|
||||
private val orchestrator: ZnsImportOrchestrator,
|
||||
private val jobRegistry: ImportJobRegistry
|
||||
) {
|
||||
|
||||
/**
|
||||
* POST /api/v1/import/zns
|
||||
* Nimmt eine .zip oder .dat Datei entgegen und startet den asynchronen Import.
|
||||
* Rückgabe: 202 Accepted mit JobId.
|
||||
*/
|
||||
@PostMapping(consumes = ["multipart/form-data"])
|
||||
fun starteImport(@RequestParam("file") file: MultipartFile): ResponseEntity<ImportStartResponse> {
|
||||
val job = jobRegistry.erstelleJob()
|
||||
orchestrator.starteImport(job.jobId, file.bytes)
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED).body(ImportStartResponse(job.jobId))
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/import/zns/{jobId}/status
|
||||
* Gibt den aktuellen Fortschritt und Statusmeldungen zurück.
|
||||
*/
|
||||
@GetMapping("/{jobId}/status")
|
||||
fun holeStatus(@PathVariable jobId: String): ResponseEntity<ImportJob> {
|
||||
val job = jobRegistry.findeJob(jobId)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
return ResponseEntity.ok(job)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package at.mocode.zns.import.service.config
|
||||
|
||||
import at.mocode.clubs.domain.repository.VereinRepository
|
||||
import at.mocode.clubs.infrastructure.persistence.ExposedVereinRepository
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.horses.infrastructure.persistence.HorseRepositoryImpl
|
||||
import at.mocode.officials.domain.repository.FunktionaerRepository
|
||||
import at.mocode.officials.infrastructure.persistence.ExposedFunktionaerRepository
|
||||
import at.mocode.persons.domain.repository.ReiterRepository
|
||||
import at.mocode.persons.infrastructure.persistence.ExposedReiterRepository
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class RepositoryConfiguration {
|
||||
|
||||
@Bean
|
||||
fun vereinRepository(): VereinRepository = ExposedVereinRepository()
|
||||
|
||||
@Bean
|
||||
fun reiterRepository(): ReiterRepository = ExposedReiterRepository()
|
||||
|
||||
@Bean
|
||||
fun horseRepository(): HorseRepository = HorseRepositoryImpl()
|
||||
|
||||
@Bean
|
||||
fun funktionaerRepository(): FunktionaerRepository = ExposedFunktionaerRepository()
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package at.mocode.zns.import.service.config
|
||||
|
||||
import at.mocode.clubs.infrastructure.persistence.VereinTable
|
||||
import at.mocode.horses.infrastructure.persistence.HorseTable
|
||||
import at.mocode.officials.infrastructure.persistence.FunktionaerTable
|
||||
import at.mocode.persons.infrastructure.persistence.ReiterTable
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
|
||||
@Configuration
|
||||
@Profile("dev")
|
||||
class ZnsImportDatabaseConfiguration(
|
||||
@Value("\${spring.datasource.url}") private val jdbcUrl: String,
|
||||
@Value("\${spring.datasource.username}") private val username: String,
|
||||
@Value("\${spring.datasource.password}") private val password: String
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(ZnsImportDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service...")
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
|
||||
VereinTable, ReiterTable, HorseTable, FunktionaerTable
|
||||
)
|
||||
statements.forEach { exec(it) }
|
||||
log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package at.mocode.zns.import.service.job
|
||||
|
||||
import org.springframework.stereotype.Component
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
enum class ImportJobStatus { AUSSTEHEND, ENTPACKEN, LADE_VEREINE, LADE_REITER, LADE_PFERDE, LADE_RICHTER, ABGESCHLOSSEN, FEHLER }
|
||||
|
||||
data class ImportJob(
|
||||
val jobId: String,
|
||||
var status: ImportJobStatus = ImportJobStatus.AUSSTEHEND,
|
||||
var fortschritt: Int = 0,
|
||||
var meldungen: MutableList<String> = mutableListOf(),
|
||||
var fehler: MutableList<String> = mutableListOf(),
|
||||
var warnungen: MutableList<String> = mutableListOf()
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Component
|
||||
class ImportJobRegistry {
|
||||
private val jobs = ConcurrentHashMap<String, ImportJob>()
|
||||
|
||||
fun erstelleJob(): ImportJob {
|
||||
val job = ImportJob(jobId = Uuid.random().toString())
|
||||
jobs[job.jobId] = job
|
||||
return job
|
||||
}
|
||||
|
||||
fun findeJob(jobId: String): ImportJob? = jobs[jobId]
|
||||
|
||||
fun aktualisiereStatus(jobId: String, status: ImportJobStatus, meldung: String? = null, fortschritt: Int? = null) {
|
||||
jobs[jobId]?.let { job ->
|
||||
job.status = status
|
||||
meldung?.let { job.meldungen.add(it) }
|
||||
fortschritt?.let { job.fortschritt = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package at.mocode.zns.import.service.job
|
||||
|
||||
import at.mocode.clubs.domain.repository.VereinRepository
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.officials.domain.repository.FunktionaerRepository
|
||||
import at.mocode.persons.domain.repository.ReiterRepository
|
||||
import at.mocode.zns.importer.ZnsImportService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class ZnsImportOrchestrator(
|
||||
private val vereinRepository: VereinRepository,
|
||||
private val reiterRepository: ReiterRepository,
|
||||
private val horseRepository: HorseRepository,
|
||||
private val funktionaerRepository: FunktionaerRepository,
|
||||
private val jobRegistry: ImportJobRegistry
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
fun starteImport(jobId: String, zipBytes: ByteArray) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Entpacke ZIP-Datei...", 5)
|
||||
|
||||
val service = ZnsImportService(vereinRepository, reiterRepository, horseRepository, funktionaerRepository)
|
||||
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_VEREINE, "Lade Vereine...", 20)
|
||||
val result = service.importiereZip(zipBytes.inputStream())
|
||||
|
||||
jobRegistry.aktualisiereStatus(
|
||||
jobId, ImportJobStatus.ABGESCHLOSSEN,
|
||||
"Import abgeschlossen: ${result.vereineImportiert} Vereine, " +
|
||||
"${result.reiterImportiert} Reiter, ${result.pferdeImportiert} Pferde, " +
|
||||
"${result.richterImportiert} Richter importiert.", 100
|
||||
)
|
||||
|
||||
jobRegistry.findeJob(jobId)?.let { job ->
|
||||
job.fehler.addAll(result.fehler)
|
||||
job.warnungen.addAll(result.warnungen)
|
||||
}
|
||||
}.onFailure { ex ->
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.FEHLER, "Fehler: ${ex.message}")
|
||||
jobRegistry.findeJob(jobId)?.fehler?.add(ex.message ?: "Unbekannter Fehler")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
server:
|
||||
port: ${ZNS_IMPORT_SERVER_PORT:8095}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: zns-import-service
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
datasource:
|
||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
||||
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 50MB
|
||||
max-request-size: 50MB
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
app:
|
||||
service-name: ${spring.application.name}
|
||||
Reference in New Issue
Block a user