Lege neue leere SQLite-Datenbank atomar via Use-Case und GUI an
Neuer Menüpunkt „Datenbank → Neue Datenbank anlegen…" mit FileChooser, normalisierter Pfadprüfung gegen die aktive DB, gesammelter Überschreib- Bestätigung, DB-Busy-Sperre auf Verlauf-Tab, Flyway-Migration auf den neuesten Stand gegen eine Temp-Datei, Verbindungstest, atomarem Move (ATOMIC_MOVE + REPLACE_EXISTING) und Umstellen der aktiven DB-Referenz über einen neuen ActiveDatabaseContextPort. Konfig-Tab wechselt nach Wechsel automatisch in den Dirty-State; Hinweismeldung mit Speichern- Aufforderung wird im zentralen Meldungsbereich angezeigt. Architektur entspricht Fall B aus der Spezifikation: Bootstrap hält den Override prozessweit und verwendet ihn in resolveActiveJdbcUrl statt des Werts aus der .properties-Datei. Bei Fehlern wird die Temp-Datei zuverlässig entfernt; die aktive DB bleibt unverändert in Betrieb. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+127
-15
@@ -22,6 +22,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileLoader;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileWriter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationLoadException;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiCreateNewDatabasePort;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiPromptEditorPort;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
|
||||
@@ -46,6 +47,7 @@ import de.gecheckt.pdf.umbenenner.adapter.out.pathcheck.FilesystemPathCheckAdapt
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.pdfextraction.PdfTextExtractionPortAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.prompt.FilesystemPromptPortAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.sourcedocument.SourceDocumentCandidatesPortAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteDatabaseCreationAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteDocumentRecordRepositoryAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteProcessingAttemptRepositoryAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteSchemaInitializationAdapter;
|
||||
@@ -59,6 +61,7 @@ import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfigurat
|
||||
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.CreateNewDatabaseUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyRequest;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyResult;
|
||||
@@ -68,9 +71,11 @@ import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalDocumentContextUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ActiveDatabaseContextPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DatabaseCreationPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
|
||||
@@ -95,6 +100,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryResetDocument
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteHistoryQueryAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQueryPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultCreateNewDatabaseUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultDeleteDocumentHistoryUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultHistoryDetailsUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultHistoryOverviewUseCase;
|
||||
@@ -111,6 +117,7 @@ import de.gecheckt.pdf.umbenenner.bootstrap.adapter.AiModelCatalogDispatcher;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.GuiConfigurationPropertiesWriter;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jLogDiagnosticsAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jProcessingLogger;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.SqliteActiveDatabaseContextAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.AnotherInstanceRunningException;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.SingleInstanceGuard;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArguments;
|
||||
@@ -207,6 +214,23 @@ public class BootstrapRunner {
|
||||
private final GuiAdapterFactory guiAdapterFactory;
|
||||
private final SingleInstanceGuardFactory singleInstanceGuardFactory;
|
||||
|
||||
/**
|
||||
* Eigentümer der zur Laufzeit aktiven SQLite-Datenbankreferenz. Wird im
|
||||
* GUI-Pfad genutzt, damit der Use-Case
|
||||
* {@link DefaultCreateNewDatabaseUseCase} die aktive Datenbank umstellen kann,
|
||||
* ohne die {@code .properties}-Datei sofort schreiben zu müssen.
|
||||
* <p>
|
||||
* Solange kein Override gesetzt ist, verhalten sich alle DB-Adapter wie
|
||||
* bisher (Pfad aus der jeweils geladenen {@link StartConfiguration}).
|
||||
*/
|
||||
private final SqliteActiveDatabaseContextAdapter activeDatabaseContext = new SqliteActiveDatabaseContextAdapter();
|
||||
|
||||
/**
|
||||
* Adapter zum Anlegen einer neuen, leeren SQLite-Datenbank. Zustandslos und
|
||||
* threadsicher; wird im GUI-Pfad pro Anlage-Aufruf wiederverwendet.
|
||||
*/
|
||||
private final DatabaseCreationPort databaseCreationPort = new SqliteDatabaseCreationAdapter();
|
||||
|
||||
/**
|
||||
* Functional interface encapsulating the legacy configuration migration step.
|
||||
* <p>
|
||||
@@ -416,7 +440,7 @@ public class BootstrapRunner {
|
||||
ProviderConfiguration providerConfig = startConfig.multiProviderConfiguration().activeProviderConfiguration();
|
||||
AiInvocationPort aiInvocationPort = new AiProviderSelector().select(activeFamily, providerConfig);
|
||||
|
||||
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(startConfig);
|
||||
FingerprintPort fingerprintPort = new Sha256FingerprintAdapter();
|
||||
DocumentRecordRepository documentRecordRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
@@ -823,6 +847,7 @@ public class BootstrapRunner {
|
||||
GuiHistoricalDocumentContextPort historicalDocumentContextPort = this::resolveHistoricalDocumentContextForGui;
|
||||
GuiHistoryOverviewPort historyOverviewPort = this::loadHistoryOverviewForGui;
|
||||
GuiHistoryDetailsPort historyDetailsPort = this::loadHistoryDetailsForGui;
|
||||
GuiCreateNewDatabasePort createNewDatabasePort = this::createNewDatabaseForGui;
|
||||
GuiHistoryResetDocumentStatusPort historyResetPort = this::resetHistoryDocumentStatusForGui;
|
||||
GuiDeleteDocumentHistoryPort deleteHistoryPort = this::deleteDocumentHistoryForGui;
|
||||
// Versionsnummer aus dem MANIFEST.MF des gepackten JARs lesen; Fallback "dev" bei IDE-Start
|
||||
@@ -852,7 +877,8 @@ public class BootstrapRunner {
|
||||
historyDetailsPort,
|
||||
historyResetPort,
|
||||
deleteHistoryPort,
|
||||
this::buildGuiPromptEditorPort);
|
||||
this::buildGuiPromptEditorPort,
|
||||
createNewDatabasePort);
|
||||
}
|
||||
|
||||
Path configPath = Paths.get(configPathOverride.get());
|
||||
@@ -883,7 +909,8 @@ public class BootstrapRunner {
|
||||
historyDetailsPort,
|
||||
historyResetPort,
|
||||
deleteHistoryPort,
|
||||
this::buildGuiPromptEditorPort);
|
||||
this::buildGuiPromptEditorPort,
|
||||
createNewDatabasePort);
|
||||
}
|
||||
|
||||
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
||||
@@ -897,7 +924,7 @@ public class BootstrapRunner {
|
||||
miniRunLauncher, resetPort, manualRenamePort, manualCopyPort,
|
||||
historicalDocumentContextPort, applicationVersion, promptEditorPort,
|
||||
historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort,
|
||||
this::buildGuiPromptEditorPort);
|
||||
this::buildGuiPromptEditorPort, createNewDatabasePort);
|
||||
} catch (GuiConfigurationLoadException e) {
|
||||
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
|
||||
e.getMessage(), e);
|
||||
@@ -924,7 +951,8 @@ public class BootstrapRunner {
|
||||
historyDetailsPort,
|
||||
historyResetPort,
|
||||
deleteHistoryPort,
|
||||
this::buildGuiPromptEditorPort);
|
||||
this::buildGuiPromptEditorPort,
|
||||
createNewDatabasePort);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1171,7 +1199,7 @@ public class BootstrapRunner {
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
|
||||
String jdbcUrl = buildJdbcUrl(config);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(config);
|
||||
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
ProcessingLogger resetLogger = new Log4jProcessingLogger(
|
||||
DefaultResetDocumentStatusUseCase.class,
|
||||
@@ -1229,7 +1257,7 @@ public class BootstrapRunner {
|
||||
*/
|
||||
private ManualFileRenameUseCase buildProductionManualFileRenameUseCase(
|
||||
StartConfiguration startConfig) {
|
||||
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(startConfig);
|
||||
DocumentRecordRepository documentRecordRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
@@ -1263,7 +1291,7 @@ public class BootstrapRunner {
|
||||
*/
|
||||
private ManualFileCopyUseCase buildProductionManualFileCopyUseCase(
|
||||
StartConfiguration startConfig) {
|
||||
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(startConfig);
|
||||
DocumentRecordRepository documentRecordRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
@@ -1442,7 +1470,7 @@ public class BootstrapRunner {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
String jdbcUrl = buildJdbcUrl(config);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(config);
|
||||
DocumentRecordRepository documentRecordRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
ResolveHistoricalDocumentContextUseCase useCase =
|
||||
@@ -1475,7 +1503,7 @@ public class BootstrapRunner {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
String jdbcUrl = buildJdbcUrl(config);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(config);
|
||||
HistoryQueryPort historyQueryPort = new SqliteHistoryQueryAdapter(jdbcUrl);
|
||||
DefaultHistoryOverviewUseCase useCase = new DefaultHistoryOverviewUseCase(historyQueryPort);
|
||||
return useCase.loadOverview(query);
|
||||
@@ -1505,7 +1533,7 @@ public class BootstrapRunner {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
String jdbcUrl = buildJdbcUrl(config);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(config);
|
||||
HistoryQueryPort historyQueryPort = new SqliteHistoryQueryAdapter(jdbcUrl);
|
||||
DefaultHistoryDetailsUseCase useCase = new DefaultHistoryDetailsUseCase(historyQueryPort);
|
||||
return useCase.loadDetails(fingerprint);
|
||||
@@ -1536,7 +1564,7 @@ public class BootstrapRunner {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
String jdbcUrl = buildJdbcUrl(config);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(config);
|
||||
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
DefaultHistoryResetDocumentStatusUseCase useCase =
|
||||
new DefaultHistoryResetDocumentStatusUseCase(unitOfWorkPort);
|
||||
@@ -1568,7 +1596,7 @@ public class BootstrapRunner {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
String jdbcUrl = buildJdbcUrl(config);
|
||||
String jdbcUrl = resolveActiveJdbcUrl(config);
|
||||
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
DefaultDeleteDocumentHistoryUseCase useCase =
|
||||
new DefaultDeleteDocumentHistoryUseCase(unitOfWorkPort);
|
||||
@@ -1582,6 +1610,53 @@ public class BootstrapRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge-Methode zum Anlegen einer neuen, leeren SQLite-Datenbank für die GUI.
|
||||
* <p>
|
||||
* Verdrahtet pro Aufruf einen frischen {@link DefaultCreateNewDatabaseUseCase} mit dem
|
||||
* gemeinsam gehaltenen {@link #databaseCreationPort} und dem
|
||||
* {@link #activeDatabaseContext}. Die zugrundeliegende Liefer-Lambda für den aktiven
|
||||
* DB-Pfad bevorzugt das Override im Kontext-Port; ist keines gesetzt, fällt sie auf den
|
||||
* {@code sqliteFile}-Wert der aus {@code configFilePath} geladenen Konfiguration zurück.
|
||||
* <p>
|
||||
* Pfad-Validierungs- und Move-Schritte verbleiben im Use-Case; der Adapter führt
|
||||
* Anlage, Migration und Verbindungstest aus.
|
||||
*
|
||||
* @param configFilePath der aktuell in der GUI bekannte Konfigurationspfad oder
|
||||
* {@code null}, wenn noch keine Konfiguration geladen wurde
|
||||
* @param targetFile der vom Benutzer gewählte Zielpfad; darf nicht {@code null} sein
|
||||
* @return strukturiertes Ergebnis des Use-Cases; nie {@code null}
|
||||
*/
|
||||
CreateNewDatabaseUseCase.CreateNewDatabaseResult createNewDatabaseForGui(
|
||||
Path configFilePath, Path targetFile) {
|
||||
Objects.requireNonNull(targetFile, "targetFile must not be null");
|
||||
LOG.info("GUI-Datenbankanlage: angeforderter Zielpfad = {}", targetFile);
|
||||
DefaultCreateNewDatabaseUseCase.ActiveDatabasePathSupplier activeDbSupplier = () -> {
|
||||
Optional<Path> override = activeDatabaseContext.activeDatabaseOverride();
|
||||
if (override.isPresent()) {
|
||||
return override.get();
|
||||
}
|
||||
if (configFilePath == null || !Files.exists(configFilePath)) {
|
||||
// Ohne Konfiguration und ohne Override: Zielpfad selbst als „aktiv" werten,
|
||||
// damit die Pfadprüfung nur das Selbst-Überschreiben verhindert.
|
||||
return targetFile.toAbsolutePath().normalize();
|
||||
}
|
||||
try {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
return config.sqliteFile().toAbsolutePath().normalize();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("GUI-Datenbankanlage: aktive DB konnte nicht aus {} ermittelt werden: {} — "
|
||||
+ "Zielpfad wird stattdessen als aktiv angenommen.",
|
||||
configFilePath, e.getMessage());
|
||||
return targetFile.toAbsolutePath().normalize();
|
||||
}
|
||||
};
|
||||
DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
|
||||
databaseCreationPort, activeDatabaseContext, activeDbSupplier);
|
||||
return useCase.createNewDatabase(targetFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link ResetDocumentStatusResult} where every requested fingerprint is
|
||||
* recorded as a failure with the given error message.
|
||||
@@ -1724,7 +1799,7 @@ public class BootstrapRunner {
|
||||
* @param config the validated startup configuration containing the SQLite file path
|
||||
*/
|
||||
private void initializeSchema(StartConfiguration config) {
|
||||
schemaInitPortFactory.create(buildJdbcUrl(config)).initializeSchema();
|
||||
schemaInitPortFactory.create(resolveActiveJdbcUrl(config)).initializeSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1870,11 +1945,48 @@ public class BootstrapRunner {
|
||||
|
||||
/**
|
||||
* Builds the JDBC URL for the SQLite database from the configured file path.
|
||||
* <p>
|
||||
* Diese Variante berücksichtigt {@em kein} Override – sie wird ausschließlich für
|
||||
* den Headless-Pfad und für GUI-Tests aufgerufen, in denen kein
|
||||
* {@link ActiveDatabaseContextPort} verfügbar ist.
|
||||
*
|
||||
* @param config the startup configuration containing the SQLite file path
|
||||
* @return the JDBC URL in the form {@code jdbc:sqlite:/path/to/file.db}
|
||||
*/
|
||||
static String buildJdbcUrl(StartConfiguration config) {
|
||||
return "jdbc:sqlite:" + config.sqliteFile().toAbsolutePath().toString().replace('\\', '/');
|
||||
return buildSqliteJdbcUrl(config.sqliteFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die JDBC-URL der zur Laufzeit aktiven SQLite-Datenbank.
|
||||
* <p>
|
||||
* Hat der {@link ActiveDatabaseContextPort} einen Override gesetzt (z. B. nachdem
|
||||
* der Benutzer im GUI eine neue Datenbank angelegt hat), wird dieser verwendet.
|
||||
* Andernfalls greift der Wert aus der übergebenen {@link StartConfiguration}.
|
||||
*
|
||||
* @param config die geladene Startup-Konfiguration; nie {@code null}
|
||||
* @return die JDBC-URL der aktiven SQLite-Datei
|
||||
*/
|
||||
String resolveActiveJdbcUrl(StartConfiguration config) {
|
||||
Optional<Path> override = activeDatabaseContext.activeDatabaseOverride();
|
||||
if (override.isPresent()) {
|
||||
return buildSqliteJdbcUrl(override.get());
|
||||
}
|
||||
return buildSqliteJdbcUrl(config.sqliteFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Pfad der zur Laufzeit aktiven SQLite-Datei.
|
||||
*
|
||||
* @param config die geladene Startup-Konfiguration; nie {@code null}
|
||||
* @return der absolute Pfad der aktuell aktiven Datenbankdatei
|
||||
*/
|
||||
Path resolveActiveDatabasePath(StartConfiguration config) {
|
||||
return activeDatabaseContext.activeDatabaseOverride()
|
||||
.orElseGet(() -> config.sqliteFile().toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
private static String buildSqliteJdbcUrl(Path sqliteFile) {
|
||||
return "jdbc:sqlite:" + sqliteFile.toAbsolutePath().toString().replace('\\', '/');
|
||||
}
|
||||
}
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ActiveDatabaseContextPort;
|
||||
|
||||
/**
|
||||
* Bootstrap-interne Implementierung des {@link ActiveDatabaseContextPort}.
|
||||
* <p>
|
||||
* Hält einen prozessweiten, threadsicheren Override-Pfad, der in der Bootstrap-Schicht
|
||||
* Vorrang vor dem in der {@code .properties}-Datei konfigurierten SQLite-Pfad hat.
|
||||
* Solange kein Override gesetzt ist, verhält sich die Anwendung wie bisher: alle
|
||||
* SQLite-Adapter werden mit der aus der jeweils geladenen Konfigurationsdatei
|
||||
* abgeleiteten JDBC-URL verdrahtet.
|
||||
* <p>
|
||||
* Setzt der Use-Case
|
||||
* {@link de.gecheckt.pdf.umbenenner.application.usecase.DefaultCreateNewDatabaseUseCase}
|
||||
* den Override über {@link #switchActiveDatabase(Path)}, verwenden alle nachfolgenden
|
||||
* GUI-DB-Operationen (History, Reset, Löschen, Verarbeitungsläufe) die hinterlegte
|
||||
* Datei – auch wenn der Benutzer die Konfigurationsdatei noch nicht gespeichert hat.
|
||||
*
|
||||
* <h2>Lebensdauer</h2>
|
||||
* <p>Die gehaltene Referenz lebt für die Dauer des laufenden GUI-Prozesses. Sie ist
|
||||
* nicht persistent: nach einem Neustart greift wieder der Wert aus der
|
||||
* Konfigurationsdatei.
|
||||
*
|
||||
* <h2>Threading</h2>
|
||||
* <p>Alle Lese- und Schreibzugriffe gehen über eine {@link AtomicReference}; die Klasse
|
||||
* ist daher uneingeschränkt threadsicher.
|
||||
*/
|
||||
public class SqliteActiveDatabaseContextAdapter implements ActiveDatabaseContextPort {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(SqliteActiveDatabaseContextAdapter.class);
|
||||
|
||||
private final AtomicReference<Path> override = new AtomicReference<>();
|
||||
|
||||
/**
|
||||
* Standardkonstruktor.
|
||||
*/
|
||||
public SqliteActiveDatabaseContextAdapter() {
|
||||
// kein Zustand außer der AtomicReference
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Override-Pfad auf die übergebene Datei. Nachfolgende DB-Operationen
|
||||
* verwenden diese Datei, sofern sie {@link #activeDatabaseOverride()} abfragen,
|
||||
* statt direkt die Konfigurationsdatei zu konsultieren.
|
||||
*
|
||||
* @param newDbFile absoluter Pfad der neuen aktiven Datenbankdatei; darf nicht
|
||||
* {@code null} sein
|
||||
* @throws NullPointerException wenn {@code newDbFile} {@code null} ist
|
||||
*/
|
||||
@Override
|
||||
public void switchActiveDatabase(Path newDbFile) {
|
||||
Objects.requireNonNull(newDbFile, "newDbFile darf nicht null sein");
|
||||
Path absolute = newDbFile.toAbsolutePath().normalize();
|
||||
Path previous = override.getAndSet(absolute);
|
||||
if (previous == null) {
|
||||
LOG.info("Aktive SQLite-Datenbank wurde umgestellt auf: {}", absolute);
|
||||
} else {
|
||||
LOG.info("Aktive SQLite-Datenbank wurde von {} auf {} umgestellt.",
|
||||
previous, absolute);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den aktuell gesetzten Override, falls vorhanden.
|
||||
*
|
||||
* @return der Override-Pfad oder {@link Optional#empty()}
|
||||
*/
|
||||
@Override
|
||||
public Optional<Path> activeDatabaseOverride() {
|
||||
return Optional.ofNullable(override.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt den Override (für Tests oder kontrolliertes Zurücksetzen). Nach dem
|
||||
* Aufruf greift wieder der Wert aus der konfigurierten {@code .properties}-Datei.
|
||||
*/
|
||||
public void clearOverride() {
|
||||
Path previous = override.getAndSet(null);
|
||||
if (previous != null) {
|
||||
LOG.info("Aktiver SQLite-Datenbank-Override wurde entfernt (zuletzt: {}).", previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests für {@link SqliteActiveDatabaseContextAdapter}.
|
||||
* <p>
|
||||
* Prüft das Setzen, Auslesen und Zurücksetzen des Override-Pfads sowie die Pflicht-Prüfung
|
||||
* gegen {@code null}-Argumente.
|
||||
*/
|
||||
class SqliteActiveDatabaseContextAdapterTest {
|
||||
|
||||
@Test
|
||||
void activeDatabaseOverride_returnsEmpty_initially() {
|
||||
SqliteActiveDatabaseContextAdapter adapter = new SqliteActiveDatabaseContextAdapter();
|
||||
assertThat(adapter.activeDatabaseOverride()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void switchActiveDatabase_setsAbsoluteOverride() {
|
||||
SqliteActiveDatabaseContextAdapter adapter = new SqliteActiveDatabaseContextAdapter();
|
||||
Path target = Path.of("data/foo.sqlite");
|
||||
|
||||
adapter.switchActiveDatabase(target);
|
||||
|
||||
Optional<Path> override = adapter.activeDatabaseOverride();
|
||||
assertThat(override).isPresent();
|
||||
assertThat(override.get()).isEqualTo(target.toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void switchActiveDatabase_replacesPreviousOverride() {
|
||||
SqliteActiveDatabaseContextAdapter adapter = new SqliteActiveDatabaseContextAdapter();
|
||||
adapter.switchActiveDatabase(Path.of("first.sqlite"));
|
||||
adapter.switchActiveDatabase(Path.of("second.sqlite"));
|
||||
|
||||
assertThat(adapter.activeDatabaseOverride())
|
||||
.isPresent()
|
||||
.get()
|
||||
.isEqualTo(Path.of("second.sqlite").toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearOverride_removesPreviouslySetOverride() {
|
||||
SqliteActiveDatabaseContextAdapter adapter = new SqliteActiveDatabaseContextAdapter();
|
||||
adapter.switchActiveDatabase(Path.of("foo.sqlite"));
|
||||
|
||||
adapter.clearOverride();
|
||||
|
||||
assertThat(adapter.activeDatabaseOverride()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void switchActiveDatabase_rejectsNull() {
|
||||
SqliteActiveDatabaseContextAdapter adapter = new SqliteActiveDatabaseContextAdapter();
|
||||
assertThatThrownBy(() -> adapter.switchActiveDatabase(null))
|
||||
.isInstanceOf(NullPointerException.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user