Fix #41: Historischen KI-Dateinamen für übersprungene Dokumente in Ergebnistabelle anzeigen
Neue Komponenten: - ResolveHistoricalFileNameUseCase (port/in) und DefaultResolveHistoricalFileNameUseCase (usecase) - GuiHistoricalFileNamePort (GUI-interner Port, folgt dem Muster von GuiManualFileRenamePort) GuiBatchRunCoordinator ruft in toRow() für SKIPPED-Zeilen ohne finalName den historicalFileNamePort auf und trägt den Rückgabewert als neuen Dateinamen ein. Bootstrap verdrahtet resolveHistoricalFileNameForGui als GuiHistoricalFileNamePort und übergibt ihn über GuiStartupContext an den GUI-Adapter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+9
@@ -17,6 +17,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunTab;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunTab;
|
||||||
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiHistoricalFileNamePort;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiMiniRunLauncher;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiMiniRunLauncher;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort;
|
||||||
@@ -371,6 +372,12 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
*/
|
*/
|
||||||
private final GuiManualFileRenamePort manualFileRenamePort;
|
private final GuiManualFileRenamePort manualFileRenamePort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Port used by the processing-run coordinator to resolve the historical AI-proposed
|
||||||
|
* filename for skipped documents. Supplied by Bootstrap via the startup context.
|
||||||
|
*/
|
||||||
|
private final GuiHistoricalFileNamePort historicalFileNamePort;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Second main tab of the window that drives the live processing-run view. Created
|
* Second main tab of the window that drives the live processing-run view. Created
|
||||||
* during workspace construction and wired into the shared {@link #tabPane} alongside
|
* during workspace construction and wired into the shared {@link #tabPane} alongside
|
||||||
@@ -446,6 +453,7 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
this.miniRunLauncher = effectiveContext.miniRunLauncher();
|
this.miniRunLauncher = effectiveContext.miniRunLauncher();
|
||||||
this.resetDocumentStatusPort = effectiveContext.resetDocumentStatusPort();
|
this.resetDocumentStatusPort = effectiveContext.resetDocumentStatusPort();
|
||||||
this.manualFileRenamePort = effectiveContext.manualFileRenamePort();
|
this.manualFileRenamePort = effectiveContext.manualFileRenamePort();
|
||||||
|
this.historicalFileNamePort = effectiveContext.historicalFileNamePort();
|
||||||
this.batchRunTab = new GuiBatchRunTab(
|
this.batchRunTab = new GuiBatchRunTab(
|
||||||
() -> this.batchRunLauncher,
|
() -> this.batchRunLauncher,
|
||||||
() -> this.miniRunLauncher,
|
() -> this.miniRunLauncher,
|
||||||
@@ -454,6 +462,7 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
this::isSavedConfigurationReady,
|
this::isSavedConfigurationReady,
|
||||||
this::applyBatchRunLockState,
|
this::applyBatchRunLockState,
|
||||||
() -> this.manualFileRenamePort,
|
() -> this.manualFileRenamePort,
|
||||||
|
() -> this.historicalFileNamePort,
|
||||||
this::editorSourceFolder,
|
this::editorSourceFolder,
|
||||||
this::editorTargetFolder);
|
this::editorTargetFolder);
|
||||||
|
|
||||||
|
|||||||
+21
-6
@@ -6,6 +6,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
|
||||||
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiHistoricalFileNamePort;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiMiniRunLauncher;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiMiniRunLauncher;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort;
|
||||||
@@ -40,7 +41,9 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
|||||||
* to execute regular batch runs, the {@link GuiMiniRunLauncher} used to execute targeted
|
* to execute regular batch runs, the {@link GuiMiniRunLauncher} used to execute targeted
|
||||||
* mini-runs for selected documents, the {@link GuiResetDocumentStatusPort} used to
|
* mini-runs for selected documents, the {@link GuiResetDocumentStatusPort} used to
|
||||||
* reset the persistence status of selected documents, and the
|
* reset the persistence status of selected documents, and the
|
||||||
* {@link GuiManualFileRenamePort} used to manually rename a target file from the GUI.
|
* {@link GuiManualFileRenamePort} used to manually rename a target file from the GUI, and
|
||||||
|
* the {@link GuiHistoricalFileNamePort} used to retrieve the historical AI-proposed filename
|
||||||
|
* for documents that were skipped in the current run.
|
||||||
* <p>
|
* <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.
|
||||||
@@ -59,7 +62,8 @@ public record GuiStartupContext(
|
|||||||
GuiBatchRunLauncher batchRunLauncher,
|
GuiBatchRunLauncher batchRunLauncher,
|
||||||
GuiMiniRunLauncher miniRunLauncher,
|
GuiMiniRunLauncher miniRunLauncher,
|
||||||
GuiResetDocumentStatusPort resetDocumentStatusPort,
|
GuiResetDocumentStatusPort resetDocumentStatusPort,
|
||||||
GuiManualFileRenamePort manualFileRenamePort) {
|
GuiManualFileRenamePort manualFileRenamePort,
|
||||||
|
GuiHistoricalFileNamePort historicalFileNamePort) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a fully wired startup context.
|
* Creates a fully wired startup context.
|
||||||
@@ -81,6 +85,8 @@ public record GuiStartupContext(
|
|||||||
* documents; must not be {@code null}
|
* documents; must not be {@code null}
|
||||||
* @param manualFileRenamePort bridge that renames a target file manually from the GUI;
|
* @param manualFileRenamePort bridge that renames a target file manually from the GUI;
|
||||||
* must not be {@code null}
|
* must not be {@code null}
|
||||||
|
* @param historicalFileNamePort bridge that resolves the historical AI-proposed filename
|
||||||
|
* for skipped documents; must not be {@code null}
|
||||||
*/
|
*/
|
||||||
public GuiStartupContext {
|
public GuiStartupContext {
|
||||||
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
|
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
|
||||||
@@ -109,6 +115,8 @@ public record GuiStartupContext(
|
|||||||
"resetDocumentStatusPort must not be null");
|
"resetDocumentStatusPort must not be null");
|
||||||
manualFileRenamePort = Objects.requireNonNull(manualFileRenamePort,
|
manualFileRenamePort = Objects.requireNonNull(manualFileRenamePort,
|
||||||
"manualFileRenamePort must not be null");
|
"manualFileRenamePort must not be null");
|
||||||
|
historicalFileNamePort = Objects.requireNonNull(historicalFileNamePort,
|
||||||
|
"historicalFileNamePort must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,7 +156,8 @@ public record GuiStartupContext(
|
|||||||
this(initialState, startupNotice, configurationFileLoader, configurationFileWriter,
|
this(initialState, startupNotice, configurationFileLoader, configurationFileWriter,
|
||||||
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
||||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
||||||
miniRunLauncher, resetDocumentStatusPort, rejectingManualFileRenamePort());
|
miniRunLauncher, resetDocumentStatusPort, rejectingManualFileRenamePort(),
|
||||||
|
noOpHistoricalFileNamePort());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,7 +191,8 @@ public record GuiStartupContext(
|
|||||||
this(initialState, startupNotice, configurationFileLoader, configurationFileWriter,
|
this(initialState, startupNotice, configurationFileLoader, configurationFileWriter,
|
||||||
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
||||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
||||||
rejectingMiniRunLauncher(), rejectingResetPort(), rejectingManualFileRenamePort());
|
rejectingMiniRunLauncher(), rejectingResetPort(), rejectingManualFileRenamePort(),
|
||||||
|
noOpHistoricalFileNamePort());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,7 +227,7 @@ public record GuiStartupContext(
|
|||||||
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
||||||
technicalTestOrchestrator, correctionExecutionService,
|
technicalTestOrchestrator, correctionExecutionService,
|
||||||
rejectingBatchRunLauncher(), rejectingMiniRunLauncher(), rejectingResetPort(),
|
rejectingBatchRunLauncher(), rejectingMiniRunLauncher(), rejectingResetPort(),
|
||||||
rejectingManualFileRenamePort());
|
rejectingManualFileRenamePort(), noOpHistoricalFileNamePort());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GuiBatchRunLauncher rejectingBatchRunLauncher() {
|
private static GuiBatchRunLauncher rejectingBatchRunLauncher() {
|
||||||
@@ -246,6 +256,10 @@ public record GuiStartupContext(
|
|||||||
"Kein Umbennennungs-Port in diesem Startkontext verfügbar.");
|
"Kein Umbennennungs-Port in diesem Startkontext verfügbar.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GuiHistoricalFileNamePort noOpHistoricalFileNamePort() {
|
||||||
|
return (configPath, fingerprint) -> java.util.Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a blank startup context with no-op implementations for all ports and services.
|
* Creates a blank startup context with no-op implementations for all ports and services.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -319,6 +333,7 @@ public record GuiStartupContext(
|
|||||||
noOpBatchRunLauncher,
|
noOpBatchRunLauncher,
|
||||||
rejectingMiniRunLauncher(),
|
rejectingMiniRunLauncher(),
|
||||||
rejectingResetPort(),
|
rejectingResetPort(),
|
||||||
rejectingManualFileRenamePort());
|
rejectingManualFileRenamePort(),
|
||||||
|
noOpHistoricalFileNamePort());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-8
@@ -17,6 +17,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken;
|
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver;
|
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent;
|
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
|
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.RunSummary;
|
import de.gecheckt.pdf.umbenenner.application.port.in.RunSummary;
|
||||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||||
@@ -112,6 +113,7 @@ public final class GuiBatchRunCoordinator {
|
|||||||
private final Function<Runnable, Thread> threadFactory;
|
private final Function<Runnable, Thread> threadFactory;
|
||||||
private final Consumer<Runnable> fxDispatcher;
|
private final Consumer<Runnable> fxDispatcher;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
|
private final GuiHistoricalFileNamePort historicalFileNamePort;
|
||||||
private final AtomicReference<Thread> activeWorker = new AtomicReference<>();
|
private final AtomicReference<Thread> activeWorker = new AtomicReference<>();
|
||||||
private final AtomicBoolean cancellationRequested = new AtomicBoolean();
|
private final AtomicBoolean cancellationRequested = new AtomicBoolean();
|
||||||
|
|
||||||
@@ -152,6 +154,27 @@ public final class GuiBatchRunCoordinator {
|
|||||||
defaultThreadFactory(), defaultFxDispatcher(), listener);
|
defaultThreadFactory(), defaultFxDispatcher(), listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the coordinator with all ports and the historical file name port, using the
|
||||||
|
* default worker-thread factory and JavaFX Application Thread dispatcher.
|
||||||
|
*
|
||||||
|
* @param launcher bridge to Bootstrap for regular batch runs; must not be null
|
||||||
|
* @param miniRunLauncher bridge to Bootstrap for targeted mini-runs; must not be null
|
||||||
|
* @param resetPort bridge to Bootstrap for status-reset-only operations; must
|
||||||
|
* not be null
|
||||||
|
* @param listener GUI listener invoked on the FX thread; must not be null
|
||||||
|
* @param historicalFileNamePort port for resolving the historical AI-proposed filename for
|
||||||
|
* skipped documents; must not be null
|
||||||
|
*/
|
||||||
|
public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
|
||||||
|
GuiMiniRunLauncher miniRunLauncher,
|
||||||
|
GuiResetDocumentStatusPort resetPort,
|
||||||
|
Listener listener,
|
||||||
|
GuiHistoricalFileNamePort historicalFileNamePort) {
|
||||||
|
this(launcher, miniRunLauncher, resetPort,
|
||||||
|
defaultThreadFactory(), defaultFxDispatcher(), listener, historicalFileNamePort);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the coordinator with custom hooks for the worker-thread factory and the
|
* Creates the coordinator with custom hooks for the worker-thread factory and the
|
||||||
* UI-thread dispatcher.
|
* UI-thread dispatcher.
|
||||||
@@ -176,12 +199,43 @@ public final class GuiBatchRunCoordinator {
|
|||||||
Function<Runnable, Thread> threadFactory,
|
Function<Runnable, Thread> threadFactory,
|
||||||
Consumer<Runnable> fxDispatcher,
|
Consumer<Runnable> fxDispatcher,
|
||||||
Listener listener) {
|
Listener listener) {
|
||||||
|
this(launcher, miniRunLauncher, resetPort, threadFactory, fxDispatcher, listener,
|
||||||
|
noOpHistoricalFileNamePort());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the coordinator with all ports, custom thread factory, FX dispatcher and
|
||||||
|
* historical file name port.
|
||||||
|
* <p>
|
||||||
|
* This is the canonical constructor. All other constructors delegate here.
|
||||||
|
*
|
||||||
|
* @param launcher bridge to Bootstrap for regular batch runs; must not be null
|
||||||
|
* @param miniRunLauncher bridge to Bootstrap for targeted mini-runs; must not be null
|
||||||
|
* @param resetPort bridge to Bootstrap for status-reset-only operations; must
|
||||||
|
* not be null
|
||||||
|
* @param threadFactory factory returning a ready-to-start worker thread; must not
|
||||||
|
* be null
|
||||||
|
* @param fxDispatcher dispatcher that schedules a runnable on the JavaFX Application
|
||||||
|
* Thread; must not be null
|
||||||
|
* @param listener GUI listener; must not be null
|
||||||
|
* @param historicalFileNamePort port for resolving the historical AI-proposed filename for
|
||||||
|
* skipped documents; must not be null
|
||||||
|
*/
|
||||||
|
public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
|
||||||
|
GuiMiniRunLauncher miniRunLauncher,
|
||||||
|
GuiResetDocumentStatusPort resetPort,
|
||||||
|
Function<Runnable, Thread> threadFactory,
|
||||||
|
Consumer<Runnable> fxDispatcher,
|
||||||
|
Listener listener,
|
||||||
|
GuiHistoricalFileNamePort historicalFileNamePort) {
|
||||||
this.launcher = Objects.requireNonNull(launcher, "launcher must not be null");
|
this.launcher = Objects.requireNonNull(launcher, "launcher must not be null");
|
||||||
this.miniRunLauncher = Objects.requireNonNull(miniRunLauncher, "miniRunLauncher must not be null");
|
this.miniRunLauncher = Objects.requireNonNull(miniRunLauncher, "miniRunLauncher must not be null");
|
||||||
this.resetPort = Objects.requireNonNull(resetPort, "resetPort must not be null");
|
this.resetPort = Objects.requireNonNull(resetPort, "resetPort must not be null");
|
||||||
this.threadFactory = Objects.requireNonNull(threadFactory, "threadFactory must not be null");
|
this.threadFactory = Objects.requireNonNull(threadFactory, "threadFactory must not be null");
|
||||||
this.fxDispatcher = Objects.requireNonNull(fxDispatcher, "fxDispatcher must not be null");
|
this.fxDispatcher = Objects.requireNonNull(fxDispatcher, "fxDispatcher must not be null");
|
||||||
this.listener = Objects.requireNonNull(listener, "listener must not be null");
|
this.listener = Objects.requireNonNull(listener, "listener must not be null");
|
||||||
|
this.historicalFileNamePort = Objects.requireNonNull(
|
||||||
|
historicalFileNamePort, "historicalFileNamePort must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -382,7 +436,7 @@ public final class GuiBatchRunCoordinator {
|
|||||||
LOG.info("GUI-Verarbeitungslauf: Worker-Thread gestartet für Konfiguration {}.",
|
LOG.info("GUI-Verarbeitungslauf: Worker-Thread gestartet für Konfiguration {}.",
|
||||||
configFilePath);
|
configFilePath);
|
||||||
observerSummary.set(null);
|
observerSummary.set(null);
|
||||||
BatchRunProgressObserver observer = buildDispatchingObserver();
|
BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
|
||||||
BatchRunCancellationToken token = cancellationRequested::get;
|
BatchRunCancellationToken token = cancellationRequested::get;
|
||||||
GuiBatchRunLaunchOutcome outcome;
|
GuiBatchRunLaunchOutcome outcome;
|
||||||
try {
|
try {
|
||||||
@@ -405,7 +459,7 @@ public final class GuiBatchRunCoordinator {
|
|||||||
LOG.info("GUI-Mini-Verarbeitungslauf: Worker-Thread gestartet für {} Dokument(e), "
|
LOG.info("GUI-Mini-Verarbeitungslauf: Worker-Thread gestartet für {} Dokument(e), "
|
||||||
+ "Konfiguration {}.", fingerprintFilter.size(), configFilePath);
|
+ "Konfiguration {}.", fingerprintFilter.size(), configFilePath);
|
||||||
observerSummary.set(null);
|
observerSummary.set(null);
|
||||||
BatchRunProgressObserver observer = buildDispatchingObserver();
|
BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
|
||||||
BatchRunCancellationToken token = cancellationRequested::get;
|
BatchRunCancellationToken token = cancellationRequested::get;
|
||||||
GuiBatchRunLaunchOutcome outcome;
|
GuiBatchRunLaunchOutcome outcome;
|
||||||
try {
|
try {
|
||||||
@@ -476,7 +530,7 @@ public final class GuiBatchRunCoordinator {
|
|||||||
*/
|
*/
|
||||||
private final AtomicReference<RunSummary> observerSummary = new AtomicReference<>();
|
private final AtomicReference<RunSummary> observerSummary = new AtomicReference<>();
|
||||||
|
|
||||||
private BatchRunProgressObserver buildDispatchingObserver() {
|
private BatchRunProgressObserver buildDispatchingObserver(Path configFilePath) {
|
||||||
return new BatchRunProgressObserver() {
|
return new BatchRunProgressObserver() {
|
||||||
@Override
|
@Override
|
||||||
public void onRunStarted(RunId runId, int totalCandidates) {
|
public void onRunStarted(RunId runId, int totalCandidates) {
|
||||||
@@ -485,23 +539,44 @@ public final class GuiBatchRunCoordinator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDocumentCompleted(DocumentCompletionEvent event) {
|
public void onDocumentCompleted(DocumentCompletionEvent event) {
|
||||||
GuiBatchRunResultRow row = toRow(event);
|
GuiBatchRunResultRow row = toRow(event, configFilePath);
|
||||||
fxDispatcher.accept(() -> listener.onDocumentCompleted(row));
|
fxDispatcher.accept(() -> listener.onDocumentCompleted(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRunEnded(RunSummary summary) {
|
public void onRunEnded(RunSummary summary) {
|
||||||
observerSummary.set(summary);
|
observerSummary.set(summary);
|
||||||
// No FX dispatch here: the worker thread invokes the listener's
|
// Kein FX-Dispatch hier: der Worker-Thread ruft onRunEnded über finishRun()
|
||||||
// onRunEnded via finishRun() once the launcher has returned, ensuring
|
// auf, nachdem der Launcher zurückgekehrt ist.
|
||||||
// the outcome carries the launcher's terminal verdict.
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GuiBatchRunResultRow toRow(DocumentCompletionEvent event) {
|
/**
|
||||||
|
* Wandelt ein {@link DocumentCompletionEvent} in eine {@link GuiBatchRunResultRow} um.
|
||||||
|
* <p>
|
||||||
|
* Für übersprungene Dokumente ({@link DocumentCompletionStatus#SKIPPED}) ohne
|
||||||
|
* vorhandenen Dateinamen wird der historische KI-Dateiname über den
|
||||||
|
* {@link GuiHistoricalFileNamePort} nachgeladen. Schlägt die Abfrage fehl, bleibt
|
||||||
|
* die Spalte leer. Die Methode läuft auf dem Worker-Thread.
|
||||||
|
*
|
||||||
|
* @param event das abgeschlossene Kandidatenereignis; darf nicht {@code null} sein
|
||||||
|
* @param configFilePath Pfad zur aktiven Konfigurationsdatei; darf nicht {@code null} sein
|
||||||
|
* @return eine neue {@link GuiBatchRunResultRow}; nie {@code null}
|
||||||
|
*/
|
||||||
|
private GuiBatchRunResultRow toRow(DocumentCompletionEvent event, Path configFilePath) {
|
||||||
Optional<String> finalName = event.finalFileName() == null
|
Optional<String> finalName = event.finalFileName() == null
|
||||||
? Optional.empty() : Optional.of(event.finalFileName());
|
? Optional.empty() : Optional.of(event.finalFileName());
|
||||||
|
// Historischen KI-Dateinamen für übersprungene Dokumente nachladen
|
||||||
|
if (finalName.isEmpty() && event.status() == DocumentCompletionStatus.SKIPPED) {
|
||||||
|
try {
|
||||||
|
finalName = historicalFileNamePort.resolveHistoricalFileName(
|
||||||
|
configFilePath, event.fingerprint());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("Historischer Dateiname konnte nicht abgefragt werden für {}: {}",
|
||||||
|
event.originalFileName(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
Optional<LocalDate> date = event.resolvedDate() == null
|
Optional<LocalDate> date = event.resolvedDate() == null
|
||||||
? Optional.empty() : Optional.of(event.resolvedDate());
|
? Optional.empty() : Optional.of(event.resolvedDate());
|
||||||
Optional<String> reasoning = event.aiReasoning() == null || event.aiReasoning().isBlank()
|
Optional<String> reasoning = event.aiReasoning() == null || event.aiReasoning().isBlank()
|
||||||
@@ -520,6 +595,10 @@ public final class GuiBatchRunCoordinator {
|
|||||||
duration);
|
duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GuiHistoricalFileNamePort noOpHistoricalFileNamePort() {
|
||||||
|
return (configPath, fingerprint) -> Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
private static Function<Runnable, Thread> defaultThreadFactory() {
|
private static Function<Runnable, Thread> defaultThreadFactory() {
|
||||||
return task -> {
|
return task -> {
|
||||||
Thread thread = new Thread(task, WORKER_THREAD_NAME);
|
Thread thread = new Thread(task, WORKER_THREAD_NAME);
|
||||||
|
|||||||
+10
-1
@@ -193,6 +193,7 @@ public final class GuiBatchRunTab {
|
|||||||
private final Runnable onRunStateChanged;
|
private final Runnable onRunStateChanged;
|
||||||
private final GuiBatchRunCoordinator coordinator;
|
private final GuiBatchRunCoordinator coordinator;
|
||||||
private final Supplier<GuiManualFileRenamePort> manualFileRenamePortSupplier;
|
private final Supplier<GuiManualFileRenamePort> manualFileRenamePortSupplier;
|
||||||
|
private final Supplier<GuiHistoricalFileNamePort> historicalFileNamePortSupplier;
|
||||||
private final Supplier<Optional<Path>> sourceFolderSupplier;
|
private final Supplier<Optional<Path>> sourceFolderSupplier;
|
||||||
private final Supplier<Optional<String>> targetFolderSupplier;
|
private final Supplier<Optional<String>> targetFolderSupplier;
|
||||||
|
|
||||||
@@ -231,6 +232,8 @@ public final class GuiBatchRunTab {
|
|||||||
* null sein
|
* null sein
|
||||||
* @param manualFileRenamePortSupplier Supplier für den manuellen Umbennennungs-Port;
|
* @param manualFileRenamePortSupplier Supplier für den manuellen Umbennennungs-Port;
|
||||||
* darf nicht null sein
|
* darf nicht null sein
|
||||||
|
* @param historicalFileNamePortSupplier Supplier für den historischen Dateiname-Port;
|
||||||
|
* darf nicht null sein
|
||||||
* @param sourceFolderSupplier Supplier für den konfigurierten Quellordner;
|
* @param sourceFolderSupplier Supplier für den konfigurierten Quellordner;
|
||||||
* darf leeres Optional zurückliefern
|
* darf leeres Optional zurückliefern
|
||||||
* @param targetFolderSupplier Supplier für den konfigurierten Zielordner als
|
* @param targetFolderSupplier Supplier für den konfigurierten Zielordner als
|
||||||
@@ -243,6 +246,7 @@ public final class GuiBatchRunTab {
|
|||||||
BooleanSupplier savedConfigurationReadyCheck,
|
BooleanSupplier savedConfigurationReadyCheck,
|
||||||
Runnable onRunStateChanged,
|
Runnable onRunStateChanged,
|
||||||
Supplier<GuiManualFileRenamePort> manualFileRenamePortSupplier,
|
Supplier<GuiManualFileRenamePort> manualFileRenamePortSupplier,
|
||||||
|
Supplier<GuiHistoricalFileNamePort> historicalFileNamePortSupplier,
|
||||||
Supplier<Optional<Path>> sourceFolderSupplier,
|
Supplier<Optional<Path>> sourceFolderSupplier,
|
||||||
Supplier<Optional<String>> targetFolderSupplier) {
|
Supplier<Optional<String>> targetFolderSupplier) {
|
||||||
Objects.requireNonNull(launcherSupplier, "launcherSupplier must not be null");
|
Objects.requireNonNull(launcherSupplier, "launcherSupplier must not be null");
|
||||||
@@ -254,6 +258,8 @@ public final class GuiBatchRunTab {
|
|||||||
this.onRunStateChanged = Objects.requireNonNull(onRunStateChanged, "onRunStateChanged must not be null");
|
this.onRunStateChanged = Objects.requireNonNull(onRunStateChanged, "onRunStateChanged must not be null");
|
||||||
this.manualFileRenamePortSupplier = Objects.requireNonNull(
|
this.manualFileRenamePortSupplier = Objects.requireNonNull(
|
||||||
manualFileRenamePortSupplier, "manualFileRenamePortSupplier must not be null");
|
manualFileRenamePortSupplier, "manualFileRenamePortSupplier must not be null");
|
||||||
|
this.historicalFileNamePortSupplier = Objects.requireNonNull(
|
||||||
|
historicalFileNamePortSupplier, "historicalFileNamePortSupplier must not be null");
|
||||||
this.sourceFolderSupplier = Objects.requireNonNull(
|
this.sourceFolderSupplier = Objects.requireNonNull(
|
||||||
sourceFolderSupplier, "sourceFolderSupplier must not be null");
|
sourceFolderSupplier, "sourceFolderSupplier must not be null");
|
||||||
this.targetFolderSupplier = Objects.requireNonNull(
|
this.targetFolderSupplier = Objects.requireNonNull(
|
||||||
@@ -266,7 +272,8 @@ public final class GuiBatchRunTab {
|
|||||||
miniRunLauncherSupplier.get().launch(configPath, filter, observer, token),
|
miniRunLauncherSupplier.get().launch(configPath, filter, observer, token),
|
||||||
(configPath, fingerprints) ->
|
(configPath, fingerprints) ->
|
||||||
resetPortSupplier.get().reset(configPath, fingerprints),
|
resetPortSupplier.get().reset(configPath, fingerprints),
|
||||||
new CoordinatorListener());
|
new CoordinatorListener(),
|
||||||
|
historicalFileNamePortSupplier.get());
|
||||||
|
|
||||||
this.tab.setClosable(false);
|
this.tab.setClosable(false);
|
||||||
this.tab.setContent(buildContent());
|
this.tab.setContent(buildContent());
|
||||||
@@ -306,6 +313,7 @@ public final class GuiBatchRunTab {
|
|||||||
savedConfigurationReadyCheck,
|
savedConfigurationReadyCheck,
|
||||||
onRunStateChanged,
|
onRunStateChanged,
|
||||||
() -> GuiBatchRunTab::rejectingRename,
|
() -> GuiBatchRunTab::rejectingRename,
|
||||||
|
() -> (cfgPath, fp) -> Optional.empty(),
|
||||||
Optional::empty,
|
Optional::empty,
|
||||||
Optional::empty);
|
Optional::empty);
|
||||||
}
|
}
|
||||||
@@ -338,6 +346,7 @@ public final class GuiBatchRunTab {
|
|||||||
savedConfigurationReadyCheck,
|
savedConfigurationReadyCheck,
|
||||||
onRunStateChanged,
|
onRunStateChanged,
|
||||||
() -> GuiBatchRunTab::rejectingRename,
|
() -> GuiBatchRunTab::rejectingRename,
|
||||||
|
() -> (cfgPath, fp) -> Optional.empty(),
|
||||||
Optional::empty,
|
Optional::empty,
|
||||||
Optional::empty);
|
Optional::empty);
|
||||||
}
|
}
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI-interner Port zum Abfragen des historischen KI-Dateinamens einer Quelldatei.
|
||||||
|
* <p>
|
||||||
|
* Wird im Verarbeitungslauf-Tab genutzt, um für übersprungene Dokumente
|
||||||
|
* ({@link de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus#SKIPPED})
|
||||||
|
* den aus einem früheren Lauf bekannten Zieldateinamen nachzuschlagen und in der Spalte
|
||||||
|
* „Neuer Dateiname" der Ergebnistabelle anzuzeigen.
|
||||||
|
* <p>
|
||||||
|
* Die Bootstrap-Schicht liefert die konkrete Implementierung. Sie lädt die Konfiguration
|
||||||
|
* aus {@code configFilePath}, baut den zugehörigen Use-Case auf und gibt das Ergebnis
|
||||||
|
* zurück. Technische Fehler beim Laden oder Abfragen dürfen nicht als Exception propagiert
|
||||||
|
* werden; sie werden intern behandelt und als leeres {@link Optional} zurückgegeben.
|
||||||
|
* <p>
|
||||||
|
* Die Implementierung läuft auf dem Worker-Thread des {@link GuiBatchRunCoordinator}
|
||||||
|
* und darf blockieren.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface GuiHistoricalFileNamePort {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den letzten erfolgreich geschriebenen Zieldateinamen für das durch
|
||||||
|
* {@code fingerprint} identifizierte Dokument zurück, oder ein leeres
|
||||||
|
* {@link Optional}, wenn kein solcher Name verfügbar ist.
|
||||||
|
*
|
||||||
|
* @param configFilePath Pfad zur aktiven {@code .properties}-Konfigurationsdatei;
|
||||||
|
* darf nicht {@code null} sein
|
||||||
|
* @param fingerprint inhaltsbasierte Dokumentenidentität; darf nicht {@code null} sein
|
||||||
|
* @return der historische Zieldateiname, oder leer wenn nicht vorhanden
|
||||||
|
*/
|
||||||
|
Optional<String> resolveHistoricalFileName(Path configFilePath, DocumentFingerprint fingerprint);
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inbound port for resolving the historical target filename for a previously processed document.
|
||||||
|
* <p>
|
||||||
|
* Used to populate the "Neuer Dateiname" column in the GUI result table for documents that
|
||||||
|
* were skipped in the current run because they were already in a terminal state from a
|
||||||
|
* previous run. For documents that previously reached
|
||||||
|
* {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus#SUCCESS}, the last
|
||||||
|
* successfully written target filename is returned. For all other terminal states
|
||||||
|
* (e.g. {@code FAILED_FINAL}) that were never copied to the target folder, an empty
|
||||||
|
* {@link Optional} is returned.
|
||||||
|
* <p>
|
||||||
|
* <strong>Architecture boundary:</strong> Implementations of this port must not expose
|
||||||
|
* JDBC, SQLite, filesystem or HTTP types through this interface. All infrastructure details
|
||||||
|
* remain in the adapter layer.
|
||||||
|
*/
|
||||||
|
public interface ResolveHistoricalFileNameUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last successfully written target filename for the document identified
|
||||||
|
* by the given fingerprint, or an empty {@link Optional} if no such filename exists.
|
||||||
|
* <p>
|
||||||
|
* The method never throws: technical failures during the repository lookup are caught
|
||||||
|
* and result in an empty return value.
|
||||||
|
*
|
||||||
|
* @param fingerprint content-based document identity; must not be {@code null}
|
||||||
|
* @return the last target filename written for this document, or empty if the document
|
||||||
|
* never reached a successful terminal state or the lookup failed
|
||||||
|
* @throws NullPointerException if {@code fingerprint} is {@code null}
|
||||||
|
*/
|
||||||
|
Optional<String> resolveHistoricalFileName(DocumentFingerprint fingerprint);
|
||||||
|
}
|
||||||
+7
@@ -28,6 +28,13 @@
|
|||||||
* — Event and summary value types carried to the observer</li>
|
* — Event and summary value types carried to the observer</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
|
* <p>
|
||||||
|
* Query use cases (for GUI adapters):
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalFileNameUseCase}
|
||||||
|
* — Resolves the last known target filename for a document identified by its fingerprint</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
* Architecture Rule: Inbound ports are independent of implementation and contain no business logic.
|
* Architecture Rule: Inbound ports are independent of implementation and contain no business logic.
|
||||||
* They define "what can be done to the application". All dependencies point inward;
|
* They define "what can be done to the application". All dependencies point inward;
|
||||||
* adapters depend on ports, not vice versa.
|
* adapters depend on ports, not vice versa.
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalFileNameUseCase;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
|
||||||
|
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of {@link ResolveHistoricalFileNameUseCase}.
|
||||||
|
* <p>
|
||||||
|
* Queries the {@link DocumentRecordRepository} for the master record of the given fingerprint.
|
||||||
|
* If the record represents a document that previously reached a successful terminal state,
|
||||||
|
* the last known target filename ({@code lastTargetFileName}) is returned.
|
||||||
|
* <p>
|
||||||
|
* For all other terminal states (e.g. documents that finally failed without ever producing
|
||||||
|
* a target copy) or when no master record exists, an empty {@link Optional} is returned.
|
||||||
|
* Technical failures during the repository lookup are caught silently and treated as
|
||||||
|
* an absent result so that the calling GUI layer is never forced to handle exceptions
|
||||||
|
* from this query path.
|
||||||
|
*/
|
||||||
|
public class DefaultResolveHistoricalFileNameUseCase implements ResolveHistoricalFileNameUseCase {
|
||||||
|
|
||||||
|
private final DocumentRecordRepository documentRecordRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the use case with the required document master record repository.
|
||||||
|
*
|
||||||
|
* @param documentRecordRepository repository for reading document master records;
|
||||||
|
* must not be {@code null}
|
||||||
|
* @throws NullPointerException if {@code documentRecordRepository} is {@code null}
|
||||||
|
*/
|
||||||
|
public DefaultResolveHistoricalFileNameUseCase(
|
||||||
|
DocumentRecordRepository documentRecordRepository) {
|
||||||
|
this.documentRecordRepository = Objects.requireNonNull(
|
||||||
|
documentRecordRepository, "documentRecordRepository must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last successfully written target filename for the given fingerprint,
|
||||||
|
* or an empty {@link Optional} if no such filename exists.
|
||||||
|
* <p>
|
||||||
|
* The method queries the document master record. Only documents with a prior
|
||||||
|
* successful terminal state carry a non-null {@code lastTargetFileName}. For all
|
||||||
|
* other states (unknown, processable, finally failed) and on any technical lookup
|
||||||
|
* failure, an empty {@link Optional} is returned.
|
||||||
|
*
|
||||||
|
* @param fingerprint content-based document identity; must not be {@code null}
|
||||||
|
* @return the historical target filename, or empty if not available
|
||||||
|
* @throws NullPointerException if {@code fingerprint} is {@code null}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Optional<String> resolveHistoricalFileName(DocumentFingerprint fingerprint) {
|
||||||
|
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
|
||||||
|
try {
|
||||||
|
DocumentRecordLookupResult result =
|
||||||
|
documentRecordRepository.findByFingerprint(fingerprint);
|
||||||
|
if (result instanceof DocumentTerminalSuccess success) {
|
||||||
|
return Optional.ofNullable(success.record().lastTargetFileName());
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure;
|
||||||
|
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||||
|
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
||||||
|
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link DefaultResolveHistoricalFileNameUseCase}.
|
||||||
|
*/
|
||||||
|
class DefaultResolveHistoricalFileNameUseCaseTest {
|
||||||
|
|
||||||
|
private static final DocumentFingerprint FP = new DocumentFingerprint("a".repeat(64));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructor_withNullRepository_throws() {
|
||||||
|
assertThatNullPointerException()
|
||||||
|
.isThrownBy(() -> new DefaultResolveHistoricalFileNameUseCase(null))
|
||||||
|
.withMessageContaining("documentRecordRepository");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveHistoricalFileName_withNullFingerprint_throws() {
|
||||||
|
var useCase = new DefaultResolveHistoricalFileNameUseCase(stubRepo(new DocumentUnknown()));
|
||||||
|
assertThatNullPointerException()
|
||||||
|
.isThrownBy(() -> useCase.resolveHistoricalFileName(null))
|
||||||
|
.withMessageContaining("fingerprint");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveHistoricalFileName_forSuccessRecord_returnsLastTargetFileName() {
|
||||||
|
DocumentRecord record = buildRecord(ProcessingStatus.SUCCESS, "2026-01-01 - Rechnung.pdf");
|
||||||
|
var useCase = new DefaultResolveHistoricalFileNameUseCase(
|
||||||
|
stubRepo(new DocumentTerminalSuccess(record)));
|
||||||
|
|
||||||
|
Optional<String> result = useCase.resolveHistoricalFileName(FP);
|
||||||
|
assertThat(result).contains("2026-01-01 - Rechnung.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveHistoricalFileName_forSuccessRecordWithNullLastTargetFileName_returnsEmpty() {
|
||||||
|
DocumentRecord record = buildRecord(ProcessingStatus.SUCCESS, null);
|
||||||
|
var useCase = new DefaultResolveHistoricalFileNameUseCase(
|
||||||
|
stubRepo(new DocumentTerminalSuccess(record)));
|
||||||
|
|
||||||
|
assertThat(useCase.resolveHistoricalFileName(FP)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveHistoricalFileName_forUnknownDocument_returnsEmpty() {
|
||||||
|
var useCase = new DefaultResolveHistoricalFileNameUseCase(stubRepo(new DocumentUnknown()));
|
||||||
|
|
||||||
|
assertThat(useCase.resolveHistoricalFileName(FP)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveHistoricalFileName_forFinalFailure_returnsEmpty() {
|
||||||
|
DocumentRecord record = buildRecord(ProcessingStatus.FAILED_FINAL, null);
|
||||||
|
var useCase = new DefaultResolveHistoricalFileNameUseCase(
|
||||||
|
stubRepo(new DocumentTerminalFinalFailure(record)));
|
||||||
|
|
||||||
|
assertThat(useCase.resolveHistoricalFileName(FP)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveHistoricalFileName_forProcessableDocument_returnsEmpty() {
|
||||||
|
DocumentRecord record = buildRecord(ProcessingStatus.READY_FOR_AI, null);
|
||||||
|
var useCase = new DefaultResolveHistoricalFileNameUseCase(
|
||||||
|
stubRepo(new DocumentKnownProcessable(record)));
|
||||||
|
|
||||||
|
assertThat(useCase.resolveHistoricalFileName(FP)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveHistoricalFileName_forPersistenceLookupFailure_returnsEmpty() {
|
||||||
|
var useCase = new DefaultResolveHistoricalFileNameUseCase(
|
||||||
|
stubRepo(new PersistenceLookupTechnicalFailure("DB-Fehler", null)));
|
||||||
|
|
||||||
|
assertThat(useCase.resolveHistoricalFileName(FP)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveHistoricalFileName_whenRepositoryThrows_returnsEmpty() {
|
||||||
|
DocumentRecordRepository throwingRepo = new DocumentRecordRepository() {
|
||||||
|
@Override
|
||||||
|
public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fingerprint) {
|
||||||
|
throw new DocumentPersistenceException("Verbindungsfehler", null);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void create(DocumentRecord record) {}
|
||||||
|
@Override
|
||||||
|
public void update(DocumentRecord record) {}
|
||||||
|
@Override
|
||||||
|
public void deleteByFingerprint(DocumentFingerprint fingerprint) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
var useCase = new DefaultResolveHistoricalFileNameUseCase(throwingRepo);
|
||||||
|
|
||||||
|
assertThat(useCase.resolveHistoricalFileName(FP)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Hilfsmethoden
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static DocumentRecordRepository stubRepo(DocumentRecordLookupResult result) {
|
||||||
|
return new DocumentRecordRepository() {
|
||||||
|
@Override
|
||||||
|
public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fingerprint) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void create(DocumentRecord record) {}
|
||||||
|
@Override
|
||||||
|
public void update(DocumentRecord record) {}
|
||||||
|
@Override
|
||||||
|
public void deleteByFingerprint(DocumentFingerprint fingerprint) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DocumentRecord buildRecord(ProcessingStatus status, String lastTargetFileName) {
|
||||||
|
return new DocumentRecord(
|
||||||
|
FP,
|
||||||
|
new SourceDocumentLocator("quell/pfad"),
|
||||||
|
"original.pdf",
|
||||||
|
status,
|
||||||
|
FailureCounters.zero(),
|
||||||
|
null,
|
||||||
|
status == ProcessingStatus.SUCCESS ? Instant.now() : null,
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now(),
|
||||||
|
lastTargetFileName != null ? "ziel/ordner" : null,
|
||||||
|
lastTargetFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
-4
@@ -24,6 +24,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationLoadException;
|
|||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
|
||||||
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiHistoricalFileNamePort;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
|
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
|
||||||
@@ -58,6 +59,8 @@ import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
|
|||||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameRequest;
|
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameRequest;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameResult;
|
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameResult;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameUseCase;
|
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameUseCase;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalFileNameUseCase;
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResolveHistoricalFileNameUseCase;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
|
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
|
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
|
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
|
||||||
@@ -683,6 +686,7 @@ public class BootstrapRunner {
|
|||||||
de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort resetPort =
|
de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort resetPort =
|
||||||
this::resetDocumentStatusForGui;
|
this::resetDocumentStatusForGui;
|
||||||
GuiManualFileRenamePort manualRenamePort = this::performGuiManualFileRename;
|
GuiManualFileRenamePort manualRenamePort = this::performGuiManualFileRename;
|
||||||
|
GuiHistoricalFileNamePort historicalFileNamePort = this::resolveHistoricalFileNameForGui;
|
||||||
|
|
||||||
if (configPathOverride.isEmpty()) {
|
if (configPathOverride.isEmpty()) {
|
||||||
return new GuiStartupContext(
|
return new GuiStartupContext(
|
||||||
@@ -699,7 +703,8 @@ public class BootstrapRunner {
|
|||||||
batchRunLauncher,
|
batchRunLauncher,
|
||||||
miniRunLauncher,
|
miniRunLauncher,
|
||||||
resetPort,
|
resetPort,
|
||||||
manualRenamePort);
|
manualRenamePort,
|
||||||
|
historicalFileNamePort);
|
||||||
}
|
}
|
||||||
|
|
||||||
Path configPath = Paths.get(configPathOverride.get());
|
Path configPath = Paths.get(configPathOverride.get());
|
||||||
@@ -721,7 +726,8 @@ public class BootstrapRunner {
|
|||||||
batchRunLauncher,
|
batchRunLauncher,
|
||||||
miniRunLauncher,
|
miniRunLauncher,
|
||||||
resetPort,
|
resetPort,
|
||||||
manualRenamePort);
|
manualRenamePort,
|
||||||
|
historicalFileNamePort);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
||||||
@@ -730,7 +736,7 @@ public class BootstrapRunner {
|
|||||||
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);
|
miniRunLauncher, resetPort, manualRenamePort, historicalFileNamePort);
|
||||||
} 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);
|
||||||
@@ -748,7 +754,8 @@ public class BootstrapRunner {
|
|||||||
batchRunLauncher,
|
batchRunLauncher,
|
||||||
miniRunLauncher,
|
miniRunLauncher,
|
||||||
resetPort,
|
resetPort,
|
||||||
manualRenamePort);
|
manualRenamePort,
|
||||||
|
historicalFileNamePort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,6 +1069,49 @@ public class BootstrapRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the historical AI-proposed target filename for a document identified by
|
||||||
|
* {@code fingerprint}, using the configuration at {@code configFilePath}.
|
||||||
|
* <p>
|
||||||
|
* Loads the configuration, initialises the schema and delegates to
|
||||||
|
* {@link ResolveHistoricalFileNameUseCase}. Technical errors during loading or querying
|
||||||
|
* are caught and returned as an empty {@link Optional}; they are never propagated to the
|
||||||
|
* caller.
|
||||||
|
* <p>
|
||||||
|
* Runs on the GUI worker thread. Blocking I/O is therefore acceptable.
|
||||||
|
*
|
||||||
|
* @param configFilePath path to the active {@code .properties} file; must not be {@code null}
|
||||||
|
* @param fingerprint content-based document identity; must not be {@code null}
|
||||||
|
* @return the last successfully written target filename, or empty if not available
|
||||||
|
*/
|
||||||
|
Optional<String> resolveHistoricalFileNameForGui(
|
||||||
|
Path configFilePath,
|
||||||
|
DocumentFingerprint fingerprint) {
|
||||||
|
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||||
|
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
|
||||||
|
|
||||||
|
if (!Files.exists(configFilePath)) {
|
||||||
|
LOG.debug("Historischer Dateiname: Konfigurationsdatei nicht gefunden: {}", configFilePath);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
migrateConfigurationIfNeeded(configFilePath);
|
||||||
|
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||||
|
initializeSchema(config);
|
||||||
|
String jdbcUrl = buildJdbcUrl(config);
|
||||||
|
DocumentRecordRepository documentRecordRepository =
|
||||||
|
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||||
|
ResolveHistoricalFileNameUseCase useCase =
|
||||||
|
new DefaultResolveHistoricalFileNameUseCase(documentRecordRepository);
|
||||||
|
return useCase.resolveHistoricalFileName(fingerprint);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.debug("Historischer Dateiname konnte nicht abgefragt werden für {}: {}",
|
||||||
|
fingerprint.sha256Hex(), e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a {@link ResetDocumentStatusResult} where every requested fingerprint is
|
* Builds a {@link ResetDocumentStatusResult} where every requested fingerprint is
|
||||||
* recorded as a failure with the given error message.
|
* recorded as a failure with the given error message.
|
||||||
|
|||||||
Reference in New Issue
Block a user