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:
+15
-5
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+26
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+242
-9
@@ -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);
|
||||||
|
|||||||
+11
-1
@@ -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"); },
|
||||||
|
|||||||
Reference in New Issue
Block a user