V2.9: Integrierte PDF-Vorschau und editierbarer Dateiname im Verarbeitungslauf
Neu im Tab "Verarbeitungslauf": - Integrierte PDF-Vorschau der Quelldatei mit Lazy Rendering (Seite 1 sofort, weitere Seiten on-demand), Cache pro Selektion, "latest preview request wins" - Editierbarer KI-Dateinamenvorschlag mit Live-Validierung, Dirty-State-Dialog bei Zeilen-/Tabwechsel, Schließen und Laufstart, atomare FS+DB-Transaktion inkl. Rollback und Fingerprint-basierter Konfliktauflösung Architektur: - Neuer Application-Use-Case ManualFileRenameUseCase und Outbound-Port TargetFileRenamePort mit Filesystem-Adapter - Neuer GuiManualFileRenamePort, verdrahtet im Bootstrap - GuiBatchRunResultRow um correctedFileName erweitert - GuiBatchRunTab auf SplitPane-Layout (60/40) umgebaut, Detail-Panel mit KI-Begründung, FileNameEditorPane und PdfPreviewPane - Spike-Code (PdfViewerSpike) entfernt, produktive Implementierung ersetzt Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn das zu umbennende Dokument in der Persistenz nicht gefunden wurde.
|
||||
* <p>
|
||||
* Gibt an, dass kein Dokument-Stammsatz mit dem angegebenen Fingerprint existiert.
|
||||
* Dies kann eintreten, wenn der Fingerprint ungültig ist oder der Datensatz
|
||||
* zwischenzeitlich gelöscht wurde.
|
||||
*
|
||||
* @param reason menschenlesbare Begründung, warum das Dokument nicht gefunden wurde;
|
||||
* nie null
|
||||
*/
|
||||
public record ManualFileRenameDocumentNotFound(String reason) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code reason} null ist
|
||||
*/
|
||||
public ManualFileRenameDocumentNotFound {
|
||||
Objects.requireNonNull(reason, "reason must not be null");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn die Umbenennung der Zieldatei im Dateisystem fehlgeschlagen ist.
|
||||
* <p>
|
||||
* Gibt an, dass ein technischer Fehler beim Dateisystemzugriff aufgetreten ist,
|
||||
* z. B. fehlende Schreibrechte, gesperrte Datei durch einen anderen Prozess oder
|
||||
* ein nicht erreichbares Netzlaufwerk. Ebenfalls verwendet, wenn der Zielordner-Port
|
||||
* einen technischen Fehler meldet.
|
||||
* <p>
|
||||
* Gemäß dem Alles-oder-Nichts-Prinzip wird in diesem Fall die Persistenz nicht
|
||||
* aktualisiert.
|
||||
*
|
||||
* @param message menschenlesbare Beschreibung des aufgetretenen Fehlers; nie null
|
||||
*/
|
||||
public record ManualFileRenameFileSystemFailure(String message) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public ManualFileRenameFileSystemFailure {
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn das Dokument sich in einem ungültigen Zustand für eine manuelle
|
||||
* Umbenennung befindet.
|
||||
* <p>
|
||||
* Eine Umbenennung ist nur möglich, wenn das Dokument den Status
|
||||
* {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus#SUCCESS} hat und
|
||||
* ein gültiger {@code lastTargetFileName} sowie {@code lastTargetPath} vorhanden sind.
|
||||
* Dieses Ergebnis wird zurückgegeben, wenn eine dieser Voraussetzungen nicht erfüllt ist,
|
||||
* z. B.:
|
||||
* <ul>
|
||||
* <li>der Status ist nicht {@code SUCCESS} (z. B. {@code FAILED_FINAL}), oder</li>
|
||||
* <li>{@code lastTargetFileName} oder {@code lastTargetPath} ist {@code null}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param reason menschenlesbare Begründung für den ungültigen Zustand; nie null
|
||||
*/
|
||||
public record ManualFileRenameInvalidState(String reason) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code reason} null ist
|
||||
*/
|
||||
public ManualFileRenameInvalidState {
|
||||
Objects.requireNonNull(reason, "reason must not be null");
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn keine Umbenennung notwendig ist, weil die Zieldatei mit dem
|
||||
* gewünschten Namen bereits vorhanden ist und denselben Inhalt hat (gleicher Fingerprint).
|
||||
* <p>
|
||||
* Dieses Ergebnis tritt auf, wenn:
|
||||
* <ul>
|
||||
* <li>der gewünschte neue Dateiname bereits dem aktuellen {@code lastTargetFileName}
|
||||
* entspricht, oder</li>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort#resolveUniqueFilename}
|
||||
* einen {@link de.gecheckt.pdf.umbenenner.application.port.out.ExistingIdenticalTargetFile}
|
||||
* zurückliefert (identischer Fingerprint im Zielordner).</li>
|
||||
* </ul>
|
||||
* Weder Dateisystem noch Persistenz werden in diesem Fall verändert.
|
||||
*
|
||||
* @param existingFileName der Dateiname (ohne Pfad) der bereits vorhandenen identischen
|
||||
* Datei; nie null
|
||||
*/
|
||||
public record ManualFileRenameNoOpIdenticalTarget(String existingFileName) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code existingFileName} null ist
|
||||
*/
|
||||
public ManualFileRenameNoOpIdenticalTarget {
|
||||
Objects.requireNonNull(existingFileName, "existingFileName must not be null");
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn die Persistenzaktualisierung nach erfolgreicher Dateisystem-Umbenennung
|
||||
* fehlgeschlagen ist.
|
||||
* <p>
|
||||
* Gibt an, dass die Zieldatei im Dateisystem erfolgreich umbenannt werden konnte, jedoch
|
||||
* die anschließende Aktualisierung des Dokument-Stammsatzes in der Persistenz fehlgeschlagen
|
||||
* ist. Der Use-Case versucht in diesem Fall, die Dateisystem-Umbenennung rückgängig zu
|
||||
* machen (Best-Effort-Rollback).
|
||||
* <p>
|
||||
* Schlägt auch der Rollback fehl, wird dies auf ERROR-Ebene protokolliert. In jedem Fall
|
||||
* bleibt dieses Ergebnis die Rückgabe, sodass der Aufrufer den Benutzer informieren kann.
|
||||
*
|
||||
* @param message menschenlesbare Beschreibung des Persistenzfehlers; nie null
|
||||
*/
|
||||
public record ManualFileRenamePersistenceFailure(String message) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public ManualFileRenamePersistenceFailure {
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Anfrage an den {@link ManualFileRenameUseCase} zum manuellen Umbenennen einer Zieldatei.
|
||||
* <p>
|
||||
* Der Benutzer gibt im GUI ausschließlich den Basistitel ohne {@code .pdf}-Endung an.
|
||||
* Der Use-Case hängt die Erweiterung selbst an.
|
||||
*
|
||||
* @param fingerprint Inhalts-Fingerabdruck des Dokuments, das umbenannt werden soll;
|
||||
* nie null
|
||||
* @param desiredBaseFileName gewünschter Basisdateiname ohne {@code .pdf}-Endung;
|
||||
* nie null; darf nicht leer oder nur aus Leerzeichen bestehen
|
||||
*/
|
||||
public record ManualFileRenameRequest(
|
||||
DocumentFingerprint fingerprint,
|
||||
String desiredBaseFileName) {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung der Pflichtfelder.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code fingerprint} oder
|
||||
* {@code desiredBaseFileName} null sind
|
||||
* @throws IllegalArgumentException wenn {@code desiredBaseFileName} leer ist
|
||||
*/
|
||||
public ManualFileRenameRequest {
|
||||
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
|
||||
Objects.requireNonNull(desiredBaseFileName, "desiredBaseFileName must not be null");
|
||||
if (desiredBaseFileName.isBlank()) {
|
||||
throw new IllegalArgumentException("desiredBaseFileName must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
/**
|
||||
* Versiegeltes Ergebnis-Interface für eine manuelle Dateiumbenennung via
|
||||
* {@link ManualFileRenameUseCase}.
|
||||
* <p>
|
||||
* Mögliche Ergebnisse:
|
||||
* <ul>
|
||||
* <li>{@link ManualFileRenameSuccess} – Umbenennung war erfolgreich (ggf. mit Suffix).</li>
|
||||
* <li>{@link ManualFileRenameNoOpIdenticalTarget} – keine Aktion erforderlich, da die
|
||||
* Zieldatei bereits denselben Inhalt hat.</li>
|
||||
* <li>{@link ManualFileRenameDocumentNotFound} – das Dokument wurde in der Persistenz
|
||||
* nicht gefunden.</li>
|
||||
* <li>{@link ManualFileRenameInvalidState} – das Dokument befindet sich in einem
|
||||
* ungültigen Zustand für eine Umbenennung.</li>
|
||||
* <li>{@link ManualFileRenameSourceFileMissing} – die bisherige Zieldatei existiert
|
||||
* im Zielordner nicht mehr.</li>
|
||||
* <li>{@link ManualFileRenameFileSystemFailure} – ein technischer Dateisystemfehler
|
||||
* ist aufgetreten.</li>
|
||||
* <li>{@link ManualFileRenamePersistenceFailure} – die Persistenzaktualisierung ist
|
||||
* fehlgeschlagen (Dateisystem ggf. zurückgerollt).</li>
|
||||
* </ul>
|
||||
*/
|
||||
public sealed interface ManualFileRenameResult
|
||||
permits ManualFileRenameSuccess,
|
||||
ManualFileRenameNoOpIdenticalTarget,
|
||||
ManualFileRenameDocumentNotFound,
|
||||
ManualFileRenameInvalidState,
|
||||
ManualFileRenameSourceFileMissing,
|
||||
ManualFileRenameFileSystemFailure,
|
||||
ManualFileRenamePersistenceFailure {
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn die bisherige Zieldatei im Zielordner nicht mehr vorhanden ist.
|
||||
* <p>
|
||||
* Gibt an, dass die in der Persistenz gespeicherte Zieldatei ({@code lastTargetFileName})
|
||||
* zum Zeitpunkt des Umbenennungsversuchs nicht mehr im Zielordner existiert. Dies kann
|
||||
* eintreten, wenn die Datei zwischenzeitlich von einem externen Prozess gelöscht oder
|
||||
* verschoben wurde.
|
||||
* <p>
|
||||
* Gemäß dem Alles-oder-Nichts-Prinzip wird in diesem Fall die Persistenz nicht
|
||||
* aktualisiert.
|
||||
*
|
||||
* @param expectedFileName der Dateiname (ohne Pfad), der im Zielordner erwartet wurde,
|
||||
* aber nicht gefunden wurde; nie null
|
||||
*/
|
||||
public record ManualFileRenameSourceFileMissing(String expectedFileName) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code expectedFileName} null ist
|
||||
*/
|
||||
public ManualFileRenameSourceFileMissing {
|
||||
Objects.requireNonNull(expectedFileName, "expectedFileName must not be null");
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis einer erfolgreich abgeschlossenen manuellen Dateiumbenennung.
|
||||
* <p>
|
||||
* Gibt an, dass die Zieldatei im Dateisystem erfolgreich umbenannt und der
|
||||
* Dokument-Stammsatz in der Persistenz aktualisiert wurde.
|
||||
*
|
||||
* @param previousFileName der Dateiname (ohne Pfad) vor der Umbenennung; nie null
|
||||
* @param appliedFileName der tatsächlich angewendete Dateiname (ohne Pfad) nach der
|
||||
* Umbenennung; kann bei Konflikten ein Suffix wie {@code (1)}
|
||||
* enthalten; nie null
|
||||
* @param conflictSuffixApplied {@code true} wenn dem gewünschten Basisdateinamen ein
|
||||
* Konflikt-Suffix angehängt wurde, weil der Wunschname bereits
|
||||
* durch eine andere Datei belegt war
|
||||
*/
|
||||
public record ManualFileRenameSuccess(
|
||||
String previousFileName,
|
||||
String appliedFileName,
|
||||
boolean conflictSuffixApplied) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung der Pflichtfelder.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code previousFileName} oder
|
||||
* {@code appliedFileName} null sind
|
||||
*/
|
||||
public ManualFileRenameSuccess {
|
||||
Objects.requireNonNull(previousFileName, "previousFileName must not be null");
|
||||
Objects.requireNonNull(appliedFileName, "appliedFileName must not be null");
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
/**
|
||||
* Inbound-Port für die manuelle Umbenennung einer bereits erfolgreich verarbeiteten
|
||||
* Zieldatei.
|
||||
* <p>
|
||||
* Ermöglicht dem Benutzer, den von der KI vorgeschlagenen Dateinamen nachträglich
|
||||
* zu korrigieren. Der Use-Case führt die Umbenennung als atomare Operation durch:
|
||||
* Dateisystem und Persistenz werden entweder beide aktualisiert oder beide bleiben
|
||||
* im vorherigen Zustand.
|
||||
* <p>
|
||||
* Eine Umbenennung ist nur für Dokumente mit Status
|
||||
* {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus#SUCCESS} zulässig,
|
||||
* die einen bekannten letzten Zieldateinamen haben.
|
||||
* <p>
|
||||
* <strong>Konfliktsemantik:</strong> Existiert im Zielordner bereits eine Datei mit dem
|
||||
* gewünschten Namen, wird anhand des Inhalts-Fingerprints entschieden:
|
||||
* <ul>
|
||||
* <li>Gleicher Fingerprint → keine Aktion ({@link ManualFileRenameNoOpIdenticalTarget})</li>
|
||||
* <li>Verschiedener Fingerprint → automatische Suffix-Vergabe ({@code (1)}, {@code (2)}, …)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface ManualFileRenameUseCase {
|
||||
|
||||
/**
|
||||
* Benennt die Zieldatei eines erfolgreich verarbeiteten Dokuments manuell um.
|
||||
* <p>
|
||||
* Der Aufruf ist atomar: Entweder werden Dateisystem und Persistenz beide
|
||||
* aktualisiert, oder beide bleiben unverändert. Bei einem Persistenzfehler
|
||||
* nach erfolgreicher Dateisystem-Umbenennung wird die Umbenennung im Dateisystem
|
||||
* im Rahmen eines Best-Effort-Rollbacks rückgängig gemacht.
|
||||
*
|
||||
* @param request die Umbenennungsanfrage mit Fingerprint und gewünschtem Basisdateinamen;
|
||||
* darf nicht null sein
|
||||
* @return das Ergebnis der Umbenennung; nie null
|
||||
* @throws NullPointerException wenn {@code request} null ist
|
||||
*/
|
||||
ManualFileRenameResult rename(ManualFileRenameRequest request);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis einer fehlgeschlagenen Umbenennung, weil die Quelldatei im Zielordner
|
||||
* nicht mehr vorhanden ist.
|
||||
* <p>
|
||||
* Gibt an, dass {@link TargetFileRenamePort#rename(String, String)} die Datei mit dem
|
||||
* angegebenen {@code oldFileName} nicht gefunden hat. Dies kann eintreten, wenn die
|
||||
* Datei zwischenzeitlich von einem anderen Prozess gelöscht oder verschoben wurde.
|
||||
*
|
||||
* @param oldFileName der Dateiname (ohne Pfad), der im Zielordner erwartet wurde, aber
|
||||
* nicht gefunden wurde; nie null
|
||||
*/
|
||||
public record TargetFileRenameFailureFileNotFound(String oldFileName) implements TargetFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code oldFileName} null ist
|
||||
*/
|
||||
public TargetFileRenameFailureFileNotFound {
|
||||
Objects.requireNonNull(oldFileName, "oldFileName must not be null");
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis einer fehlgeschlagenen Umbenennung, weil der gewünschte neue Dateiname im
|
||||
* Zielordner bereits existiert und nicht die gleiche Datei ist.
|
||||
* <p>
|
||||
* Dieser Zustand sollte durch eine vorherige Auflösung via
|
||||
* {@link TargetFolderPort#resolveUniqueFilename(String, de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint)}
|
||||
* normalerweise verhindert werden. Das Ergebnis dient der defensiven Fehlerbehandlung
|
||||
* für Race-Conditions oder unvorhergesehene Konkurrenz durch andere Prozesse.
|
||||
*
|
||||
* @param newFileName der Dateiname (ohne Pfad), der bereits im Zielordner existiert;
|
||||
* nie null
|
||||
*/
|
||||
public record TargetFileRenameFailureTargetExists(String newFileName) implements TargetFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code newFileName} null ist
|
||||
*/
|
||||
public TargetFileRenameFailureTargetExists {
|
||||
Objects.requireNonNull(newFileName, "newFileName must not be null");
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Outbound-Port für das Umbenennen einer bereits existierenden Datei im Zielordner.
|
||||
* <p>
|
||||
* Dieser Port kapselt die reine Dateisystem-Operation des Umbenennens. Er ist
|
||||
* provider-neutral und kennt ausschließlich opake Dateinamen-Strings – keine
|
||||
* {@code Path}-, {@code File}- oder NIO-Typen. Die Übersetzung in tatsächliche
|
||||
* Dateisystemoperationen obliegt ausschließlich der Adapter-Implementierung.
|
||||
* <p>
|
||||
* <strong>Zuständigkeit:</strong> Dieser Port ist nicht für die Suffix-Logik bei
|
||||
* Namenskollisionen zuständig. Die Auflösung eines eindeutigen Zieldateinamens
|
||||
* (inkl. Suffix-Vergabe) erfolgt über
|
||||
* {@link TargetFolderPort#resolveUniqueFilename(String, de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint)}.
|
||||
* <p>
|
||||
* <strong>Architekturgrenze:</strong> Keine {@code Path}-, {@code File}-, NIO- oder
|
||||
* JDBC-Typen erscheinen in diesem Interface oder in Typen, die es referenziert.
|
||||
*/
|
||||
public interface TargetFileRenamePort {
|
||||
|
||||
/**
|
||||
* Benennt eine existierende Datei im Zielordner von {@code oldFileName} zu
|
||||
* {@code newFileName} um.
|
||||
* <p>
|
||||
* Die Methode erwartet, dass {@code oldFileName} im Zielordner vorhanden ist.
|
||||
* Ist {@code newFileName} bereits vorhanden und nicht identisch mit {@code oldFileName},
|
||||
* wird {@link TargetFileRenameFailureTargetExists} zurückgegeben. Die eigentliche
|
||||
* Konfliktvermeidung (Suffix-Vergabe) liegt im Verantwortungsbereich des Aufrufers.
|
||||
*
|
||||
* @param oldFileName der aktuell im Zielordner vorhandene Dateiname (ohne Pfad);
|
||||
* darf nicht null oder leer sein
|
||||
* @param newFileName der gewünschte neue Dateiname (ohne Pfad);
|
||||
* darf nicht null oder leer sein
|
||||
* @return {@link TargetFileRenameSuccess} bei Erfolg,
|
||||
* {@link TargetFileRenameFailureFileNotFound} wenn {@code oldFileName} nicht existiert,
|
||||
* {@link TargetFileRenameFailureTargetExists} wenn {@code newFileName} bereits durch
|
||||
* eine andere Datei belegt ist,
|
||||
* {@link TargetFileRenameTechnicalFailure} bei einem sonstigen technischen Fehler
|
||||
*/
|
||||
TargetFileRenameResult rename(String oldFileName, String newFileName);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Versiegeltes Ergebnis-Interface für eine Umbenennung einer Zieldatei via
|
||||
* {@link TargetFileRenamePort}.
|
||||
* <p>
|
||||
* Mögliche Ergebnisse:
|
||||
* <ul>
|
||||
* <li>{@link TargetFileRenameSuccess} – die Umbenennung war erfolgreich.</li>
|
||||
* <li>{@link TargetFileRenameFailureFileNotFound} – die ursprüngliche Datei wurde
|
||||
* im Zielordner nicht gefunden.</li>
|
||||
* <li>{@link TargetFileRenameFailureTargetExists} – der gewünschte neue Dateiname
|
||||
* existiert bereits und gehört zu einer anderen Datei.</li>
|
||||
* <li>{@link TargetFileRenameTechnicalFailure} – ein technischer Fehler beim
|
||||
* Dateisystemzugriff ist aufgetreten.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public sealed interface TargetFileRenameResult
|
||||
permits TargetFileRenameSuccess,
|
||||
TargetFileRenameFailureFileNotFound,
|
||||
TargetFileRenameFailureTargetExists,
|
||||
TargetFileRenameTechnicalFailure {
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Ergebnis einer erfolgreichen Umbenennung einer Zieldatei via {@link TargetFileRenamePort}.
|
||||
* <p>
|
||||
* Gibt an, dass die Datei im Zielordner erfolgreich von ihrem alten auf den neuen
|
||||
* Dateinamen umbenannt wurde.
|
||||
*/
|
||||
public record TargetFileRenameSuccess() implements TargetFileRenameResult {
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis einer technisch fehlgeschlagenen Umbenennung einer Zieldatei.
|
||||
* <p>
|
||||
* Gibt an, dass beim Umbenennen ein nicht klassifizierbarer technischer Fehler
|
||||
* aufgetreten ist, z. B. fehlende Schreibrechte, gesperrte Datei durch einen anderen
|
||||
* Prozess oder ein nicht erreichbares Netzlaufwerk.
|
||||
*
|
||||
* @param message menschenlesbare Beschreibung des aufgetretenen Fehlers; nie null
|
||||
*/
|
||||
public record TargetFileRenameTechnicalFailure(String message) implements TargetFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public TargetFileRenameTechnicalFailure {
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameDocumentNotFound;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameFileSystemFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameInvalidState;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameNoOpIdenticalTarget;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenamePersistenceFailure;
|
||||
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.ManualFileRenameSourceFileMissing;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
|
||||
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.ExistingIdenticalTargetFile;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ResolvedTargetFilename;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameFailureFileNotFound;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameFailureTargetExists;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenamePort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameTechnicalFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderTechnicalFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Standardimplementierung von {@link ManualFileRenameUseCase}.
|
||||
* <p>
|
||||
* Führt die manuelle Umbenennung einer Zieldatei als atomare Operation durch:
|
||||
* Entweder werden Dateisystem und Persistenz beide aktualisiert, oder beide
|
||||
* bleiben im vorherigen Zustand.
|
||||
* <p>
|
||||
* <strong>Ablauf:</strong>
|
||||
* <ol>
|
||||
* <li>Dokument-Stammsatz aus dem Repository laden und Zustand prüfen.</li>
|
||||
* <li>Prüfen, ob der gewünschte Name bereits dem aktuellen entspricht (No-Op).</li>
|
||||
* <li>Eindeutigen Zieldateinamen über {@link TargetFolderPort} auflösen.</li>
|
||||
* <li>Zieldatei im Dateisystem umbenennen via {@link TargetFileRenamePort}.</li>
|
||||
* <li>Dokument-Stammsatz in der Persistenz aktualisieren.</li>
|
||||
* <li>Bei Persistenzfehler: Best-Effort-Rollback der Dateisystem-Umbenennung.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Eine Umbenennung ist ausschließlich für Dokumente mit Status
|
||||
* {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus#SUCCESS} zulässig,
|
||||
* die einen bekannten letzten Zieldateinamen und Zielpfad haben.
|
||||
*/
|
||||
public class DefaultManualFileRenameUseCase implements ManualFileRenameUseCase {
|
||||
|
||||
private final DocumentRecordRepository repository;
|
||||
private final TargetFolderPort targetFolderPort;
|
||||
private final TargetFileRenamePort targetFileRenamePort;
|
||||
private final UnitOfWorkPort unitOfWorkPort;
|
||||
private final ClockPort clock;
|
||||
private final ProcessingLogger logger;
|
||||
|
||||
/**
|
||||
* Erstellt den Use-Case mit allen erforderlichen Ports.
|
||||
*
|
||||
* @param repository Repository zum Lesen und Schreiben des Dokument-Stammsatzes;
|
||||
* darf nicht null sein
|
||||
* @param targetFolderPort Port zur Auflösung eindeutiger Zieldateinamen;
|
||||
* darf nicht null sein
|
||||
* @param targetFileRenamePort Port zum physischen Umbenennen einer Zieldatei;
|
||||
* darf nicht null sein
|
||||
* @param unitOfWorkPort Port zur atomaren Persistenzaktualisierung;
|
||||
* darf nicht null sein
|
||||
* @param clock Port zur Abfrage des aktuellen Zeitstempels;
|
||||
* darf nicht null sein
|
||||
* @param logger für die Protokollierung von Betriebsereignissen;
|
||||
* darf nicht null sein
|
||||
* @throws NullPointerException wenn einer der Parameter null ist
|
||||
*/
|
||||
public DefaultManualFileRenameUseCase(
|
||||
DocumentRecordRepository repository,
|
||||
TargetFolderPort targetFolderPort,
|
||||
TargetFileRenamePort targetFileRenamePort,
|
||||
UnitOfWorkPort unitOfWorkPort,
|
||||
ClockPort clock,
|
||||
ProcessingLogger logger) {
|
||||
this.repository = Objects.requireNonNull(repository, "repository must not be null");
|
||||
this.targetFolderPort = Objects.requireNonNull(targetFolderPort, "targetFolderPort must not be null");
|
||||
this.targetFileRenamePort = Objects.requireNonNull(targetFileRenamePort, "targetFileRenamePort must not be null");
|
||||
this.unitOfWorkPort = Objects.requireNonNull(unitOfWorkPort, "unitOfWorkPort must not be null");
|
||||
this.clock = Objects.requireNonNull(clock, "clock must not be null");
|
||||
this.logger = Objects.requireNonNull(logger, "logger must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Benennt die Zieldatei eines erfolgreich verarbeiteten Dokuments manuell um.
|
||||
* <p>
|
||||
* Der Aufruf ist atomar: Entweder werden Dateisystem und Persistenz beide
|
||||
* aktualisiert, oder beide bleiben unverändert. Bei einem Persistenzfehler nach
|
||||
* erfolgreicher Dateisystem-Umbenennung wird die Umbenennung im Dateisystem im
|
||||
* Rahmen eines Best-Effort-Rollbacks rückgängig gemacht.
|
||||
*
|
||||
* @param request die Umbenennungsanfrage mit Fingerprint und gewünschtem Basisdateinamen;
|
||||
* darf nicht null sein
|
||||
* @return das Ergebnis der Umbenennung; nie null
|
||||
* @throws NullPointerException wenn {@code request} null ist
|
||||
*/
|
||||
@Override
|
||||
public ManualFileRenameResult rename(ManualFileRenameRequest request) {
|
||||
Objects.requireNonNull(request, "request must not be null");
|
||||
|
||||
DocumentFingerprint fingerprint = request.fingerprint();
|
||||
String desiredFullName = request.desiredBaseFileName() + ".pdf";
|
||||
|
||||
logger.info("Manuelle Umbenennung angefordert: Fingerprint={}, Zielname={}",
|
||||
fingerprint.sha256Hex(), desiredFullName);
|
||||
|
||||
// Schritt 1: Dokument-Stammsatz laden und Zustand prüfen
|
||||
var lookupResult = repository.findByFingerprint(fingerprint);
|
||||
|
||||
if (lookupResult instanceof DocumentTerminalFinalFailure) {
|
||||
logger.warn("Manuelle Umbenennung verweigert: Dokument hat terminalen Fehlerstatus. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameInvalidState(
|
||||
"Dokument ist final fehlgeschlagen und kann nicht umbenannt werden.");
|
||||
}
|
||||
|
||||
if (!(lookupResult instanceof DocumentTerminalSuccess terminalSuccess)) {
|
||||
logger.warn("Manuelle Umbenennung verweigert: Dokument nicht gefunden oder nicht im Erfolgsstatus. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameDocumentNotFound(
|
||||
"Kein erfolgreich verarbeitetes Dokument mit dem angegebenen Fingerprint gefunden.");
|
||||
}
|
||||
|
||||
DocumentRecord record = terminalSuccess.record();
|
||||
|
||||
if (record.lastTargetFileName() == null || record.lastTargetPath() == null) {
|
||||
logger.warn("Manuelle Umbenennung verweigert: Kein Zieldateiname im Stammsatz vorhanden. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameInvalidState(
|
||||
"Dokument hat keinen gespeicherten Zieldateinamen und kann nicht umbenannt werden.");
|
||||
}
|
||||
|
||||
String currentFileName = record.lastTargetFileName();
|
||||
|
||||
// Schritt 2: Prüfen, ob der gewünschte Name bereits dem aktuellen entspricht
|
||||
if (desiredFullName.equals(currentFileName)) {
|
||||
logger.info("Manuelle Umbenennung: Kein Handlungsbedarf, Name ist bereits identisch. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameNoOpIdenticalTarget(currentFileName);
|
||||
}
|
||||
|
||||
// Schritt 3: Eindeutigen Zieldateinamen über TargetFolderPort auflösen
|
||||
var resolutionResult = targetFolderPort.resolveUniqueFilename(desiredFullName, fingerprint);
|
||||
|
||||
if (resolutionResult instanceof ExistingIdenticalTargetFile identical) {
|
||||
logger.info("Manuelle Umbenennung: Identische Datei bereits im Zielordner vorhanden. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameNoOpIdenticalTarget(identical.existingFilename());
|
||||
}
|
||||
|
||||
if (resolutionResult instanceof TargetFolderTechnicalFailure folderFailure) {
|
||||
logger.warn("Manuelle Umbenennung fehlgeschlagen: Technischer Fehler beim Zielordner-Zugriff. Fingerprint={}, Ursache={}",
|
||||
fingerprint.sha256Hex(), folderFailure.errorMessage());
|
||||
return new ManualFileRenameFileSystemFailure(
|
||||
"Zielordner nicht zugänglich: " + folderFailure.errorMessage());
|
||||
}
|
||||
|
||||
// resolutionResult ist jetzt ResolvedTargetFilename
|
||||
String appliedFileName = ((ResolvedTargetFilename) resolutionResult).resolvedFilename();
|
||||
|
||||
// Schritt 4: Zieldatei im Dateisystem umbenennen
|
||||
var renameResult = targetFileRenamePort.rename(currentFileName, appliedFileName);
|
||||
|
||||
if (renameResult instanceof TargetFileRenameFailureFileNotFound notFound) {
|
||||
logger.warn("Manuelle Umbenennung fehlgeschlagen: Bisherige Zieldatei nicht gefunden. Fingerprint={}, Datei={}",
|
||||
fingerprint.sha256Hex(), notFound.oldFileName());
|
||||
return new ManualFileRenameSourceFileMissing(notFound.oldFileName());
|
||||
}
|
||||
|
||||
if (renameResult instanceof TargetFileRenameFailureTargetExists targetExists) {
|
||||
logger.warn("Manuelle Umbenennung fehlgeschlagen: Zieldatei bereits vorhanden (defensiv). Fingerprint={}, Datei={}",
|
||||
fingerprint.sha256Hex(), targetExists.newFileName());
|
||||
return new ManualFileRenameFileSystemFailure(
|
||||
"Zieldatei bereits vorhanden: " + targetExists.newFileName());
|
||||
}
|
||||
|
||||
if (renameResult instanceof TargetFileRenameTechnicalFailure technical) {
|
||||
logger.warn("Manuelle Umbenennung fehlgeschlagen: Technischer Dateisystemfehler. Fingerprint={}, Ursache={}",
|
||||
fingerprint.sha256Hex(), technical.message());
|
||||
return new ManualFileRenameFileSystemFailure(technical.message());
|
||||
}
|
||||
|
||||
// Schritt 5: Persistenz aktualisieren (renameResult ist jetzt TargetFileRenameSuccess)
|
||||
DocumentRecord updatedRecord = new DocumentRecord(
|
||||
record.fingerprint(),
|
||||
record.lastKnownSourceLocator(),
|
||||
record.lastKnownSourceFileName(),
|
||||
record.overallStatus(),
|
||||
record.failureCounters(),
|
||||
record.lastFailureInstant(),
|
||||
record.lastSuccessInstant(),
|
||||
record.createdAt(),
|
||||
clock.now(),
|
||||
record.lastTargetPath(),
|
||||
appliedFileName);
|
||||
|
||||
try {
|
||||
unitOfWorkPort.executeInTransaction(tx -> tx.updateDocumentRecord(updatedRecord));
|
||||
} catch (RuntimeException persistenceException) {
|
||||
// Best-Effort-Rollback: Dateisystem-Umbenennung rückgängig machen
|
||||
String errorMessage = persistenceException.getMessage() != null
|
||||
? persistenceException.getMessage()
|
||||
: persistenceException.getClass().getSimpleName();
|
||||
|
||||
logger.warn("Manuelle Umbenennung: Persistenzfehler nach erfolgreicher Dateisystem-Umbenennung. " +
|
||||
"Versuche Rollback. Fingerprint={}, Ursache={}", fingerprint.sha256Hex(), errorMessage);
|
||||
|
||||
var rollbackResult = targetFileRenamePort.rename(appliedFileName, currentFileName);
|
||||
if (!(rollbackResult instanceof TargetFileRenameSuccess)) {
|
||||
logger.error("Rollback der Dateisystem-Umbenennung fehlgeschlagen: {} → {}. " +
|
||||
"Dateisystem und Persistenz sind möglicherweise inkonsistent. Fingerprint={}",
|
||||
appliedFileName, currentFileName, fingerprint.sha256Hex());
|
||||
}
|
||||
|
||||
return new ManualFileRenamePersistenceFailure(
|
||||
"Persistenzfehler nach Umbenennung: " + errorMessage);
|
||||
}
|
||||
|
||||
boolean conflictSuffixApplied = !appliedFileName.equals(desiredFullName);
|
||||
|
||||
logger.info("Manuelle Umbenennung erfolgreich: {} → {} (Suffix angewendet: {})",
|
||||
currentFileName, appliedFileName, conflictSuffixApplied);
|
||||
|
||||
return new ManualFileRenameSuccess(currentFileName, appliedFileName, conflictSuffixApplied);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user