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 6f0f08f..fb79cbb 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 @@ -51,6 +51,11 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint; * context for documents that were skipped in the current run, and the resolved application * version string that the status bar displays at the bottom of the main window. *
+ * The optional {@code applicationContextError} carries a human-readable German error + * message when the bootstrap-side application run context could not be initialised at + * startup (e.g., invalid or incomplete configuration). An empty value signals that the + * run context was built successfully and batch runs can be launched immediately. + *
* 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.
*/
@@ -78,7 +83,8 @@ public record GuiStartupContext(
GuiHistoryResetDocumentStatusPort historyResetDocumentStatusPort,
GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort,
GuiPromptEditorPortFactory promptEditorPortFactory,
- GuiCreateNewDatabasePort createNewDatabasePort) {
+ GuiCreateNewDatabasePort createNewDatabasePort,
+ Optional
+ * Holds the fully validated {@link StartConfiguration} and the resolved JDBC URL for the
+ * active SQLite database. This context is created after a successful migrate → load →
+ * validate → schema-init sequence and is reused for all subsequent batch runs and
+ * database operations without repeating those steps.
+ *
+ * When the configuration changes (e.g., the user opens a different file), a new context
+ * must be built. Structural configuration changes require a GUI restart to take effect.
+ *
+ * Thread-safety: this record is immutable and safe for concurrent access.
+ */
+record ApplicationRunContext(StartConfiguration startConfiguration, String jdbcUrl) {
+
+ ApplicationRunContext {
+ Objects.requireNonNull(startConfiguration, "startConfiguration must not be null");
+ Objects.requireNonNull(jdbcUrl, "jdbcUrl must not be null");
+ }
+}
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 d1369ae..d611bf0 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
@@ -231,6 +231,21 @@ public class BootstrapRunner {
*/
private final DatabaseCreationPort databaseCreationPort = new SqliteDatabaseCreationAdapter();
+ /**
+ * The application run context built once at GUI startup from the initially loaded
+ * configuration file. When present, it carries a fully validated
+ * {@link de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration} and
+ * 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
+ * {@link #launchGuiBatchRun}, {@link #launchGuiMiniBatchRun}, and
+ * {@link #resetDocumentStatusForGui}. {@code volatile} ensures visibility
+ * across threads without explicit synchronisation on the happy path.
+ */
+ private volatile Optional
@@ -878,7 +893,8 @@ public class BootstrapRunner {
historyResetPort,
deleteHistoryPort,
this::buildGuiPromptEditorPort,
- createNewDatabasePort);
+ createNewDatabasePort,
+ Optional.empty());
}
Path configPath = Paths.get(configPathOverride.get());
@@ -910,7 +926,8 @@ public class BootstrapRunner {
historyResetPort,
deleteHistoryPort,
this::buildGuiPromptEditorPort,
- createNewDatabasePort);
+ createNewDatabasePort,
+ Optional.empty());
}
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
@@ -918,13 +935,14 @@ public class BootstrapRunner {
GuiConfigurationEditorState loadedState = loadGuiConfigurationState(configPath);
GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort(
loadedState.values().promptTemplateFile());
+ Optional
+ * Runs the full init sequence (migrate → load → validate → schema-init) and, on success,
+ * stores the result in {@link #guiApplicationRunContext}. On any failure the field is
+ * cleared and an Optional carrying a human-readable German error message is returned so
+ * the caller can surface it in the startup context without aborting the GUI launch.
+ *
+ * @param configFilePath path to the {@code .properties} configuration file; must exist on disk
+ * @return {@link Optional#empty()} on success; an Optional with the error message on failure
+ */
+ private Optional
+ * Skips the migrate → load → validate → schema-init sequence because the context was
+ * already built at startup. Only the run-lock, use-case wiring and execution are performed.
+ *
+ * @param ctx the validated application run context; must not be null
+ * @param progressObserver observer forwarded into the use case; must not be null
+ * @param cancellationToken token forwarded into the use case; must not be null
+ * @return the outcome for the run; never null
+ */
+ private GuiBatchRunLaunchOutcome executeRun(
+ ApplicationRunContext ctx,
+ de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver,
+ de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken) {
+ try {
+ RunLockPort runLockPort = runLockPortFactory.create(
+ resolveLockFilePath(ctx.startConfiguration()));
+ BatchRunContext runContext = createRunContext();
+ BatchRunProcessingUseCase useCase = buildProductionBatchUseCase(
+ ctx.startConfiguration(), runLockPort, progressObserver, cancellationToken);
+ BatchRunOutcome outcome = useCase.execute(runContext);
+ runContext.setEndInstant(Instant.now());
+ return mapGuiRunOutcome(outcome, runContext);
+ } catch (RuntimeException e) {
+ LOG.error("GUI-Verarbeitungslauf: Unerwarteter Fehler: {}", e.getMessage(), e);
+ return GuiBatchRunLaunchOutcome.failedAfterStart(
+ "Unerwarteter Fehler im Verarbeitungslauf: "
+ + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+ }
+ }
+
+ /**
+ * Executes one targeted mini-run using a pre-built {@link ApplicationRunContext}.
+ *
+ * Skips the migrate → load → validate → schema-init sequence because the context was
+ * already built at startup. Only the run-lock, use-case wiring, fingerprint filter and
+ * execution are performed.
+ *
+ * @param ctx the validated application run context; must not be null
+ * @param fingerprintFilter the set of document fingerprints to process; must not be null
+ * @param progressObserver observer forwarded into the use case; must not be null
+ * @param cancellationToken token forwarded into the use case; must not be null
+ * @return the outcome for the mini-run; never null
+ */
+ private GuiBatchRunLaunchOutcome executeRunWithFilter(
+ ApplicationRunContext ctx,
+ Set
+ * Skips the migrate → load → validate → schema-init sequence. Only the run-lock
+ * acquisition and the reset use-case execution are performed.
+ *
+ * @param ctx the validated application run context; must not be null
+ * @param fingerprints the set of fingerprints to reset; must not be null
+ * @return the result of the reset operation; never null
+ */
+ private ResetDocumentStatusResult executeResetWithContext(
+ ApplicationRunContext ctx,
+ Set
- * Mirrors the headless bootstrap pipeline: legacy migration, configuration loading and
- * validation, SQLite schema initialisation, run-lock acquisition, use-case wiring, and
- * execution. Forwards the supplied observer and cancellation token into the wired use
- * case so the GUI receives live progress callbacks and can request a soft-stop.
+ * When a pre-built {@link ApplicationRunContext} is available (built at GUI startup),
+ * it is used directly and the migrate → load → validate → schema-init sequence is
+ * skipped. When no context is available (e.g., configuration was invalid at startup),
+ * the full init sequence runs per call.
+ *
+ * Forwards the supplied observer and cancellation token into the wired use case so the
+ * GUI receives live progress callbacks and can request a soft-stop.
*
* All known hard startup failures are mapped to
* {@link GuiBatchRunLaunchOutcome#rejected(String)}. Unexpected runtime exceptions
@@ -1059,6 +1241,13 @@ public class BootstrapRunner {
Objects.requireNonNull(progressObserver, "progressObserver must not be null");
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath);
+
+ Optional
+ * When a pre-built {@link ApplicationRunContext} is available (built at GUI startup),
+ * it is used directly and the migrate → load → validate → schema-init sequence is
+ * skipped. When no context is available, the full init sequence runs per call.
+ *
+ * The run lock is acquired to prevent racing with a concurrent headless run. If the lock
+ * is unavailable, all fingerprints are returned as failures with a German message.
+ *
+ * @param configFilePath path to the {@code .properties} configuration; must exist on disk
+ * @param fingerprintFilter the set of document fingerprints to process; must not be null
+ * @param progressObserver observer forwarded into the use case; must not be null
+ * @param cancellationToken token forwarded into the use case; must not be null
+ * @return the outcome for the mini-run; never null
+ */
GuiBatchRunLaunchOutcome launchGuiMiniBatchRun(
Path configFilePath,
Set
+ * When a pre-built {@link ApplicationRunContext} is available (built at GUI startup),
+ * it is used directly and the migrate → load → validate → schema-init sequence is
+ * skipped. When no context is available, the full init sequence runs per call.
+ *
+ * The run lock is acquired to avoid racing with a concurrent headless run. If the lock
+ * is unavailable, all fingerprints are returned as failures with a German message.
+ *
+ * @param configFilePath path to the {@code .properties} configuration; must exist on disk
+ * @param fingerprints the set of document fingerprints to reset; must not be null
+ * @return the result of the reset operation; never null
+ */
ResetDocumentStatusResult resetDocumentStatusForGui(
Path configFilePath,
Set
+ * The config port factory throws {@link de.gecheckt.pdf.umbenenner.adapter.out.configuration.ConfigurationLoadingException}
+ * so that {@code initializeApplicationRunContext()} can handle it gracefully (storing
+ * the error in {@code applicationContextError}) without crashing the GUI launch sequence.
+ * Tests that verify the absence of a startup notice or the presence of a loaded editor
+ * state are not affected by the context-init failure.
+ *
+ * Run-lock and schema-init factories still throw hard errors because they must never be
+ * reached from the GUI path in test scenarios without a valid application run context.
*/
private BootstrapRunner runnerWithGuiFactory(BootstrapRunner.GuiAdapterFactory guiAdapterFactory) {
return new BootstrapRunner(
path -> { /* no-op migration */ },
- configPath -> { throw new AssertionError("ConfigurationPort must not be called in GUI mode"); },
+ configPath -> { throw new de.gecheckt.pdf.umbenenner.adapter.out.configuration
+ .ConfigurationLoadingException("Stub: no config port in GUI test context"); },
lockFile -> { throw new AssertionError("RunLockPort must not be called in GUI mode"); },
() -> { throw new AssertionError("Validator must not be called in GUI mode"); },
jdbcUrl -> { throw new AssertionError("SchemaInitPort must not be called in GUI mode"); },