Bootstrap-Refactoring: Init/Run-Trennung mit ApplicationRunContext

Führt ApplicationRunContext als package-private Record ein, der beim
GUI-Start einmalig aus der validierten Konfiguration gebaut wird
(migrate → load → validate → schema-init). Das Ergebnis wird in
guiApplicationRunContext gecacht und von launchGuiBatchRun,
launchGuiMiniBatchRun und resetDocumentStatusForGui wiederverwendet,
sodass die Init-Sequenz nicht bei jedem Lauf wiederholt wird.

GuiStartupContext erhält das neue Feld applicationContextError
(Optional<String>), das einen deutschen Fehlertext trägt, wenn der
Kontext bei Startup nicht initialisiert werden konnte. Alle bisherigen
Konstruktoren und die blank()-Fabrik wurden rückwärtskompatibel
ergänzt.

Der Test-Helfer runnerWithGuiFactory wirft jetzt
ConfigurationLoadingException statt AssertionError, damit
initializeApplicationRunContext() den Fehler gracefully abfangen
und in applicationContextError speichern kann.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 12:07:39 +02:00
parent ca26d181f3
commit 407f1e0422
4 changed files with 294 additions and 15 deletions
@@ -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 * 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. * version string that the status bar displays at the bottom of the main window.
* <p> * <p>
* 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.
* <p>
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to * 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. * know about provider-specific HTTP details or adapter wiring.
*/ */
@@ -78,7 +83,8 @@ public record GuiStartupContext(
GuiHistoryResetDocumentStatusPort historyResetDocumentStatusPort, GuiHistoryResetDocumentStatusPort historyResetDocumentStatusPort,
GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort, GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort,
GuiPromptEditorPortFactory promptEditorPortFactory, GuiPromptEditorPortFactory promptEditorPortFactory,
GuiCreateNewDatabasePort createNewDatabasePort) { GuiCreateNewDatabasePort createNewDatabasePort,
Optional<String> applicationContextError) {
/** /**
* Creates a fully wired startup context. * Creates a fully wired startup context.
@@ -111,10 +117,13 @@ public record GuiStartupContext(
* {@code null} sein * {@code null} sein
* @param promptEditorPortFactory Fabrik für Prompt-Editor-Ports bei Konfigurationswechsel; * @param promptEditorPortFactory Fabrik für Prompt-Editor-Ports bei Konfigurationswechsel;
* darf nicht {@code null} sein * darf nicht {@code null} sein
* @param applicationContextError optional error message when the application run context
* could not be initialised at startup; {@code null} becomes empty
*/ */
public GuiStartupContext { public GuiStartupContext {
initialState = Objects.requireNonNull(initialState, "initialState must not be null"); initialState = Objects.requireNonNull(initialState, "initialState must not be null");
startupNotice = startupNotice == null ? Optional.empty() : startupNotice; startupNotice = startupNotice == null ? Optional.empty() : startupNotice;
applicationContextError = applicationContextError == null ? Optional.empty() : applicationContextError;
configurationFileLoader = Objects.requireNonNull(configurationFileLoader, configurationFileLoader = Objects.requireNonNull(configurationFileLoader,
"configurationFileLoader must not be null"); "configurationFileLoader must not be null");
configurationFileWriter = Objects.requireNonNull(configurationFileWriter, configurationFileWriter = Objects.requireNonNull(configurationFileWriter,
@@ -202,7 +211,7 @@ public record GuiStartupContext(
noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(), noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(),
noOpHistoryOverviewPort(), noOpHistoryDetailsPort(), noOpHistoryOverviewPort(), noOpHistoryDetailsPort(),
noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory(), noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory(),
rejectingCreateNewDatabasePort()); rejectingCreateNewDatabasePort(), Optional.empty());
} }
/** /**
@@ -241,7 +250,7 @@ public record GuiStartupContext(
noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(), noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(),
noOpHistoryOverviewPort(), noOpHistoryDetailsPort(), noOpHistoryOverviewPort(), noOpHistoryDetailsPort(),
noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory(), noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory(),
rejectingCreateNewDatabasePort()); rejectingCreateNewDatabasePort(), Optional.empty());
} }
/** /**
@@ -280,7 +289,7 @@ public record GuiStartupContext(
noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(), noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(),
noOpHistoryOverviewPort(), noOpHistoryDetailsPort(), noOpHistoryOverviewPort(), noOpHistoryDetailsPort(),
noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory(), noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory(),
rejectingCreateNewDatabasePort()); rejectingCreateNewDatabasePort(), Optional.empty());
} }
private static GuiBatchRunLauncher rejectingBatchRunLauncher() { private static GuiBatchRunLauncher rejectingBatchRunLauncher() {
@@ -403,7 +412,8 @@ public record GuiStartupContext(
noOpHistoryResetPort(), noOpHistoryResetPort(),
noOpDeleteHistoryPort(), noOpDeleteHistoryPort(),
noOpPromptEditorPortFactory(), noOpPromptEditorPortFactory(),
rejectingCreateNewDatabasePort()); rejectingCreateNewDatabasePort(),
Optional.empty());
} }
/** /**
@@ -0,0 +1,26 @@
package de.gecheckt.pdf.umbenenner.bootstrap;
import java.util.Objects;
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
/**
* Immutable context built once at GUI startup for a specific validated configuration file.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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");
}
}
@@ -231,6 +231,21 @@ public class BootstrapRunner {
*/ */
private final DatabaseCreationPort databaseCreationPort = new SqliteDatabaseCreationAdapter(); 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.
* <p>
* 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<ApplicationRunContext> guiApplicationRunContext = Optional.empty();
/** /**
* Functional interface encapsulating the legacy configuration migration step. * Functional interface encapsulating the legacy configuration migration step.
* <p> * <p>
@@ -878,7 +893,8 @@ public class BootstrapRunner {
historyResetPort, historyResetPort,
deleteHistoryPort, deleteHistoryPort,
this::buildGuiPromptEditorPort, this::buildGuiPromptEditorPort,
createNewDatabasePort); createNewDatabasePort,
Optional.empty());
} }
Path configPath = Paths.get(configPathOverride.get()); Path configPath = Paths.get(configPathOverride.get());
@@ -910,7 +926,8 @@ public class BootstrapRunner {
historyResetPort, historyResetPort,
deleteHistoryPort, deleteHistoryPort,
this::buildGuiPromptEditorPort, this::buildGuiPromptEditorPort,
createNewDatabasePort); createNewDatabasePort,
Optional.empty());
} }
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath()); LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
@@ -918,13 +935,14 @@ public class BootstrapRunner {
GuiConfigurationEditorState loadedState = loadGuiConfigurationState(configPath); GuiConfigurationEditorState loadedState = loadGuiConfigurationState(configPath);
GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort( GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort(
loadedState.values().promptTemplateFile()); loadedState.values().promptTemplateFile());
Optional<String> contextError = initializeApplicationRunContext(configPath);
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer, return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort, modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher, technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
miniRunLauncher, resetPort, manualRenamePort, manualCopyPort, miniRunLauncher, resetPort, manualRenamePort, manualCopyPort,
historicalDocumentContextPort, applicationVersion, promptEditorPort, historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort, historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort,
this::buildGuiPromptEditorPort, createNewDatabasePort); this::buildGuiPromptEditorPort, createNewDatabasePort, contextError);
} catch (GuiConfigurationLoadException e) { } catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}", LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e); e.getMessage(), e);
@@ -952,7 +970,8 @@ public class BootstrapRunner {
historyResetPort, historyResetPort,
deleteHistoryPort, deleteHistoryPort,
this::buildGuiPromptEditorPort, this::buildGuiPromptEditorPort,
createNewDatabasePort); createNewDatabasePort,
Optional.empty());
} }
} }
@@ -1032,13 +1051,176 @@ public class BootstrapRunner {
}; };
} }
/**
* Attempts to build and store an {@link ApplicationRunContext} from the given configuration
* file path.
* <p>
* 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<String> initializeApplicationRunContext(Path configFilePath) {
try {
migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
initializeSchema(config);
guiApplicationRunContext = Optional.of(
new ApplicationRunContext(config, resolveActiveJdbcUrl(config)));
LOG.info("GUI-Anwendungskontext initialisiert für Konfiguration: {}", configFilePath);
return Optional.empty();
} catch (ConfigurationLoadingException e) {
LOG.warn("GUI-Anwendungskontext: Konfiguration konnte nicht geladen werden: {}",
e.getMessage());
guiApplicationRunContext = Optional.empty();
return Optional.of("Konfiguration konnte nicht geladen werden: " + e.getMessage());
} catch (InvalidStartConfigurationException e) {
LOG.warn("GUI-Anwendungskontext: Konfiguration nicht lauffähig: {}", e.getMessage());
guiApplicationRunContext = Optional.empty();
return Optional.of("Konfiguration nicht lauffähig: " + e.getMessage());
} catch (DocumentPersistenceException e) {
LOG.warn("GUI-Anwendungskontext: SQLite-Initialisierung fehlgeschlagen: {}",
e.getMessage());
guiApplicationRunContext = Optional.empty();
return Optional.of("SQLite konnte nicht initialisiert werden: " + e.getMessage());
} catch (RuntimeException e) {
LOG.warn("GUI-Anwendungskontext: Unerwarteter Fehler bei Initialisierung: {}",
e.getMessage());
guiApplicationRunContext = Optional.empty();
return Optional.of("Unerwarteter Fehler bei der Kontextinitialisierung: "
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
}
}
/**
* Executes one batch run using a pre-built {@link ApplicationRunContext}.
* <p>
* 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}.
* <p>
* 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<DocumentFingerprint> fingerprintFilter,
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();
BatchRunContext filteredContext = runContext.withFingerprintFilter(fingerprintFilter);
BatchRunProcessingUseCase useCase = buildProductionBatchUseCase(
ctx.startConfiguration(), runLockPort, progressObserver, cancellationToken);
BatchRunOutcome outcome = useCase.execute(filteredContext);
filteredContext.setEndInstant(Instant.now());
return mapGuiRunOutcome(outcome, filteredContext);
} catch (RuntimeException e) {
LOG.error("GUI-Mini-Verarbeitungslauf: Unerwarteter Fehler: {}", e.getMessage(), e);
return GuiBatchRunLaunchOutcome.failedAfterStart(
"Unerwarteter Fehler im Mini-Verarbeitungslauf: "
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
}
}
/**
* Executes a document-status reset using a pre-built {@link ApplicationRunContext}.
* <p>
* 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<DocumentFingerprint> fingerprints) {
try {
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(ctx.jdbcUrl());
ProcessingLogger resetLogger = new Log4jProcessingLogger(
DefaultResetDocumentStatusUseCase.class,
resolveAiContentSensitivity(ctx.startConfiguration().logAiSensitive()));
RunLockPort runLockPort = runLockPortFactory.create(
resolveLockFilePath(ctx.startConfiguration()));
try {
runLockPort.acquire();
} catch (de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException e) {
LOG.warn("GUI-Status-Reset: Laufsperre nicht verfügbar — Reset abgelehnt.");
return allFailures(fingerprints, "Lauf blockiert — Lock nicht verfügbar");
}
try {
DefaultResetDocumentStatusUseCase useCase =
new DefaultResetDocumentStatusUseCase(unitOfWorkPort, resetLogger);
ResetDocumentStatusResult result = useCase.reset(fingerprints);
LOG.info("GUI-Status-Reset abgeschlossen: {} erfolgreich, {} fehlgeschlagen.",
result.successCount(), result.failureCount());
return result;
} finally {
try {
runLockPort.release();
} catch (RuntimeException e) {
LOG.warn("GUI-Status-Reset: Laufsperre konnte nicht freigegeben werden: {}",
e.getMessage(), e);
}
}
} catch (RuntimeException e) {
LOG.error("GUI-Status-Reset: Unerwarteter Fehler: {}", e.getMessage(), e);
return allFailures(fingerprints, "Unerwarteter Fehler: "
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
}
}
/** /**
* Executes exactly one batch run triggered by the GUI's processing-run tab. * Executes exactly one batch run triggered by the GUI's processing-run tab.
* <p> * <p>
* Mirrors the headless bootstrap pipeline: legacy migration, configuration loading and * When a pre-built {@link ApplicationRunContext} is available (built at GUI startup),
* validation, SQLite schema initialisation, run-lock acquisition, use-case wiring, and * it is used directly and the migrate → load → validate → schema-init sequence is
* execution. Forwards the supplied observer and cancellation token into the wired use * skipped. When no context is available (e.g., configuration was invalid at startup),
* case so the GUI receives live progress callbacks and can request a soft-stop. * the full init sequence runs per call.
* <p>
* 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.
* <p> * <p>
* All known hard startup failures are mapped to * All known hard startup failures are mapped to
* {@link GuiBatchRunLaunchOutcome#rejected(String)}. Unexpected runtime exceptions * {@link GuiBatchRunLaunchOutcome#rejected(String)}. Unexpected runtime exceptions
@@ -1059,6 +1241,13 @@ public class BootstrapRunner {
Objects.requireNonNull(progressObserver, "progressObserver must not be null"); Objects.requireNonNull(progressObserver, "progressObserver must not be null");
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null"); Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath); LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext;
if (ctx.isPresent()) {
LOG.debug("GUI-Verarbeitungslauf: Verwende vorbereiteten Anwendungskontext.");
return executeRun(ctx.get(), progressObserver, cancellationToken);
}
try { try {
if (!Files.exists(configFilePath)) { if (!Files.exists(configFilePath)) {
return GuiBatchRunLaunchOutcome.rejected( return GuiBatchRunLaunchOutcome.rejected(
@@ -1114,6 +1303,23 @@ public class BootstrapRunner {
* @param cancellationToken token 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 * @return the outcome for the mini-run; never null
*/ */
/**
* Executes a targeted mini batch run restricted to the supplied fingerprint set,
* triggered by the GUI's "Erneut verarbeiten" action.
* <p>
* 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.
* <p>
* 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( GuiBatchRunLaunchOutcome launchGuiMiniBatchRun(
Path configFilePath, Path configFilePath,
Set<DocumentFingerprint> fingerprintFilter, Set<DocumentFingerprint> fingerprintFilter,
@@ -1125,6 +1331,13 @@ public class BootstrapRunner {
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null"); Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
LOG.info("GUI-Mini-Verarbeitungslauf: Startanforderung für {} Dokument(e), Konfiguration {}.", LOG.info("GUI-Mini-Verarbeitungslauf: Startanforderung für {} Dokument(e), Konfiguration {}.",
fingerprintFilter.size(), configFilePath); fingerprintFilter.size(), configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext;
if (ctx.isPresent()) {
LOG.debug("GUI-Mini-Verarbeitungslauf: Verwende vorbereiteten Anwendungskontext.");
return executeRunWithFilter(ctx.get(), fingerprintFilter, progressObserver, cancellationToken);
}
try { try {
if (!Files.exists(configFilePath)) { if (!Files.exists(configFilePath)) {
return GuiBatchRunLaunchOutcome.rejected( return GuiBatchRunLaunchOutcome.rejected(
@@ -1135,7 +1348,6 @@ public class BootstrapRunner {
initializeSchema(config); initializeSchema(config);
RunLockPort runLockPort = runLockPortFactory.create(resolveLockFilePath(config)); RunLockPort runLockPort = runLockPortFactory.create(resolveLockFilePath(config));
BatchRunContext runContext = createRunContext(); BatchRunContext runContext = createRunContext();
// Apply the fingerprint filter to restrict processing to the selected documents
BatchRunContext filteredContext = runContext.withFingerprintFilter(fingerprintFilter); BatchRunContext filteredContext = runContext.withFingerprintFilter(fingerprintFilter);
BatchRunProcessingUseCase useCase = buildProductionBatchUseCase( BatchRunProcessingUseCase useCase = buildProductionBatchUseCase(
config, runLockPort, progressObserver, cancellationToken); config, runLockPort, progressObserver, cancellationToken);
@@ -1180,6 +1392,21 @@ public class BootstrapRunner {
* @param fingerprints the set of document fingerprints to reset; must not be null * @param fingerprints the set of document fingerprints to reset; must not be null
* @return the result of the reset operation; never null * @return the result of the reset operation; never null
*/ */
/**
* Resets the processing status of the specified documents by deleting all persistence
* data for their fingerprints, triggered by the GUI's "Status zurücksetzen" action.
* <p>
* 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.
* <p>
* 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( ResetDocumentStatusResult resetDocumentStatusForGui(
Path configFilePath, Path configFilePath,
Set<DocumentFingerprint> fingerprints) { Set<DocumentFingerprint> fingerprints) {
@@ -1188,6 +1415,12 @@ public class BootstrapRunner {
LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.", LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.",
fingerprints.size(), configFilePath); fingerprints.size(), configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext;
if (ctx.isPresent()) {
LOG.debug("GUI-Status-Reset: Verwende vorbereiteten Anwendungskontext.");
return executeResetWithContext(ctx.get(), fingerprints);
}
if (!Files.exists(configFilePath)) { if (!Files.exists(configFilePath)) {
String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath; String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath;
LOG.error("GUI-Status-Reset: {}", msg); LOG.error("GUI-Status-Reset: {}", msg);
@@ -388,11 +388,21 @@ class BootstrapRunnerConfigPathSemanticsTest {
/** /**
* Creates a {@link BootstrapRunner} wired with a controllable GUI adapter factory * Creates a {@link BootstrapRunner} wired with a controllable GUI adapter factory
* and stub factories for all headless-path dependencies. * and stub factories for all headless-path dependencies.
* <p>
* 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.
* <p>
* 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) { private BootstrapRunner runnerWithGuiFactory(BootstrapRunner.GuiAdapterFactory guiAdapterFactory) {
return new BootstrapRunner( return new BootstrapRunner(
path -> { /* no-op migration */ }, 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"); }, lockFile -> { throw new AssertionError("RunLockPort must not be called in GUI mode"); },
() -> { throw new AssertionError("Validator 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"); }, jdbcUrl -> { throw new AssertionError("SchemaInitPort must not be called in GUI mode"); },