GUI: ApplicationRunContext beim Datei-Öffnen proaktiv aufbauen

Bisher wurde der ApplicationRunContext nur beim --config-Startpfad
erzeugt. Der auto-load-Pfad (letzte Konfiguration aus Preferences)
baute keinen Kontext auf, was Scheduler und Batch-Vorinitialisierung
blockierte.

Neu: GuiApplicationContextInitializer-Callback, den Bootstrap für
jeden GUI-Startpfad bereitstellt. openConfigurationFile() ruft ihn
im Hintergrund-Thread auf; das Scheduler-Ergebnis wird via
Platform.runLater() an GuiSchedulerTab.onSchedulerAvailable()
übergeben.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 07:11:27 +02:00
parent b7f9184344
commit 4bc70dae75
5 changed files with 164 additions and 15 deletions
@@ -0,0 +1,66 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.nio.file.Path;
import java.util.Optional;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase;
/**
* Callback invoked by the workspace on a background thread after a configuration file
* has been successfully loaded from disk.
* <p>
* Bootstrap supplies an implementation that builds the application run context
* (migrate → load → validate → schema-init sequence) and, on success, also initialises
* the automatic scheduler. The workspace calls this initializer inside the same
* background submit that loads the editor state, so the JavaFX Application Thread is
* never blocked.
* <p>
* In isolated GUI tests a {@link #noOp() no-op} implementation can be used so that no
* Bootstrap wiring is required.
*/
@FunctionalInterface
public interface GuiApplicationContextInitializer {
/**
* Attempts to initialise the application run context for the supplied configuration file.
* <p>
* If context initialisation succeeds and the configuration enables the scheduler, the
* scheduler is also wired and its use case is returned in the result. The caller is
* responsible for handing the scheduler use case to the scheduler tab on the JavaFX
* Application Thread via {@code Platform.runLater}.
* <p>
* This method must be called on a background worker thread, not on the JavaFX Application
* Thread.
*
* @param configFilePath path to the {@code .properties} configuration file; must exist on disk
* @return the result of the initialisation attempt; never {@code null}
*/
InitResult initialize(Path configFilePath);
/**
* Returns a no-op initializer that always reports success and no scheduler.
* <p>
* Suitable for GUI tests and startup paths where no Bootstrap wiring is available.
*
* @return no-op initializer; never {@code null}
*/
static GuiApplicationContextInitializer noOp() {
return configFilePath -> new InitResult(Optional.empty(), Optional.empty());
}
/**
* Result of a context initialisation attempt.
*
* @param contextError empty on success; a human-readable German error message
* when initialisation failed — the GUI remains functional
* but falls back to per-run initialisation for batch runs;
* must not be {@code null}
* @param schedulerControlUseCase the scheduler use case when the configuration enables the
* scheduler and initialisation succeeded; empty otherwise;
* must not be {@code null}
*/
record InitResult(
Optional<String> contextError,
Optional<SchedulerControlUseCase> schedulerControlUseCase) {
}
}
@@ -436,6 +436,13 @@ public final class GuiConfigurationEditorWorkspace {
*/
private final GuiCreateNewDatabasePort createNewDatabasePort;
/**
* Callback, der nach jedem erfolgreichen Datei-Öffnen auf dem Hintergrund-Thread
* aufgerufen wird, um den Bootstrap-seitigen Anwendungskontext und den Scheduler
* zu initialisieren.
*/
private final GuiApplicationContextInitializer applicationContextInitializer;
/**
* Aktiver DB-Busy-Zustand während einer laufenden Datenbank-Anlage. Solange
* dieser Zustand aktiv ist, sind alle DB-lesenden und DB-schreibenden Aktionen
@@ -561,6 +568,7 @@ public final class GuiConfigurationEditorWorkspace {
this.historicalDocumentContextPort = effectiveContext.historicalDocumentContextPort();
this.promptEditorPortFactory = effectiveContext.promptEditorPortFactory();
this.createNewDatabasePort = effectiveContext.createNewDatabasePort();
this.applicationContextInitializer = effectiveContext.applicationContextInitializer();
this.batchRunTab = new GuiBatchRunTab(
() -> this.batchRunLauncher,
() -> this.miniRunLauncher,
@@ -1024,7 +1032,13 @@ public final class GuiConfigurationEditorWorkspace {
GuiConfigurationEditorState loadedState = configurationFileLoader.load(configFilePath);
// Speichern des Pfads als letzte geladene Konfiguration
saveLastConfigurationPath(configFilePath);
Platform.runLater(() -> applyEditorState(loadedState));
// Anwendungskontext und Scheduler initialisieren; Ergebnis auf dem FX-Thread auswerten.
GuiApplicationContextInitializer.InitResult initResult =
applicationContextInitializer.initialize(configFilePath);
Platform.runLater(() -> {
applyEditorState(loadedState);
initResult.schedulerControlUseCase().ifPresent(schedulerTab::onSchedulerAvailable);
});
} catch (Exception exception) {
Platform.runLater(() -> showError("Konfiguration konnte nicht geladen werden: "
+ safeMessage(exception)));
@@ -69,7 +69,10 @@ public final class GuiSchedulerTab {
DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault());
private final Tab tab = new Tab(TAB_TITLE);
private final Optional<SchedulerControlUseCase> schedulerUseCase;
// Not final: may be updated via onSchedulerAvailable after the tab was created without a use
// case (e.g., when auto-load initialises the scheduler after the workspace was already built).
// Declared volatile so worker-thread reads (executeStart/Stop) see the write from the FX thread.
private volatile Optional<SchedulerControlUseCase> schedulerUseCase;
private final Supplier<Boolean> isConfigDirty;
// -------------------------------------------------------------------------
@@ -134,6 +137,33 @@ public final class GuiSchedulerTab {
return tab;
}
/**
* Macht den Scheduler-Use-Case für diesen Tab verfügbar, nachdem er nach einem
* erfolgreichen Datei-Öffnen initialisiert wurde.
* <p>
* Wird vom Workspace auf dem JavaFX Application Thread aufgerufen, nachdem der
* {@link GuiApplicationContextInitializer} auf einem Hintergrund-Thread einen
* {@link SchedulerControlUseCase} geliefert hat. Hat keine Wirkung, wenn bereits
* ein Use Case vorhanden ist.
* <p>
* Muss auf dem JavaFX Application Thread aufgerufen werden.
*
* @param useCase der neu initialisierte Use Case; darf nicht {@code null} sein
*/
public void onSchedulerAvailable(SchedulerControlUseCase useCase) {
if (schedulerUseCase.isPresent()) {
return;
}
schedulerUseCase = Optional.of(useCase);
intervalField.setText(String.valueOf(useCase.getIntervalSeconds()));
intervalField.setEditable(true);
intervalField.setDisable(false);
// Buttons werden im nächsten updateStatus-Tick (1 Hz) korrekt gesetzt;
// vorab grob aktivieren damit kein misleadender Disabled-Zustand bleibt.
startButton.setDisable(false);
startButton.setTooltip(null);
}
/**
* Aktualisiert alle Tab-Elemente anhand des aktuellen Scheduler-Status.
* <p>
@@ -70,6 +70,10 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* no locking is performed (e.g., no valid configuration was loaded at startup, or locking is
* not required in this context).
* <p>
* The {@code applicationContextInitializer} is invoked on a background thread each time the
* workspace loads a configuration file (auto-load at startup and manual open). Bootstrap
* provides an implementation that builds the application run context and wires the scheduler.
* <p>
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to
* know about provider-specific HTTP details or adapter wiring.
*/
@@ -100,7 +104,8 @@ public record GuiStartupContext(
GuiCreateNewDatabasePort createNewDatabasePort,
Optional<String> applicationContextError,
Optional<SchedulerControlUseCase> schedulerControlUseCase,
Optional<ConfigurationFileLockPort> configurationFileLockPort) {
Optional<ConfigurationFileLockPort> configurationFileLockPort,
GuiApplicationContextInitializer applicationContextInitializer) {
private static final String NO_PROMPT_PORT_MSG = "Kein Prompt-Editor-Port in diesem Startkontext verfügbar.";
private static final String NO_PORT_MSG = "Kein Port in diesem Startkontext.";
@@ -188,6 +193,8 @@ public record GuiStartupContext(
"createNewDatabasePort must not be null");
schedulerControlUseCase = schedulerControlUseCase == null ? Optional.empty() : schedulerControlUseCase;
configurationFileLockPort = configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
applicationContextInitializer = applicationContextInitializer == null
? GuiApplicationContextInitializer.noOp() : applicationContextInitializer;
}
/**
@@ -255,7 +262,8 @@ public record GuiStartupContext(
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort,
deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort,
applicationContextError, Optional.empty(), Optional.empty());
applicationContextError, Optional.empty(), Optional.empty(),
GuiApplicationContextInitializer.noOp());
}
/**
@@ -326,7 +334,8 @@ public record GuiStartupContext(
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort,
deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort,
applicationContextError, schedulerControlUseCase, Optional.empty());
applicationContextError, schedulerControlUseCase, Optional.empty(),
GuiApplicationContextInitializer.noOp());
}
/**