Fix #31: Manuelle Dateinamen-Eingabe für nicht verarbeitete Dateien
Nicht-erfolgreiche Zeilen (FAILED, FAILED_RETRYABLE, SKIPPED_FINAL_FAILURE) können im Detailbereich des Verarbeitungslauf-Tabs nun einen manuellen Zieldateinamen erhalten. Beim Bestätigen wird die Quelldatei mit dem benutzerdefinierten Namen ins Zielverzeichnis kopiert und der Stammsatz atomar auf SUCCESS gehoben. Neuer Inbound-Port ManualFileCopyUseCase mit sealed Result-Hierarchie, Default-Implementierung mit Best-Effort-Rollback bei Persistenzfehler sowie GUI-Brücke GuiManualFileCopyPort. Die GUI entscheidet anhand des Status zwischen Umbenennen (SUCCESS, SKIPPED_ALREADY_PROCESSED) und Kopieren (FAILED_*, SKIPPED_FINAL_FAILURE). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+109
-1
@@ -25,6 +25,7 @@ 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.GuiBatchRunLauncher;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiHistoricalDocumentContextPort;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileCopyPort;
|
||||
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.GuiConfigurationEditorStateFactory;
|
||||
@@ -56,6 +57,9 @@ import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfigurat
|
||||
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyRequest;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyUseCase;
|
||||
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.ManualFileRenameUseCase;
|
||||
@@ -84,6 +88,7 @@ import de.gecheckt.pdf.umbenenner.application.service.AiNamingService;
|
||||
import de.gecheckt.pdf.umbenenner.application.service.AiResponseValidator;
|
||||
import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultManualFileCopyUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultManualFileRenameUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResetDocumentStatusUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
|
||||
@@ -687,6 +692,7 @@ public class BootstrapRunner {
|
||||
de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort resetPort =
|
||||
this::resetDocumentStatusForGui;
|
||||
GuiManualFileRenamePort manualRenamePort = this::performGuiManualFileRename;
|
||||
GuiManualFileCopyPort manualCopyPort = this::performGuiManualFileCopy;
|
||||
GuiHistoricalDocumentContextPort historicalDocumentContextPort = this::resolveHistoricalDocumentContextForGui;
|
||||
|
||||
if (configPathOverride.isEmpty()) {
|
||||
@@ -705,6 +711,7 @@ public class BootstrapRunner {
|
||||
miniRunLauncher,
|
||||
resetPort,
|
||||
manualRenamePort,
|
||||
manualCopyPort,
|
||||
historicalDocumentContextPort);
|
||||
}
|
||||
|
||||
@@ -728,6 +735,7 @@ public class BootstrapRunner {
|
||||
miniRunLauncher,
|
||||
resetPort,
|
||||
manualRenamePort,
|
||||
manualCopyPort,
|
||||
historicalDocumentContextPort);
|
||||
}
|
||||
|
||||
@@ -737,7 +745,8 @@ public class BootstrapRunner {
|
||||
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
|
||||
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
||||
miniRunLauncher, resetPort, manualRenamePort, historicalDocumentContextPort);
|
||||
miniRunLauncher, resetPort, manualRenamePort, manualCopyPort,
|
||||
historicalDocumentContextPort);
|
||||
} catch (GuiConfigurationLoadException e) {
|
||||
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
|
||||
e.getMessage(), e);
|
||||
@@ -756,6 +765,7 @@ public class BootstrapRunner {
|
||||
miniRunLauncher,
|
||||
resetPort,
|
||||
manualRenamePort,
|
||||
manualCopyPort,
|
||||
historicalDocumentContextPort);
|
||||
}
|
||||
}
|
||||
@@ -1007,6 +1017,40 @@ public class BootstrapRunner {
|
||||
processingLogger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen vollständig verdrahteten {@link ManualFileCopyUseCase} für den
|
||||
* gegebenen Startkonfigurations-Stand.
|
||||
* <p>
|
||||
* Teilt die Wiring-Konventionen mit dem Batch-Pfad: SQLite-URL-Aufbau, Adapter-Instanzen
|
||||
* und Logger-Konfiguration werden nach dem gleichen Muster erzeugt.
|
||||
*
|
||||
* @param startConfig die validierte Startkonfiguration; darf nicht null sein
|
||||
* @return ein einsatzbereiter Use-Case; nie null
|
||||
*/
|
||||
private ManualFileCopyUseCase buildProductionManualFileCopyUseCase(
|
||||
StartConfiguration startConfig) {
|
||||
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||
DocumentRecordRepository documentRecordRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
TargetFolderPort targetFolderPort =
|
||||
new FilesystemTargetFolderAdapter(startConfig.targetFolder());
|
||||
TargetFileCopyPort targetFileCopyPort =
|
||||
new FilesystemTargetFileCopyAdapter(startConfig.targetFolder());
|
||||
ClockPort clockPort = new SystemClockAdapter();
|
||||
AiContentSensitivity aiContentSensitivity =
|
||||
resolveAiContentSensitivity(startConfig.logAiSensitive());
|
||||
ProcessingLogger processingLogger = new Log4jProcessingLogger(
|
||||
DefaultManualFileCopyUseCase.class, aiContentSensitivity);
|
||||
return new DefaultManualFileCopyUseCase(
|
||||
documentRecordRepository,
|
||||
targetFolderPort,
|
||||
targetFileCopyPort,
|
||||
unitOfWorkPort,
|
||||
clockPort,
|
||||
processingLogger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine manuelle Umbenennung einer Zieldatei durch, ausgelöst von der GUI.
|
||||
* <p>
|
||||
@@ -1070,6 +1114,70 @@ public class BootstrapRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine manuelle Kopie der Quelldatei eines bislang nicht erfolgreich
|
||||
* verarbeiteten Dokuments mit benutzerdefiniertem Zieldateinamen ins Zielverzeichnis
|
||||
* durch, ausgelöst von der GUI.
|
||||
* <p>
|
||||
* Lädt und validiert die Konfiguration aus {@code configFilePath}, baut den Use-Case
|
||||
* auf und delegiert die Operation. Alle Fehler beim Laden oder Validieren der
|
||||
* Konfiguration werden als strukturiertes {@link ManualFileCopyResult} zurückgegeben.
|
||||
*
|
||||
* @param configFilePath Pfad zur {@code .properties}-Datei; muss existieren
|
||||
* @param request die Kopieranfrage; darf nicht null sein
|
||||
* @return das Ergebnis der Kopieroperation; nie null
|
||||
*/
|
||||
ManualFileCopyResult performGuiManualFileCopy(
|
||||
Path configFilePath,
|
||||
ManualFileCopyRequest request) {
|
||||
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||
Objects.requireNonNull(request, "request must not be null");
|
||||
LOG.info("GUI-Dateikopie: Anfrage für Fingerprint={}, Zielname={}.",
|
||||
request.fingerprint().sha256Hex(), request.desiredBaseFileName());
|
||||
|
||||
if (!Files.exists(configFilePath)) {
|
||||
String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath;
|
||||
LOG.error("GUI-Dateikopie: {}", msg);
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileCopyFileSystemFailure(msg);
|
||||
}
|
||||
|
||||
try {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
ManualFileCopyUseCase useCase = buildProductionManualFileCopyUseCase(config);
|
||||
ManualFileCopyResult result = useCase.copy(request);
|
||||
LOG.info("GUI-Dateikopie abgeschlossen: Ergebnis={}.", result.getClass().getSimpleName());
|
||||
return result;
|
||||
} catch (ConfigurationLoadingException e) {
|
||||
LOG.error("GUI-Dateikopie: Konfiguration konnte nicht geladen werden: {}",
|
||||
e.getMessage(), e);
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileCopyPersistenceFailure(
|
||||
"Konfiguration konnte nicht geladen werden: " + e.getMessage());
|
||||
} catch (InvalidStartConfigurationException e) {
|
||||
LOG.error("GUI-Dateikopie: Konfiguration ist nicht lauffähig: {}", e.getMessage());
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileCopyPersistenceFailure(
|
||||
"Die Konfiguration ist nicht lauffähig: " + e.getMessage());
|
||||
} catch (DocumentPersistenceException e) {
|
||||
LOG.error("GUI-Dateikopie: SQLite-Initialisierung fehlgeschlagen: {}",
|
||||
e.getMessage(), e);
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileCopyPersistenceFailure(
|
||||
"SQLite-Datenbank konnte nicht vorbereitet werden: " + e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("GUI-Dateikopie: Unerwarteter Fehler: {}", e.getMessage(), e);
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileCopyFileSystemFailure(
|
||||
"Unerwarteter Fehler: "
|
||||
+ (e.getMessage() == null
|
||||
? e.getClass().getSimpleName()
|
||||
: e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the historical processing context for the document identified by
|
||||
* {@code fingerprint}, using the configuration at {@code configFilePath}.
|
||||
|
||||
Reference in New Issue
Block a user