SonarQube: fix alle BLOCKER- und CRITICAL-Issues (S3252, S2479, S1186, S1192, S2699, S5783, S3776)

- S3252: GuiStatusRefreshTimeline nutzt Animation.INDEFINITE statt Timeline.INDEFINITE
- S2479: Narrow-No-Break-Space (U+202F) in GuiTooltipTexts durch normales Leerzeichen ersetzt
- S1186: 134 leere Stub-Methoden in 18 Test- und Produktionsdateien kommentiert
- S1192: ~49 duplizierte String-Literale in ~25 Klassen als Konstanten extrahiert
- S2699: fehlende Assertions in SqliteSchemaInitializationAdapterTest und FilesystemTargetFolderAdapterTest ergaenzt
- S5783: Lambda-geprufte Ausnahme in SqliteSchemaInitializationAdapterTest in private Hilfsmethode extrahiert
- S3776: kognitive Komplexitaet in 8 Methoden durch Methodenextraktion auf unter 15 gesenkt
  (EarlyLogDirectoryInitializer, CliArgumentParser, GuiConfigurationEditorWorkspace,
   GuiHistoryTab x2, GuiBatchRunTab x2, DefaultManualFileCopyUseCase)
- Kompilierungsfehler behoben: private-Modifier in CorrectionOutcome-Interface entfernt,
  selbstreferenzielle Konstante in ModelCatalogResult korrigiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 21:27:59 +02:00
parent 14da7ee789
commit b7f9184344
49 changed files with 974 additions and 511 deletions
@@ -124,6 +124,12 @@ import javafx.stage.Window;
* Thread via {@code Platform.runLater}. * Thread via {@code Platform.runLater}.
*/ */
public final class GuiConfigurationEditorWorkspace { public final class GuiConfigurationEditorWorkspace {
private static final String NO_PROMPT_PATH_MSG = "Kein Prompt-Pfad konfiguriert.";
private static final String OPERATION_VALIDATE = "Validierung";
private static final String PROPERTIES_FILTER_EXT = "*.properties";
private static final String PROPERTIES_FILTER_DESC = "Properties-Dateien";
private static final Logger LOG = LogManager.getLogger(GuiConfigurationEditorWorkspace.class); private static final Logger LOG = LogManager.getLogger(GuiConfigurationEditorWorkspace.class);
private static final String WELCOME_TEXT = private static final String WELCOME_TEXT =
@@ -985,7 +991,7 @@ public final class GuiConfigurationEditorWorkspace {
Window owner = root.getScene() == null ? null : root.getScene().getWindow(); Window owner = root.getScene() == null ? null : root.getScene().getWindow();
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Konfiguration öffnen"); fileChooser.setTitle("Konfiguration öffnen");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Properties-Dateien", "*.properties")); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(PROPERTIES_FILTER_DESC, PROPERTIES_FILTER_EXT));
if (owner != null && editorState.hasLoadedFileSnapshot()) { if (owner != null && editorState.hasLoadedFileSnapshot()) {
Path currentPath = editorState.loadedFileSnapshot().orElseThrow().filePath(); Path currentPath = editorState.loadedFileSnapshot().orElseThrow().filePath();
Path parent = currentPath.getParent(); Path parent = currentPath.getParent();
@@ -1055,7 +1061,7 @@ public final class GuiConfigurationEditorWorkspace {
FileChooser fileChooser = saveFileChooserFactory.get(); FileChooser fileChooser = saveFileChooserFactory.get();
fileChooser.setTitle("Konfiguration speichern"); fileChooser.setTitle("Konfiguration speichern");
fileChooser.getExtensionFilters().add( fileChooser.getExtensionFilters().add(
new FileChooser.ExtensionFilter("Properties-Dateien", "*.properties")); new FileChooser.ExtensionFilter(PROPERTIES_FILTER_DESC, PROPERTIES_FILTER_EXT));
// Propose the default path relative to the working directory. // Propose the default path relative to the working directory.
Path proposedDir = DEFAULT_SAVE_PATH.getParent(); Path proposedDir = DEFAULT_SAVE_PATH.getParent();
@@ -1504,7 +1510,7 @@ public final class GuiConfigurationEditorWorkspace {
FileChooser fileChooser = saveFileChooserFactory.get(); FileChooser fileChooser = saveFileChooserFactory.get();
fileChooser.setTitle("Konfiguration speichern"); fileChooser.setTitle("Konfiguration speichern");
fileChooser.getExtensionFilters().add( fileChooser.getExtensionFilters().add(
new FileChooser.ExtensionFilter("Properties-Dateien", "*.properties")); new FileChooser.ExtensionFilter(PROPERTIES_FILTER_DESC, PROPERTIES_FILTER_EXT));
java.io.File proposedDirFile = DEFAULT_SAVE_PATH.getParent().toAbsolutePath().toFile(); java.io.File proposedDirFile = DEFAULT_SAVE_PATH.getParent().toAbsolutePath().toFile();
if (proposedDirFile.exists()) { if (proposedDirFile.exists()) {
fileChooser.setInitialDirectory(proposedDirFile); fileChooser.setInitialDirectory(proposedDirFile);
@@ -1604,13 +1610,13 @@ public final class GuiConfigurationEditorWorkspace {
@Override @Override
public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() { public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() {
return new de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure( return new de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure(
"NO_PATH", "Kein Prompt-Pfad konfiguriert."); "NO_PATH", NO_PROMPT_PATH_MSG);
} }
@Override @Override
public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) { public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) {
return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.WriteFailed( return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.WriteFailed(
"Kein Prompt-Pfad konfiguriert.", null); NO_PROMPT_PATH_MSG, null);
} }
@Override @Override
@@ -1619,7 +1625,7 @@ public final class GuiConfigurationEditorWorkspace {
de.gecheckt.pdf.umbenenner.application.validation.technicaltest de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.CreatePromptFile suggestion) { .CorrectionSuggestion.CreatePromptFile suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted(suggestion, "Kein Prompt-Pfad konfiguriert."); .CorrectionOutcome.NotAttempted(suggestion, NO_PROMPT_PATH_MSG);
} }
}; };
} }
@@ -1696,26 +1702,27 @@ public final class GuiConfigurationEditorWorkspace {
// Tab-Wechsel-Schutz: Beim Wechsel weg vom Verarbeitungslauf-Tab prüfen ob // Tab-Wechsel-Schutz: Beim Wechsel weg vom Verarbeitungslauf-Tab prüfen ob
// der Dateiname-Editor ungespeicherte Änderungen hat. // der Dateiname-Editor ungespeicherte Änderungen hat.
// Gleiches gilt für den Prompt-Tab. // Gleiches gilt für den Prompt-Tab.
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> { tabPane.getSelectionModel().selectedItemProperty().addListener(
if (oldTab == null || newTab == null) { (obs, oldTab, newTab) -> handleTabSwitch(oldTab, newTab));
return; }
private void handleTabSwitch(javafx.scene.control.Tab oldTab, javafx.scene.control.Tab newTab) {
if (oldTab == null || newTab == null) {
return;
}
if (oldTab == batchRunTab.tab() && batchRunTab.hasUnsavedFilenameEdits()) {
boolean shouldDiscard = batchRunTab.confirmDiscardUnsavedFilenameEdits();
if (!shouldDiscard) {
Platform.runLater(() -> tabPane.getSelectionModel().select(oldTab));
} }
if (oldTab == batchRunTab.tab() && batchRunTab.hasUnsavedFilenameEdits()) { } else if (oldTab == promptEditorTab.tab() && promptEditorTab.hasDirtyContent()) {
// Selektion kurz unterdrücken um Rekursion zu vermeiden boolean shouldDiscard = promptEditorTab.confirmDiscardIfDirty();
boolean shouldDiscard = batchRunTab.confirmDiscardUnsavedFilenameEdits(); if (!shouldDiscard) {
if (!shouldDiscard) { Platform.runLater(() -> tabPane.getSelectionModel().select(oldTab));
// Zurück zum Verarbeitungslauf-Tab } else {
Platform.runLater(() -> tabPane.getSelectionModel().select(oldTab)); promptEditorTab.discardChanges();
}
} else if (oldTab == promptEditorTab.tab() && promptEditorTab.hasDirtyContent()) {
boolean shouldDiscard = promptEditorTab.confirmDiscardIfDirty();
if (!shouldDiscard) {
Platform.runLater(() -> tabPane.getSelectionModel().select(oldTab));
} else {
promptEditorTab.discardChanges();
}
} }
}); }
} }
private void configureActionBar() { private void configureActionBar() {
@@ -2610,7 +2617,7 @@ public final class GuiConfigurationEditorWorkspace {
for (EditorValidationFinding finding : report.findings()) { for (EditorValidationFinding finding : report.findings()) {
GuiMessageSeverity severity = toGuiSeverity(finding.severity()); GuiMessageSeverity severity = toGuiSeverity(finding.severity());
messages.add(GuiMessageEntry.of(severity, finding.message(), "Validierung")); messages.add(GuiMessageEntry.of(severity, finding.message(), OPERATION_VALIDATE));
if (finding.hasFieldKey()) { if (finding.hasFieldKey()) {
fieldFindings.add(new GuiFieldFinding(finding.fieldKey().orElseThrow(), fieldFindings.add(new GuiFieldFinding(finding.fieldKey().orElseThrow(),
severity, finding.message())); severity, finding.message()));
@@ -2619,7 +2626,7 @@ public final class GuiConfigurationEditorWorkspace {
// Replace validation-related entries; preserve model-catalog messages (from coordinator) // Replace validation-related entries; preserve model-catalog messages (from coordinator)
pendingMessages.removeIf(m -> m.source().isPresent() pendingMessages.removeIf(m -> m.source().isPresent()
&& "Validierung".equals(m.source().get())); && OPERATION_VALIDATE.equals(m.source().get()));
pendingMessages.addAll(messages); pendingMessages.addAll(messages);
pendingFieldFindings.clear(); pendingFieldFindings.clear();
@@ -2675,7 +2682,7 @@ public final class GuiConfigurationEditorWorkspace {
// Drop silent auto-validation entries so the central message area is not flooded // Drop silent auto-validation entries so the central message area is not flooded
// by keystroke-level background checks; explicit action entries always accumulate. // by keystroke-level background checks; explicit action entries always accumulate.
pendingMessages.removeIf(m -> m.source().isPresent() pendingMessages.removeIf(m -> m.source().isPresent()
&& "Validierung".equals(m.source().get())); && OPERATION_VALIDATE.equals(m.source().get()));
// Append a timestamped confirmation plus each concrete finding as its own entry. // Append a timestamped confirmation plus each concrete finding as its own entry.
int findingCount = report.findings().size(); int findingCount = report.findings().size();
@@ -51,6 +51,10 @@ import javafx.application.Platform;
* {@code Platform.runLater}. * {@code Platform.runLater}.
*/ */
public final class GuiModelCatalogCoordinator { public final class GuiModelCatalogCoordinator {
private static final String LOG_MODEL_FETCH_FMT = "GUI-Modellabruf: {} (Provider: {})";
private static final String OPERATION_MODELLABRUF = "Modellabruf";
private static final Logger LOG = LogManager.getLogger(GuiModelCatalogCoordinator.class); private static final Logger LOG = LogManager.getLogger(GuiModelCatalogCoordinator.class);
@@ -203,7 +207,7 @@ public final class GuiModelCatalogCoordinator {
String previousManualValue) { String previousManualValue) {
// Remove any previous message entries from an earlier retrieval so messages do not // Remove any previous message entries from an earlier retrieval so messages do not
// accumulate across repeated triggers of the same retrieval action. // accumulate across repeated triggers of the same retrieval action.
pendingMessages.removeIf(msg -> "Modellabruf".equals(msg.source().orElse(""))); pendingMessages.removeIf(msg -> OPERATION_MODELLABRUF.equals(msg.source().orElse("")));
String displayName = displayNameFor(family); String displayName = displayNameFor(family);
@@ -213,28 +217,28 @@ public final class GuiModelCatalogCoordinator {
container.applyModelList(models, previousManualValue); container.applyModelList(models, previousManualValue);
String message = "Modellliste für " + displayName + " geladen (" String message = "Modellliste für " + displayName + " geladen ("
+ models.size() + " " + (models.size() == 1 ? "Eintrag" : "Einträge") + ")."; + models.size() + " " + (models.size() == 1 ? "Eintrag" : "Einträge") + ").";
pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, message, "Modellabruf")); pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, message, OPERATION_MODELLABRUF));
LOG.info("GUI-Modellabruf: {} (Provider: {})", message, family.getIdentifier()); LOG.info(LOG_MODEL_FETCH_FMT, message, family.getIdentifier());
} }
case ModelCatalogResult.EmptyList emptyList -> { case ModelCatalogResult.EmptyList emptyList -> {
container.applyManualFallback(GuiModelSource.LIST_UNAVAILABLE_MANUAL_INPUT); container.applyManualFallback(GuiModelSource.LIST_UNAVAILABLE_MANUAL_INPUT);
String message = "Provider " + displayName String message = "Provider " + displayName
+ " liefert aktuell keine Modelle. Manuelle Eingabe aktiv."; + " liefert aktuell keine Modelle. Manuelle Eingabe aktiv.";
pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.HINT, message, "Modellabruf")); pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.HINT, message, OPERATION_MODELLABRUF));
LOG.warn("GUI-Modellabruf: {} (Provider: {})", message, family.getIdentifier()); LOG.warn(LOG_MODEL_FETCH_FMT, message, family.getIdentifier());
} }
case ModelCatalogResult.IncompleteConfiguration incomplete -> { case ModelCatalogResult.IncompleteConfiguration incomplete -> {
container.applyManualFallback(GuiModelSource.LIST_UNAVAILABLE_MANUAL_INPUT); container.applyManualFallback(GuiModelSource.LIST_UNAVAILABLE_MANUAL_INPUT);
String message = "Modellliste nicht abrufbar: " + incomplete.missingReason() String message = "Modellliste nicht abrufbar: " + incomplete.missingReason()
+ ". Manuelle Eingabe aktiv."; + ". Manuelle Eingabe aktiv.";
pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.WARNING, message, "Modellabruf")); pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.WARNING, message, OPERATION_MODELLABRUF));
LOG.warn("GUI-Modellabruf: {} (Provider: {})", message, family.getIdentifier()); LOG.warn(LOG_MODEL_FETCH_FMT, message, family.getIdentifier());
} }
case ModelCatalogResult.TechnicalFailure failure -> { case ModelCatalogResult.TechnicalFailure failure -> {
container.applyManualFallback(GuiModelSource.LIST_FAILED_MANUAL_INPUT); container.applyManualFallback(GuiModelSource.LIST_FAILED_MANUAL_INPUT);
String message = "Modellliste nicht abrufbar (" + failure.errorCategory() String message = "Modellliste nicht abrufbar (" + failure.errorCategory()
+ "). Manuelle Eingabe aktiv."; + "). Manuelle Eingabe aktiv.";
pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.ERROR, message, "Modellabruf")); pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.ERROR, message, OPERATION_MODELLABRUF));
LOG.warn("GUI-Modellabruf: {} Detail: {} (Provider: {})", LOG.warn("GUI-Modellabruf: {} Detail: {} (Provider: {})",
message, failure.errorDetail(), family.getIdentifier()); message, failure.errorDetail(), family.getIdentifier());
} }
@@ -54,6 +54,9 @@ import javafx.scene.layout.VBox;
* Hintergrund-Worker-Thread ({@code gui-scheduler-control}) ausgeführt. * Hintergrund-Worker-Thread ({@code gui-scheduler-control}) ausgeführt.
*/ */
public final class GuiSchedulerTab { public final class GuiSchedulerTab {
private static final String HEADER_LABEL_STYLE = "-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #7f8c8d;";
private static final Logger LOG = LogManager.getLogger(GuiSchedulerTab.class); private static final Logger LOG = LogManager.getLogger(GuiSchedulerTab.class);
@@ -177,7 +180,7 @@ public final class GuiSchedulerTab {
} }
private VBox buildControlArea() { private VBox buildControlArea() {
statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #7f8c8d;"); statusLabel.setStyle(HEADER_LABEL_STYLE);
stopButton.setDisable(true); stopButton.setDisable(true);
HBox buttonBox = new HBox(10, startButton, stopButton); HBox buttonBox = new HBox(10, startButton, stopButton);
@@ -248,7 +251,7 @@ public final class GuiSchedulerTab {
switch (status.state()) { switch (status.state()) {
case STOPPED -> { case STOPPED -> {
statusLabel.setText("○ Gestoppt"); statusLabel.setText("○ Gestoppt");
statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #7f8c8d;"); statusLabel.setStyle(HEADER_LABEL_STYLE);
} }
case STARTING -> { case STARTING -> {
statusLabel.setText("⟳ Wird gestartet…"); statusLabel.setText("⟳ Wird gestartet…");
@@ -264,7 +267,7 @@ public final class GuiSchedulerTab {
} }
case STOPPING_BATCH_ACTIVE -> { case STOPPING_BATCH_ACTIVE -> {
statusLabel.setText("○ Gestoppt aktueller Lauf läuft noch"); statusLabel.setText("○ Gestoppt aktueller Lauf läuft noch");
statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #7f8c8d;"); statusLabel.setStyle(HEADER_LABEL_STYLE);
} }
} }
} }
@@ -101,6 +101,9 @@ public record GuiStartupContext(
Optional<String> applicationContextError, Optional<String> applicationContextError,
Optional<SchedulerControlUseCase> schedulerControlUseCase, Optional<SchedulerControlUseCase> schedulerControlUseCase,
Optional<ConfigurationFileLockPort> configurationFileLockPort) { Optional<ConfigurationFileLockPort> configurationFileLockPort) {
private static final String NO_PROMPT_PORT_MSG = "Kein Prompt-Editor-Port in diesem Startkontext verfügbar.";
private static final String NO_PORT_MSG = "Kein Port in diesem Startkontext.";
/** /**
* Creates a fully wired startup context. * Creates a fully wired startup context.
@@ -524,21 +527,21 @@ public record GuiStartupContext(
createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.CreateDirectory suggestion) { .CorrectionSuggestion.CreateDirectory suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext."); .CorrectionOutcome.NotAttempted(suggestion, NO_PORT_MSG);
} }
@Override @Override
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.CreatePromptFile suggestion) { .CorrectionSuggestion.CreatePromptFile suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext."); .CorrectionOutcome.NotAttempted(suggestion, NO_PORT_MSG);
} }
@Override @Override
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.PrepareSqlitePath suggestion) { .CorrectionSuggestion.PrepareSqlitePath suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext."); .CorrectionOutcome.NotAttempted(suggestion, NO_PORT_MSG);
} }
}; };
CorrectionExecutionService noOpCorrectionService = new CorrectionExecutionService(noOpResourceCreationPort); CorrectionExecutionService noOpCorrectionService = new CorrectionExecutionService(noOpResourceCreationPort);
@@ -599,13 +602,13 @@ public record GuiStartupContext(
@Override @Override
public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() { public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() {
return new de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure( return new de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure(
"NO_OP", "Kein Prompt-Editor-Port in diesem Startkontext verfügbar."); "NO_OP", NO_PROMPT_PORT_MSG);
} }
@Override @Override
public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) { public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) {
return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.WriteFailed( return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.WriteFailed(
"Kein Prompt-Editor-Port in diesem Startkontext verfügbar.", null); NO_PROMPT_PORT_MSG, null);
} }
@Override @Override
@@ -615,7 +618,7 @@ public record GuiStartupContext(
.CorrectionSuggestion.CreatePromptFile suggestion) { .CorrectionSuggestion.CreatePromptFile suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted( .CorrectionOutcome.NotAttempted(
suggestion, "Kein Prompt-Editor-Port in diesem Startkontext verfügbar."); suggestion, NO_PROMPT_PORT_MSG);
} }
}; };
} }
@@ -29,6 +29,9 @@ import javafx.scene.layout.Region;
* Die Klasse selbst erzwingt dies nicht; der Aufrufer trägt die Verantwortung. * Die Klasse selbst erzwingt dies nicht; der Aufrufer trägt die Verantwortung.
*/ */
public final class GuiStatusBar { public final class GuiStatusBar {
private static final String LABEL_STYLE = "-fx-font-size: 11px; -fx-text-fill: #555555;";
/** Anzeigetext wenn keine Konfiguration geladen ist. */ /** Anzeigetext wenn keine Konfiguration geladen ist. */
static final String KEIN_PROFIL_TEXT = "Kein Profil geladen"; static final String KEIN_PROFIL_TEXT = "Kein Profil geladen";
@@ -58,16 +61,16 @@ public final class GuiStatusBar {
// Linkes Segment: Versionsanzeige // Linkes Segment: Versionsanzeige
this.versionLabel = new Label(VERSION_PREFIX + this.applicationVersion); this.versionLabel = new Label(VERSION_PREFIX + this.applicationVersion);
this.versionLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;"); this.versionLabel.setStyle(LABEL_STYLE);
// Mittleres Segment: Provider und Modell // Mittleres Segment: Provider und Modell
this.providerLabel = new Label(KEIN_PROFIL_TEXT); this.providerLabel = new Label(KEIN_PROFIL_TEXT);
this.providerLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;"); this.providerLabel.setStyle(LABEL_STYLE);
this.providerLabel.setAlignment(Pos.CENTER); this.providerLabel.setAlignment(Pos.CENTER);
// Rechtes Segment: Konfigurationspfad // Rechtes Segment: Konfigurationspfad
this.configPathLabel = new Label(KEIN_PROFIL_TEXT); this.configPathLabel = new Label(KEIN_PROFIL_TEXT);
this.configPathLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;"); this.configPathLabel.setStyle(LABEL_STYLE);
this.configPathLabel.setAlignment(Pos.CENTER_RIGHT); this.configPathLabel.setAlignment(Pos.CENTER_RIGHT);
// Abstandhalter zwischen den Segmenten // Abstandhalter zwischen den Segmenten
@@ -4,6 +4,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase; import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase;
import javafx.animation.Animation;
import javafx.animation.KeyFrame; import javafx.animation.KeyFrame;
import javafx.animation.Timeline; import javafx.animation.Timeline;
import javafx.util.Duration; import javafx.util.Duration;
@@ -42,7 +43,7 @@ public final class GuiStatusRefreshTimeline {
Objects.requireNonNull(onRefresh, "onRefresh must not be null"); Objects.requireNonNull(onRefresh, "onRefresh must not be null");
this.timeline = new Timeline( this.timeline = new Timeline(
new KeyFrame(Duration.seconds(1), e -> onRefresh.run())); new KeyFrame(Duration.seconds(1), e -> onRefresh.run()));
this.timeline.setCycleCount(Timeline.INDEFINITE); this.timeline.setCycleCount(Animation.INDEFINITE);
} }
/** /**
@@ -87,7 +87,7 @@ public final class GuiTooltipTexts {
/** Tooltip für das Eingabefeld „Basis-URL". */ /** Tooltip für das Eingabefeld „Basis-URL". */
public static final String PROVIDER_BASIS_URL = public static final String PROVIDER_BASIS_URL =
"Basis-URL des KI-Dienstes (z.B. https://api.openai.com/v1)."; "Basis-URL des KI-Dienstes (z. B. https://api.openai.com/v1).";
/** Tooltip für das Eingabefeld „Timeout". */ /** Tooltip für das Eingabefeld „Timeout". */
public static final String PROVIDER_TIMEOUT = public static final String PROVIDER_TIMEOUT =
@@ -63,6 +63,9 @@ import javafx.scene.control.Alert;
* </ol> * </ol>
*/ */
public final class GuiBatchRunCoordinator { public final class GuiBatchRunCoordinator {
private static final String CONFIG_FILE_NOT_NULL = "configFilePath must not be null";
private static final Logger LOG = LogManager.getLogger(GuiBatchRunCoordinator.class); private static final Logger LOG = LogManager.getLogger(GuiBatchRunCoordinator.class);
private static final String WORKER_THREAD_NAME = "gui-batch-run"; private static final String WORKER_THREAD_NAME = "gui-batch-run";
@@ -353,7 +356,7 @@ public final class GuiBatchRunCoordinator {
* @throws NullPointerException if {@code configFilePath} is {@code null} * @throws NullPointerException if {@code configFilePath} is {@code null}
*/ */
public boolean start(Path configFilePath) { public boolean start(Path configFilePath) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
if (isRunning()) { if (isRunning()) {
return false; return false;
} }
@@ -379,7 +382,7 @@ public final class GuiBatchRunCoordinator {
*/ */
public boolean startMiniRun(Path configFilePath, public boolean startMiniRun(Path configFilePath,
Set<DocumentFingerprint> fingerprintFilter) { Set<DocumentFingerprint> fingerprintFilter) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null"); Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null");
if (isRunning()) { if (isRunning()) {
return false; return false;
@@ -411,7 +414,7 @@ public final class GuiBatchRunCoordinator {
*/ */
public boolean startReprocessing(Path configFilePath, public boolean startReprocessing(Path configFilePath,
Set<DocumentFingerprint> fingerprintFilter) { Set<DocumentFingerprint> fingerprintFilter) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null"); Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null");
if (isRunning()) { if (isRunning()) {
return false; return false;
@@ -452,7 +455,7 @@ public final class GuiBatchRunCoordinator {
* @throws NullPointerException if any argument is {@code null} * @throws NullPointerException if any argument is {@code null}
*/ */
public boolean startReset(Path configFilePath, Set<DocumentFingerprint> fingerprints) { public boolean startReset(Path configFilePath, Set<DocumentFingerprint> fingerprints) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprints, "fingerprints must not be null"); Objects.requireNonNull(fingerprints, "fingerprints must not be null");
if (isRunning()) { if (isRunning()) {
return false; return false;
@@ -111,6 +111,11 @@ import javafx.scene.layout.VBox;
* dafür, Hintergrundereignisse vor dem Callback auf den FX-Thread zu übertragen. * dafür, Hintergrundereignisse vor dem Callback auf den FX-Thread zu übertragen.
*/ */
public final class GuiBatchRunTab { public final class GuiBatchRunTab {
private static final String COPY_FAILED_LOG = "Manuelle Dateikopie fehlgeschlagen: {}";
private static final String RENAME_FAILED_LOG = "Manuelle Dateiumbenennung fehlgeschlagen: {}";
private static final String DIRTY_STATE_MSG = "Dateiname-Editor: Ungespeicherte Änderungen";
private static final Logger LOG = LogManager.getLogger(GuiBatchRunTab.class); private static final Logger LOG = LogManager.getLogger(GuiBatchRunTab.class);
@@ -820,7 +825,7 @@ public final class GuiBatchRunTab {
return; return;
} }
fileNameEditor.discardChanges(); fileNameEditor.discardChanges();
LOG.debug("Dateiname-Editor: Ungespeicherte Änderung Benutzer hat verworfen"); LOG.debug(DIRTY_STATE_MSG);
} }
// Neue Zeile laden // Neue Zeile laden
@@ -957,55 +962,55 @@ public final class GuiBatchRunTab {
*/ */
private void handleCopyResult(ManualFileCopyResult result, GuiBatchRunResultRow row) { private void handleCopyResult(ManualFileCopyResult result, GuiBatchRunResultRow row) {
switch (result) { switch (result) {
case ManualFileCopySuccess success -> { case ManualFileCopySuccess success -> applyCopySuccess(success, row);
LOG.info("Manuelle Dateikopie erfolgreich: {} → {} (Suffix: {})", case ManualFileCopyNoOpIdenticalTarget noOp -> applyCopyNoOpIdentical(noOp, row);
row.originalFileName(), success.appliedFileName(),
success.conflictSuffixApplied());
GuiBatchRunResultRow updatedRow = buildSuccessRowAfterCopy(row, success.appliedFileName());
currentlySelectedRow = updatedRow;
fileNameEditor.clearDirtyState();
upsertResultRowByFingerprint(updatedRow);
String targetFolder = targetFolderSupplier.get().orElse("");
fileNameEditor.loadSelection(updatedRow, targetFolder);
String msg = "Datei kopiert und gespeichert: " + success.appliedFileName();
if (success.conflictSuffixApplied()) {
msg += " (Suffix wegen Namenskonflikt angehängt)";
}
showMessage(msg);
refreshAggregateCountersFromItems();
}
case ManualFileCopyNoOpIdenticalTarget noOp -> {
LOG.info("Manuelle Dateikopie: identische Zieldatei {} bereits vorhanden kein Schreibvorgang.",
noOp.existingFileName());
GuiBatchRunResultRow updatedRow = buildSuccessRowAfterCopy(row, noOp.existingFileName());
currentlySelectedRow = updatedRow;
fileNameEditor.clearDirtyState();
upsertResultRowByFingerprint(updatedRow);
String targetFolder = targetFolderSupplier.get().orElse("");
fileNameEditor.loadSelection(updatedRow, targetFolder);
showMessage("Identische Datei bereits vorhanden Status auf SUCCESS gesetzt");
refreshAggregateCountersFromItems();
}
case ManualFileCopyDocumentNotFound notFound -> { case ManualFileCopyDocumentNotFound notFound -> {
LOG.warn("Manuelle Dateikopie fehlgeschlagen: {}", notFound.reason()); LOG.warn(COPY_FAILED_LOG, notFound.reason());
showMessage("Fehler: Dokument nicht gefunden " + notFound.reason()); showMessage("Fehler: Dokument nicht gefunden " + notFound.reason());
} }
case ManualFileCopyInvalidState invalidState -> { case ManualFileCopyInvalidState invalidState -> {
LOG.warn("Manuelle Dateikopie fehlgeschlagen: {}", invalidState.reason()); LOG.warn(COPY_FAILED_LOG, invalidState.reason());
showMessage("Fehler: Ungültiger Dokumentstatus " + invalidState.reason()); showMessage("Fehler: Ungültiger Dokumentstatus " + invalidState.reason());
} }
case ManualFileCopyFileSystemFailure fsFail -> { case ManualFileCopyFileSystemFailure fsFail -> {
LOG.warn("Manuelle Dateikopie fehlgeschlagen: {}", fsFail.message()); LOG.warn(COPY_FAILED_LOG, fsFail.message());
showMessage("Dateisystemfehler: " + fsFail.message()); showMessage("Dateisystemfehler: " + fsFail.message());
} }
case ManualFileCopyPersistenceFailure persistFail -> { case ManualFileCopyPersistenceFailure persistFail -> {
LOG.warn("Manuelle Dateikopie fehlgeschlagen: {}", persistFail.message()); LOG.warn(COPY_FAILED_LOG, persistFail.message());
showMessage("Persistenzfehler (Zielkopie wurde zurückgerollt): " showMessage("Persistenzfehler (Zielkopie wurde zurückgerollt): " + persistFail.message());
+ persistFail.message());
} }
} }
} }
private void applyCopySuccess(ManualFileCopySuccess success, GuiBatchRunResultRow row) {
LOG.info("Manuelle Dateikopie erfolgreich: {} → {} (Suffix: {})",
row.originalFileName(), success.appliedFileName(), success.conflictSuffixApplied());
GuiBatchRunResultRow updatedRow = buildSuccessRowAfterCopy(row, success.appliedFileName());
currentlySelectedRow = updatedRow;
fileNameEditor.clearDirtyState();
upsertResultRowByFingerprint(updatedRow);
fileNameEditor.loadSelection(updatedRow, targetFolderSupplier.get().orElse(""));
String msg = "Datei kopiert und gespeichert: " + success.appliedFileName();
if (success.conflictSuffixApplied()) {
msg += " (Suffix wegen Namenskonflikt angehängt)";
}
showMessage(msg);
refreshAggregateCountersFromItems();
}
private void applyCopyNoOpIdentical(ManualFileCopyNoOpIdenticalTarget noOp, GuiBatchRunResultRow row) {
LOG.info("Manuelle Dateikopie: identische Zieldatei {} bereits vorhanden kein Schreibvorgang.",
noOp.existingFileName());
GuiBatchRunResultRow updatedRow = buildSuccessRowAfterCopy(row, noOp.existingFileName());
currentlySelectedRow = updatedRow;
fileNameEditor.clearDirtyState();
upsertResultRowByFingerprint(updatedRow);
fileNameEditor.loadSelection(updatedRow, targetFolderSupplier.get().orElse(""));
showMessage("Identische Datei bereits vorhanden Status auf SUCCESS gesetzt");
refreshAggregateCountersFromItems();
}
/** /**
* Baut eine neue Zeilen-Sicht für ein Dokument, das per manueller Dateikopie auf * Baut eine neue Zeilen-Sicht für ein Dokument, das per manueller Dateikopie auf
* {@code SUCCESS} gehoben wurde. Status, korrigierter Dateiname und das Zurücksetzen * {@code SUCCESS} gehoben wurde. Status, korrigierter Dateiname und das Zurücksetzen
@@ -1105,24 +1110,24 @@ public final class GuiBatchRunTab {
noOp.existingFileName()); noOp.existingFileName());
} }
case ManualFileRenameDocumentNotFound notFound -> { case ManualFileRenameDocumentNotFound notFound -> {
LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", notFound.reason()); LOG.warn(RENAME_FAILED_LOG, notFound.reason());
showMessage("Fehler: Dokument nicht gefunden " + notFound.reason()); showMessage("Fehler: Dokument nicht gefunden " + notFound.reason());
} }
case ManualFileRenameInvalidState invalidState -> { case ManualFileRenameInvalidState invalidState -> {
LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", invalidState.reason()); LOG.warn(RENAME_FAILED_LOG, invalidState.reason());
showMessage("Fehler: Ungültiger Dokumentstatus " + invalidState.reason()); showMessage("Fehler: Ungültiger Dokumentstatus " + invalidState.reason());
} }
case ManualFileRenameSourceFileMissing sourceMissing -> { case ManualFileRenameSourceFileMissing sourceMissing -> {
LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", LOG.warn(RENAME_FAILED_LOG,
sourceMissing.expectedFileName()); sourceMissing.expectedFileName());
showMessage("Zieldatei nicht gefunden Umbenennung nicht möglich"); showMessage("Zieldatei nicht gefunden Umbenennung nicht möglich");
} }
case ManualFileRenameFileSystemFailure fsFail -> { case ManualFileRenameFileSystemFailure fsFail -> {
LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", fsFail.message()); LOG.warn(RENAME_FAILED_LOG, fsFail.message());
showMessage("Dateisystemfehler: " + fsFail.message()); showMessage("Dateisystemfehler: " + fsFail.message());
} }
case ManualFileRenamePersistenceFailure persistFail -> { case ManualFileRenamePersistenceFailure persistFail -> {
LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", persistFail.message()); LOG.warn(RENAME_FAILED_LOG, persistFail.message());
showMessage("Persistenzfehler (Dateisystem wurde zurückgerollt): " showMessage("Persistenzfehler (Dateisystem wurde zurückgerollt): "
+ persistFail.message()); + persistFail.message());
} }
@@ -1263,7 +1268,7 @@ public final class GuiBatchRunTab {
return; return;
} }
fileNameEditor.discardChanges(); fileNameEditor.discardChanges();
LOG.debug("Dateiname-Editor: Ungespeicherte Änderung Benutzer hat verworfen"); LOG.debug(DIRTY_STATE_MSG);
} }
if (!savedConfigurationReadyCheck.getAsBoolean()) { if (!savedConfigurationReadyCheck.getAsBoolean()) {
showMessage(NO_SAVED_CONFIGURATION_HINT); showMessage(NO_SAVED_CONFIGURATION_HINT);
@@ -1317,7 +1322,7 @@ public final class GuiBatchRunTab {
return; return;
} }
fileNameEditor.discardChanges(); fileNameEditor.discardChanges();
LOG.debug("Dateiname-Editor: Ungespeicherte Änderung Benutzer hat verworfen"); LOG.debug(DIRTY_STATE_MSG);
} }
if (!savedConfigurationReadyCheck.getAsBoolean()) { if (!savedConfigurationReadyCheck.getAsBoolean()) {
showMessage(NO_SAVED_CONFIGURATION_HINT); showMessage(NO_SAVED_CONFIGURATION_HINT);
@@ -1562,35 +1567,12 @@ public final class GuiBatchRunTab {
return builder.toString(); return builder.toString();
} }
if (row.status() == DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED) { if (row.status() == DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED) {
builder.append('\n'); return appendSkippedAlreadyProcessed(builder, row);
row.historicalContext().ifPresentOrElse(ctx -> {
ctx.lastSuccessInstant().ifPresentOrElse(
instant -> builder.append("Bereits erfolgreich verarbeitet am ")
.append(DETAIL_DATE_FORMAT.format(
instant.atZone(ZoneId.systemDefault())))
.append('.'),
() -> builder.append("Bereits erfolgreich verarbeitet."));
ctx.lastTargetFileName().ifPresent(name ->
builder.append('\n').append("Zieldatei: ").append(name).append('.'));
}, () -> builder.append("Bereits erfolgreich verarbeitet."));
return builder.toString();
} }
if (row.status() == DocumentCompletionStatus.SKIPPED_FINAL_FAILURE) { if (row.status() == DocumentCompletionStatus.SKIPPED_FINAL_FAILURE) {
builder.append('\n'); return appendSkippedFinalFailure(builder, row);
row.historicalContext().ifPresentOrElse(ctx ->
ctx.lastFailureInstant().ifPresentOrElse(
instant -> builder.append("Endg\u00fcltig fehlgeschlagen am ")
.append(DETAIL_DATE_FORMAT.format(
instant.atZone(ZoneId.systemDefault())))
.append(". Erneute Verarbeitung nur nach Reset m\u00f6glich."),
() -> builder.append(
"Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich.")),
() -> builder.append(
"Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich."));
return builder.toString();
} }
if (row.status() == DocumentCompletionStatus.FAILED_PERMANENT) { if (row.status() == DocumentCompletionStatus.FAILED_PERMANENT) {
// Erweiterter Erkl\u00e4rungstext gem\u00e4\u00df Spezifikation #51 \u2013 dauerhaft fehlgeschlagen
builder.append('\n').append(ProcessingStatusPresentation.DETAIL_TEXT_FAILED_PERMANENT); builder.append('\n').append(ProcessingStatusPresentation.DETAIL_TEXT_FAILED_PERMANENT);
row.aiFailureMessage().ifPresent(msg -> row.aiFailureMessage().ifPresent(msg ->
builder.append("\n\nFehlerdetail: ") builder.append("\n\nFehlerdetail: ")
@@ -1611,6 +1593,34 @@ public final class GuiBatchRunTab {
return builder.toString(); return builder.toString();
} }
private static String appendSkippedAlreadyProcessed(StringBuilder builder, GuiBatchRunResultRow row) {
builder.append('\n');
row.historicalContext().ifPresentOrElse(ctx -> {
ctx.lastSuccessInstant().ifPresentOrElse(
instant -> builder.append("Bereits erfolgreich verarbeitet am ")
.append(DETAIL_DATE_FORMAT.format(instant.atZone(ZoneId.systemDefault())))
.append('.'),
() -> builder.append("Bereits erfolgreich verarbeitet."));
ctx.lastTargetFileName().ifPresent(name ->
builder.append('\n').append("Zieldatei: ").append(name).append('.'));
}, () -> builder.append("Bereits erfolgreich verarbeitet."));
return builder.toString();
}
private static String appendSkippedFinalFailure(StringBuilder builder, GuiBatchRunResultRow row) {
builder.append('\n');
row.historicalContext().ifPresentOrElse(ctx ->
ctx.lastFailureInstant().ifPresentOrElse(
instant -> builder.append("Endg\u00fcltig fehlgeschlagen am ")
.append(DETAIL_DATE_FORMAT.format(instant.atZone(ZoneId.systemDefault())))
.append(". Erneute Verarbeitung nur nach Reset m\u00f6glich."),
() -> builder.append(
"Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich.")),
() -> builder.append(
"Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich."));
return builder.toString();
}
private static GuiBatchRunLaunchOutcome rejectingMiniLaunch( private static GuiBatchRunLaunchOutcome rejectingMiniLaunch(
Path p, Set<DocumentFingerprint> f, Path p, Set<DocumentFingerprint> f,
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver o, de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver o,
@@ -20,6 +20,9 @@ import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
* Alle Methoden sind statisch. * Alle Methoden sind statisch.
*/ */
public final class ProcessingStatusPresentation { public final class ProcessingStatusPresentation {
private static final String STATUS_NOT_NULL = "status darf nicht null sein";
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Icons (Unicode-Zeichen, zuverlässig darstellbar unter Windows 10+) // Icons (Unicode-Zeichen, zuverlässig darstellbar unter Windows 10+)
@@ -166,7 +169,7 @@ public final class ProcessingStatusPresentation {
* @throws NullPointerException wenn {@code status} {@code null} ist * @throws NullPointerException wenn {@code status} {@code null} ist
*/ */
public static String iconFor(DocumentCompletionStatus status) { public static String iconFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein"); Objects.requireNonNull(status, STATUS_NOT_NULL);
return switch (status) { return switch (status) {
case SUCCESS -> ICON_SUCCESS; case SUCCESS -> ICON_SUCCESS;
case FAILED_RETRYABLE -> ICON_FAILED_RETRYABLE; case FAILED_RETRYABLE -> ICON_FAILED_RETRYABLE;
@@ -187,7 +190,7 @@ public final class ProcessingStatusPresentation {
* @throws NullPointerException wenn {@code status} {@code null} ist * @throws NullPointerException wenn {@code status} {@code null} ist
*/ */
public static String cssColorFor(DocumentCompletionStatus status) { public static String cssColorFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein"); Objects.requireNonNull(status, STATUS_NOT_NULL);
return switch (status) { return switch (status) {
case SUCCESS -> COLOR_SUCCESS; case SUCCESS -> COLOR_SUCCESS;
case FAILED_RETRYABLE -> COLOR_FAILED_RETRYABLE; case FAILED_RETRYABLE -> COLOR_FAILED_RETRYABLE;
@@ -205,7 +208,7 @@ public final class ProcessingStatusPresentation {
* @throws NullPointerException wenn {@code status} {@code null} ist * @throws NullPointerException wenn {@code status} {@code null} ist
*/ */
public static String tooltipFor(DocumentCompletionStatus status) { public static String tooltipFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein"); Objects.requireNonNull(status, STATUS_NOT_NULL);
return switch (status) { return switch (status) {
case SUCCESS -> TOOLTIP_SUCCESS; case SUCCESS -> TOOLTIP_SUCCESS;
case FAILED_RETRYABLE -> TOOLTIP_FAILED_RETRYABLE; case FAILED_RETRYABLE -> TOOLTIP_FAILED_RETRYABLE;
@@ -224,7 +227,7 @@ public final class ProcessingStatusPresentation {
* @throws NullPointerException wenn {@code status} {@code null} ist * @throws NullPointerException wenn {@code status} {@code null} ist
*/ */
public static String summaryCategoryFor(DocumentCompletionStatus status) { public static String summaryCategoryFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein"); Objects.requireNonNull(status, STATUS_NOT_NULL);
return switch (status) { return switch (status) {
case SUCCESS -> SUMMARY_CATEGORY_SUCCESS; case SUCCESS -> SUMMARY_CATEGORY_SUCCESS;
case FAILED_RETRYABLE -> SUMMARY_CATEGORY_FAILED_RETRYABLE; case FAILED_RETRYABLE -> SUMMARY_CATEGORY_FAILED_RETRYABLE;
@@ -243,7 +246,7 @@ public final class ProcessingStatusPresentation {
* @throws NullPointerException wenn {@code status} {@code null} ist * @throws NullPointerException wenn {@code status} {@code null} ist
*/ */
public static StatusVisuals visualsFor(DocumentCompletionStatus status) { public static StatusVisuals visualsFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein"); Objects.requireNonNull(status, STATUS_NOT_NULL);
return new StatusVisuals( return new StatusVisuals(
iconFor(status), iconFor(status),
cssColorFor(status), cssColorFor(status),
@@ -264,7 +267,7 @@ public final class ProcessingStatusPresentation {
* @throws NullPointerException wenn {@code status} {@code null} ist * @throws NullPointerException wenn {@code status} {@code null} ist
*/ */
public static String displayTextFor(ProcessingStatus status) { public static String displayTextFor(ProcessingStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein"); Objects.requireNonNull(status, STATUS_NOT_NULL);
return switch (status) { return switch (status) {
case SUCCESS -> "✓ Erfolgreich"; case SUCCESS -> "✓ Erfolgreich";
case FAILED_RETRYABLE -> "↻ Temporärer Fehler"; case FAILED_RETRYABLE -> "↻ Temporärer Fehler";
@@ -87,6 +87,11 @@ import javafx.scene.layout.VBox;
* Verarbeitungslaufs deaktiviert. * Verarbeitungslaufs deaktiviert.
*/ */
public final class GuiHistoryTab { public final class GuiHistoryTab {
private static final String BOLD_STYLE = "-fx-font-weight: bold;";
private static final String NO_ERROR_DETAILS_MSG = "Keine Fehlerdetails gespeichert.";
private static final String NO_CONFIG_LOADED_MSG = "Keine Konfiguration geladen.";
private static final Logger LOG = LogManager.getLogger(GuiHistoryTab.class); private static final Logger LOG = LogManager.getLogger(GuiHistoryTab.class);
@@ -421,20 +426,20 @@ public final class GuiHistoryTab {
addDetailRow(5, "Aktualisiert:", detailUpdatedLabel); addDetailRow(5, "Aktualisiert:", detailUpdatedLabel);
Label detailTitle = new Label("Dokument-Details"); Label detailTitle = new Label("Dokument-Details");
detailTitle.setStyle("-fx-font-weight: bold;"); detailTitle.setStyle(BOLD_STYLE);
// Versuche-Tabelle // Versuche-Tabelle
buildAttemptsTable(); buildAttemptsTable();
Label attemptsTitle = new Label("Verarbeitungsversuche"); Label attemptsTitle = new Label("Verarbeitungsversuche");
attemptsTitle.setStyle("-fx-font-weight: bold;"); attemptsTitle.setStyle(BOLD_STYLE);
// Fehlerursache (aus letztem Fehler-Versuch) // Fehlerursache (aus letztem Fehler-Versuch)
failureArea.setEditable(false); failureArea.setEditable(false);
failureArea.setWrapText(true); failureArea.setWrapText(true);
failureArea.setPrefRowCount(3); failureArea.setPrefRowCount(3);
failureArea.setPromptText("Keine Fehlerdetails gespeichert."); failureArea.setPromptText(NO_ERROR_DETAILS_MSG);
Label failureTitle = new Label("Fehlerursache (letzter Fehler-Versuch)"); Label failureTitle = new Label("Fehlerursache (letzter Fehler-Versuch)");
failureTitle.setStyle("-fx-font-weight: bold;"); failureTitle.setStyle(BOLD_STYLE);
failureArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_FAILURE_AREA)); failureArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_FAILURE_AREA));
@@ -445,7 +450,7 @@ public final class GuiHistoryTab {
reasoningArea.setText(DETAIL_PLACEHOLDER); reasoningArea.setText(DETAIL_PLACEHOLDER);
reasoningArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_REASONING_AREA)); reasoningArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_REASONING_AREA));
Label reasoningTitle = new Label("KI-Begründung (ausgewählter Versuch)"); Label reasoningTitle = new Label("KI-Begründung (ausgewählter Versuch)");
reasoningTitle.setStyle("-fx-font-weight: bold;"); reasoningTitle.setStyle(BOLD_STYLE);
VBox rightPane = new VBox(8, VBox rightPane = new VBox(8,
detailTitle, detailGrid, detailTitle, detailGrid,
@@ -579,7 +584,7 @@ public final class GuiHistoryTab {
Path configPath = configPathSupplier.get(); Path configPath = configPathSupplier.get();
if (configPath == null) { if (configPath == null) {
statusBarLabel.setText("Keine Konfiguration geladen bitte zuerst eine Konfigurationsdatei öffnen."); statusBarLabel.setText("Keine Konfiguration geladen bitte zuerst eine Konfigurationsdatei öffnen.");
overviewTable.setPlaceholder(new Label("Keine Konfiguration geladen.")); overviewTable.setPlaceholder(new Label(NO_CONFIG_LOADED_MSG));
return; return;
} }
@@ -666,7 +671,7 @@ public final class GuiHistoryTab {
Path configPath = configPathSupplier.get(); Path configPath = configPathSupplier.get();
if (configPath == null) { if (configPath == null) {
showInfo("Keine Konfiguration geladen."); showInfo(NO_CONFIG_LOADED_MSG);
return; return;
} }
@@ -674,28 +679,10 @@ public final class GuiHistoryTab {
.filter(r -> r.overallStatus() == ProcessingStatus.SUCCESS) .filter(r -> r.overallStatus() == ProcessingStatus.SUCCESS)
.count(); .count();
StringBuilder sb = new StringBuilder();
sb.append("Setzt den Status auf READY_FOR_AI zurück.\n");
sb.append("Fehlerzähler und letzter Fehlerzeitpunkt werden gelöscht.\n");
sb.append("Die Versuchshistorie bleibt vollständig erhalten.\n\n");
if (selectedItems.size() == 1) {
sb.append("Quelldatei: ").append(selectedItems.get(0).sourceFileName());
} else {
sb.append(selectedItems.size()).append(" Einträge werden zurückgesetzt.");
}
if (successCount > 0) {
sb.append("\n\nHinweis: ").append(successCount)
.append(" der ausgewählten Einträge ")
.append(successCount == 1 ? "hat" : "haben")
.append(" Status \"Erfolgreich\". ")
.append(successCount == 1 ? "Dieser Eintrag wird" : "Diese Einträge werden")
.append(" erneut verarbeitet.");
}
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setTitle("Status zurücksetzen"); confirm.setTitle("Status zurücksetzen");
confirm.setHeaderText("Status zurücksetzen?"); confirm.setHeaderText("Status zurücksetzen?");
confirm.setContentText(sb.toString()); confirm.setContentText(buildResetConfirmationText(selectedItems, successCount));
Optional<ButtonType> choice = confirm.showAndWait(); Optional<ButtonType> choice = confirm.showAndWait();
if (choice.isEmpty() || choice.get() != ButtonType.OK) return; if (choice.isEmpty() || choice.get() != ButtonType.OK) return;
@@ -729,6 +716,27 @@ public final class GuiHistoryTab {
}); });
} }
private static String buildResetConfirmationText(List<DocumentHistoryRow> selectedItems, long successCount) {
StringBuilder sb = new StringBuilder();
sb.append("Setzt den Status auf READY_FOR_AI zurück.\n");
sb.append("Fehlerzähler und letzter Fehlerzeitpunkt werden gelöscht.\n");
sb.append("Die Versuchshistorie bleibt vollständig erhalten.\n\n");
if (selectedItems.size() == 1) {
sb.append("Quelldatei: ").append(selectedItems.get(0).sourceFileName());
} else {
sb.append(selectedItems.size()).append(" Einträge werden zurückgesetzt.");
}
if (successCount > 0) {
sb.append("\n\nHinweis: ").append(successCount)
.append(" der ausgewählten Einträge ")
.append(successCount == 1 ? "hat" : "haben")
.append(" Status \"Erfolgreich\". ")
.append(successCount == 1 ? "Dieser Eintrag wird" : "Diese Einträge werden")
.append(" erneut verarbeitet.");
}
return sb.toString();
}
private void handleDeleteAction() { private void handleDeleteAction() {
if (runningCheck.getAsBoolean()) { if (runningCheck.getAsBoolean()) {
showInfo(LAUF_AKTIV_HINWEIS); showInfo(LAUF_AKTIV_HINWEIS);
@@ -741,7 +749,7 @@ public final class GuiHistoryTab {
Path configPath = configPathSupplier.get(); Path configPath = configPathSupplier.get();
if (configPath == null) { if (configPath == null) {
showInfo("Keine Konfiguration geladen."); showInfo(NO_CONFIG_LOADED_MSG);
return; return;
} }
@@ -818,23 +826,26 @@ public final class GuiHistoryTab {
// Fehlerursache aus letztem Fehler-Versuch anzeigen // Fehlerursache aus letztem Fehler-Versuch anzeigen
showLastFailureMessage(result.attempts(), record.overallStatus()); showLastFailureMessage(result.attempts(), record.overallStatus());
// Neuesten Versuch selektieren und Begründung anzeigen selectLatestAttemptAndShowReasoning(result.attempts());
if (!result.attempts().isEmpty()) { attemptsTable.getSelectionModel().selectedItemProperty().addListener(
ProcessingAttempt last = result.attempts().get(result.attempts().size() - 1); (obs, old, attempt) -> onAttemptSelected(attempt));
}
private void selectLatestAttemptAndShowReasoning(java.util.List<ProcessingAttempt> attempts) {
if (!attempts.isEmpty()) {
ProcessingAttempt last = attempts.get(attempts.size() - 1);
attemptsTable.getSelectionModel().select(last); attemptsTable.getSelectionModel().select(last);
showReasoning(last); showReasoning(last);
} else { } else {
reasoningArea.setText(""); reasoningArea.setText("");
reasoningArea.setPromptText(NO_REASONING_TEXT); reasoningArea.setPromptText(NO_REASONING_TEXT);
} }
}
// KI-Begründung bei Versuchs-Selektion aktualisieren private void onAttemptSelected(ProcessingAttempt attempt) {
attemptsTable.getSelectionModel().selectedItemProperty().addListener( if (attempt != null) {
(obs, old, attempt) -> { showReasoning(attempt);
if (attempt != null) { }
showReasoning(attempt);
}
});
} }
/** /**
@@ -865,7 +876,7 @@ public final class GuiHistoryTab {
failureArea.setText(failureMessage != null failureArea.setText(failureMessage != null
? AiFailureMessageTranslator.translate(failureMessage) : ""); ? AiFailureMessageTranslator.translate(failureMessage) : "");
failureArea.setPromptText("Keine Fehlerdetails gespeichert."); failureArea.setPromptText(NO_ERROR_DETAILS_MSG);
} }
private void showReasoning(ProcessingAttempt attempt) { private void showReasoning(ProcessingAttempt attempt) {
@@ -883,7 +894,7 @@ public final class GuiHistoryTab {
clearDetailFields(); clearDetailFields();
attemptsItems.clear(); attemptsItems.clear();
failureArea.setText(""); failureArea.setText("");
failureArea.setPromptText("Keine Fehlerdetails gespeichert."); failureArea.setPromptText(NO_ERROR_DETAILS_MSG);
reasoningArea.setText(DETAIL_PLACEHOLDER); reasoningArea.setText(DETAIL_PLACEHOLDER);
} }
@@ -906,7 +917,7 @@ public final class GuiHistoryTab {
private void addDetailRow(int row, String labelText, Label valueLabel) { private void addDetailRow(int row, String labelText, Label valueLabel) {
Label label = new Label(labelText); Label label = new Label(labelText);
label.setStyle("-fx-font-weight: bold;"); label.setStyle(BOLD_STYLE);
valueLabel.setMaxWidth(Double.MAX_VALUE); valueLabel.setMaxWidth(Double.MAX_VALUE);
GridPane.setHgrow(valueLabel, Priority.ALWAYS); GridPane.setHgrow(valueLabel, Priority.ALWAYS);
detailGrid.add(label, 0, row); detailGrid.add(label, 0, row);
@@ -119,9 +119,15 @@ class GuiBatchRunCoordinatorMiniRunTest {
void startReset_invokesResetPortAndDispatchesResult() { void startReset_invokesResetPortAndDispatchesResult() {
AtomicReference<ResetDocumentStatusResult> captured = new AtomicReference<>(); AtomicReference<ResetDocumentStatusResult> captured = new AtomicReference<>();
GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() { GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() {
@Override public void onRunStarted(RunId runId, int totalCandidates) { } @Override public void onRunStarted(RunId runId, int totalCandidates) {
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } // intentionally empty
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } }
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) {
// intentionally empty
}
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) {
// intentionally empty
}
@Override public void onResetCompleted(ResetDocumentStatusResult result) { @Override public void onResetCompleted(ResetDocumentStatusResult result) {
captured.set(result); captured.set(result);
} }
@@ -170,9 +176,15 @@ class GuiBatchRunCoordinatorMiniRunTest {
void startReset_portThrowsException_mapsToAllFailures() { void startReset_portThrowsException_mapsToAllFailures() {
AtomicReference<ResetDocumentStatusResult> captured = new AtomicReference<>(); AtomicReference<ResetDocumentStatusResult> captured = new AtomicReference<>();
GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() { GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() {
@Override public void onRunStarted(RunId runId, int totalCandidates) { } @Override public void onRunStarted(RunId runId, int totalCandidates) {
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } // intentionally empty
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } }
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) {
// intentionally empty
}
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) {
// intentionally empty
}
@Override public void onResetCompleted(ResetDocumentStatusResult result) { @Override public void onResetCompleted(ResetDocumentStatusResult result) {
captured.set(result); captured.set(result);
} }
@@ -198,9 +210,15 @@ class GuiBatchRunCoordinatorMiniRunTest {
void listenerDefaultOnResetCompleted_doesNotThrow() { void listenerDefaultOnResetCompleted_doesNotThrow() {
// Verify the default implementation is safe to call. // Verify the default implementation is safe to call.
GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() { GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() {
@Override public void onRunStarted(RunId runId, int totalCandidates) { } @Override public void onRunStarted(RunId runId, int totalCandidates) {
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } // intentionally empty
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } }
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) {
// intentionally empty
}
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) {
// intentionally empty
}
}; };
listener.onResetCompleted(new ResetDocumentStatusResult(0, Set.of(), Map.of())); listener.onResetCompleted(new ResetDocumentStatusResult(0, Set.of(), Map.of()));
} }
@@ -223,9 +241,15 @@ class GuiBatchRunCoordinatorMiniRunTest {
private static GuiBatchRunCoordinator.Listener noOpListener() { private static GuiBatchRunCoordinator.Listener noOpListener() {
return new GuiBatchRunCoordinator.Listener() { return new GuiBatchRunCoordinator.Listener() {
@Override public void onRunStarted(RunId runId, int totalCandidates) { } @Override public void onRunStarted(RunId runId, int totalCandidates) {
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } // intentionally empty
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } }
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) {
// intentionally empty
}
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) {
// intentionally empty
}
}; };
} }
@@ -247,8 +247,12 @@ class GuiBatchRunCoordinatorTest {
GuiBatchRunCoordinator coordinator = new GuiBatchRunCoordinator( GuiBatchRunCoordinator coordinator = new GuiBatchRunCoordinator(
launcher, syncThreadFactory(), syncDispatcher(), launcher, syncThreadFactory(), syncDispatcher(),
new GuiBatchRunCoordinator.Listener() { new GuiBatchRunCoordinator.Listener() {
@Override public void onRunStarted(RunId runId, int totalCandidates) { } @Override public void onRunStarted(RunId runId, int totalCandidates) {
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } // intentionally empty
}
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) {
// intentionally empty
}
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) {
captured.set(outcome); captured.set(outcome);
} }
@@ -270,8 +274,12 @@ class GuiBatchRunCoordinatorTest {
GuiBatchRunCoordinator coordinator = new GuiBatchRunCoordinator( GuiBatchRunCoordinator coordinator = new GuiBatchRunCoordinator(
launcher, syncThreadFactory(), syncDispatcher(), launcher, syncThreadFactory(), syncDispatcher(),
new GuiBatchRunCoordinator.Listener() { new GuiBatchRunCoordinator.Listener() {
@Override public void onRunStarted(RunId runId, int totalCandidates) { } @Override public void onRunStarted(RunId runId, int totalCandidates) {
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } // intentionally empty
}
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) {
// intentionally empty
}
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) {
captured.set(outcome); captured.set(outcome);
} }
@@ -322,9 +330,15 @@ class GuiBatchRunCoordinatorTest {
private static GuiBatchRunCoordinator.Listener noOpListener() { private static GuiBatchRunCoordinator.Listener noOpListener() {
return new GuiBatchRunCoordinator.Listener() { return new GuiBatchRunCoordinator.Listener() {
@Override public void onRunStarted(RunId runId, int totalCandidates) { } @Override public void onRunStarted(RunId runId, int totalCandidates) {
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } // intentionally empty
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } }
@Override public void onDocumentCompleted(GuiBatchRunResultRow row) {
// intentionally empty
}
@Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) {
// intentionally empty
}
}; };
} }
@@ -95,6 +95,10 @@ import de.gecheckt.pdf.umbenenner.domain.model.AiRequestRepresentation;
* </ul> * </ul>
*/ */
public class OpenAiHttpAdapter implements AiInvocationPort { public class OpenAiHttpAdapter implements AiInvocationPort {
private static final String NO_CHOICE_CONTENT_SENTINEL = "NO_CHOICE_CONTENT";
private static final String JSON_KEY_CONTENT = "content";
private static final Logger LOG = LogManager.getLogger(OpenAiHttpAdapter.class); private static final Logger LOG = LogManager.getLogger(OpenAiHttpAdapter.class);
@@ -248,20 +252,20 @@ public class OpenAiHttpAdapter implements AiInvocationPort {
JSONArray choices = json.optJSONArray("choices"); JSONArray choices = json.optJSONArray("choices");
if (choices == null || choices.isEmpty()) { if (choices == null || choices.isEmpty()) {
LOG.warn("OpenAI response contained no choices"); LOG.warn("OpenAI response contained no choices");
return new AiInvocationTechnicalFailure(request, "NO_CHOICE_CONTENT", return new AiInvocationTechnicalFailure(request, NO_CHOICE_CONTENT_SENTINEL,
"OpenAI response contained no choices"); "OpenAI response contained no choices");
} }
JSONObject firstChoice = choices.getJSONObject(0); JSONObject firstChoice = choices.getJSONObject(0);
JSONObject message = firstChoice.optJSONObject("message"); JSONObject message = firstChoice.optJSONObject("message");
if (message == null) { if (message == null) {
LOG.warn("OpenAI response choice contained no message"); LOG.warn("OpenAI response choice contained no message");
return new AiInvocationTechnicalFailure(request, "NO_CHOICE_CONTENT", return new AiInvocationTechnicalFailure(request, NO_CHOICE_CONTENT_SENTINEL,
"OpenAI response choice contained no message"); "OpenAI response choice contained no message");
} }
String content = message.optString("content", null); String content = message.optString(JSON_KEY_CONTENT, null);
if (content == null || content.isBlank()) { if (content == null || content.isBlank()) {
LOG.warn("OpenAI response message.content is absent or blank"); LOG.warn("OpenAI response message.content is absent or blank");
return new AiInvocationTechnicalFailure(request, "NO_CHOICE_CONTENT", return new AiInvocationTechnicalFailure(request, NO_CHOICE_CONTENT_SENTINEL,
"OpenAI response message.content is absent or blank"); "OpenAI response message.content is absent or blank");
} }
return new AiInvocationSuccess(request, new AiRawResponse(content)); return new AiInvocationSuccess(request, new AiRawResponse(content));
@@ -347,11 +351,11 @@ public class OpenAiHttpAdapter implements AiInvocationPort {
JSONObject systemMessage = new JSONObject(); JSONObject systemMessage = new JSONObject();
systemMessage.put("role", "system"); systemMessage.put("role", "system");
systemMessage.put("content", request.promptContent()); systemMessage.put(JSON_KEY_CONTENT, request.promptContent());
JSONObject userMessage = new JSONObject(); JSONObject userMessage = new JSONObject();
userMessage.put("role", "user"); userMessage.put("role", "user");
userMessage.put("content", request.documentText()); userMessage.put(JSON_KEY_CONTENT, request.documentText());
body.put("messages", new org.json.JSONArray() body.put("messages", new org.json.JSONArray()
.put(systemMessage) .put(systemMessage)
@@ -46,6 +46,10 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalog
* </ul> * </ul>
*/ */
public class ClaudeModelCatalogAdapter implements AiModelCatalogPort { public class ClaudeModelCatalogAdapter implements AiModelCatalogPort {
private static final String FAILURE_CODE_CONNECTION = "CONNECTION_FAILURE";
private static final String FAILURE_CODE_UNKNOWN = "UNKNOWN";
private static final Logger LOG = LogManager.getLogger(ClaudeModelCatalogAdapter.class); private static final Logger LOG = LogManager.getLogger(ClaudeModelCatalogAdapter.class);
@@ -133,28 +137,28 @@ public class ClaudeModelCatalogAdapter implements AiModelCatalogPort {
} catch (java.net.http.HttpTimeoutException e) { } catch (java.net.http.HttpTimeoutException e) {
LOG.warn("Claude model catalogue: request timed out {}", e.getMessage()); LOG.warn("Claude model catalogue: request timed out {}", e.getMessage());
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Zeitüberschreitung beim Modellabruf: " + e.getMessage()); "Zeitüberschreitung beim Modellabruf: " + e.getMessage());
} catch (java.net.ConnectException e) { } catch (java.net.ConnectException e) {
LOG.warn("Claude model catalogue: connection failed {}", e.getMessage()); LOG.warn("Claude model catalogue: connection failed {}", e.getMessage());
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage()); "Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage());
} catch (java.net.UnknownHostException e) { } catch (java.net.UnknownHostException e) {
LOG.warn("Claude model catalogue: hostname not resolvable {}", e.getMessage()); LOG.warn("Claude model catalogue: hostname not resolvable {}", e.getMessage());
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Hostname nicht auflösbar: " + e.getMessage()); "Hostname nicht auflösbar: " + e.getMessage());
} catch (java.io.IOException e) { } catch (java.io.IOException e) {
LOG.warn("Claude model catalogue: IO error {}", e.getMessage()); LOG.warn("Claude model catalogue: IO error {}", e.getMessage());
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"E/A-Fehler beim Modellabruf: " + e.getMessage()); "E/A-Fehler beim Modellabruf: " + e.getMessage());
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.warn("Claude model catalogue: request interrupted"); LOG.warn("Claude model catalogue: request interrupted");
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Modellabruf wurde unterbrochen."); "Modellabruf wurde unterbrochen.");
} catch (Exception e) { } catch (Exception e) {
LOG.error("Claude model catalogue: unexpected error", e); LOG.error("Claude model catalogue: unexpected error", e);
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "UNKNOWN", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
"Unerwarteter Fehler: " + e.getMessage()); "Unerwarteter Fehler: " + e.getMessage());
} }
} }
@@ -188,7 +192,7 @@ public class ClaudeModelCatalogAdapter implements AiModelCatalogPort {
if (status != 200) { if (status != 200) {
LOG.warn("Claude model catalogue: unexpected HTTP status {}", status); LOG.warn("Claude model catalogue: unexpected HTTP status {}", status);
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "UNKNOWN", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
"Unerwarteter HTTP-Status: " + status); "Unerwarteter HTTP-Status: " + status);
} }
@@ -291,24 +295,24 @@ public class ClaudeModelCatalogAdapter implements AiModelCatalogPort {
return handleResponse(response); return handleResponse(response);
} catch (java.net.http.HttpTimeoutException e) { } catch (java.net.http.HttpTimeoutException e) {
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Zeitüberschreitung beim Modellabruf: " + e.getMessage()); "Zeitüberschreitung beim Modellabruf: " + e.getMessage());
} catch (java.net.ConnectException e) { } catch (java.net.ConnectException e) {
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage()); "Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage());
} catch (java.net.UnknownHostException e) { } catch (java.net.UnknownHostException e) {
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Hostname nicht auflösbar: " + e.getMessage()); "Hostname nicht auflösbar: " + e.getMessage());
} catch (java.io.IOException e) { } catch (java.io.IOException e) {
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"E/A-Fehler beim Modellabruf: " + e.getMessage()); "E/A-Fehler beim Modellabruf: " + e.getMessage());
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Modellabruf wurde unterbrochen."); "Modellabruf wurde unterbrochen.");
} catch (Exception e) { } catch (Exception e) {
LOG.error("Claude model catalogue: unexpected error", e); LOG.error("Claude model catalogue: unexpected error", e);
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "UNKNOWN", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
"Unerwarteter Fehler: " + e.getMessage()); "Unerwarteter Fehler: " + e.getMessage());
} }
} }
@@ -46,6 +46,10 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalog
* </ul> * </ul>
*/ */
public class OpenAiCompatibleModelCatalogAdapter implements AiModelCatalogPort { public class OpenAiCompatibleModelCatalogAdapter implements AiModelCatalogPort {
private static final String FAILURE_CODE_CONNECTION = "CONNECTION_FAILURE";
private static final String FAILURE_CODE_UNKNOWN = "UNKNOWN";
private static final Logger LOG = LogManager.getLogger(OpenAiCompatibleModelCatalogAdapter.class); private static final Logger LOG = LogManager.getLogger(OpenAiCompatibleModelCatalogAdapter.class);
@@ -129,28 +133,28 @@ public class OpenAiCompatibleModelCatalogAdapter implements AiModelCatalogPort {
} catch (java.net.http.HttpTimeoutException e) { } catch (java.net.http.HttpTimeoutException e) {
LOG.warn("OpenAI-compatible model catalogue: request timed out {}", e.getMessage()); LOG.warn("OpenAI-compatible model catalogue: request timed out {}", e.getMessage());
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Zeitüberschreitung beim Modellabruf: " + e.getMessage()); "Zeitüberschreitung beim Modellabruf: " + e.getMessage());
} catch (java.net.ConnectException e) { } catch (java.net.ConnectException e) {
LOG.warn("OpenAI-compatible model catalogue: connection failed {}", e.getMessage()); LOG.warn("OpenAI-compatible model catalogue: connection failed {}", e.getMessage());
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage()); "Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage());
} catch (java.net.UnknownHostException e) { } catch (java.net.UnknownHostException e) {
LOG.warn("OpenAI-compatible model catalogue: hostname not resolvable {}", e.getMessage()); LOG.warn("OpenAI-compatible model catalogue: hostname not resolvable {}", e.getMessage());
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Hostname nicht auflösbar: " + e.getMessage()); "Hostname nicht auflösbar: " + e.getMessage());
} catch (java.io.IOException e) { } catch (java.io.IOException e) {
LOG.warn("OpenAI-compatible model catalogue: IO error {}", e.getMessage()); LOG.warn("OpenAI-compatible model catalogue: IO error {}", e.getMessage());
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"E/A-Fehler beim Modellabruf: " + e.getMessage()); "E/A-Fehler beim Modellabruf: " + e.getMessage());
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.warn("OpenAI-compatible model catalogue: request interrupted"); LOG.warn("OpenAI-compatible model catalogue: request interrupted");
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Modellabruf wurde unterbrochen."); "Modellabruf wurde unterbrochen.");
} catch (Exception e) { } catch (Exception e) {
LOG.error("OpenAI-compatible model catalogue: unexpected error", e); LOG.error("OpenAI-compatible model catalogue: unexpected error", e);
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "UNKNOWN", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
"Unerwarteter Fehler: " + e.getMessage()); "Unerwarteter Fehler: " + e.getMessage());
} }
} }
@@ -184,7 +188,7 @@ public class OpenAiCompatibleModelCatalogAdapter implements AiModelCatalogPort {
if (status != 200) { if (status != 200) {
LOG.warn("OpenAI-compatible model catalogue: unexpected HTTP status {}", status); LOG.warn("OpenAI-compatible model catalogue: unexpected HTTP status {}", status);
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "UNKNOWN", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
"Unerwarteter HTTP-Status: " + status); "Unerwarteter HTTP-Status: " + status);
} }
@@ -285,24 +289,24 @@ public class OpenAiCompatibleModelCatalogAdapter implements AiModelCatalogPort {
return handleResponse(response); return handleResponse(response);
} catch (java.net.http.HttpTimeoutException e) { } catch (java.net.http.HttpTimeoutException e) {
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Zeitüberschreitung beim Modellabruf: " + e.getMessage()); "Zeitüberschreitung beim Modellabruf: " + e.getMessage());
} catch (java.net.ConnectException e) { } catch (java.net.ConnectException e) {
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage()); "Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage());
} catch (java.net.UnknownHostException e) { } catch (java.net.UnknownHostException e) {
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Hostname nicht auflösbar: " + e.getMessage()); "Hostname nicht auflösbar: " + e.getMessage());
} catch (java.io.IOException e) { } catch (java.io.IOException e) {
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"E/A-Fehler beim Modellabruf: " + e.getMessage()); "E/A-Fehler beim Modellabruf: " + e.getMessage());
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
"Modellabruf wurde unterbrochen."); "Modellabruf wurde unterbrochen.");
} catch (Exception e) { } catch (Exception e) {
LOG.error("OpenAI-compatible model catalogue: unexpected error", e); LOG.error("OpenAI-compatible model catalogue: unexpected error", e);
return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "UNKNOWN", return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
"Unerwarteter Fehler: " + e.getMessage()); "Unerwarteter Fehler: " + e.getMessage());
} }
} }
@@ -48,6 +48,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.PromptIdentifier;
* werden propagiert. * werden propagiert.
*/ */
public class FilesystemPromptPortAdapter implements PromptPort { public class FilesystemPromptPortAdapter implements PromptPort {
private static final String SAVE_FAILED_LOG_MSG = "Prompt speichern fehlgeschlagen: {}";
private static final Logger LOG = LogManager.getLogger(FilesystemPromptPortAdapter.class); private static final Logger LOG = LogManager.getLogger(FilesystemPromptPortAdapter.class);
@@ -125,7 +127,7 @@ public class FilesystemPromptPortAdapter implements PromptPort {
if (targetDir == null || !Files.isDirectory(targetDir)) { if (targetDir == null || !Files.isDirectory(targetDir)) {
String message = "Zielordner der Prompt-Datei existiert nicht: " String message = "Zielordner der Prompt-Datei existiert nicht: "
+ (targetDir != null ? targetDir.toAbsolutePath() : "unbekannt"); + (targetDir != null ? targetDir.toAbsolutePath() : "unbekannt");
LOG.warn("Prompt speichern fehlgeschlagen: {}", message); LOG.warn(SAVE_FAILED_LOG_MSG, message);
return new PromptSaveResult.TargetDirectoryMissing(message); return new PromptSaveResult.TargetDirectoryMissing(message);
} }
@@ -138,7 +140,7 @@ public class FilesystemPromptPortAdapter implements PromptPort {
} catch (IOException e) { } catch (IOException e) {
beräumeTempDatei(tempFile); beräumeTempDatei(tempFile);
String message = "Fehler beim Schreiben der temporären Prompt-Datei: " + e.getMessage(); String message = "Fehler beim Schreiben der temporären Prompt-Datei: " + e.getMessage();
LOG.warn("Prompt speichern fehlgeschlagen: {}", message, e); LOG.warn(SAVE_FAILED_LOG_MSG, message, e);
return new PromptSaveResult.WriteFailed(message, e); return new PromptSaveResult.WriteFailed(message, e);
} }
@@ -155,7 +157,7 @@ public class FilesystemPromptPortAdapter implements PromptPort {
} catch (IOException e) { } catch (IOException e) {
beräumeTempDatei(tempFile); beräumeTempDatei(tempFile);
String message = "Fehler beim atomaren Verschieben der Prompt-Datei: " + e.getMessage(); String message = "Fehler beim atomaren Verschieben der Prompt-Datei: " + e.getMessage();
LOG.warn("Prompt speichern fehlgeschlagen: {}", message, e); LOG.warn(SAVE_FAILED_LOG_MSG, message, e);
return new PromptSaveResult.AtomicMoveFailed(message); return new PromptSaveResult.AtomicMoveFailed(message);
} }
} }
@@ -43,6 +43,8 @@ import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceC
* Ausnahmen an den Aufrufer weitergegeben. * Ausnahmen an den Aufrufer weitergegeben.
*/ */
public class FilesystemResourceCreationAdapter implements ResourceCreationPort { public class FilesystemResourceCreationAdapter implements ResourceCreationPort {
private static final String INVALID_PATH_PREFIX = "Ungültiger Pfad: ";
private static final Logger LOG = LogManager.getLogger(FilesystemResourceCreationAdapter.class); private static final Logger LOG = LogManager.getLogger(FilesystemResourceCreationAdapter.class);
@@ -66,7 +68,7 @@ public class FilesystemResourceCreationAdapter implements ResourceCreationPort {
public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory suggestion) { public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory suggestion) {
Path path = toPath(suggestion.path()); Path path = toPath(suggestion.path());
if (path == null) { if (path == null) {
String msg = "Ungültiger Pfad: " + suggestion.path(); String msg = INVALID_PATH_PREFIX + suggestion.path();
LOG.warn("Ordner anlegen fehlgeschlagen: {}", msg); LOG.warn("Ordner anlegen fehlgeschlagen: {}", msg);
return new CorrectionOutcome.Failed(suggestion, msg); return new CorrectionOutcome.Failed(suggestion, msg);
} }
@@ -114,7 +116,7 @@ public class FilesystemResourceCreationAdapter implements ResourceCreationPort {
public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile suggestion) { public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile suggestion) {
Path path = toPath(suggestion.path()); Path path = toPath(suggestion.path());
if (path == null) { if (path == null) {
String msg = "Ungültiger Pfad: " + suggestion.path(); String msg = INVALID_PATH_PREFIX + suggestion.path();
LOG.warn("Prompt-Datei erzeugen fehlgeschlagen: {}", msg); LOG.warn("Prompt-Datei erzeugen fehlgeschlagen: {}", msg);
return new CorrectionOutcome.Failed(suggestion, msg); return new CorrectionOutcome.Failed(suggestion, msg);
} }
@@ -164,7 +166,7 @@ public class FilesystemResourceCreationAdapter implements ResourceCreationPort {
public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath suggestion) { public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath suggestion) {
Path path = toPath(suggestion.path()); Path path = toPath(suggestion.path());
if (path == null) { if (path == null) {
String msg = "Ungültiger Pfad: " + suggestion.path(); String msg = INVALID_PATH_PREFIX + suggestion.path();
LOG.warn("SQLite-Pfad vorbereiten fehlgeschlagen: {}", msg); LOG.warn("SQLite-Pfad vorbereiten fehlgeschlagen: {}", msg);
return new CorrectionOutcome.Failed(suggestion, msg); return new CorrectionOutcome.Failed(suggestion, msg);
} }
@@ -43,6 +43,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
* application/domain type. * application/domain type.
*/ */
public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttemptRepository { public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttemptRepository {
private static final String FINGERPRINT_NOT_NULL = "fingerprint must not be null";
private static final Logger logger = LogManager.getLogger(SqliteProcessingAttemptRepositoryAdapter.class); private static final Logger logger = LogManager.getLogger(SqliteProcessingAttemptRepositoryAdapter.class);
@@ -78,7 +80,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
*/ */
@Override @Override
public int loadNextAttemptNumber(DocumentFingerprint fingerprint) { public int loadNextAttemptNumber(DocumentFingerprint fingerprint) {
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL);
String sql = """ String sql = """
SELECT COALESCE(MAX(attempt_number), 0) + 1 AS next_attempt_number SELECT COALESCE(MAX(attempt_number), 0) + 1 AS next_attempt_number
@@ -204,7 +206,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
*/ */
@Override @Override
public List<ProcessingAttempt> findAllByFingerprint(DocumentFingerprint fingerprint) { public List<ProcessingAttempt> findAllByFingerprint(DocumentFingerprint fingerprint) {
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL);
String sql = """ String sql = """
SELECT SELECT
@@ -255,7 +257,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
*/ */
@Override @Override
public ProcessingAttempt findLatestProposalReadyAttempt(DocumentFingerprint fingerprint) { public ProcessingAttempt findLatestProposalReadyAttempt(DocumentFingerprint fingerprint) {
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL);
String sql = """ String sql = """
SELECT SELECT
@@ -422,7 +424,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
*/ */
@Override @Override
public void deleteAllByFingerprint(DocumentFingerprint fingerprint) { public void deleteAllByFingerprint(DocumentFingerprint fingerprint) {
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL);
String sql = "DELETE FROM processing_attempt WHERE fingerprint = ?"; String sql = "DELETE FROM processing_attempt WHERE fingerprint = ?";
@@ -62,6 +62,11 @@ import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitiali
* Domain-/Application-Typen erscheinen keine JDBC- oder SQLite-Typen. * Domain-/Application-Typen erscheinen keine JDBC- oder SQLite-Typen.
*/ */
public class SqliteSchemaInitializationAdapter implements PersistenceSchemaInitializationPort { public class SqliteSchemaInitializationAdapter implements PersistenceSchemaInitializationPort {
private static final String TABLE_DOCUMENT_RECORD = "document_record";
private static final String TABLE_PROCESSING_ATTEMPT = "processing_attempt";
private static final String COL_FINGERPRINT = "fingerprint";
private static final Logger logger = LogManager.getLogger(SqliteSchemaInitializationAdapter.class); private static final Logger logger = LogManager.getLogger(SqliteSchemaInitializationAdapter.class);
@@ -71,7 +76,7 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti
/** Alle erwarteten Spalten der Tabelle {@code document_record}. */ /** Alle erwarteten Spalten der Tabelle {@code document_record}. */
private static final Set<String> EXPECTED_COLUMNS_DOCUMENT_RECORD = Set.of( private static final Set<String> EXPECTED_COLUMNS_DOCUMENT_RECORD = Set.of(
"id", "fingerprint", "last_known_source_locator", "last_known_source_file_name", "id", COL_FINGERPRINT, "last_known_source_locator", "last_known_source_file_name",
"overall_status", "content_error_count", "transient_error_count", "overall_status", "content_error_count", "transient_error_count",
"last_failure_instant", "last_success_instant", "created_at", "updated_at", "last_failure_instant", "last_success_instant", "created_at", "updated_at",
"last_target_path", "last_target_file_name" "last_target_path", "last_target_file_name"
@@ -79,7 +84,7 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti
/** Alle erwarteten Spalten der Tabelle {@code processing_attempt}. */ /** Alle erwarteten Spalten der Tabelle {@code processing_attempt}. */
private static final Set<String> EXPECTED_COLUMNS_PROCESSING_ATTEMPT = Set.of( private static final Set<String> EXPECTED_COLUMNS_PROCESSING_ATTEMPT = Set.of(
"id", "fingerprint", "run_id", "attempt_number", "started_at", "ended_at", "id", COL_FINGERPRINT, "run_id", "attempt_number", "started_at", "ended_at",
"status", "failure_class", "failure_message", "retryable", "status", "failure_class", "failure_message", "retryable",
"model_name", "prompt_identifier", "processed_page_count", "sent_character_count", "model_name", "prompt_identifier", "processed_page_count", "sent_character_count",
"ai_raw_response", "ai_reasoning", "resolved_date", "date_source", "ai_raw_response", "ai_reasoning", "resolved_date", "date_source",
@@ -286,8 +291,8 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti
return DbState.FLYWAY_MANAGED; return DbState.FLYWAY_MANAGED;
} }
// "Leer" = keine Tabellen vorhanden (unabhängig von Dateigröße) // "Leer" = keine Tabellen vorhanden (unabhängig von Dateigröße)
boolean hasFachlicheTabellen = tables.contains("document_record") boolean hasFachlicheTabellen = tables.contains(TABLE_DOCUMENT_RECORD)
|| tables.contains("processing_attempt"); || tables.contains(TABLE_PROCESSING_ATTEMPT);
if (hasFachlicheTabellen) { if (hasFachlicheTabellen) {
return DbState.EXISTING_WITHOUT_FLYWAY; return DbState.EXISTING_WITHOUT_FLYWAY;
} }
@@ -320,25 +325,25 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti
// Tabellen prüfen // Tabellen prüfen
Set<String> tabellen = readTableNames(meta); Set<String> tabellen = readTableNames(meta);
if (!tabellen.contains("document_record")) { if (!tabellen.contains(TABLE_DOCUMENT_RECORD)) {
fehler.add("Tabelle 'document_record' fehlt"); fehler.add("Tabelle 'document_record' fehlt");
} }
if (!tabellen.contains("processing_attempt")) { if (!tabellen.contains(TABLE_PROCESSING_ATTEMPT)) {
fehler.add("Tabelle 'processing_attempt' fehlt"); fehler.add("Tabelle 'processing_attempt' fehlt");
} }
// Spalten prüfen nur wenn Tabellen vorhanden // Spalten prüfen nur wenn Tabellen vorhanden
if (tabellen.contains("document_record")) { if (tabellen.contains(TABLE_DOCUMENT_RECORD)) {
pruefeSpaltenvollstaendigkeit(meta, "document_record", pruefeSpaltenvollstaendigkeit(meta, TABLE_DOCUMENT_RECORD,
EXPECTED_COLUMNS_DOCUMENT_RECORD, fehler); EXPECTED_COLUMNS_DOCUMENT_RECORD, fehler);
} }
if (tabellen.contains("processing_attempt")) { if (tabellen.contains(TABLE_PROCESSING_ATTEMPT)) {
pruefeSpaltenvollstaendigkeit(meta, "processing_attempt", pruefeSpaltenvollstaendigkeit(meta, TABLE_PROCESSING_ATTEMPT,
EXPECTED_COLUMNS_PROCESSING_ATTEMPT, fehler); EXPECTED_COLUMNS_PROCESSING_ATTEMPT, fehler);
} }
// Indizes prüfen // Indizes prüfen
if (tabellen.contains("document_record") && tabellen.contains("processing_attempt")) { if (tabellen.contains(TABLE_DOCUMENT_RECORD) && tabellen.contains(TABLE_PROCESSING_ATTEMPT)) {
Set<String> vorhandeneIndizes = readIndexNames(meta); Set<String> vorhandeneIndizes = readIndexNames(meta);
for (String erwartetIndex : EXPECTED_INDEXES) { for (String erwartetIndex : EXPECTED_INDEXES) {
if (!vorhandeneIndizes.contains(erwartetIndex)) { if (!vorhandeneIndizes.contains(erwartetIndex)) {
@@ -348,10 +353,10 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti
} }
// Constraints prüfen (soweit per Metadata prüfbar) // Constraints prüfen (soweit per Metadata prüfbar)
if (tabellen.contains("document_record")) { if (tabellen.contains(TABLE_DOCUMENT_RECORD)) {
pruefeUniqueConstraintAufFingerprint(conn, fehler); pruefeUniqueConstraintAufFingerprint(conn, fehler);
} }
if (tabellen.contains("processing_attempt")) { if (tabellen.contains(TABLE_PROCESSING_ATTEMPT)) {
pruefeForeignKeyAufDocumentRecord(conn, fehler); pruefeForeignKeyAufDocumentRecord(conn, fehler);
} }
@@ -399,10 +404,10 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti
private void pruefeUniqueConstraintAufFingerprint(Connection conn, private void pruefeUniqueConstraintAufFingerprint(Connection conn,
List<String> fehler) throws SQLException { List<String> fehler) throws SQLException {
boolean uniqueGefunden = false; boolean uniqueGefunden = false;
try (ResultSet rs = conn.getMetaData().getIndexInfo(null, null, "document_record", true, false)) { try (ResultSet rs = conn.getMetaData().getIndexInfo(null, null, TABLE_DOCUMENT_RECORD, true, false)) {
while (rs.next()) { while (rs.next()) {
String spalte = rs.getString("COLUMN_NAME"); String spalte = rs.getString("COLUMN_NAME");
if ("fingerprint".equalsIgnoreCase(spalte)) { if (COL_FINGERPRINT.equalsIgnoreCase(spalte)) {
uniqueGefunden = true; uniqueGefunden = true;
break; break;
} }
@@ -424,12 +429,12 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti
private void pruefeForeignKeyAufDocumentRecord(Connection conn, private void pruefeForeignKeyAufDocumentRecord(Connection conn,
List<String> fehler) throws SQLException { List<String> fehler) throws SQLException {
boolean fkGefunden = false; boolean fkGefunden = false;
try (ResultSet rs = conn.getMetaData().getImportedKeys(null, null, "processing_attempt")) { try (ResultSet rs = conn.getMetaData().getImportedKeys(null, null, TABLE_PROCESSING_ATTEMPT)) {
while (rs.next()) { while (rs.next()) {
String pkTabelle = rs.getString("PKTABLE_NAME"); String pkTabelle = rs.getString("PKTABLE_NAME");
String fkSpalte = rs.getString("FKCOLUMN_NAME"); String fkSpalte = rs.getString("FKCOLUMN_NAME");
if ("document_record".equalsIgnoreCase(pkTabelle) if (TABLE_DOCUMENT_RECORD.equalsIgnoreCase(pkTabelle)
&& "fingerprint".equalsIgnoreCase(fkSpalte)) { && COL_FINGERPRINT.equalsIgnoreCase(fkSpalte)) {
fkGefunden = true; fkGefunden = true;
break; break;
} }
@@ -561,7 +566,7 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti
*/ */
private static Set<String> readIndexNames(DatabaseMetaData meta) throws SQLException { private static Set<String> readIndexNames(DatabaseMetaData meta) throws SQLException {
Set<String> names = new HashSet<>(); Set<String> names = new HashSet<>();
for (String tabelle : new String[]{"document_record", "processing_attempt"}) { for (String tabelle : new String[]{TABLE_DOCUMENT_RECORD, TABLE_PROCESSING_ATTEMPT}) {
try (ResultSet rs = meta.getIndexInfo(null, null, tabelle, false, false)) { try (ResultSet rs = meta.getIndexInfo(null, null, tabelle, false, false)) {
while (rs.next()) { while (rs.next()) {
String indexName = rs.getString("INDEX_NAME"); String indexName = rs.getString("INDEX_NAME");
@@ -24,6 +24,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* and processing attempt repositories. * and processing attempt repositories.
*/ */
public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort { public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
private static final String ROLLBACK_FAILED_MSG = "Rollback fehlgeschlagen: {}";
private static final Logger logger = LogManager.getLogger(SqliteUnitOfWorkAdapter.class); private static final Logger logger = LogManager.getLogger(SqliteUnitOfWorkAdapter.class);
@@ -57,7 +59,7 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
connection.rollback(); connection.rollback();
logger.debug("Transaktion zurückgerollt (Dokumentfehler): {}", e.getMessage()); logger.debug("Transaktion zurückgerollt (Dokumentfehler): {}", e.getMessage());
} catch (SQLException rollbackEx) { } catch (SQLException rollbackEx) {
logger.error("Rollback fehlgeschlagen: {}", rollbackEx.getMessage(), rollbackEx); logger.error(ROLLBACK_FAILED_MSG, rollbackEx.getMessage(), rollbackEx);
} }
throw e; throw e;
} catch (RuntimeException e) { } catch (RuntimeException e) {
@@ -66,7 +68,7 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
connection.rollback(); connection.rollback();
logger.debug("Transaktion zurückgerollt (Laufzeitfehler): {}", e.getMessage()); logger.debug("Transaktion zurückgerollt (Laufzeitfehler): {}", e.getMessage());
} catch (SQLException rollbackEx) { } catch (SQLException rollbackEx) {
logger.error("Rollback fehlgeschlagen: {}", rollbackEx.getMessage(), rollbackEx); logger.error(ROLLBACK_FAILED_MSG, rollbackEx.getMessage(), rollbackEx);
} }
throw new DocumentPersistenceException("Transaktion fehlgeschlagen: " + e.getMessage(), e); throw new DocumentPersistenceException("Transaktion fehlgeschlagen: " + e.getMessage(), e);
} catch (SQLException e) { } catch (SQLException e) {
@@ -75,7 +77,7 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
connection.rollback(); connection.rollback();
logger.debug("Transaktion zurückgerollt (SQL-Fehler): {}", e.getMessage()); logger.debug("Transaktion zurückgerollt (SQL-Fehler): {}", e.getMessage());
} catch (SQLException rollbackEx) { } catch (SQLException rollbackEx) {
logger.error("Rollback fehlgeschlagen: {}", rollbackEx.getMessage(), rollbackEx); logger.error(ROLLBACK_FAILED_MSG, rollbackEx.getMessage(), rollbackEx);
} }
throw new DocumentPersistenceException("Transaktion fehlgeschlagen: " + e.getMessage(), e); throw new DocumentPersistenceException("Transaktion fehlgeschlagen: " + e.getMessage(), e);
} }
@@ -214,10 +214,20 @@ class AnthropicClaudeAdapterIntegrationTest {
* where log output is not relevant to the assertion. * where log output is not relevant to the assertion.
*/ */
private static class NoOpProcessingLogger implements ProcessingLogger { private static class NoOpProcessingLogger implements ProcessingLogger {
@Override public void info(String message, Object... args) {} @Override public void info(String message, Object... args) {
@Override public void debug(String message, Object... args) {} // intentionally empty
@Override public void warn(String message, Object... args) {} }
@Override public void error(String message, Object... args) {} @Override public void debug(String message, Object... args) {
@Override public void debugSensitiveAiContent(String message, Object... args) {} // intentionally empty
}
@Override public void warn(String message, Object... args) {
// intentionally empty
}
@Override public void error(String message, Object... args) {
// intentionally empty
}
@Override public void debugSensitiveAiContent(String message, Object... args) {
// intentionally empty
}
} }
} }
@@ -1,6 +1,7 @@
package de.gecheckt.pdf.umbenenner.adapter.out.sqlite; package de.gecheckt.pdf.umbenenner.adapter.out.sqlite;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.nio.file.Files; import java.nio.file.Files;
@@ -192,12 +193,14 @@ class SqliteSchemaInitializationAdapterTest {
String jdbcUrl = jdbcUrl(dir, "fall3.db"); String jdbcUrl = jdbcUrl(dir, "fall3.db");
SqliteSchemaInitializationAdapter adapter = new SqliteSchemaInitializationAdapter(jdbcUrl); SqliteSchemaInitializationAdapter adapter = new SqliteSchemaInitializationAdapter(jdbcUrl);
// Erster Aufruf (Fall 1) assertThatCode(() -> {
adapter.initializeSchema(); // Erster Aufruf (Fall 1)
// Zweiter Aufruf (Fall 3) darf nicht werfen adapter.initializeSchema();
adapter.initializeSchema(); // Zweiter Aufruf (Fall 3) darf nicht werfen
// Dritter Aufruf (Fall 3) ebenfalls idempotent adapter.initializeSchema();
adapter.initializeSchema(); // Dritter Aufruf (Fall 3) ebenfalls idempotent
adapter.initializeSchema();
}).doesNotThrowAnyException();
} }
@Test @Test
@@ -253,16 +256,19 @@ class SqliteSchemaInitializationAdapterTest {
ds.setUrl(jdbcUrl); ds.setUrl(jdbcUrl);
try (Connection conn = ds.getConnection()) { try (Connection conn = ds.getConnection()) {
assertThatThrownBy(() -> { assertThatThrownBy(() -> insertOrphanedProcessingAttempt(conn))
try (var ps = conn.prepareStatement(""" .isInstanceOf(SQLException.class);
INSERT INTO processing_attempt }
(fingerprint, run_id, attempt_number, started_at, ended_at, status, retryable) }
VALUES ('nichtvorhanden', 'run-1', 1, '2026-01-01T00:00:00Z',
'2026-01-01T00:01:00Z', 'FAILED_RETRYABLE', 1) private static void insertOrphanedProcessingAttempt(Connection conn) throws SQLException {
""")) { try (var ps = conn.prepareStatement("""
ps.executeUpdate(); INSERT INTO processing_attempt
} (fingerprint, run_id, attempt_number, started_at, ended_at, status, retryable)
}).isInstanceOf(SQLException.class); VALUES ('nichtvorhanden', 'run-1', 1, '2026-01-01T00:00:00Z',
'2026-01-01T00:01:00Z', 'FAILED_RETRYABLE', 1)
""")) {
ps.executeUpdate();
} }
} }
@@ -1,6 +1,7 @@
package de.gecheckt.pdf.umbenenner.adapter.out.targetfolder; package de.gecheckt.pdf.umbenenner.adapter.out.targetfolder;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.io.IOException; import java.io.IOException;
@@ -219,8 +220,8 @@ class FilesystemTargetFolderAdapterTest {
@Test @Test
void tryDeleteTargetFile_fileDoesNotExist_doesNotThrow() { void tryDeleteTargetFile_fileDoesNotExist_doesNotThrow() {
// Must not throw even if the file is absent assertThatCode(() -> adapter.tryDeleteTargetFile("nonexistent.pdf"))
adapter.tryDeleteTargetFile("nonexistent.pdf"); .doesNotThrowAnyException();
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -18,6 +18,8 @@ public sealed interface PromptSaveResult
PromptSaveResult.WriteFailed, PromptSaveResult.WriteFailed,
PromptSaveResult.TargetDirectoryMissing, PromptSaveResult.TargetDirectoryMissing,
PromptSaveResult.AtomicMoveFailed { PromptSaveResult.AtomicMoveFailed {
String MESSAGE_NOT_NULL = "message must not be null";
/** /**
* Die Prompt-Datei wurde erfolgreich gespeichert. * Die Prompt-Datei wurde erfolgreich gespeichert.
@@ -53,7 +55,7 @@ public sealed interface PromptSaveResult
* @throws NullPointerException wenn {@code message} null ist * @throws NullPointerException wenn {@code message} null ist
*/ */
public WriteFailed { public WriteFailed {
java.util.Objects.requireNonNull(message, "message must not be null"); java.util.Objects.requireNonNull(message, MESSAGE_NOT_NULL);
} }
} }
@@ -71,7 +73,7 @@ public sealed interface PromptSaveResult
* @throws NullPointerException wenn {@code message} null ist * @throws NullPointerException wenn {@code message} null ist
*/ */
public TargetDirectoryMissing { public TargetDirectoryMissing {
java.util.Objects.requireNonNull(message, "message must not be null"); java.util.Objects.requireNonNull(message, MESSAGE_NOT_NULL);
} }
} }
@@ -90,7 +92,7 @@ public sealed interface PromptSaveResult
* @throws NullPointerException wenn {@code message} null ist * @throws NullPointerException wenn {@code message} null ist
*/ */
public AtomicMoveFailed { public AtomicMoveFailed {
java.util.Objects.requireNonNull(message, "message must not be null"); java.util.Objects.requireNonNull(message, MESSAGE_NOT_NULL);
} }
} }
} }
@@ -29,6 +29,8 @@ public sealed interface ModelCatalogResult
ModelCatalogResult.EmptyList, ModelCatalogResult.EmptyList,
ModelCatalogResult.IncompleteConfiguration, ModelCatalogResult.IncompleteConfiguration,
ModelCatalogResult.TechnicalFailure { ModelCatalogResult.TechnicalFailure {
String PROVIDER_ID_NOT_NULL = "providerIdentifier must not be null";
/** /**
* The provider returned a non-empty list of available model identifiers. * The provider returned a non-empty list of available model identifiers.
@@ -55,7 +57,7 @@ public sealed interface ModelCatalogResult
* @throws IllegalArgumentException if {@code models} is empty * @throws IllegalArgumentException if {@code models} is empty
*/ */
public Success { public Success {
Objects.requireNonNull(providerIdentifier, "providerIdentifier must not be null"); Objects.requireNonNull(providerIdentifier, PROVIDER_ID_NOT_NULL);
Objects.requireNonNull(models, "models must not be null"); Objects.requireNonNull(models, "models must not be null");
Objects.requireNonNull(loadedAt, "loadedAt must not be null"); Objects.requireNonNull(loadedAt, "loadedAt must not be null");
if (models.isEmpty()) { if (models.isEmpty()) {
@@ -88,7 +90,7 @@ public sealed interface ModelCatalogResult
* @throws NullPointerException if any parameter is {@code null} * @throws NullPointerException if any parameter is {@code null}
*/ */
public EmptyList { public EmptyList {
Objects.requireNonNull(providerIdentifier, "providerIdentifier must not be null"); Objects.requireNonNull(providerIdentifier, PROVIDER_ID_NOT_NULL);
Objects.requireNonNull(loadedAt, "loadedAt must not be null"); Objects.requireNonNull(loadedAt, "loadedAt must not be null");
} }
} }
@@ -118,7 +120,7 @@ public sealed interface ModelCatalogResult
* @throws NullPointerException if any parameter is {@code null} * @throws NullPointerException if any parameter is {@code null}
*/ */
public IncompleteConfiguration { public IncompleteConfiguration {
Objects.requireNonNull(providerIdentifier, "providerIdentifier must not be null"); Objects.requireNonNull(providerIdentifier, PROVIDER_ID_NOT_NULL);
Objects.requireNonNull(missingReason, "missingReason must not be null"); Objects.requireNonNull(missingReason, "missingReason must not be null");
} }
} }
@@ -153,7 +155,7 @@ public sealed interface ModelCatalogResult
* @throws NullPointerException if any parameter is {@code null} * @throws NullPointerException if any parameter is {@code null}
*/ */
public TechnicalFailure { public TechnicalFailure {
Objects.requireNonNull(providerIdentifier, "providerIdentifier must not be null"); Objects.requireNonNull(providerIdentifier, PROVIDER_ID_NOT_NULL);
Objects.requireNonNull(errorCategory, "errorCategory must not be null"); Objects.requireNonNull(errorCategory, "errorCategory must not be null");
Objects.requireNonNull(errorDetail, "errorDetail must not be null"); Objects.requireNonNull(errorDetail, "errorDetail must not be null");
} }
@@ -38,6 +38,10 @@ import de.gecheckt.pdf.umbenenner.domain.model.ParsedAiResponse;
* of {@link AiResponseValidator}. * of {@link AiResponseValidator}.
*/ */
public final class AiResponseParser { public final class AiResponseParser {
private static final String JSON_KEY_TITLE = "title";
private static final String JSON_KEY_REASONING = "reasoning";
private AiResponseParser() { private AiResponseParser() {
// Static utility no instances // Static utility no instances
@@ -81,19 +85,19 @@ public final class AiResponseParser {
} }
// Validate mandatory field: title // Validate mandatory field: title
if (!json.has("title") || json.isNull("title")) { if (!json.has(JSON_KEY_TITLE) || json.isNull(JSON_KEY_TITLE)) {
return new AiResponseParsingFailure("MISSING_TITLE", "AI response missing mandatory field 'title'"); return new AiResponseParsingFailure("MISSING_TITLE", "AI response missing mandatory field 'title'");
} }
String title = json.getString("title"); String title = json.getString(JSON_KEY_TITLE);
if (title.isBlank()) { if (title.isBlank()) {
return new AiResponseParsingFailure("BLANK_TITLE", "AI response field 'title' is blank"); return new AiResponseParsingFailure("BLANK_TITLE", "AI response field 'title' is blank");
} }
// Validate mandatory field: reasoning // Validate mandatory field: reasoning
if (!json.has("reasoning") || json.isNull("reasoning")) { if (!json.has(JSON_KEY_REASONING) || json.isNull(JSON_KEY_REASONING)) {
return new AiResponseParsingFailure("MISSING_REASONING", "AI response missing mandatory field 'reasoning'"); return new AiResponseParsingFailure("MISSING_REASONING", "AI response missing mandatory field 'reasoning'");
} }
String reasoning = json.getString("reasoning"); String reasoning = json.getString(JSON_KEY_REASONING);
// Optional field: date // Optional field: date
String dateString = null; String dateString = null;
@@ -70,6 +70,17 @@ public class DefaultManualFileCopyUseCase implements ManualFileCopyUseCase {
private final ClockPort clock; private final ClockPort clock;
private final ProcessingLogger logger; private final ProcessingLogger logger;
/** Ergebnis der Dokument-Stammsatz-Auflösung: entweder ein Record oder ein Fehler. */
private record RecordLookupOutcome(DocumentRecord record, ManualFileCopyResult failure) {
boolean hasFailed() { return failure != null; }
}
/** Ergebnis der Zieldateinamen-Auflösung: entweder Name + No-Op-Flag oder ein Fehler. */
private record FilenameLookupOutcome(String appliedFileName, boolean noOpIdentical,
ManualFileCopyResult failure) {
boolean hasFailed() { return failure != null; }
}
/** /**
* Erstellt den Use-Case mit allen erforderlichen Ports. * Erstellt den Use-Case mit allen erforderlichen Ports.
* *
@@ -127,86 +138,144 @@ public class DefaultManualFileCopyUseCase implements ManualFileCopyUseCase {
logger.info("Manuelle Dateikopie angefordert: Fingerprint={}, Zielname={}", logger.info("Manuelle Dateikopie angefordert: Fingerprint={}, Zielname={}",
fingerprint.sha256Hex(), desiredFullName); fingerprint.sha256Hex(), desiredFullName);
// Schritt 1: Dokument-Stammsatz laden und Zustand prüfen RecordLookupOutcome recordOutcome = loadAndValidateRecord(fingerprint);
if (recordOutcome.hasFailed()) {
return recordOutcome.failure();
}
DocumentRecord record = recordOutcome.record();
FilenameLookupOutcome filenameOutcome = resolveTargetFilename(fingerprint, desiredFullName);
if (filenameOutcome.hasFailed()) {
return filenameOutcome.failure();
}
String appliedFileName = filenameOutcome.appliedFileName();
boolean noOpIdentical = filenameOutcome.noOpIdentical();
if (!noOpIdentical) {
ManualFileCopyResult copyFailure = performFileCopy(fingerprint, record, appliedFileName);
if (copyFailure != null) {
return copyFailure;
}
}
return persistAndBuildResult(fingerprint, record, appliedFileName, noOpIdentical, desiredFullName);
}
/**
* Lädt den Dokument-Stammsatz und prüft, ob der aktuelle Status eine manuelle
* Kopie erlaubt.
*
* @param fingerprint der Fingerprint des Dokuments
* @return Outcome mit geladenem Record oder mit einem Fehler-Ergebnis
*/
private RecordLookupOutcome loadAndValidateRecord(DocumentFingerprint fingerprint) {
DocumentRecordLookupResult lookupResult = repository.findByFingerprint(fingerprint); DocumentRecordLookupResult lookupResult = repository.findByFingerprint(fingerprint);
DocumentRecord record;
if (lookupResult instanceof DocumentTerminalFinalFailure terminalFailure) { if (lookupResult instanceof DocumentTerminalFinalFailure terminalFailure) {
record = terminalFailure.record(); return new RecordLookupOutcome(terminalFailure.record(), null);
} else if (lookupResult instanceof DocumentKnownProcessable known) { } else if (lookupResult instanceof DocumentKnownProcessable known) {
record = known.record(); DocumentRecord record = known.record();
ProcessingStatus status = record.overallStatus(); if (record.overallStatus() == ProcessingStatus.SUCCESS) {
if (status == ProcessingStatus.SUCCESS) {
// Defensiv: SUCCESS sollte über DocumentTerminalSuccess auflösen, nicht hier.
logger.warn("Manuelle Dateikopie verweigert: Dokument bereits SUCCESS. Fingerprint={}", logger.warn("Manuelle Dateikopie verweigert: Dokument bereits SUCCESS. Fingerprint={}",
fingerprint.sha256Hex()); fingerprint.sha256Hex());
return new ManualFileCopyInvalidState( return new RecordLookupOutcome(null, new ManualFileCopyInvalidState(
"Dokument ist bereits erfolgreich verarbeitet. Bitte die Umbenennung der " "Dokument ist bereits erfolgreich verarbeitet. Bitte die Umbenennung der "
+ "Zieldatei verwenden."); + "Zieldatei verwenden."));
} }
return new RecordLookupOutcome(record, null);
} else if (lookupResult instanceof DocumentTerminalSuccess) { } else if (lookupResult instanceof DocumentTerminalSuccess) {
logger.warn("Manuelle Dateikopie verweigert: Dokument bereits SUCCESS. Fingerprint={}", logger.warn("Manuelle Dateikopie verweigert: Dokument bereits SUCCESS. Fingerprint={}",
fingerprint.sha256Hex()); fingerprint.sha256Hex());
return new ManualFileCopyInvalidState( return new RecordLookupOutcome(null, new ManualFileCopyInvalidState(
"Dokument ist bereits erfolgreich verarbeitet. Bitte die Umbenennung der " "Dokument ist bereits erfolgreich verarbeitet. Bitte die Umbenennung der "
+ "Zieldatei verwenden."); + "Zieldatei verwenden."));
} else if (lookupResult instanceof DocumentUnknown) { } else if (lookupResult instanceof DocumentUnknown) {
logger.warn("Manuelle Dateikopie verweigert: Dokument unbekannt. Fingerprint={}", logger.warn("Manuelle Dateikopie verweigert: Dokument unbekannt. Fingerprint={}",
fingerprint.sha256Hex()); fingerprint.sha256Hex());
return new ManualFileCopyDocumentNotFound( return new RecordLookupOutcome(null, new ManualFileCopyDocumentNotFound(
"Kein Dokument mit dem angegebenen Fingerprint gefunden."); "Kein Dokument mit dem angegebenen Fingerprint gefunden."));
} else if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) { } else if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) {
logger.warn("Manuelle Dateikopie fehlgeschlagen: Lookup-Fehler. Fingerprint={}, Ursache={}", logger.warn("Manuelle Dateikopie fehlgeschlagen: Lookup-Fehler. Fingerprint={}, Ursache={}",
fingerprint.sha256Hex(), failure.errorMessage()); fingerprint.sha256Hex(), failure.errorMessage());
return new ManualFileCopyPersistenceFailure( return new RecordLookupOutcome(null, new ManualFileCopyPersistenceFailure(
"Persistenzfehler beim Laden des Dokumentstammsatzes: " + failure.errorMessage()); "Persistenzfehler beim Laden des Dokumentstammsatzes: " + failure.errorMessage()));
} else {
// Defensiv: nicht erreichbar dank sealed type, aber erforderlich für die Compiler-
// Vollständigkeitsprüfung in älteren Werkzeugen.
return new ManualFileCopyDocumentNotFound(
"Unbekanntes Lookup-Ergebnis: " + lookupResult.getClass().getSimpleName());
} }
// Defensiv: nicht erreichbar dank sealed type, aber erforderlich für die Compiler-
// Vollständigkeitsprüfung in älteren Werkzeugen.
return new RecordLookupOutcome(null, new ManualFileCopyDocumentNotFound(
"Unbekanntes Lookup-Ergebnis: " + lookupResult.getClass().getSimpleName()));
}
// Schritt 2: Eindeutigen Zieldateinamen über TargetFolderPort auflösen /**
* Löst über {@link TargetFolderPort} einen eindeutigen Zieldateinamen auf.
*
* @param fingerprint der Fingerprint des Dokuments
* @param desiredFullName der gewünschte vollständige Dateiname
* @return Outcome mit aufgelöstem Dateinamen und No-Op-Flag oder mit einem Fehler-Ergebnis
*/
private FilenameLookupOutcome resolveTargetFilename(DocumentFingerprint fingerprint,
String desiredFullName) {
TargetFilenameResolutionResult resolutionResult = TargetFilenameResolutionResult resolutionResult =
targetFolderPort.resolveUniqueFilename(desiredFullName, fingerprint); targetFolderPort.resolveUniqueFilename(desiredFullName, fingerprint);
boolean noOpIdentical = false;
String appliedFileName;
if (resolutionResult instanceof ExistingIdenticalTargetFile identical) { if (resolutionResult instanceof ExistingIdenticalTargetFile identical) {
noOpIdentical = true;
appliedFileName = identical.existingFilename();
logger.info("Manuelle Dateikopie: Identische Datei bereits im Zielordner vorhanden. Fingerprint={}", logger.info("Manuelle Dateikopie: Identische Datei bereits im Zielordner vorhanden. Fingerprint={}",
fingerprint.sha256Hex()); fingerprint.sha256Hex());
return new FilenameLookupOutcome(identical.existingFilename(), true, null);
} else if (resolutionResult instanceof TargetFolderTechnicalFailure folderFailure) { } else if (resolutionResult instanceof TargetFolderTechnicalFailure folderFailure) {
logger.warn("Manuelle Dateikopie fehlgeschlagen: Zielordnerzugriff. Fingerprint={}, Ursache={}", logger.warn("Manuelle Dateikopie fehlgeschlagen: Zielordnerzugriff. Fingerprint={}, Ursache={}",
fingerprint.sha256Hex(), folderFailure.errorMessage()); fingerprint.sha256Hex(), folderFailure.errorMessage());
return new ManualFileCopyFileSystemFailure( return new FilenameLookupOutcome(null, false, new ManualFileCopyFileSystemFailure(
"Zielordner nicht zugänglich: " + folderFailure.errorMessage()); "Zielordner nicht zugänglich: " + folderFailure.errorMessage()));
} else if (resolutionResult instanceof ResolvedTargetFilename resolved) { } else if (resolutionResult instanceof ResolvedTargetFilename resolved) {
appliedFileName = resolved.resolvedFilename(); return new FilenameLookupOutcome(resolved.resolvedFilename(), false, null);
} else { }
return new FilenameLookupOutcome(null, false, new ManualFileCopyFileSystemFailure(
"Unbekanntes Auflösungsergebnis: " + resolutionResult.getClass().getSimpleName()));
}
/**
* Kopiert die Quelldatei physisch in den Zielordner.
*
* @param fingerprint der Fingerprint des Dokuments (für Logging)
* @param record der aktuelle Dokument-Stammsatz
* @param appliedFileName der aufgelöste Zieldateiname
* @return ein Fehler-Ergebnis bei Misserfolg, {@code null} bei Erfolg
*/
private ManualFileCopyResult performFileCopy(DocumentFingerprint fingerprint,
DocumentRecord record,
String appliedFileName) {
var copyResult = targetFileCopyPort.copyToTarget(
record.lastKnownSourceLocator(), appliedFileName);
if (copyResult instanceof TargetFileCopyTechnicalFailure technicalFailure) {
logger.warn("Manuelle Dateikopie fehlgeschlagen: Dateisystemfehler. Fingerprint={}, Ursache={}",
fingerprint.sha256Hex(), technicalFailure.errorMessage());
return new ManualFileCopyFileSystemFailure(technicalFailure.errorMessage());
}
if (!(copyResult instanceof TargetFileCopySuccess)) {
return new ManualFileCopyFileSystemFailure( return new ManualFileCopyFileSystemFailure(
"Unbekanntes Auflösungsergebnis: " + resolutionResult.getClass().getSimpleName()); "Unbekanntes Kopier-Ergebnis: " + copyResult.getClass().getSimpleName());
} }
return null;
}
// Schritt 3: Quelldatei kopieren nur wenn keine identische Zieldatei existiert /**
if (!noOpIdentical) { * Aktualisiert den Dokument-Stammsatz in der Persistenz und gibt das finale
var copyResult = targetFileCopyPort.copyToTarget( * Operationsergebnis zurück. Bei einem Persistenzfehler nach erfolgter Zielkopie
record.lastKnownSourceLocator(), appliedFileName); * wird ein Best-Effort-Rollback der neu geschriebenen Datei durchgeführt.
if (copyResult instanceof TargetFileCopyTechnicalFailure technicalFailure) { *
logger.warn("Manuelle Dateikopie fehlgeschlagen: Dateisystemfehler. Fingerprint={}, Ursache={}", * @param fingerprint der Fingerprint des Dokuments
fingerprint.sha256Hex(), technicalFailure.errorMessage()); * @param record der bisher gültige Dokument-Stammsatz
return new ManualFileCopyFileSystemFailure(technicalFailure.errorMessage()); * @param appliedFileName der tatsächlich verwendete Zieldateiname
} * @param noOpIdentical true, wenn keine neue Kopie geschrieben wurde
if (!(copyResult instanceof TargetFileCopySuccess)) { * @param desiredFullName der ursprünglich gewünschte Zieldateiname
return new ManualFileCopyFileSystemFailure( * @return das finale Operationsergebnis
"Unbekanntes Kopier-Ergebnis: " + copyResult.getClass().getSimpleName()); */
} private ManualFileCopyResult persistAndBuildResult(DocumentFingerprint fingerprint,
} DocumentRecord record,
String appliedFileName,
// Schritt 4: Dokument-Stammsatz aktualisieren boolean noOpIdentical,
String desiredFullName) {
var now = clock.now(); var now = clock.now();
DocumentRecord updatedRecord = new DocumentRecord( DocumentRecord updatedRecord = new DocumentRecord(
record.fingerprint(), record.fingerprint(),
@@ -248,17 +317,15 @@ public class DefaultManualFileCopyUseCase implements ManualFileCopyUseCase {
"Persistenzfehler nach Kopie: " + errorMessage); "Persistenzfehler nach Kopie: " + errorMessage);
} }
boolean conflictSuffixApplied = !noOpIdentical && !appliedFileName.equals(desiredFullName);
if (noOpIdentical) { if (noOpIdentical) {
logger.info("Manuelle Dateikopie abgeschlossen ohne Schreibvorgang: identische Zieldatei {}.", logger.info("Manuelle Dateikopie abgeschlossen ohne Schreibvorgang: identische Zieldatei {}.",
appliedFileName); appliedFileName);
return new ManualFileCopyNoOpIdenticalTarget(appliedFileName); return new ManualFileCopyNoOpIdenticalTarget(appliedFileName);
} }
boolean conflictSuffixApplied = !appliedFileName.equals(desiredFullName);
logger.info("Manuelle Dateikopie erfolgreich: {} (Suffix angewendet: {})", logger.info("Manuelle Dateikopie erfolgreich: {} (Suffix angewendet: {})",
appliedFileName, conflictSuffixApplied); appliedFileName, conflictSuffixApplied);
return new ManualFileCopySuccess(appliedFileName, conflictSuffixApplied); return new ManualFileCopySuccess(appliedFileName, conflictSuffixApplied);
} }
} }
@@ -24,6 +24,8 @@ public record EditorValidationFinding(
Optional<String> fieldKey, Optional<String> fieldKey,
EditorValidationSeverity severity, EditorValidationSeverity severity,
String message) { String message) {
private static final String FIELD_KEY_NOT_NULL = "fieldKey must not be null";
/** /**
* Erstellt einen neuen Validierungsbefund. * Erstellt einen neuen Validierungsbefund.
@@ -47,7 +49,7 @@ public record EditorValidationFinding(
* @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#ERROR} * @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#ERROR}
*/ */
public static EditorValidationFinding error(String fieldKey, String message) { public static EditorValidationFinding error(String fieldKey, String message) {
Objects.requireNonNull(fieldKey, "fieldKey must not be null"); Objects.requireNonNull(fieldKey, FIELD_KEY_NOT_NULL);
return new EditorValidationFinding(Optional.of(fieldKey), EditorValidationSeverity.ERROR, message); return new EditorValidationFinding(Optional.of(fieldKey), EditorValidationSeverity.ERROR, message);
} }
@@ -59,7 +61,7 @@ public record EditorValidationFinding(
* @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#WARNING} * @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#WARNING}
*/ */
public static EditorValidationFinding warning(String fieldKey, String message) { public static EditorValidationFinding warning(String fieldKey, String message) {
Objects.requireNonNull(fieldKey, "fieldKey must not be null"); Objects.requireNonNull(fieldKey, FIELD_KEY_NOT_NULL);
return new EditorValidationFinding(Optional.of(fieldKey), EditorValidationSeverity.WARNING, message); return new EditorValidationFinding(Optional.of(fieldKey), EditorValidationSeverity.WARNING, message);
} }
@@ -71,7 +73,7 @@ public record EditorValidationFinding(
* @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#HINT} * @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#HINT}
*/ */
public static EditorValidationFinding hint(String fieldKey, String message) { public static EditorValidationFinding hint(String fieldKey, String message) {
Objects.requireNonNull(fieldKey, "fieldKey must not be null"); Objects.requireNonNull(fieldKey, FIELD_KEY_NOT_NULL);
return new EditorValidationFinding(Optional.of(fieldKey), EditorValidationSeverity.HINT, message); return new EditorValidationFinding(Optional.of(fieldKey), EditorValidationSeverity.HINT, message);
} }
@@ -93,7 +95,7 @@ public record EditorValidationFinding(
* @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#INFO} * @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#INFO}
*/ */
public static EditorValidationFinding info(String fieldKey, String message) { public static EditorValidationFinding info(String fieldKey, String message) {
Objects.requireNonNull(fieldKey, "fieldKey must not be null"); Objects.requireNonNull(fieldKey, FIELD_KEY_NOT_NULL);
return new EditorValidationFinding(Optional.of(fieldKey), EditorValidationSeverity.INFO, message); return new EditorValidationFinding(Optional.of(fieldKey), EditorValidationSeverity.INFO, message);
} }
@@ -21,6 +21,8 @@ public sealed interface CorrectionOutcome
permits CorrectionOutcome.Applied, permits CorrectionOutcome.Applied,
CorrectionOutcome.Failed, CorrectionOutcome.Failed,
CorrectionOutcome.NotAttempted { CorrectionOutcome.NotAttempted {
String SUGGESTION_NOT_NULL = "suggestion must not be null";
/** /**
* Gibt den Korrekturvorschlag zurück, auf den sich dieses Ergebnis bezieht. * Gibt den Korrekturvorschlag zurück, auf den sich dieses Ergebnis bezieht.
@@ -47,7 +49,7 @@ public sealed interface CorrectionOutcome
* @throws NullPointerException wenn ein Parameter {@code null} ist * @throws NullPointerException wenn ein Parameter {@code null} ist
*/ */
public Applied { public Applied {
Objects.requireNonNull(suggestion, "suggestion must not be null"); Objects.requireNonNull(suggestion, SUGGESTION_NOT_NULL);
Objects.requireNonNull(message, "message must not be null"); Objects.requireNonNull(message, "message must not be null");
} }
} }
@@ -70,7 +72,7 @@ public sealed interface CorrectionOutcome
* @throws NullPointerException wenn ein Parameter {@code null} ist * @throws NullPointerException wenn ein Parameter {@code null} ist
*/ */
public Failed { public Failed {
Objects.requireNonNull(suggestion, "suggestion must not be null"); Objects.requireNonNull(suggestion, SUGGESTION_NOT_NULL);
Objects.requireNonNull(errorMessage, "errorMessage must not be null"); Objects.requireNonNull(errorMessage, "errorMessage must not be null");
} }
} }
@@ -97,7 +99,7 @@ public sealed interface CorrectionOutcome
* @throws NullPointerException wenn ein Parameter {@code null} ist * @throws NullPointerException wenn ein Parameter {@code null} ist
*/ */
public NotAttempted { public NotAttempted {
Objects.requireNonNull(suggestion, "suggestion must not be null"); Objects.requireNonNull(suggestion, SUGGESTION_NOT_NULL);
Objects.requireNonNull(reason, "reason must not be null"); Objects.requireNonNull(reason, "reason must not be null");
} }
} }
@@ -21,6 +21,9 @@ public sealed interface CorrectionSuggestion
permits CorrectionSuggestion.CreateDirectory, permits CorrectionSuggestion.CreateDirectory,
CorrectionSuggestion.CreatePromptFile, CorrectionSuggestion.CreatePromptFile,
CorrectionSuggestion.PrepareSqlitePath { CorrectionSuggestion.PrepareSqlitePath {
String PATH_NOT_NULL = "path must not be null";
String DESCRIPTION_NOT_NULL = "descriptionForUser must not be null";
/** /**
* Gibt eine kurze deutsche Beschreibung der vorgeschlagenen Korrektur zurück, * Gibt eine kurze deutsche Beschreibung der vorgeschlagenen Korrektur zurück,
@@ -53,8 +56,8 @@ public sealed interface CorrectionSuggestion
* @throws IllegalArgumentException wenn {@code path} leer ist * @throws IllegalArgumentException wenn {@code path} leer ist
*/ */
public CreateDirectory { public CreateDirectory {
Objects.requireNonNull(path, "path must not be null"); Objects.requireNonNull(path, PATH_NOT_NULL);
Objects.requireNonNull(descriptionForUser, "descriptionForUser must not be null"); Objects.requireNonNull(descriptionForUser, DESCRIPTION_NOT_NULL);
if (path.isBlank()) { if (path.isBlank()) {
throw new IllegalArgumentException("path must not be blank"); throw new IllegalArgumentException("path must not be blank");
} }
@@ -93,8 +96,8 @@ public sealed interface CorrectionSuggestion
* {@code maxTitleLength < 1} * {@code maxTitleLength < 1}
*/ */
public CreatePromptFile { public CreatePromptFile {
Objects.requireNonNull(path, "path must not be null"); Objects.requireNonNull(path, PATH_NOT_NULL);
Objects.requireNonNull(descriptionForUser, "descriptionForUser must not be null"); Objects.requireNonNull(descriptionForUser, DESCRIPTION_NOT_NULL);
if (path.isBlank()) { if (path.isBlank()) {
throw new IllegalArgumentException("path must not be blank"); throw new IllegalArgumentException("path must not be blank");
} }
@@ -129,8 +132,8 @@ public sealed interface CorrectionSuggestion
* @throws IllegalArgumentException wenn {@code path} leer ist * @throws IllegalArgumentException wenn {@code path} leer ist
*/ */
public PrepareSqlitePath { public PrepareSqlitePath {
Objects.requireNonNull(path, "path must not be null"); Objects.requireNonNull(path, PATH_NOT_NULL);
Objects.requireNonNull(descriptionForUser, "descriptionForUser must not be null"); Objects.requireNonNull(descriptionForUser, DESCRIPTION_NOT_NULL);
if (path.isBlank()) { if (path.isBlank()) {
throw new IllegalArgumentException("path must not be blank"); throw new IllegalArgumentException("path must not be blank");
} }
@@ -1652,6 +1652,7 @@ class BatchRunProcessingUseCaseTest {
@Override @Override
public void debugSensitiveAiContent(String message, Object... args) { public void debugSensitiveAiContent(String message, Object... args) {
// intentionally empty
} }
@Override @Override
@@ -300,8 +300,12 @@ class BatchRunProgressObservationTest {
} }
private static final class NoOpLock implements RunLockPort { private static final class NoOpLock implements RunLockPort {
@Override public void acquire() { } @Override public void acquire() {
@Override public void release() { } // intentionally empty
}
@Override public void release() {
// intentionally empty
}
@Override @Override
public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() { public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
return java.util.Optional.empty(); return java.util.Optional.empty();
@@ -335,11 +339,21 @@ class BatchRunProgressObservationTest {
} }
private static final class SilentLogger implements ProcessingLogger { private static final class SilentLogger implements ProcessingLogger {
@Override public void info(String message, Object... args) { } @Override public void info(String message, Object... args) {
@Override public void warn(String message, Object... args) { } // intentionally empty
@Override public void error(String message, Object... args) { } }
@Override public void debug(String message, Object... args) { } @Override public void warn(String message, Object... args) {
@Override public void debugSensitiveAiContent(String message, Object... args) { } // intentionally empty
}
@Override public void error(String message, Object... args) {
// intentionally empty
}
@Override public void debug(String message, Object... args) {
// intentionally empty
}
@Override public void debugSensitiveAiContent(String message, Object... args) {
// intentionally empty
}
} }
private static final class RecordingObserver implements BatchRunProgressObserver { private static final class RecordingObserver implements BatchRunProgressObserver {
@@ -465,21 +479,31 @@ class BatchRunProgressObservationTest {
@Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint f) { @Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint f) {
return new DocumentUnknown(); return new DocumentUnknown();
} }
@Override public void create(de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { } @Override public void create(de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) {
@Override public void update(de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { } // intentionally empty
@Override public void deleteByFingerprint(DocumentFingerprint fingerprint) { } }
@Override public void update(de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) {
// intentionally empty
}
@Override public void deleteByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
} }
private static final class NoAttempts implements ProcessingAttemptRepository { private static final class NoAttempts implements ProcessingAttemptRepository {
static final NoAttempts INSTANCE = new NoAttempts(); static final NoAttempts INSTANCE = new NoAttempts();
@Override public int loadNextAttemptNumber(DocumentFingerprint fingerprint) { return 1; } @Override public int loadNextAttemptNumber(DocumentFingerprint fingerprint) { return 1; }
@Override public void save(ProcessingAttempt attempt) { } @Override public void save(ProcessingAttempt attempt) {
// intentionally empty
}
@Override public List<ProcessingAttempt> findAllByFingerprint(DocumentFingerprint fingerprint) { @Override public List<ProcessingAttempt> findAllByFingerprint(DocumentFingerprint fingerprint) {
return List.of(); return List.of();
} }
@Override public ProcessingAttempt findLatestProposalReadyAttempt( @Override public ProcessingAttempt findLatestProposalReadyAttempt(
DocumentFingerprint fingerprint) { return null; } DocumentFingerprint fingerprint) { return null; }
@Override public void deleteAllByFingerprint(DocumentFingerprint fingerprint) { } @Override public void deleteAllByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
} }
private static final class NoUow implements UnitOfWorkPort { private static final class NoUow implements UnitOfWorkPort {
@@ -499,7 +523,9 @@ class BatchRunProgressObservationTest {
return new ResolvedTargetFilename(baseFilename); return new ResolvedTargetFilename(baseFilename);
} }
@Override public String getTargetFolderLocator() { return "/tmp/target"; } @Override public String getTargetFolderLocator() { return "/tmp/target"; }
@Override public void tryDeleteTargetFile(String filename) { } @Override public void tryDeleteTargetFile(String filename) {
// intentionally empty
}
} }
private static final class NoTargetCopy implements TargetFileCopyPort { private static final class NoTargetCopy implements TargetFileCopyPort {
@@ -83,17 +83,25 @@ class DefaultDeleteDocumentHistoryUseCaseTest {
UnitOfWorkPort failingPort = operations -> UnitOfWorkPort failingPort = operations ->
operations.accept(new UnitOfWorkPort.TransactionOperations() { operations.accept(new UnitOfWorkPort.TransactionOperations() {
@Override @Override
public void saveProcessingAttempt(ProcessingAttempt attempt) { } public void saveProcessingAttempt(ProcessingAttempt attempt) {
// intentionally empty
}
@Override @Override
public void createDocumentRecord(DocumentRecord record) { } public void createDocumentRecord(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void updateDocumentRecord(DocumentRecord record) { } public void updateDocumentRecord(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
throw new DocumentPersistenceException("Simulated DB error"); throw new DocumentPersistenceException("Simulated DB error");
} }
@Override @Override
public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) {
// intentionally empty
}
}); });
DefaultDeleteDocumentHistoryUseCase useCase = DefaultDeleteDocumentHistoryUseCase useCase =
@@ -110,11 +118,21 @@ class DefaultDeleteDocumentHistoryUseCaseTest {
private static UnitOfWorkPort noOpPort() { private static UnitOfWorkPort noOpPort() {
return operations -> operations.accept(new UnitOfWorkPort.TransactionOperations() { return operations -> operations.accept(new UnitOfWorkPort.TransactionOperations() {
@Override public void saveProcessingAttempt(ProcessingAttempt a) { } @Override public void saveProcessingAttempt(ProcessingAttempt a) {
@Override public void createDocumentRecord(DocumentRecord r) { } // intentionally empty
@Override public void updateDocumentRecord(DocumentRecord r) { } }
@Override public void resetDocumentByFingerprint(DocumentFingerprint fp) { } @Override public void createDocumentRecord(DocumentRecord r) {
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fp) { } // intentionally empty
}
@Override public void updateDocumentRecord(DocumentRecord r) {
// intentionally empty
}
@Override public void resetDocumentByFingerprint(DocumentFingerprint fp) {
// intentionally empty
}
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fp) {
// intentionally empty
}
}); });
} }
@@ -127,9 +145,15 @@ class DefaultDeleteDocumentHistoryUseCaseTest {
final List<DocumentFingerprint> resetByFingerprintFingerprints = new ArrayList<>(); final List<DocumentFingerprint> resetByFingerprintFingerprints = new ArrayList<>();
final List<DocumentFingerprint> resetStatusForRetryFingerprints = new ArrayList<>(); final List<DocumentFingerprint> resetStatusForRetryFingerprints = new ArrayList<>();
@Override public void saveProcessingAttempt(ProcessingAttempt a) { } @Override public void saveProcessingAttempt(ProcessingAttempt a) {
@Override public void createDocumentRecord(DocumentRecord r) { } // intentionally empty
@Override public void updateDocumentRecord(DocumentRecord r) { } }
@Override public void createDocumentRecord(DocumentRecord r) {
// intentionally empty
}
@Override public void updateDocumentRecord(DocumentRecord r) {
// intentionally empty
}
@Override @Override
public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
@@ -84,13 +84,21 @@ class DefaultHistoryResetDocumentStatusUseCaseTest {
UnitOfWorkPort failingPort = operations -> UnitOfWorkPort failingPort = operations ->
operations.accept(new UnitOfWorkPort.TransactionOperations() { operations.accept(new UnitOfWorkPort.TransactionOperations() {
@Override @Override
public void saveProcessingAttempt(ProcessingAttempt attempt) { } public void saveProcessingAttempt(ProcessingAttempt attempt) {
// intentionally empty
}
@Override @Override
public void createDocumentRecord(DocumentRecord record) { } public void createDocumentRecord(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void updateDocumentRecord(DocumentRecord record) { } public void updateDocumentRecord(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
@Override @Override
public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) {
throw new DocumentPersistenceException("Simulated DB error"); throw new DocumentPersistenceException("Simulated DB error");
@@ -111,11 +119,21 @@ class DefaultHistoryResetDocumentStatusUseCaseTest {
private static UnitOfWorkPort noOpPort() { private static UnitOfWorkPort noOpPort() {
return operations -> operations.accept(new UnitOfWorkPort.TransactionOperations() { return operations -> operations.accept(new UnitOfWorkPort.TransactionOperations() {
@Override public void saveProcessingAttempt(ProcessingAttempt a) { } @Override public void saveProcessingAttempt(ProcessingAttempt a) {
@Override public void createDocumentRecord(DocumentRecord r) { } // intentionally empty
@Override public void updateDocumentRecord(DocumentRecord r) { } }
@Override public void resetDocumentByFingerprint(DocumentFingerprint fp) { } @Override public void createDocumentRecord(DocumentRecord r) {
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fp) { } // intentionally empty
}
@Override public void updateDocumentRecord(DocumentRecord r) {
// intentionally empty
}
@Override public void resetDocumentByFingerprint(DocumentFingerprint fp) {
// intentionally empty
}
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fp) {
// intentionally empty
}
}); });
} }
@@ -128,9 +146,15 @@ class DefaultHistoryResetDocumentStatusUseCaseTest {
final List<DocumentFingerprint> resetStatusForRetryFingerprints = new ArrayList<>(); final List<DocumentFingerprint> resetStatusForRetryFingerprints = new ArrayList<>();
final List<DocumentFingerprint> resetByFingerprintFingerprints = new ArrayList<>(); final List<DocumentFingerprint> resetByFingerprintFingerprints = new ArrayList<>();
@Override public void saveProcessingAttempt(ProcessingAttempt a) { } @Override public void saveProcessingAttempt(ProcessingAttempt a) {
@Override public void createDocumentRecord(DocumentRecord r) { } // intentionally empty
@Override public void updateDocumentRecord(DocumentRecord r) { } }
@Override public void createDocumentRecord(DocumentRecord r) {
// intentionally empty
}
@Override public void updateDocumentRecord(DocumentRecord r) {
// intentionally empty
}
@Override @Override
public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
@@ -100,11 +100,21 @@ class DefaultManualFileCopyUseCaseTest {
private static ProcessingLogger noOpLogger() { private static ProcessingLogger noOpLogger() {
return new ProcessingLogger() { return new ProcessingLogger() {
@Override public void info(String msg, Object... args) { } @Override public void info(String msg, Object... args) {
@Override public void debug(String msg, Object... args) { } // intentionally empty
@Override public void debugSensitiveAiContent(String msg, Object... args) { } }
@Override public void warn(String msg, Object... args) { } @Override public void debug(String msg, Object... args) {
@Override public void error(String msg, Object... args) { } // intentionally empty
}
@Override public void debugSensitiveAiContent(String msg, Object... args) {
// intentionally empty
}
@Override public void warn(String msg, Object... args) {
// intentionally empty
}
@Override public void error(String msg, Object... args) {
// intentionally empty
}
}; };
} }
@@ -115,9 +125,15 @@ class DefaultManualFileCopyUseCaseTest {
private static DocumentRecordRepository repositoryReturning(DocumentRecordLookupResult result) { private static DocumentRecordRepository repositoryReturning(DocumentRecordLookupResult result) {
return new DocumentRecordRepository() { return new DocumentRecordRepository() {
@Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fp) { return result; } @Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fp) { return result; }
@Override public void create(DocumentRecord r) { } @Override public void create(DocumentRecord r) {
@Override public void update(DocumentRecord r) { } // intentionally empty
@Override public void deleteByFingerprint(DocumentFingerprint fp) { } }
@Override public void update(DocumentRecord r) {
// intentionally empty
}
@Override public void deleteByFingerprint(DocumentFingerprint fp) {
// intentionally empty
}
}; };
} }
@@ -125,7 +141,9 @@ class DefaultManualFileCopyUseCaseTest {
return new TargetFolderPort() { return new TargetFolderPort() {
@Override public String getTargetFolderLocator() { return "/zielordner"; } @Override public String getTargetFolderLocator() { return "/zielordner"; }
@Override public TargetFilenameResolutionResult resolveUniqueFilename(String baseName, DocumentFingerprint fp) { return result; } @Override public TargetFilenameResolutionResult resolveUniqueFilename(String baseName, DocumentFingerprint fp) { return result; }
@Override public void tryDeleteTargetFile(String name) { } @Override public void tryDeleteTargetFile(String name) {
// intentionally empty
}
}; };
} }
@@ -439,7 +457,9 @@ class DefaultManualFileCopyUseCaseTest {
baseNames.add(baseName); baseNames.add(baseName);
return new ResolvedTargetFilename(baseName); return new ResolvedTargetFilename(baseName);
} }
@Override public void tryDeleteTargetFile(String name) { } @Override public void tryDeleteTargetFile(String name) {
// intentionally empty
}
}; };
DefaultManualFileCopyUseCase useCase = new DefaultManualFileCopyUseCase( DefaultManualFileCopyUseCase useCase = new DefaultManualFileCopyUseCase(
@@ -545,11 +565,21 @@ class DefaultManualFileCopyUseCaseTest {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private static class NoOpTransactionOperations implements UnitOfWorkPort.TransactionOperations { private static class NoOpTransactionOperations implements UnitOfWorkPort.TransactionOperations {
@Override public void saveProcessingAttempt(ProcessingAttempt attempt) { } @Override public void saveProcessingAttempt(ProcessingAttempt attempt) {
@Override public void createDocumentRecord(DocumentRecord record) { } // intentionally empty
@Override public void updateDocumentRecord(DocumentRecord record) { } }
@Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } @Override public void createDocumentRecord(DocumentRecord record) {
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } // intentionally empty
}
@Override public void updateDocumentRecord(DocumentRecord record) {
// intentionally empty
}
@Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) {
// intentionally empty
}
} }
private static class RecordCapturingTransactionOperations implements UnitOfWorkPort.TransactionOperations { private static class RecordCapturingTransactionOperations implements UnitOfWorkPort.TransactionOperations {
@@ -559,10 +589,18 @@ class DefaultManualFileCopyUseCaseTest {
this.captured = captured; this.captured = captured;
} }
@Override public void saveProcessingAttempt(ProcessingAttempt attempt) { } @Override public void saveProcessingAttempt(ProcessingAttempt attempt) {
@Override public void createDocumentRecord(DocumentRecord record) { } // intentionally empty
}
@Override public void createDocumentRecord(DocumentRecord record) {
// intentionally empty
}
@Override public void updateDocumentRecord(DocumentRecord record) { captured.add(record); } @Override public void updateDocumentRecord(DocumentRecord record) { captured.add(record); }
@Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } // intentionally empty
}
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) {
// intentionally empty
}
} }
} }
@@ -118,11 +118,21 @@ class DefaultManualFileRenameUseCaseTest {
private static ProcessingLogger noOpLogger() { private static ProcessingLogger noOpLogger() {
return new ProcessingLogger() { return new ProcessingLogger() {
@Override public void info(String msg, Object... args) { } @Override public void info(String msg, Object... args) {
@Override public void debug(String msg, Object... args) { } // intentionally empty
@Override public void debugSensitiveAiContent(String msg, Object... args) { } }
@Override public void warn(String msg, Object... args) { } @Override public void debug(String msg, Object... args) {
@Override public void error(String msg, Object... args) { } // intentionally empty
}
@Override public void debugSensitiveAiContent(String msg, Object... args) {
// intentionally empty
}
@Override public void warn(String msg, Object... args) {
// intentionally empty
}
@Override public void error(String msg, Object... args) {
// intentionally empty
}
}; };
} }
@@ -133,9 +143,15 @@ class DefaultManualFileRenameUseCaseTest {
private static DocumentRecordRepository repositoryReturning(DocumentRecordLookupResult result) { private static DocumentRecordRepository repositoryReturning(DocumentRecordLookupResult result) {
return new DocumentRecordRepository() { return new DocumentRecordRepository() {
@Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fp) { return result; } @Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fp) { return result; }
@Override public void create(DocumentRecord r) { } @Override public void create(DocumentRecord r) {
@Override public void update(DocumentRecord r) { } // intentionally empty
@Override public void deleteByFingerprint(DocumentFingerprint fp) { } }
@Override public void update(DocumentRecord r) {
// intentionally empty
}
@Override public void deleteByFingerprint(DocumentFingerprint fp) {
// intentionally empty
}
}; };
} }
@@ -143,7 +159,9 @@ class DefaultManualFileRenameUseCaseTest {
return new TargetFolderPort() { return new TargetFolderPort() {
@Override public String getTargetFolderLocator() { return "/zielordner"; } @Override public String getTargetFolderLocator() { return "/zielordner"; }
@Override public TargetFilenameResolutionResult resolveUniqueFilename(String baseName, DocumentFingerprint fp) { return result; } @Override public TargetFilenameResolutionResult resolveUniqueFilename(String baseName, DocumentFingerprint fp) { return result; }
@Override public void tryDeleteTargetFile(String name) { } @Override public void tryDeleteTargetFile(String name) {
// intentionally empty
}
}; };
} }
@@ -475,7 +493,9 @@ class DefaultManualFileRenameUseCaseTest {
folderArgs.add(new String[]{baseName}); folderArgs.add(new String[]{baseName});
return new ResolvedTargetFilename(baseName); return new ResolvedTargetFilename(baseName);
} }
@Override public void tryDeleteTargetFile(String name) { } @Override public void tryDeleteTargetFile(String name) {
// intentionally empty
}
}; };
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase( DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
@@ -616,11 +636,21 @@ class DefaultManualFileRenameUseCaseTest {
/** Führt keine Persistenzoperationen durch. */ /** Führt keine Persistenzoperationen durch. */
private static class NoOpTransactionOperations implements UnitOfWorkPort.TransactionOperations { private static class NoOpTransactionOperations implements UnitOfWorkPort.TransactionOperations {
@Override public void saveProcessingAttempt(ProcessingAttempt attempt) { } @Override public void saveProcessingAttempt(ProcessingAttempt attempt) {
@Override public void createDocumentRecord(DocumentRecord record) { } // intentionally empty
@Override public void updateDocumentRecord(DocumentRecord record) { } }
@Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } @Override public void createDocumentRecord(DocumentRecord record) {
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } // intentionally empty
}
@Override public void updateDocumentRecord(DocumentRecord record) {
// intentionally empty
}
@Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) {
// intentionally empty
}
} }
/** Zeichnet updateDocumentRecord-Aufrufe auf. */ /** Zeichnet updateDocumentRecord-Aufrufe auf. */
@@ -631,10 +661,18 @@ class DefaultManualFileRenameUseCaseTest {
this.captured = captured; this.captured = captured;
} }
@Override public void saveProcessingAttempt(ProcessingAttempt attempt) { } @Override public void saveProcessingAttempt(ProcessingAttempt attempt) {
@Override public void createDocumentRecord(DocumentRecord record) { } // intentionally empty
}
@Override public void createDocumentRecord(DocumentRecord record) {
// intentionally empty
}
@Override public void updateDocumentRecord(DocumentRecord record) { captured.add(record); } @Override public void updateDocumentRecord(DocumentRecord record) { captured.add(record); }
@Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } // intentionally empty
}
@Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) {
// intentionally empty
}
} }
} }
@@ -179,11 +179,21 @@ class DefaultResetDocumentStatusUseCaseTest {
private static ProcessingLogger noOpLogger() { private static ProcessingLogger noOpLogger() {
return new ProcessingLogger() { return new ProcessingLogger() {
@Override public void info(String msg, Object... args) { } @Override public void info(String msg, Object... args) {
@Override public void debug(String msg, Object... args) { } // intentionally empty
@Override public void debugSensitiveAiContent(String msg, Object... args) { } }
@Override public void warn(String msg, Object... args) { } @Override public void debug(String msg, Object... args) {
@Override public void error(String msg, Object... args) { } // intentionally empty
}
@Override public void debugSensitiveAiContent(String msg, Object... args) {
// intentionally empty
}
@Override public void warn(String msg, Object... args) {
// intentionally empty
}
@Override public void error(String msg, Object... args) {
// intentionally empty
}
}; };
} }
@@ -202,15 +212,21 @@ class DefaultResetDocumentStatusUseCaseTest {
@Override @Override
public void saveProcessingAttempt( public void saveProcessingAttempt(
de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt attempt) { } de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt attempt) {
// intentionally empty
}
@Override @Override
public void createDocumentRecord( public void createDocumentRecord(
de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { } de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void updateDocumentRecord( public void updateDocumentRecord(
de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { } de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
@@ -128,11 +128,17 @@ class DefaultResolveHistoricalDocumentContextUseCaseTest {
throw new DocumentPersistenceException("Verbindungsfehler", null); throw new DocumentPersistenceException("Verbindungsfehler", null);
} }
@Override @Override
public void create(DocumentRecord record) {} public void create(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void update(DocumentRecord record) {} public void update(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void deleteByFingerprint(DocumentFingerprint fingerprint) {} public void deleteByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
}; };
var useCase = new DefaultResolveHistoricalDocumentContextUseCase(throwingRepo); var useCase = new DefaultResolveHistoricalDocumentContextUseCase(throwingRepo);
@@ -151,11 +157,17 @@ class DefaultResolveHistoricalDocumentContextUseCaseTest {
return result; return result;
} }
@Override @Override
public void create(DocumentRecord record) {} public void create(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void update(DocumentRecord record) {} public void update(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void deleteByFingerprint(DocumentFingerprint fingerprint) {} public void deleteByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
}; };
} }
@@ -104,11 +104,17 @@ class DefaultResolveHistoricalFileNameUseCaseTest {
throw new DocumentPersistenceException("Verbindungsfehler", null); throw new DocumentPersistenceException("Verbindungsfehler", null);
} }
@Override @Override
public void create(DocumentRecord record) {} public void create(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void update(DocumentRecord record) {} public void update(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void deleteByFingerprint(DocumentFingerprint fingerprint) {} public void deleteByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
}; };
var useCase = new DefaultResolveHistoricalFileNameUseCase(throwingRepo); var useCase = new DefaultResolveHistoricalFileNameUseCase(throwingRepo);
@@ -127,11 +133,17 @@ class DefaultResolveHistoricalFileNameUseCaseTest {
return result; return result;
} }
@Override @Override
public void create(DocumentRecord record) {} public void create(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void update(DocumentRecord record) {} public void update(DocumentRecord record) {
// intentionally empty
}
@Override @Override
public void deleteByFingerprint(DocumentFingerprint fingerprint) {} public void deleteByFingerprint(DocumentFingerprint fingerprint) {
// intentionally empty
}
}; };
} }
@@ -209,6 +209,14 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
* </ul> * </ul>
*/ */
public class BootstrapRunner { public class BootstrapRunner {
private static final String CONFIG_FILE_NOT_NULL = "configFilePath must not be null";
private static final String FINGERPRINT_NOT_NULL = "fingerprint must not be null";
private static final String UNEXPECTED_ERROR_PREFIX = "Unerwarteter Fehler: ";
private static final String CONFIG_LOAD_FAILED_PREFIX = "Konfiguration konnte nicht geladen werden: ";
private static final String CONFIG_FILE_NOT_FOUND_PREFIX = "Konfigurationsdatei nicht gefunden: ";
private static final String CONFIG_NOT_RUNNABLE_PREFIX = "Die Konfiguration ist nicht lauffähig: ";
private static final Logger LOG = LogManager.getLogger(BootstrapRunner.class); private static final Logger LOG = LogManager.getLogger(BootstrapRunner.class);
private static final Path DEFAULT_CONFIG_PATH = Paths.get("config/application.properties"); private static final Path DEFAULT_CONFIG_PATH = Paths.get("config/application.properties");
@@ -942,7 +950,7 @@ public class BootstrapRunner {
configPath.toAbsolutePath()); configPath.toAbsolutePath());
return new GuiStartupContext( return new GuiStartupContext(
GuiConfigurationEditorStateFactory.createBlankStartState(), GuiConfigurationEditorStateFactory.createBlankStartState(),
Optional.of("Konfigurationsdatei nicht gefunden: " + configPath.toAbsolutePath() Optional.of(CONFIG_FILE_NOT_FOUND_PREFIX + configPath.toAbsolutePath()
+ "\nDie GUI startet ohne Konfigurationsdatei."), + "\nDie GUI startet ohne Konfigurationsdatei."),
loader, loader,
writer, writer,
@@ -995,7 +1003,7 @@ public class BootstrapRunner {
e.getMessage(), e); e.getMessage(), e);
return new GuiStartupContext( return new GuiStartupContext(
GuiConfigurationEditorStateFactory.createBlankStartState(), GuiConfigurationEditorStateFactory.createBlankStartState(),
Optional.of("Konfiguration konnte nicht geladen werden: " + e.getMessage()), Optional.of(CONFIG_LOAD_FAILED_PREFIX + e.getMessage()),
loader, loader,
writer, writer,
modelCatalogPort, modelCatalogPort,
@@ -1074,7 +1082,7 @@ public class BootstrapRunner {
} catch (RuntimeException e) { } catch (RuntimeException e) {
LOG.error("Scheduler-Tick: Unerwarteter Fehler: {}", e.getMessage(), e); LOG.error("Scheduler-Tick: Unerwarteter Fehler: {}", e.getMessage(), e);
return new BatchRunTriggerResult.Failed( return new BatchRunTriggerResult.Failed(
"Unerwarteter Fehler: " + e.getMessage(), UNEXPECTED_ERROR_PREFIX + e.getMessage(),
e.getClass().getSimpleName()); e.getClass().getSimpleName());
} }
}; };
@@ -1199,7 +1207,7 @@ public class BootstrapRunner {
LOG.warn("GUI-Anwendungskontext: Konfiguration konnte nicht geladen werden: {}", LOG.warn("GUI-Anwendungskontext: Konfiguration konnte nicht geladen werden: {}",
e.getMessage()); e.getMessage());
guiApplicationRunContext = Optional.empty(); guiApplicationRunContext = Optional.empty();
return Optional.of("Konfiguration konnte nicht geladen werden: " + e.getMessage()); return Optional.of(CONFIG_LOAD_FAILED_PREFIX + e.getMessage());
} catch (InvalidStartConfigurationException e) { } catch (InvalidStartConfigurationException e) {
LOG.warn("GUI-Anwendungskontext: Konfiguration nicht lauffähig: {}", e.getMessage()); LOG.warn("GUI-Anwendungskontext: Konfiguration nicht lauffähig: {}", e.getMessage());
guiApplicationRunContext = Optional.empty(); guiApplicationRunContext = Optional.empty();
@@ -1329,7 +1337,7 @@ public class BootstrapRunner {
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
LOG.error("GUI-Status-Reset: Unerwarteter Fehler: {}", e.getMessage(), e); LOG.error("GUI-Status-Reset: Unerwarteter Fehler: {}", e.getMessage(), e);
return allFailures(fingerprints, "Unerwarteter Fehler: " return allFailures(fingerprints, UNEXPECTED_ERROR_PREFIX
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage())); + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
} }
} }
@@ -1360,7 +1368,7 @@ public class BootstrapRunner {
Path configFilePath, Path configFilePath,
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver, de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver,
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken) { de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(progressObserver, "progressObserver must not be null"); Objects.requireNonNull(progressObserver, "progressObserver must not be null");
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null"); Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath); LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath);
@@ -1390,7 +1398,7 @@ public class BootstrapRunner {
LOG.error("GUI-Verarbeitungslauf: Konfiguration konnte nicht geladen werden: {}", LOG.error("GUI-Verarbeitungslauf: Konfiguration konnte nicht geladen werden: {}",
e.getMessage(), e); e.getMessage(), e);
return GuiBatchRunLaunchOutcome.rejected( return GuiBatchRunLaunchOutcome.rejected(
"Konfiguration konnte nicht geladen werden: " + e.getMessage()); CONFIG_LOAD_FAILED_PREFIX + e.getMessage());
} catch (InvalidStartConfigurationException e) { } catch (InvalidStartConfigurationException e) {
LOG.error("GUI-Verarbeitungslauf: Konfiguration ist nicht lauffähig: {}", e.getMessage()); LOG.error("GUI-Verarbeitungslauf: Konfiguration ist nicht lauffähig: {}", e.getMessage());
return GuiBatchRunLaunchOutcome.rejected( return GuiBatchRunLaunchOutcome.rejected(
@@ -1448,7 +1456,7 @@ public class BootstrapRunner {
Set<DocumentFingerprint> fingerprintFilter, Set<DocumentFingerprint> fingerprintFilter,
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver, de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver,
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken) { de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null"); Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null");
Objects.requireNonNull(progressObserver, "progressObserver must not be null"); Objects.requireNonNull(progressObserver, "progressObserver must not be null");
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null"); Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
@@ -1481,7 +1489,7 @@ public class BootstrapRunner {
LOG.error("GUI-Mini-Verarbeitungslauf: Konfiguration konnte nicht geladen werden: {}", LOG.error("GUI-Mini-Verarbeitungslauf: Konfiguration konnte nicht geladen werden: {}",
e.getMessage(), e); e.getMessage(), e);
return GuiBatchRunLaunchOutcome.rejected( return GuiBatchRunLaunchOutcome.rejected(
"Konfiguration konnte nicht geladen werden: " + e.getMessage()); CONFIG_LOAD_FAILED_PREFIX + e.getMessage());
} catch (InvalidStartConfigurationException e) { } catch (InvalidStartConfigurationException e) {
LOG.error("GUI-Mini-Verarbeitungslauf: Konfiguration ist nicht lauffähig: {}", e.getMessage()); LOG.error("GUI-Mini-Verarbeitungslauf: Konfiguration ist nicht lauffähig: {}", e.getMessage());
return GuiBatchRunLaunchOutcome.rejected( return GuiBatchRunLaunchOutcome.rejected(
@@ -1533,7 +1541,7 @@ public class BootstrapRunner {
ResetDocumentStatusResult resetDocumentStatusForGui( ResetDocumentStatusResult resetDocumentStatusForGui(
Path configFilePath, Path configFilePath,
Set<DocumentFingerprint> fingerprints) { Set<DocumentFingerprint> fingerprints) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprints, "fingerprints must not be null"); Objects.requireNonNull(fingerprints, "fingerprints must not be null");
LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.", LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.",
fingerprints.size(), configFilePath); fingerprints.size(), configFilePath);
@@ -1545,7 +1553,7 @@ public class BootstrapRunner {
} }
if (!Files.exists(configFilePath)) { if (!Files.exists(configFilePath)) {
String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath; String msg = CONFIG_FILE_NOT_FOUND_PREFIX + configFilePath;
LOG.error("GUI-Status-Reset: {}", msg); LOG.error("GUI-Status-Reset: {}", msg);
return allFailures(fingerprints, msg); return allFailures(fingerprints, msg);
} }
@@ -1587,16 +1595,16 @@ public class BootstrapRunner {
} catch (ConfigurationLoadingException e) { } catch (ConfigurationLoadingException e) {
LOG.error("GUI-Status-Reset: Konfiguration konnte nicht geladen werden: {}", e.getMessage(), e); LOG.error("GUI-Status-Reset: Konfiguration konnte nicht geladen werden: {}", e.getMessage(), e);
return allFailures(fingerprints, "Konfiguration konnte nicht geladen werden: " + e.getMessage()); return allFailures(fingerprints, CONFIG_LOAD_FAILED_PREFIX + e.getMessage());
} catch (InvalidStartConfigurationException e) { } catch (InvalidStartConfigurationException e) {
LOG.error("GUI-Status-Reset: Konfiguration ist nicht lauffähig: {}", e.getMessage()); LOG.error("GUI-Status-Reset: Konfiguration ist nicht lauffähig: {}", e.getMessage());
return allFailures(fingerprints, "Die Konfiguration ist nicht lauffähig: " + e.getMessage()); return allFailures(fingerprints, CONFIG_NOT_RUNNABLE_PREFIX + e.getMessage());
} catch (DocumentPersistenceException e) { } catch (DocumentPersistenceException e) {
LOG.error("GUI-Status-Reset: SQLite-Initialisierung fehlgeschlagen: {}", e.getMessage(), e); LOG.error("GUI-Status-Reset: SQLite-Initialisierung fehlgeschlagen: {}", e.getMessage(), e);
return allFailures(fingerprints, "SQLite-Datenbank konnte nicht vorbereitet werden: " + e.getMessage()); return allFailures(fingerprints, "SQLite-Datenbank konnte nicht vorbereitet werden: " + e.getMessage());
} catch (RuntimeException e) { } catch (RuntimeException e) {
LOG.error("GUI-Status-Reset: Unerwarteter Fehler: {}", e.getMessage(), e); LOG.error("GUI-Status-Reset: Unerwarteter Fehler: {}", e.getMessage(), e);
return allFailures(fingerprints, "Unerwarteter Fehler: " return allFailures(fingerprints, UNEXPECTED_ERROR_PREFIX
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage())); + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
} }
} }
@@ -1684,13 +1692,13 @@ public class BootstrapRunner {
ManualFileRenameResult performGuiManualFileRename( ManualFileRenameResult performGuiManualFileRename(
Path configFilePath, Path configFilePath,
ManualFileRenameRequest request) { ManualFileRenameRequest request) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(request, "request must not be null"); Objects.requireNonNull(request, "request must not be null");
LOG.info("GUI-Umbenennung: Anfrage für Fingerprint={}, Zielname={}.", LOG.info("GUI-Umbenennung: Anfrage für Fingerprint={}, Zielname={}.",
request.fingerprint().sha256Hex(), request.desiredBaseFileName()); request.fingerprint().sha256Hex(), request.desiredBaseFileName());
if (!Files.exists(configFilePath)) { if (!Files.exists(configFilePath)) {
String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath; String msg = CONFIG_FILE_NOT_FOUND_PREFIX + configFilePath;
LOG.error("GUI-Umbenennung: {}", msg); LOG.error("GUI-Umbenennung: {}", msg);
return new de.gecheckt.pdf.umbenenner.application.port.in return new de.gecheckt.pdf.umbenenner.application.port.in
.ManualFileRenameFileSystemFailure(msg); .ManualFileRenameFileSystemFailure(msg);
@@ -1709,12 +1717,12 @@ public class BootstrapRunner {
e.getMessage(), e); e.getMessage(), e);
return new de.gecheckt.pdf.umbenenner.application.port.in return new de.gecheckt.pdf.umbenenner.application.port.in
.ManualFileRenamePersistenceFailure( .ManualFileRenamePersistenceFailure(
"Konfiguration konnte nicht geladen werden: " + e.getMessage()); CONFIG_LOAD_FAILED_PREFIX + e.getMessage());
} catch (InvalidStartConfigurationException e) { } catch (InvalidStartConfigurationException e) {
LOG.error("GUI-Umbenennung: Konfiguration ist nicht lauffähig: {}", e.getMessage()); LOG.error("GUI-Umbenennung: Konfiguration ist nicht lauffähig: {}", e.getMessage());
return new de.gecheckt.pdf.umbenenner.application.port.in return new de.gecheckt.pdf.umbenenner.application.port.in
.ManualFileRenamePersistenceFailure( .ManualFileRenamePersistenceFailure(
"Die Konfiguration ist nicht lauffähig: " + e.getMessage()); CONFIG_NOT_RUNNABLE_PREFIX + e.getMessage());
} catch (DocumentPersistenceException e) { } catch (DocumentPersistenceException e) {
LOG.error("GUI-Umbenennung: SQLite-Initialisierung fehlgeschlagen: {}", LOG.error("GUI-Umbenennung: SQLite-Initialisierung fehlgeschlagen: {}",
e.getMessage(), e); e.getMessage(), e);
@@ -1725,7 +1733,7 @@ public class BootstrapRunner {
LOG.error("GUI-Umbenennung: Unerwarteter Fehler: {}", e.getMessage(), e); LOG.error("GUI-Umbenennung: Unerwarteter Fehler: {}", e.getMessage(), e);
return new de.gecheckt.pdf.umbenenner.application.port.in return new de.gecheckt.pdf.umbenenner.application.port.in
.ManualFileRenameFileSystemFailure( .ManualFileRenameFileSystemFailure(
"Unerwarteter Fehler: " UNEXPECTED_ERROR_PREFIX
+ (e.getMessage() == null + (e.getMessage() == null
? e.getClass().getSimpleName() ? e.getClass().getSimpleName()
: e.getMessage())); : e.getMessage()));
@@ -1748,13 +1756,13 @@ public class BootstrapRunner {
ManualFileCopyResult performGuiManualFileCopy( ManualFileCopyResult performGuiManualFileCopy(
Path configFilePath, Path configFilePath,
ManualFileCopyRequest request) { ManualFileCopyRequest request) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(request, "request must not be null"); Objects.requireNonNull(request, "request must not be null");
LOG.info("GUI-Dateikopie: Anfrage für Fingerprint={}, Zielname={}.", LOG.info("GUI-Dateikopie: Anfrage für Fingerprint={}, Zielname={}.",
request.fingerprint().sha256Hex(), request.desiredBaseFileName()); request.fingerprint().sha256Hex(), request.desiredBaseFileName());
if (!Files.exists(configFilePath)) { if (!Files.exists(configFilePath)) {
String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath; String msg = CONFIG_FILE_NOT_FOUND_PREFIX + configFilePath;
LOG.error("GUI-Dateikopie: {}", msg); LOG.error("GUI-Dateikopie: {}", msg);
return new de.gecheckt.pdf.umbenenner.application.port.in return new de.gecheckt.pdf.umbenenner.application.port.in
.ManualFileCopyFileSystemFailure(msg); .ManualFileCopyFileSystemFailure(msg);
@@ -1773,12 +1781,12 @@ public class BootstrapRunner {
e.getMessage(), e); e.getMessage(), e);
return new de.gecheckt.pdf.umbenenner.application.port.in return new de.gecheckt.pdf.umbenenner.application.port.in
.ManualFileCopyPersistenceFailure( .ManualFileCopyPersistenceFailure(
"Konfiguration konnte nicht geladen werden: " + e.getMessage()); CONFIG_LOAD_FAILED_PREFIX + e.getMessage());
} catch (InvalidStartConfigurationException e) { } catch (InvalidStartConfigurationException e) {
LOG.error("GUI-Dateikopie: Konfiguration ist nicht lauffähig: {}", e.getMessage()); LOG.error("GUI-Dateikopie: Konfiguration ist nicht lauffähig: {}", e.getMessage());
return new de.gecheckt.pdf.umbenenner.application.port.in return new de.gecheckt.pdf.umbenenner.application.port.in
.ManualFileCopyPersistenceFailure( .ManualFileCopyPersistenceFailure(
"Die Konfiguration ist nicht lauffähig: " + e.getMessage()); CONFIG_NOT_RUNNABLE_PREFIX + e.getMessage());
} catch (DocumentPersistenceException e) { } catch (DocumentPersistenceException e) {
LOG.error("GUI-Dateikopie: SQLite-Initialisierung fehlgeschlagen: {}", LOG.error("GUI-Dateikopie: SQLite-Initialisierung fehlgeschlagen: {}",
e.getMessage(), e); e.getMessage(), e);
@@ -1789,7 +1797,7 @@ public class BootstrapRunner {
LOG.error("GUI-Dateikopie: Unerwarteter Fehler: {}", e.getMessage(), e); LOG.error("GUI-Dateikopie: Unerwarteter Fehler: {}", e.getMessage(), e);
return new de.gecheckt.pdf.umbenenner.application.port.in return new de.gecheckt.pdf.umbenenner.application.port.in
.ManualFileCopyFileSystemFailure( .ManualFileCopyFileSystemFailure(
"Unerwarteter Fehler: " UNEXPECTED_ERROR_PREFIX
+ (e.getMessage() == null + (e.getMessage() == null
? e.getClass().getSimpleName() ? e.getClass().getSimpleName()
: e.getMessage())); : e.getMessage()));
@@ -1814,8 +1822,8 @@ public class BootstrapRunner {
Optional<HistoricalDocumentContext> resolveHistoricalDocumentContextForGui( Optional<HistoricalDocumentContext> resolveHistoricalDocumentContextForGui(
Path configFilePath, Path configFilePath,
DocumentFingerprint fingerprint) { DocumentFingerprint fingerprint) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL);
if (!Files.exists(configFilePath)) { if (!Files.exists(configFilePath)) {
LOG.debug("Historischer Kontext: Konfigurationsdatei nicht gefunden: {}", configFilePath); LOG.debug("Historischer Kontext: Konfigurationsdatei nicht gefunden: {}", configFilePath);
@@ -1853,7 +1861,7 @@ public class BootstrapRunner {
DefaultHistoryOverviewUseCase.HistoryOverviewResult loadHistoryOverviewForGui( DefaultHistoryOverviewUseCase.HistoryOverviewResult loadHistoryOverviewForGui(
Path configFilePath, Path configFilePath,
de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQuery query) { de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQuery query) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(query, "query must not be null"); Objects.requireNonNull(query, "query must not be null");
try { try {
migrateConfigurationIfNeeded(configFilePath); migrateConfigurationIfNeeded(configFilePath);
@@ -1883,8 +1891,8 @@ public class BootstrapRunner {
Optional<DefaultHistoryDetailsUseCase.HistoryDetailsResult> loadHistoryDetailsForGui( Optional<DefaultHistoryDetailsUseCase.HistoryDetailsResult> loadHistoryDetailsForGui(
Path configFilePath, Path configFilePath,
DocumentFingerprint fingerprint) { DocumentFingerprint fingerprint) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL);
try { try {
migrateConfigurationIfNeeded(configFilePath); migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath); StartConfiguration config = loadAndValidateConfiguration(configFilePath);
@@ -1913,8 +1921,8 @@ public class BootstrapRunner {
void resetHistoryDocumentStatusForGui( void resetHistoryDocumentStatusForGui(
Path configFilePath, Path configFilePath,
DocumentFingerprint fingerprint) { DocumentFingerprint fingerprint) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL);
LOG.info("Historien-Status-Reset für Fingerprint: {}", fingerprint.sha256Hex()); LOG.info("Historien-Status-Reset für Fingerprint: {}", fingerprint.sha256Hex());
try { try {
migrateConfigurationIfNeeded(configFilePath); migrateConfigurationIfNeeded(configFilePath);
@@ -1945,8 +1953,8 @@ public class BootstrapRunner {
void deleteDocumentHistoryForGui( void deleteDocumentHistoryForGui(
Path configFilePath, Path configFilePath,
DocumentFingerprint fingerprint) { DocumentFingerprint fingerprint) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null"); Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL);
LOG.info("Historien-Löschen für Fingerprint: {}", fingerprint.sha256Hex()); LOG.info("Historien-Löschen für Fingerprint: {}", fingerprint.sha256Hex());
try { try {
migrateConfigurationIfNeeded(configFilePath); migrateConfigurationIfNeeded(configFilePath);
@@ -69,6 +69,7 @@ public final class GuiConfigurationPropertiesWriter implements GuiConfigurationF
* Creates a new properties writer. * Creates a new properties writer.
*/ */
public GuiConfigurationPropertiesWriter() { public GuiConfigurationPropertiesWriter() {
// intentionally empty no initialization required
} }
/** /**
@@ -28,6 +28,9 @@ import java.util.Optional;
* This class is stateless and safe for concurrent use once instantiated. * This class is stateless and safe for concurrent use once instantiated.
*/ */
public class CliArgumentParser { public class CliArgumentParser {
private static final String OPTION_PREFIX = "Option ";
private static final String OPTION_HEADLESS = "--headless"; private static final String OPTION_HEADLESS = "--headless";
private static final String OPTION_CONFIG = "--config"; private static final String OPTION_CONFIG = "--config";
@@ -82,21 +85,12 @@ public class CliArgumentParser {
return new StartupArgumentsParseResult.Invalid( return new StartupArgumentsParseResult.Invalid(
"Duplicate option: " + OPTION_CONFIG); "Duplicate option: " + OPTION_CONFIG);
} }
if (i + 1 >= args.length) { StartupArgumentsParseResult validation = validateConfigPathToken(args, i);
return new StartupArgumentsParseResult.Invalid( if (validation != null) {
"Option " + OPTION_CONFIG + " requires a path argument but none was provided"); return validation;
}
String pathToken = args[i + 1];
if (pathToken.startsWith("--")) {
return new StartupArgumentsParseResult.Invalid(
"Option " + OPTION_CONFIG + " requires a path argument, but got option: " + pathToken);
}
if (pathToken.isBlank()) {
return new StartupArgumentsParseResult.Invalid(
"Option " + OPTION_CONFIG + " requires a non-blank path argument");
} }
configSeen = true; configSeen = true;
configPath = Optional.of(pathToken); configPath = Optional.of(args[i + 1]);
i += 2; i += 2;
} }
default -> { default -> {
@@ -108,4 +102,29 @@ public class CliArgumentParser {
return new StartupArgumentsParseResult.Valid(new StartupArguments(mode, configPath)); return new StartupArgumentsParseResult.Valid(new StartupArguments(mode, configPath));
} }
/**
* Validates that a path token follows the {@code --config} option at position {@code i}.
*
* @param args the argument array
* @param i the index of the {@code --config} token
* @return an {@link StartupArgumentsParseResult.Invalid} if the path token is missing or
* invalid, {@code null} if the token is acceptable
*/
private StartupArgumentsParseResult validateConfigPathToken(String[] args, int i) {
if (i + 1 >= args.length) {
return new StartupArgumentsParseResult.Invalid(
OPTION_PREFIX + OPTION_CONFIG + " requires a path argument but none was provided");
}
String pathToken = args[i + 1];
if (pathToken.startsWith("--")) {
return new StartupArgumentsParseResult.Invalid(
OPTION_PREFIX + OPTION_CONFIG + " requires a path argument, but got option: " + pathToken);
}
if (pathToken.isBlank()) {
return new StartupArgumentsParseResult.Invalid(
OPTION_PREFIX + OPTION_CONFIG + " requires a non-blank path argument");
}
return null;
}
} }
@@ -46,27 +46,35 @@ public final class EarlyLogDirectoryInitializer {
*/ */
public static void applyFromArgs(String[] args) { public static void applyFromArgs(String[] args) {
try { try {
if (System.getProperty(SYSTEM_PROPERTY_KEY) != null if (isLogPropertyAlreadySet()) {
&& !System.getProperty(SYSTEM_PROPERTY_KEY).isBlank()) {
return; return;
} }
Path configPath = resolveConfigPath(args); Path configPath = resolveConfigPath(args);
if (configPath == null || !Files.isRegularFile(configPath)) { if (configPath == null || !Files.isRegularFile(configPath)) {
return; return;
} }
Properties properties = new Properties(); applyLogDirectoryFromConfig(configPath);
try (InputStream in = Files.newInputStream(configPath)) {
properties.load(in);
}
String value = properties.getProperty(CONFIG_PROPERTY_KEY);
if (value != null && !value.isBlank()) {
System.setProperty(SYSTEM_PROPERTY_KEY, value.trim());
}
} catch (IOException | RuntimeException ignored) { } catch (IOException | RuntimeException ignored) {
// bewusst still: Log4j2-Fallback aus log4j2.xml übernimmt ansonsten // bewusst still: Log4j2-Fallback aus log4j2.xml übernimmt ansonsten
} }
} }
private static boolean isLogPropertyAlreadySet() {
String val = System.getProperty(SYSTEM_PROPERTY_KEY);
return val != null && !val.isBlank();
}
private static void applyLogDirectoryFromConfig(Path configPath) throws IOException {
Properties properties = new Properties();
try (InputStream in = Files.newInputStream(configPath)) {
properties.load(in);
}
String value = properties.getProperty(CONFIG_PROPERTY_KEY);
if (value != null && !value.isBlank()) {
System.setProperty(SYSTEM_PROPERTY_KEY, value.trim());
}
}
private static Path resolveConfigPath(String[] args) { private static Path resolveConfigPath(String[] args) {
if (args != null) { if (args != null) {
for (int i = 0; i < args.length - 1; i++) { for (int i = 0; i < args.length - 1; i++) {
@@ -450,8 +450,12 @@ class BootstrapRunnerConfigPathSemanticsTest {
} }
private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort { private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort {
@Override public void acquire() {} @Override public void acquire() {
@Override public void release() {} // intentionally empty
}
@Override public void release() {
// intentionally empty
}
@Override @Override
public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() { public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
return java.util.Optional.empty(); return java.util.Optional.empty();
@@ -460,7 +464,9 @@ class BootstrapRunnerConfigPathSemanticsTest {
private static class MockSchemaInitializationPort private static class MockSchemaInitializationPort
implements de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort { implements de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort {
@Override public void initializeSchema() {} @Override public void initializeSchema() {
// intentionally empty
}
} }
private static class MockRunBatchProcessingUseCase private static class MockRunBatchProcessingUseCase
@@ -567,10 +567,14 @@ class BootstrapRunnerEdgeCasesTest {
private static class MockRunLockPort implements RunLockPort { private static class MockRunLockPort implements RunLockPort {
@Override @Override
public void acquire() { } public void acquire() {
// intentionally empty
}
@Override @Override
public void release() { } public void release() {
// intentionally empty
}
@Override @Override
public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() { public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
@@ -256,8 +256,12 @@ class BootstrapRunnerStartupDispatchTest {
// --- Shared mock helpers (mirroring BootstrapRunnerTest pattern) --- // --- Shared mock helpers (mirroring BootstrapRunnerTest pattern) ---
private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort { private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort {
@Override public void acquire() {} @Override public void acquire() {
@Override public void release() {} // intentionally empty
}
@Override public void release() {
// intentionally empty
}
@Override @Override
public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() { public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
return java.util.Optional.empty(); return java.util.Optional.empty();
@@ -266,7 +270,9 @@ class BootstrapRunnerStartupDispatchTest {
private static class MockSchemaInitializationPort private static class MockSchemaInitializationPort
implements de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort { implements de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort {
@Override public void initializeSchema() {} @Override public void initializeSchema() {
// intentionally empty
}
} }
private static class MockRunBatchProcessingUseCase private static class MockRunBatchProcessingUseCase
@@ -546,10 +546,14 @@ class BootstrapRunnerTest {
private static class MockRunLockPort implements RunLockPort { private static class MockRunLockPort implements RunLockPort {
@Override @Override
public void acquire() { } public void acquire() {
// intentionally empty
}
@Override @Override
public void release() { } public void release() {
// intentionally empty
}
@Override @Override
public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() { public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
@@ -189,8 +189,12 @@ class BootstrapSmokeTest {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private static class NoOpRunLockPort implements RunLockPort { private static class NoOpRunLockPort implements RunLockPort {
@Override public void acquire() { } @Override public void acquire() {
@Override public void release() { } // intentionally empty
}
@Override public void release() {
// intentionally empty
}
@Override @Override
public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() { public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
return java.util.Optional.empty(); return java.util.Optional.empty();
@@ -198,6 +202,8 @@ class BootstrapSmokeTest {
} }
private static class NoOpSchemaInitializationPort implements PersistenceSchemaInitializationPort { private static class NoOpSchemaInitializationPort implements PersistenceSchemaInitializationPort {
@Override public void initializeSchema() { } @Override public void initializeSchema() {
// intentionally empty
}
} }
} }