Fix #30: Detailbereich bei SKIPPED-Zeilen mit historischen Informationen befüllen
- Teile DocumentCompletionStatus.SKIPPED in SKIPPED_ALREADY_PROCESSED und SKIPPED_FINAL_FAILURE auf, um den Skip-Grund unterscheidbar zu machen - Führe neuen Typ HistoricalDocumentContext ein (lastTargetFileName, lastSuccessInstant, lastFailureInstant, wasEverSuccessful) - Führe ResolveHistoricalDocumentContextUseCase und DefaultResolveHistoricalDocumentContextUseCase ein - Ersetze GuiHistoricalFileNamePort durch GuiHistoricalDocumentContextPort - Lade historischen Kontext für übersprungene Zeilen im Coordinator-Worker-Thread - Zeige im Detailbereich je nach Skip-Grund: SKIPPED_ALREADY_PROCESSED: "Bereits erfolgreich verarbeitet am [Datum]. Zieldatei: [Name]." SKIPPED_FINAL_FAILURE: "Endgültig fehlgeschlagen am [Datum]. Erneute Verarbeitung nur nach Reset möglich." - Passe alle betroffenen Tests an Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+14
-6
@@ -5,10 +5,10 @@ package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
* {@link BatchRunProgressObserver#onDocumentCompleted(DocumentCompletionEvent)}
|
||||
* for one processed candidate.
|
||||
* <p>
|
||||
* This enum collapses the finer-grained internal processing status into the four
|
||||
* This enum collapses the finer-grained internal processing status into five
|
||||
* buckets that an observer (e.g. a GUI progress view) needs to distinguish:
|
||||
* successful completion, retryable failure, permanent failure, and an explicit
|
||||
* skip.
|
||||
* successful completion, retryable failure, permanent failure, and two explicit
|
||||
* skip variants.
|
||||
* <p>
|
||||
* This classification is purely an observability concern — persistence,
|
||||
* retry decisions, and all other processing rules continue to work against the
|
||||
@@ -36,8 +36,16 @@ public enum DocumentCompletionStatus {
|
||||
FAILED_PERMANENT,
|
||||
|
||||
/**
|
||||
* The candidate was skipped because it was already in a terminal state (either
|
||||
* previously successful or previously finally failed).
|
||||
* Der Kandidat wurde übersprungen, weil er in einem früheren Lauf bereits
|
||||
* erfolgreich verarbeitet wurde. Der Gesamtstatus im Stammsatz lautet
|
||||
* {@code SUCCESS}.
|
||||
*/
|
||||
SKIPPED
|
||||
SKIPPED_ALREADY_PROCESSED,
|
||||
|
||||
/**
|
||||
* Der Kandidat wurde übersprungen, weil er in einem früheren Lauf bereits
|
||||
* endgültig fehlgeschlagen ist. Der Gesamtstatus im Stammsatz lautet
|
||||
* {@code FAILED_FINAL}.
|
||||
*/
|
||||
SKIPPED_FINAL_FAILURE
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Historischer Kontext eines Dokuments, das in einem früheren Lauf bereits terminal
|
||||
* abgeschlossen wurde.
|
||||
* <p>
|
||||
* Wird genutzt, um im GUI-Detailbereich bei übersprungenen Dokumenten Informationen
|
||||
* darüber anzuzeigen, wann und mit welchem Ergebnis die Datei früher verarbeitet wurde.
|
||||
* <p>
|
||||
* Für Dokumente mit früherem Erfolgsstatus ({@code wasEverSuccessful == true}) sind
|
||||
* {@code lastTargetFileName} und {@code lastSuccessInstant} belegt.
|
||||
* Für Dokumente, die endgültig fehlgeschlagen sind, ist {@code lastFailureInstant} belegt.
|
||||
*
|
||||
* @param lastTargetFileName letzter erfolgreich geschriebener Zieldateiname; leer wenn
|
||||
* das Dokument nie erfolgreich kopiert wurde
|
||||
* @param lastSuccessInstant Zeitpunkt der letzten erfolgreichen Verarbeitung; leer wenn
|
||||
* das Dokument nie erfolgreich verarbeitet wurde
|
||||
* @param lastFailureInstant Zeitpunkt des letzten Fehlschlags; leer wenn noch kein
|
||||
* Fehlschlag aufgetreten ist
|
||||
* @param wasEverSuccessful {@code true} wenn das Dokument mindestens einmal erfolgreich
|
||||
* verarbeitet wurde
|
||||
*/
|
||||
public record HistoricalDocumentContext(
|
||||
Optional<String> lastTargetFileName,
|
||||
Optional<Instant> lastSuccessInstant,
|
||||
Optional<Instant> lastFailureInstant,
|
||||
boolean wasEverSuccessful) {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Normalisierung von {@code null}-Werten.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code lastTargetFileName},
|
||||
* {@code lastSuccessInstant} oder
|
||||
* {@code lastFailureInstant} {@code null} sind
|
||||
*/
|
||||
public HistoricalDocumentContext {
|
||||
lastTargetFileName = lastTargetFileName == null ? Optional.empty() : lastTargetFileName;
|
||||
lastSuccessInstant = lastSuccessInstant == null ? Optional.empty() : lastSuccessInstant;
|
||||
lastFailureInstant = lastFailureInstant == null ? Optional.empty() : lastFailureInstant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Kontext für ein erfolgreich verarbeitetes Dokument.
|
||||
*
|
||||
* @param lastTargetFileName letzter Zieldateiname; darf {@code null} sein
|
||||
* @param lastSuccessInstant Zeitpunkt des Erfolgs; darf {@code null} sein
|
||||
* @return neuer Kontext mit {@code wasEverSuccessful == true}
|
||||
*/
|
||||
public static HistoricalDocumentContext ofSuccess(
|
||||
String lastTargetFileName, Instant lastSuccessInstant) {
|
||||
return new HistoricalDocumentContext(
|
||||
Optional.ofNullable(lastTargetFileName),
|
||||
Optional.ofNullable(lastSuccessInstant),
|
||||
Optional.empty(),
|
||||
true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Kontext für ein endgültig fehlgeschlagenes Dokument.
|
||||
*
|
||||
* @param lastFailureInstant Zeitpunkt des letzten Fehlschlags; darf {@code null} sein
|
||||
* @return neuer Kontext mit {@code wasEverSuccessful == false}
|
||||
*/
|
||||
public static HistoricalDocumentContext ofFinalFailure(Instant lastFailureInstant) {
|
||||
return new HistoricalDocumentContext(
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.ofNullable(lastFailureInstant),
|
||||
false);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Inbound-Port zum Abfragen des historischen Verarbeitungskontexts eines Dokuments.
|
||||
* <p>
|
||||
* Wird im GUI-Verarbeitungslauf-Tab eingesetzt, um für übersprungene Dokumente
|
||||
* anzuzeigen, wann und mit welchem Ergebnis die Datei in einem früheren Lauf
|
||||
* verarbeitet wurde.
|
||||
* <p>
|
||||
* Für Dokumente mit früherem Erfolgsstatus enthält der zurückgegebene Kontext den
|
||||
* letzten Zieldateinamen und den Erfolgszeitpunkt. Für endgültig fehlgeschlagene
|
||||
* Dokumente ist der letzte Fehlzeitpunkt belegt.
|
||||
* <p>
|
||||
* <strong>Architekturgrenzen:</strong> Implementierungen dieses Ports dürfen keine
|
||||
* JDBC-, SQLite-, Dateisystem- oder HTTP-Typen nach außen exponieren. Alle
|
||||
* Infrastrukturdetails verbleiben in der Adapter-Schicht.
|
||||
*/
|
||||
public interface ResolveHistoricalDocumentContextUseCase {
|
||||
|
||||
/**
|
||||
* Gibt den historischen Verarbeitungskontext für das durch den Fingerprint
|
||||
* identifizierte Dokument zurück.
|
||||
* <p>
|
||||
* Liefert einen gefüllten Kontext, wenn das Dokument einen früheren terminalen
|
||||
* Abschluss (Erfolg oder endgültiger Fehlschlag) hat. Gibt ein leeres
|
||||
* {@link Optional} zurück, wenn kein passender Stammsatz vorhanden ist oder
|
||||
* die Abfrage technisch fehlschlägt.
|
||||
* <p>
|
||||
* Wirft keine geprüften Ausnahmen: technische Abfragefehler werden intern
|
||||
* abgefangen und als leeres Ergebnis zurückgegeben.
|
||||
*
|
||||
* @param fingerprint inhaltsbasierte Dokumentenidentität; darf nicht {@code null} sein
|
||||
* @return historischer Kontext des Dokuments, oder leer wenn nicht verfügbar
|
||||
* @throws NullPointerException wenn {@code fingerprint} {@code null} ist
|
||||
*/
|
||||
Optional<HistoricalDocumentContext> resolveHistoricalDocumentContext(
|
||||
DocumentFingerprint fingerprint);
|
||||
}
|
||||
+3
-2
@@ -13,8 +13,9 @@ package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
* @param failedCount number of candidates that completed with either
|
||||
* {@link DocumentCompletionStatus#FAILED_RETRYABLE} or
|
||||
* {@link DocumentCompletionStatus#FAILED_PERMANENT}; must be ≥ 0
|
||||
* @param skippedCount number of candidates that completed with
|
||||
* {@link DocumentCompletionStatus#SKIPPED}; must be ≥ 0
|
||||
* @param skippedCount number of candidates that completed with either
|
||||
* {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED} or
|
||||
* {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}; must be ≥ 0
|
||||
*/
|
||||
public record RunSummary(int successCount, int failedCount, int skippedCount) {
|
||||
|
||||
|
||||
+5
-1
@@ -815,7 +815,11 @@ public class DocumentProcessingCoordinator {
|
||||
|
||||
logger.debug("Skip attempt #{} persisted for '{}' with status {}.",
|
||||
attemptNumber, candidate.uniqueIdentifier(), skipStatus);
|
||||
publishCompletion(candidate, fingerprint, DocumentCompletionStatus.SKIPPED,
|
||||
DocumentCompletionStatus completionStatus =
|
||||
skipStatus == ProcessingStatus.SKIPPED_ALREADY_PROCESSED
|
||||
? DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED
|
||||
: DocumentCompletionStatus.SKIPPED_FINAL_FAILURE;
|
||||
publishCompletion(candidate, fingerprint, completionStatus,
|
||||
null, null, null, null, attemptStart, now);
|
||||
return true;
|
||||
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ final class CountingCompletionObserver implements Consumer<DocumentCompletionEve
|
||||
switch (event.status()) {
|
||||
case SUCCESS -> successCount++;
|
||||
case FAILED_RETRYABLE, FAILED_PERMANENT -> failedCount++;
|
||||
case SKIPPED -> skippedCount++;
|
||||
case SKIPPED_ALREADY_PROCESSED, SKIPPED_FINAL_FAILURE -> skippedCount++;
|
||||
default -> {
|
||||
// Defensive — new status values would be a programming error.
|
||||
throw new IllegalStateException(
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalDocumentContextUseCase;
|
||||
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.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Standardimplementierung von {@link ResolveHistoricalDocumentContextUseCase}.
|
||||
* <p>
|
||||
* Fragt den {@link DocumentRecordRepository} nach dem Stammsatz des angegebenen
|
||||
* Fingerprints ab. Abhängig vom terminalen Zustand des Dokuments wird ein passender
|
||||
* {@link HistoricalDocumentContext} zurückgegeben:
|
||||
* <ul>
|
||||
* <li>Bei früherem Erfolgsstatus: Zieldateiname und Erfolgszeitpunkt aus dem
|
||||
* Stammsatz ({@code lastTargetFileName}, {@code lastSuccessInstant}).</li>
|
||||
* <li>Bei endgültigem Fehlschlag: Fehlzeitpunkt aus dem Stammsatz
|
||||
* ({@code lastFailureInstant}).</li>
|
||||
* <li>In allen anderen Fällen (unbekannt, verarbeitbar) sowie bei technischen
|
||||
* Abfragefehlern: leeres {@link Optional}.</li>
|
||||
* </ul>
|
||||
* Technische Fehler bei der Repository-Abfrage werden intern abgefangen; der Aufrufer
|
||||
* erhält stets ein leeres Ergebnis statt einer Ausnahme.
|
||||
*/
|
||||
public class DefaultResolveHistoricalDocumentContextUseCase
|
||||
implements ResolveHistoricalDocumentContextUseCase {
|
||||
|
||||
private final DocumentRecordRepository documentRecordRepository;
|
||||
|
||||
/**
|
||||
* Erstellt den Use Case mit dem erforderlichen Dokument-Stammsatz-Repository.
|
||||
*
|
||||
* @param documentRecordRepository Repository zum Lesen von Dokument-Stammsätzen;
|
||||
* darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code documentRecordRepository} {@code null} ist
|
||||
*/
|
||||
public DefaultResolveHistoricalDocumentContextUseCase(
|
||||
DocumentRecordRepository documentRecordRepository) {
|
||||
this.documentRecordRepository = Objects.requireNonNull(
|
||||
documentRecordRepository, "documentRecordRepository must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den historischen Verarbeitungskontext für das durch den Fingerprint
|
||||
* identifizierte Dokument zurück.
|
||||
* <p>
|
||||
* Für Dokumente mit früherem Erfolgsstatus enthält der Kontext Zieldateiname und
|
||||
* Erfolgszeitpunkt. Für endgültig fehlgeschlagene Dokumente ist der Fehlzeitpunkt
|
||||
* belegt. Für alle anderen Zustände oder bei Abfragefehlern wird ein leeres
|
||||
* {@link Optional} zurückgegeben.
|
||||
*
|
||||
* @param fingerprint inhaltsbasierte Dokumentenidentität; darf nicht {@code null} sein
|
||||
* @return historischer Kontext des Dokuments, oder leer wenn nicht verfügbar
|
||||
* @throws NullPointerException wenn {@code fingerprint} {@code null} ist
|
||||
*/
|
||||
@Override
|
||||
public Optional<HistoricalDocumentContext> resolveHistoricalDocumentContext(
|
||||
DocumentFingerprint fingerprint) {
|
||||
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
|
||||
try {
|
||||
DocumentRecordLookupResult result =
|
||||
documentRecordRepository.findByFingerprint(fingerprint);
|
||||
if (result instanceof DocumentTerminalSuccess success) {
|
||||
return Optional.of(HistoricalDocumentContext.ofSuccess(
|
||||
success.record().lastTargetFileName(),
|
||||
success.record().lastSuccessInstant()));
|
||||
}
|
||||
if (result instanceof DocumentTerminalFinalFailure failure) {
|
||||
return Optional.of(HistoricalDocumentContext.ofFinalFailure(
|
||||
failure.record().lastFailureInstant()));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user