diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiApplicationContextInitializer.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiApplicationContextInitializer.java new file mode 100644 index 0000000..2d42728 --- /dev/null +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiApplicationContextInitializer.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * 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}. + *

+ * 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. + *

+ * 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 contextError, + Optional schedulerControlUseCase) { + } +} diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java index 85e674f..588b3ae 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java @@ -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))); diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java index bb631fa..ab68fd4 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java @@ -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 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 schedulerUseCase; private final Supplier 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. + *

+ * 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. + *

+ * 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. *

diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java index 9e91caf..027d35c 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java @@ -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). *

+ * 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. + *

* 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 applicationContextError, Optional schedulerControlUseCase, - Optional configurationFileLockPort) { + Optional 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()); } /** diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java index bb4836d..63962d5 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java @@ -21,6 +21,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand; import de.gecheckt.pdf.umbenenner.adapter.in.scheduler.FileChannelConfigurationAccessAdapter; import de.gecheckt.pdf.umbenenner.adapter.in.scheduler.ScheduledExecutorServiceSchedulerAdapter; import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiAdapter; +import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiApplicationContextInitializer; 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; @@ -255,11 +256,13 @@ public class BootstrapRunner { * the resolved JDBC URL, so that batch runs and reset operations can skip the * migrate → load → validate → schema-init sequence for each call. *

- * Written by {@link #initializeApplicationRunContext(Path)} during - * {@link #buildGuiStartupContext(Optional)} and read by + * Written by {@link #initializeApplicationRunContext(Path)} — either directly during + * {@link #buildGuiStartupContext(Optional)} (when {@code --config} is supplied and valid), + * or via the {@link GuiApplicationContextInitializer} callback that the workspace invokes + * on a background thread after each successful file open. Read by * {@link #launchGuiBatchRun}, {@link #launchGuiMiniBatchRun}, and - * {@link #resetDocumentStatusForGui}. {@code volatile} ensures visibility - * across threads without explicit synchronisation on the happy path. + * {@link #resetDocumentStatusForGui}. {@code volatile} ensures visibility across threads + * without explicit synchronisation on the happy path. */ private volatile Optional guiApplicationRunContext = Optional.empty(); @@ -771,8 +774,12 @@ public class BootstrapRunner { * during startup is treated as a hard GUI startup failure and mapped to exit code 1. * Normal termination (user closes the window) returns exit code 0. *

- * The headless batch pipeline is not entered from this method. Configuration loading, - * schema initialization, and all batch infrastructure are not initialized in the GUI path. + * The headless batch pipeline is not entered from this method. When {@code --config} is + * supplied and the file exists, configuration loading and schema initialisation run + * immediately so the application run context is pre-built. For all other startup paths + * (no {@code --config}, missing file, load failure) a {@link GuiApplicationContextInitializer} + * callback is wired into the startup context; the workspace calls it on a background thread + * each time a configuration file is successfully opened. * * @param configPathOverride the optional {@code --config} path string from startup arguments; * must not be {@code null} @@ -915,6 +922,20 @@ public class BootstrapRunner { // Versionsnummer aus dem MANIFEST.MF des gepackten JARs lesen; Fallback "dev" bei IDE-Start String applicationVersion = ApplicationVersionProvider.resolveVersion(); + // Initializer that the workspace calls on a background thread after every successful + // file open (auto-load at startup and manual open). Builds the ApplicationRunContext + // and wires the scheduler for the newly loaded configuration file. + GuiApplicationContextInitializer contextInitializer = configFilePath -> { + stopGuiSchedulerIfActive(); + Optional initError = initializeApplicationRunContext(configFilePath); + if (initError.isEmpty()) { + tryInitializeScheduler(configFilePath); + } + return new GuiApplicationContextInitializer.InitResult( + initError, + guiSchedulerUseCase.map(s -> (SchedulerControlUseCase) s)); + }; + if (configPathOverride.isEmpty()) { return new GuiStartupContext( GuiConfigurationEditorStateFactory.createBlankStartState(), @@ -941,7 +962,10 @@ public class BootstrapRunner { deleteHistoryPort, this::buildGuiPromptEditorPort, createNewDatabasePort, - Optional.empty()); + Optional.empty(), + Optional.empty(), + Optional.empty(), + contextInitializer); } Path configPath = Paths.get(configPathOverride.get()); @@ -974,7 +998,10 @@ public class BootstrapRunner { deleteHistoryPort, this::buildGuiPromptEditorPort, createNewDatabasePort, - Optional.empty()); + Optional.empty(), + Optional.empty(), + Optional.empty(), + contextInitializer); } LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath()); @@ -997,7 +1024,7 @@ public class BootstrapRunner { historicalDocumentContextPort, applicationVersion, promptEditorPort, historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort, this::buildGuiPromptEditorPort, createNewDatabasePort, contextError, - schedulerUseCase, guiRunLockPort); + schedulerUseCase, guiRunLockPort, contextInitializer); } catch (GuiConfigurationLoadException e) { LOG.error("GUI startup: configuration could not be loaded, starting without it: {}", e.getMessage(), e); @@ -1026,7 +1053,10 @@ public class BootstrapRunner { deleteHistoryPort, this::buildGuiPromptEditorPort, createNewDatabasePort, - Optional.empty()); + Optional.empty(), + Optional.empty(), + Optional.empty(), + contextInitializer); } }