diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java index 176cfef..85e674f 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java @@ -124,6 +124,12 @@ import javafx.stage.Window; * Thread via {@code Platform.runLater}. */ 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 String WELCOME_TEXT = @@ -985,7 +991,7 @@ public final class GuiConfigurationEditorWorkspace { Window owner = root.getScene() == null ? null : root.getScene().getWindow(); FileChooser fileChooser = new FileChooser(); 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()) { Path currentPath = editorState.loadedFileSnapshot().orElseThrow().filePath(); Path parent = currentPath.getParent(); @@ -1055,7 +1061,7 @@ public final class GuiConfigurationEditorWorkspace { FileChooser fileChooser = saveFileChooserFactory.get(); fileChooser.setTitle("Konfiguration speichern"); 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. Path proposedDir = DEFAULT_SAVE_PATH.getParent(); @@ -1504,7 +1510,7 @@ public final class GuiConfigurationEditorWorkspace { FileChooser fileChooser = saveFileChooserFactory.get(); fileChooser.setTitle("Konfiguration speichern"); 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(); if (proposedDirFile.exists()) { fileChooser.setInitialDirectory(proposedDirFile); @@ -1604,13 +1610,13 @@ public final class GuiConfigurationEditorWorkspace { @Override public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() { return new de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure( - "NO_PATH", "Kein Prompt-Pfad konfiguriert."); + "NO_PATH", NO_PROMPT_PATH_MSG); } @Override public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) { return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.WriteFailed( - "Kein Prompt-Pfad konfiguriert.", null); + NO_PROMPT_PATH_MSG, null); } @Override @@ -1619,7 +1625,7 @@ public final class GuiConfigurationEditorWorkspace { de.gecheckt.pdf.umbenenner.application.validation.technicaltest .CorrectionSuggestion.CreatePromptFile suggestion) { 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 // der Dateiname-Editor ungespeicherte Änderungen hat. // Gleiches gilt für den Prompt-Tab. - tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> { - if (oldTab == null || newTab == null) { - return; + tabPane.getSelectionModel().selectedItemProperty().addListener( + (obs, oldTab, newTab) -> handleTabSwitch(oldTab, newTab)); + } + + 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()) { - // Selektion kurz unterdrücken um Rekursion zu vermeiden - boolean shouldDiscard = batchRunTab.confirmDiscardUnsavedFilenameEdits(); - if (!shouldDiscard) { - // Zurück zum Verarbeitungslauf-Tab - Platform.runLater(() -> tabPane.getSelectionModel().select(oldTab)); - } - } else if (oldTab == promptEditorTab.tab() && promptEditorTab.hasDirtyContent()) { - boolean shouldDiscard = promptEditorTab.confirmDiscardIfDirty(); - if (!shouldDiscard) { - Platform.runLater(() -> tabPane.getSelectionModel().select(oldTab)); - } else { - 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() { @@ -2610,7 +2617,7 @@ public final class GuiConfigurationEditorWorkspace { for (EditorValidationFinding finding : report.findings()) { 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()) { fieldFindings.add(new GuiFieldFinding(finding.fieldKey().orElseThrow(), severity, finding.message())); @@ -2619,7 +2626,7 @@ public final class GuiConfigurationEditorWorkspace { // Replace validation-related entries; preserve model-catalog messages (from coordinator) pendingMessages.removeIf(m -> m.source().isPresent() - && "Validierung".equals(m.source().get())); + && OPERATION_VALIDATE.equals(m.source().get())); pendingMessages.addAll(messages); pendingFieldFindings.clear(); @@ -2675,7 +2682,7 @@ public final class GuiConfigurationEditorWorkspace { // Drop silent auto-validation entries so the central message area is not flooded // by keystroke-level background checks; explicit action entries always accumulate. 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. int findingCount = report.findings().size(); diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogCoordinator.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogCoordinator.java index 636c524..b1ee471 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogCoordinator.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogCoordinator.java @@ -51,6 +51,10 @@ import javafx.application.Platform; * {@code Platform.runLater}. */ 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); @@ -203,7 +207,7 @@ public final class GuiModelCatalogCoordinator { String previousManualValue) { // Remove any previous message entries from an earlier retrieval so messages do not // 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); @@ -213,28 +217,28 @@ public final class GuiModelCatalogCoordinator { container.applyModelList(models, previousManualValue); String message = "Modellliste für " + displayName + " geladen (" + models.size() + " " + (models.size() == 1 ? "Eintrag" : "Einträge") + ")."; - pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, message, "Modellabruf")); - LOG.info("GUI-Modellabruf: {} (Provider: {})", message, family.getIdentifier()); + pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, message, OPERATION_MODELLABRUF)); + LOG.info(LOG_MODEL_FETCH_FMT, message, family.getIdentifier()); } case ModelCatalogResult.EmptyList emptyList -> { container.applyManualFallback(GuiModelSource.LIST_UNAVAILABLE_MANUAL_INPUT); String message = "Provider " + displayName + " liefert aktuell keine Modelle. Manuelle Eingabe aktiv."; - pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.HINT, message, "Modellabruf")); - LOG.warn("GUI-Modellabruf: {} (Provider: {})", message, family.getIdentifier()); + pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.HINT, message, OPERATION_MODELLABRUF)); + LOG.warn(LOG_MODEL_FETCH_FMT, message, family.getIdentifier()); } case ModelCatalogResult.IncompleteConfiguration incomplete -> { container.applyManualFallback(GuiModelSource.LIST_UNAVAILABLE_MANUAL_INPUT); String message = "Modellliste nicht abrufbar: " + incomplete.missingReason() + ". Manuelle Eingabe aktiv."; - pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.WARNING, message, "Modellabruf")); - LOG.warn("GUI-Modellabruf: {} (Provider: {})", message, family.getIdentifier()); + pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.WARNING, message, OPERATION_MODELLABRUF)); + LOG.warn(LOG_MODEL_FETCH_FMT, message, family.getIdentifier()); } case ModelCatalogResult.TechnicalFailure failure -> { container.applyManualFallback(GuiModelSource.LIST_FAILED_MANUAL_INPUT); String message = "Modellliste nicht abrufbar (" + failure.errorCategory() + "). 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: {})", message, failure.errorDetail(), family.getIdentifier()); } diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java index 774e542..bb631fa 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java @@ -54,6 +54,9 @@ import javafx.scene.layout.VBox; * Hintergrund-Worker-Thread ({@code gui-scheduler-control}) ausgeführt. */ 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); @@ -177,7 +180,7 @@ public final class GuiSchedulerTab { } 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); HBox buttonBox = new HBox(10, startButton, stopButton); @@ -248,7 +251,7 @@ public final class GuiSchedulerTab { switch (status.state()) { case STOPPED -> { statusLabel.setText("○ Gestoppt"); - statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #7f8c8d;"); + statusLabel.setStyle(HEADER_LABEL_STYLE); } case STARTING -> { statusLabel.setText("⟳ Wird gestartet…"); @@ -264,7 +267,7 @@ public final class GuiSchedulerTab { } case STOPPING_BATCH_ACTIVE -> { 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); } } } diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java index 65a91d3..9e91caf 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java @@ -101,6 +101,9 @@ public record GuiStartupContext( Optional applicationContextError, Optional schedulerControlUseCase, Optional 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. @@ -524,21 +527,21 @@ public record GuiStartupContext( createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest .CorrectionSuggestion.CreateDirectory suggestion) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest - .CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext."); + .CorrectionOutcome.NotAttempted(suggestion, NO_PORT_MSG); } @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest .CorrectionSuggestion.CreatePromptFile suggestion) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest - .CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext."); + .CorrectionOutcome.NotAttempted(suggestion, NO_PORT_MSG); } @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest .CorrectionSuggestion.PrepareSqlitePath suggestion) { 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); @@ -599,13 +602,13 @@ public record GuiStartupContext( @Override public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() { 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 public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) { 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 @@ -615,7 +618,7 @@ public record GuiStartupContext( .CorrectionSuggestion.CreatePromptFile suggestion) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest .CorrectionOutcome.NotAttempted( - suggestion, "Kein Prompt-Editor-Port in diesem Startkontext verfügbar."); + suggestion, NO_PROMPT_PORT_MSG); } }; } diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStatusBar.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStatusBar.java index be2a5ec..b063b58 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStatusBar.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStatusBar.java @@ -29,6 +29,9 @@ import javafx.scene.layout.Region; * Die Klasse selbst erzwingt dies nicht; der Aufrufer trägt die Verantwortung. */ public final class GuiStatusBar { + private static final String LABEL_STYLE = "-fx-font-size: 11px; -fx-text-fill: #555555;"; + + /** Anzeigetext wenn keine Konfiguration geladen ist. */ static final String KEIN_PROFIL_TEXT = "Kein Profil geladen"; @@ -58,16 +61,16 @@ public final class GuiStatusBar { // Linkes Segment: Versionsanzeige 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 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); // Rechtes Segment: Konfigurationspfad 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); // Abstandhalter zwischen den Segmenten diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStatusRefreshTimeline.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStatusRefreshTimeline.java index ff5eceb..841b1ff 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStatusRefreshTimeline.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStatusRefreshTimeline.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.Optional; import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase; +import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.util.Duration; @@ -42,7 +43,7 @@ public final class GuiStatusRefreshTimeline { Objects.requireNonNull(onRefresh, "onRefresh must not be null"); this.timeline = new Timeline( new KeyFrame(Duration.seconds(1), e -> onRefresh.run())); - this.timeline.setCycleCount(Timeline.INDEFINITE); + this.timeline.setCycleCount(Animation.INDEFINITE); } /** diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTooltipTexts.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTooltipTexts.java index 2e71832..c82837d 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTooltipTexts.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTooltipTexts.java @@ -87,7 +87,7 @@ public final class GuiTooltipTexts { /** Tooltip für das Eingabefeld „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". */ public static final String PROVIDER_TIMEOUT = diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java index 7474ec4..9923822 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java @@ -63,6 +63,9 @@ import javafx.scene.control.Alert; * */ 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 String WORKER_THREAD_NAME = "gui-batch-run"; @@ -353,7 +356,7 @@ public final class GuiBatchRunCoordinator { * @throws NullPointerException if {@code configFilePath} is {@code null} */ public boolean start(Path configFilePath) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); if (isRunning()) { return false; } @@ -379,7 +382,7 @@ public final class GuiBatchRunCoordinator { */ public boolean startMiniRun(Path configFilePath, Set fingerprintFilter) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null"); if (isRunning()) { return false; @@ -411,7 +414,7 @@ public final class GuiBatchRunCoordinator { */ public boolean startReprocessing(Path configFilePath, Set fingerprintFilter) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null"); if (isRunning()) { return false; @@ -452,7 +455,7 @@ public final class GuiBatchRunCoordinator { * @throws NullPointerException if any argument is {@code null} */ public boolean startReset(Path configFilePath, Set fingerprints) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); Objects.requireNonNull(fingerprints, "fingerprints must not be null"); if (isRunning()) { return false; diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java index 70a313d..d4bfe9e 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java @@ -111,6 +111,11 @@ import javafx.scene.layout.VBox; * dafür, Hintergrundereignisse vor dem Callback auf den FX-Thread zu übertragen. */ 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); @@ -820,7 +825,7 @@ public final class GuiBatchRunTab { return; } fileNameEditor.discardChanges(); - LOG.debug("Dateiname-Editor: Ungespeicherte Änderung – Benutzer hat verworfen"); + LOG.debug(DIRTY_STATE_MSG); } // Neue Zeile laden @@ -957,55 +962,55 @@ public final class GuiBatchRunTab { */ private void handleCopyResult(ManualFileCopyResult result, GuiBatchRunResultRow row) { switch (result) { - case ManualFileCopySuccess success -> { - LOG.info("Manuelle Dateikopie erfolgreich: {} → {} (Suffix: {})", - 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 ManualFileCopySuccess success -> applyCopySuccess(success, row); + case ManualFileCopyNoOpIdenticalTarget noOp -> applyCopyNoOpIdentical(noOp, row); case ManualFileCopyDocumentNotFound notFound -> { - LOG.warn("Manuelle Dateikopie fehlgeschlagen: {}", notFound.reason()); + LOG.warn(COPY_FAILED_LOG, notFound.reason()); showMessage("Fehler: Dokument nicht gefunden – " + notFound.reason()); } case ManualFileCopyInvalidState invalidState -> { - LOG.warn("Manuelle Dateikopie fehlgeschlagen: {}", invalidState.reason()); + LOG.warn(COPY_FAILED_LOG, invalidState.reason()); showMessage("Fehler: Ungültiger Dokumentstatus – " + invalidState.reason()); } case ManualFileCopyFileSystemFailure fsFail -> { - LOG.warn("Manuelle Dateikopie fehlgeschlagen: {}", fsFail.message()); + LOG.warn(COPY_FAILED_LOG, fsFail.message()); showMessage("Dateisystemfehler: " + fsFail.message()); } case ManualFileCopyPersistenceFailure persistFail -> { - LOG.warn("Manuelle Dateikopie fehlgeschlagen: {}", persistFail.message()); - showMessage("Persistenzfehler (Zielkopie wurde zurückgerollt): " - + persistFail.message()); + LOG.warn(COPY_FAILED_LOG, persistFail.message()); + showMessage("Persistenzfehler (Zielkopie wurde zurückgerollt): " + 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 * {@code SUCCESS} gehoben wurde. Status, korrigierter Dateiname und das Zurücksetzen @@ -1105,24 +1110,24 @@ public final class GuiBatchRunTab { noOp.existingFileName()); } case ManualFileRenameDocumentNotFound notFound -> { - LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", notFound.reason()); + LOG.warn(RENAME_FAILED_LOG, notFound.reason()); showMessage("Fehler: Dokument nicht gefunden – " + notFound.reason()); } case ManualFileRenameInvalidState invalidState -> { - LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", invalidState.reason()); + LOG.warn(RENAME_FAILED_LOG, invalidState.reason()); showMessage("Fehler: Ungültiger Dokumentstatus – " + invalidState.reason()); } case ManualFileRenameSourceFileMissing sourceMissing -> { - LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", + LOG.warn(RENAME_FAILED_LOG, sourceMissing.expectedFileName()); showMessage("Zieldatei nicht gefunden – Umbenennung nicht möglich"); } case ManualFileRenameFileSystemFailure fsFail -> { - LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", fsFail.message()); + LOG.warn(RENAME_FAILED_LOG, fsFail.message()); showMessage("Dateisystemfehler: " + fsFail.message()); } case ManualFileRenamePersistenceFailure persistFail -> { - LOG.warn("Manuelle Dateiumbenennung fehlgeschlagen: {}", persistFail.message()); + LOG.warn(RENAME_FAILED_LOG, persistFail.message()); showMessage("Persistenzfehler (Dateisystem wurde zurückgerollt): " + persistFail.message()); } @@ -1263,7 +1268,7 @@ public final class GuiBatchRunTab { return; } fileNameEditor.discardChanges(); - LOG.debug("Dateiname-Editor: Ungespeicherte Änderung – Benutzer hat verworfen"); + LOG.debug(DIRTY_STATE_MSG); } if (!savedConfigurationReadyCheck.getAsBoolean()) { showMessage(NO_SAVED_CONFIGURATION_HINT); @@ -1317,7 +1322,7 @@ public final class GuiBatchRunTab { return; } fileNameEditor.discardChanges(); - LOG.debug("Dateiname-Editor: Ungespeicherte Änderung – Benutzer hat verworfen"); + LOG.debug(DIRTY_STATE_MSG); } if (!savedConfigurationReadyCheck.getAsBoolean()) { showMessage(NO_SAVED_CONFIGURATION_HINT); @@ -1562,35 +1567,12 @@ public final class GuiBatchRunTab { return builder.toString(); } if (row.status() == DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED) { - 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(); + return appendSkippedAlreadyProcessed(builder, row); } if (row.status() == DocumentCompletionStatus.SKIPPED_FINAL_FAILURE) { - 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(); + return appendSkippedFinalFailure(builder, row); } 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); row.aiFailureMessage().ifPresent(msg -> builder.append("\n\nFehlerdetail: ") @@ -1611,6 +1593,34 @@ public final class GuiBatchRunTab { 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( Path p, Set f, de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver o, diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/ProcessingStatusPresentation.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/ProcessingStatusPresentation.java index e642bf3..af21ef1 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/ProcessingStatusPresentation.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/ProcessingStatusPresentation.java @@ -20,6 +20,9 @@ import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus; * Alle Methoden sind statisch. */ 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+) @@ -166,7 +169,7 @@ public final class ProcessingStatusPresentation { * @throws NullPointerException wenn {@code status} {@code null} ist */ public static String iconFor(DocumentCompletionStatus status) { - Objects.requireNonNull(status, "status darf nicht null sein"); + Objects.requireNonNull(status, STATUS_NOT_NULL); return switch (status) { case SUCCESS -> ICON_SUCCESS; case FAILED_RETRYABLE -> ICON_FAILED_RETRYABLE; @@ -187,7 +190,7 @@ public final class ProcessingStatusPresentation { * @throws NullPointerException wenn {@code status} {@code null} ist */ public static String cssColorFor(DocumentCompletionStatus status) { - Objects.requireNonNull(status, "status darf nicht null sein"); + Objects.requireNonNull(status, STATUS_NOT_NULL); return switch (status) { case SUCCESS -> COLOR_SUCCESS; case FAILED_RETRYABLE -> COLOR_FAILED_RETRYABLE; @@ -205,7 +208,7 @@ public final class ProcessingStatusPresentation { * @throws NullPointerException wenn {@code status} {@code null} ist */ public static String tooltipFor(DocumentCompletionStatus status) { - Objects.requireNonNull(status, "status darf nicht null sein"); + Objects.requireNonNull(status, STATUS_NOT_NULL); return switch (status) { case SUCCESS -> TOOLTIP_SUCCESS; case FAILED_RETRYABLE -> TOOLTIP_FAILED_RETRYABLE; @@ -224,7 +227,7 @@ public final class ProcessingStatusPresentation { * @throws NullPointerException wenn {@code status} {@code null} ist */ public static String summaryCategoryFor(DocumentCompletionStatus status) { - Objects.requireNonNull(status, "status darf nicht null sein"); + Objects.requireNonNull(status, STATUS_NOT_NULL); return switch (status) { case SUCCESS -> SUMMARY_CATEGORY_SUCCESS; case FAILED_RETRYABLE -> SUMMARY_CATEGORY_FAILED_RETRYABLE; @@ -243,7 +246,7 @@ public final class ProcessingStatusPresentation { * @throws NullPointerException wenn {@code status} {@code null} ist */ public static StatusVisuals visualsFor(DocumentCompletionStatus status) { - Objects.requireNonNull(status, "status darf nicht null sein"); + Objects.requireNonNull(status, STATUS_NOT_NULL); return new StatusVisuals( iconFor(status), cssColorFor(status), @@ -264,7 +267,7 @@ public final class ProcessingStatusPresentation { * @throws NullPointerException wenn {@code status} {@code null} ist */ public static String displayTextFor(ProcessingStatus status) { - Objects.requireNonNull(status, "status darf nicht null sein"); + Objects.requireNonNull(status, STATUS_NOT_NULL); return switch (status) { case SUCCESS -> "✓ Erfolgreich"; case FAILED_RETRYABLE -> "↻ Temporärer Fehler"; diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java index 0c7f03a..c934dbc 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java @@ -87,6 +87,11 @@ import javafx.scene.layout.VBox; * Verarbeitungslaufs deaktiviert. */ 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); @@ -421,20 +426,20 @@ public final class GuiHistoryTab { addDetailRow(5, "Aktualisiert:", detailUpdatedLabel); Label detailTitle = new Label("Dokument-Details"); - detailTitle.setStyle("-fx-font-weight: bold;"); + detailTitle.setStyle(BOLD_STYLE); // Versuche-Tabelle buildAttemptsTable(); Label attemptsTitle = new Label("Verarbeitungsversuche"); - attemptsTitle.setStyle("-fx-font-weight: bold;"); + attemptsTitle.setStyle(BOLD_STYLE); // Fehlerursache (aus letztem Fehler-Versuch) failureArea.setEditable(false); failureArea.setWrapText(true); failureArea.setPrefRowCount(3); - failureArea.setPromptText("Keine Fehlerdetails gespeichert."); + failureArea.setPromptText(NO_ERROR_DETAILS_MSG); 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)); @@ -445,7 +450,7 @@ public final class GuiHistoryTab { reasoningArea.setText(DETAIL_PLACEHOLDER); reasoningArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_REASONING_AREA)); 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, detailTitle, detailGrid, @@ -579,7 +584,7 @@ public final class GuiHistoryTab { Path configPath = configPathSupplier.get(); if (configPath == null) { 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; } @@ -666,7 +671,7 @@ public final class GuiHistoryTab { Path configPath = configPathSupplier.get(); if (configPath == null) { - showInfo("Keine Konfiguration geladen."); + showInfo(NO_CONFIG_LOADED_MSG); return; } @@ -674,28 +679,10 @@ public final class GuiHistoryTab { .filter(r -> r.overallStatus() == ProcessingStatus.SUCCESS) .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); confirm.setTitle("Status zurücksetzen"); confirm.setHeaderText("Status zurücksetzen?"); - confirm.setContentText(sb.toString()); + confirm.setContentText(buildResetConfirmationText(selectedItems, successCount)); Optional choice = confirm.showAndWait(); if (choice.isEmpty() || choice.get() != ButtonType.OK) return; @@ -729,6 +716,27 @@ public final class GuiHistoryTab { }); } + private static String buildResetConfirmationText(List 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() { if (runningCheck.getAsBoolean()) { showInfo(LAUF_AKTIV_HINWEIS); @@ -741,7 +749,7 @@ public final class GuiHistoryTab { Path configPath = configPathSupplier.get(); if (configPath == null) { - showInfo("Keine Konfiguration geladen."); + showInfo(NO_CONFIG_LOADED_MSG); return; } @@ -818,23 +826,26 @@ public final class GuiHistoryTab { // Fehlerursache aus letztem Fehler-Versuch anzeigen showLastFailureMessage(result.attempts(), record.overallStatus()); - // Neuesten Versuch selektieren und Begründung anzeigen - if (!result.attempts().isEmpty()) { - ProcessingAttempt last = result.attempts().get(result.attempts().size() - 1); + selectLatestAttemptAndShowReasoning(result.attempts()); + attemptsTable.getSelectionModel().selectedItemProperty().addListener( + (obs, old, attempt) -> onAttemptSelected(attempt)); + } + + private void selectLatestAttemptAndShowReasoning(java.util.List attempts) { + if (!attempts.isEmpty()) { + ProcessingAttempt last = attempts.get(attempts.size() - 1); attemptsTable.getSelectionModel().select(last); showReasoning(last); } else { reasoningArea.setText(""); reasoningArea.setPromptText(NO_REASONING_TEXT); } + } - // KI-Begründung bei Versuchs-Selektion aktualisieren - attemptsTable.getSelectionModel().selectedItemProperty().addListener( - (obs, old, attempt) -> { - if (attempt != null) { - showReasoning(attempt); - } - }); + private void onAttemptSelected(ProcessingAttempt attempt) { + if (attempt != null) { + showReasoning(attempt); + } } /** @@ -865,7 +876,7 @@ public final class GuiHistoryTab { failureArea.setText(failureMessage != null ? AiFailureMessageTranslator.translate(failureMessage) : ""); - failureArea.setPromptText("Keine Fehlerdetails gespeichert."); + failureArea.setPromptText(NO_ERROR_DETAILS_MSG); } private void showReasoning(ProcessingAttempt attempt) { @@ -883,7 +894,7 @@ public final class GuiHistoryTab { clearDetailFields(); attemptsItems.clear(); failureArea.setText(""); - failureArea.setPromptText("Keine Fehlerdetails gespeichert."); + failureArea.setPromptText(NO_ERROR_DETAILS_MSG); reasoningArea.setText(DETAIL_PLACEHOLDER); } @@ -906,7 +917,7 @@ public final class GuiHistoryTab { private void addDetailRow(int row, String labelText, Label valueLabel) { Label label = new Label(labelText); - label.setStyle("-fx-font-weight: bold;"); + label.setStyle(BOLD_STYLE); valueLabel.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(valueLabel, Priority.ALWAYS); detailGrid.add(label, 0, row); diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorMiniRunTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorMiniRunTest.java index dff2f6c..d1813c3 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorMiniRunTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorMiniRunTest.java @@ -119,9 +119,15 @@ class GuiBatchRunCoordinatorMiniRunTest { void startReset_invokesResetPortAndDispatchesResult() { AtomicReference captured = new AtomicReference<>(); GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() { - @Override public void onRunStarted(RunId runId, int totalCandidates) { } - @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } - @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } + @Override public void onRunStarted(RunId runId, int totalCandidates) { + // intentionally empty + } + @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { + // intentionally empty + } + @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { + // intentionally empty + } @Override public void onResetCompleted(ResetDocumentStatusResult result) { captured.set(result); } @@ -170,9 +176,15 @@ class GuiBatchRunCoordinatorMiniRunTest { void startReset_portThrowsException_mapsToAllFailures() { AtomicReference captured = new AtomicReference<>(); GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() { - @Override public void onRunStarted(RunId runId, int totalCandidates) { } - @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } - @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } + @Override public void onRunStarted(RunId runId, int totalCandidates) { + // intentionally empty + } + @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { + // intentionally empty + } + @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { + // intentionally empty + } @Override public void onResetCompleted(ResetDocumentStatusResult result) { captured.set(result); } @@ -198,9 +210,15 @@ class GuiBatchRunCoordinatorMiniRunTest { void listenerDefaultOnResetCompleted_doesNotThrow() { // Verify the default implementation is safe to call. GuiBatchRunCoordinator.Listener listener = new GuiBatchRunCoordinator.Listener() { - @Override public void onRunStarted(RunId runId, int totalCandidates) { } - @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } - @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } + @Override public void onRunStarted(RunId runId, int totalCandidates) { + // intentionally empty + } + @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())); } @@ -223,9 +241,15 @@ class GuiBatchRunCoordinatorMiniRunTest { private static GuiBatchRunCoordinator.Listener noOpListener() { return new GuiBatchRunCoordinator.Listener() { - @Override public void onRunStarted(RunId runId, int totalCandidates) { } - @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } - @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } + @Override public void onRunStarted(RunId runId, int totalCandidates) { + // intentionally empty + } + @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { + // intentionally empty + } + @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { + // intentionally empty + } }; } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorTest.java index 1fc147c..164dd29 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorTest.java @@ -247,8 +247,12 @@ class GuiBatchRunCoordinatorTest { GuiBatchRunCoordinator coordinator = new GuiBatchRunCoordinator( launcher, syncThreadFactory(), syncDispatcher(), new GuiBatchRunCoordinator.Listener() { - @Override public void onRunStarted(RunId runId, int totalCandidates) { } - @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } + @Override public void onRunStarted(RunId runId, int totalCandidates) { + // intentionally empty + } + @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { + // intentionally empty + } @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { captured.set(outcome); } @@ -270,8 +274,12 @@ class GuiBatchRunCoordinatorTest { GuiBatchRunCoordinator coordinator = new GuiBatchRunCoordinator( launcher, syncThreadFactory(), syncDispatcher(), new GuiBatchRunCoordinator.Listener() { - @Override public void onRunStarted(RunId runId, int totalCandidates) { } - @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } + @Override public void onRunStarted(RunId runId, int totalCandidates) { + // intentionally empty + } + @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { + // intentionally empty + } @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { captured.set(outcome); } @@ -322,9 +330,15 @@ class GuiBatchRunCoordinatorTest { private static GuiBatchRunCoordinator.Listener noOpListener() { return new GuiBatchRunCoordinator.Listener() { - @Override public void onRunStarted(RunId runId, int totalCandidates) { } - @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { } - @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { } + @Override public void onRunStarted(RunId runId, int totalCandidates) { + // intentionally empty + } + @Override public void onDocumentCompleted(GuiBatchRunResultRow row) { + // intentionally empty + } + @Override public void onRunEnded(RunSummary summary, GuiBatchRunLaunchOutcome outcome) { + // intentionally empty + } }; } diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/ai/OpenAiHttpAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/ai/OpenAiHttpAdapter.java index 9e2119b..9855b21 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/ai/OpenAiHttpAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/ai/OpenAiHttpAdapter.java @@ -95,6 +95,10 @@ import de.gecheckt.pdf.umbenenner.domain.model.AiRequestRepresentation; * */ 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); @@ -248,20 +252,20 @@ public class OpenAiHttpAdapter implements AiInvocationPort { JSONArray choices = json.optJSONArray("choices"); if (choices == null || choices.isEmpty()) { 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"); } JSONObject firstChoice = choices.getJSONObject(0); JSONObject message = firstChoice.optJSONObject("message"); if (message == null) { 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"); } - String content = message.optString("content", null); + String content = message.optString(JSON_KEY_CONTENT, null); if (content == null || content.isBlank()) { 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"); } return new AiInvocationSuccess(request, new AiRawResponse(content)); @@ -347,11 +351,11 @@ public class OpenAiHttpAdapter implements AiInvocationPort { JSONObject systemMessage = new JSONObject(); systemMessage.put("role", "system"); - systemMessage.put("content", request.promptContent()); + systemMessage.put(JSON_KEY_CONTENT, request.promptContent()); JSONObject userMessage = new JSONObject(); userMessage.put("role", "user"); - userMessage.put("content", request.documentText()); + userMessage.put(JSON_KEY_CONTENT, request.documentText()); body.put("messages", new org.json.JSONArray() .put(systemMessage) diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/modelcatalog/ClaudeModelCatalogAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/modelcatalog/ClaudeModelCatalogAdapter.java index ef478b9..8cbe216 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/modelcatalog/ClaudeModelCatalogAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/modelcatalog/ClaudeModelCatalogAdapter.java @@ -46,6 +46,10 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalog * */ 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); @@ -133,28 +137,28 @@ public class ClaudeModelCatalogAdapter implements AiModelCatalogPort { } catch (java.net.http.HttpTimeoutException e) { 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()); } catch (java.net.ConnectException e) { 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()); } catch (java.net.UnknownHostException e) { 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()); } catch (java.io.IOException e) { 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()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); 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."); } catch (Exception 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()); } } @@ -188,7 +192,7 @@ public class ClaudeModelCatalogAdapter implements AiModelCatalogPort { if (status != 200) { 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); } @@ -291,24 +295,24 @@ public class ClaudeModelCatalogAdapter implements AiModelCatalogPort { return handleResponse(response); } 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()); } 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()); } 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()); } 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()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", + return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION, "Modellabruf wurde unterbrochen."); } catch (Exception 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()); } } diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/modelcatalog/OpenAiCompatibleModelCatalogAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/modelcatalog/OpenAiCompatibleModelCatalogAdapter.java index a678d80..09fd3ec 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/modelcatalog/OpenAiCompatibleModelCatalogAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/modelcatalog/OpenAiCompatibleModelCatalogAdapter.java @@ -46,6 +46,10 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalog * */ 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); @@ -129,28 +133,28 @@ public class OpenAiCompatibleModelCatalogAdapter implements AiModelCatalogPort { } catch (java.net.http.HttpTimeoutException e) { 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()); } catch (java.net.ConnectException e) { 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()); } catch (java.net.UnknownHostException e) { 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()); } catch (java.io.IOException e) { 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()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); 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."); } catch (Exception 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()); } } @@ -184,7 +188,7 @@ public class OpenAiCompatibleModelCatalogAdapter implements AiModelCatalogPort { if (status != 200) { 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); } @@ -285,24 +289,24 @@ public class OpenAiCompatibleModelCatalogAdapter implements AiModelCatalogPort { return handleResponse(response); } 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()); } 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()); } 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()); } 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()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "CONNECTION_FAILURE", + return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION, "Modellabruf wurde unterbrochen."); } catch (Exception 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()); } } diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/prompt/FilesystemPromptPortAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/prompt/FilesystemPromptPortAdapter.java index 2094be5..842c8f4 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/prompt/FilesystemPromptPortAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/prompt/FilesystemPromptPortAdapter.java @@ -48,6 +48,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.PromptIdentifier; * werden propagiert. */ 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); @@ -125,7 +127,7 @@ public class FilesystemPromptPortAdapter implements PromptPort { if (targetDir == null || !Files.isDirectory(targetDir)) { String message = "Zielordner der Prompt-Datei existiert nicht: " + (targetDir != null ? targetDir.toAbsolutePath() : "unbekannt"); - LOG.warn("Prompt speichern fehlgeschlagen: {}", message); + LOG.warn(SAVE_FAILED_LOG_MSG, message); return new PromptSaveResult.TargetDirectoryMissing(message); } @@ -138,7 +140,7 @@ public class FilesystemPromptPortAdapter implements PromptPort { } catch (IOException e) { beräumeTempDatei(tempFile); 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); } @@ -155,7 +157,7 @@ public class FilesystemPromptPortAdapter implements PromptPort { } catch (IOException e) { beräumeTempDatei(tempFile); 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); } } diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/resourcecreation/FilesystemResourceCreationAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/resourcecreation/FilesystemResourceCreationAdapter.java index 660625c..1e4925f 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/resourcecreation/FilesystemResourceCreationAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/resourcecreation/FilesystemResourceCreationAdapter.java @@ -43,6 +43,8 @@ import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceC * Ausnahmen an den Aufrufer weitergegeben. */ public class FilesystemResourceCreationAdapter implements ResourceCreationPort { + private static final String INVALID_PATH_PREFIX = "Ungültiger Pfad: "; + private static final Logger LOG = LogManager.getLogger(FilesystemResourceCreationAdapter.class); @@ -66,7 +68,7 @@ public class FilesystemResourceCreationAdapter implements ResourceCreationPort { public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory suggestion) { Path path = toPath(suggestion.path()); if (path == null) { - String msg = "Ungültiger Pfad: " + suggestion.path(); + String msg = INVALID_PATH_PREFIX + suggestion.path(); LOG.warn("Ordner anlegen fehlgeschlagen: {}", msg); return new CorrectionOutcome.Failed(suggestion, msg); } @@ -114,7 +116,7 @@ public class FilesystemResourceCreationAdapter implements ResourceCreationPort { public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile suggestion) { Path path = toPath(suggestion.path()); if (path == null) { - String msg = "Ungültiger Pfad: " + suggestion.path(); + String msg = INVALID_PATH_PREFIX + suggestion.path(); LOG.warn("Prompt-Datei erzeugen fehlgeschlagen: {}", msg); return new CorrectionOutcome.Failed(suggestion, msg); } @@ -164,7 +166,7 @@ public class FilesystemResourceCreationAdapter implements ResourceCreationPort { public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath suggestion) { Path path = toPath(suggestion.path()); if (path == null) { - String msg = "Ungültiger Pfad: " + suggestion.path(); + String msg = INVALID_PATH_PREFIX + suggestion.path(); LOG.warn("SQLite-Pfad vorbereiten fehlgeschlagen: {}", msg); return new CorrectionOutcome.Failed(suggestion, msg); } diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteProcessingAttemptRepositoryAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteProcessingAttemptRepositoryAdapter.java index 74e8c71..d4d6579 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteProcessingAttemptRepositoryAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteProcessingAttemptRepositoryAdapter.java @@ -43,6 +43,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId; * application/domain type. */ 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); @@ -78,7 +80,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem */ @Override public int loadNextAttemptNumber(DocumentFingerprint fingerprint) { - Objects.requireNonNull(fingerprint, "fingerprint must not be null"); + Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL); String sql = """ SELECT COALESCE(MAX(attempt_number), 0) + 1 AS next_attempt_number @@ -204,7 +206,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem */ @Override public List findAllByFingerprint(DocumentFingerprint fingerprint) { - Objects.requireNonNull(fingerprint, "fingerprint must not be null"); + Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL); String sql = """ SELECT @@ -255,7 +257,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem */ @Override public ProcessingAttempt findLatestProposalReadyAttempt(DocumentFingerprint fingerprint) { - Objects.requireNonNull(fingerprint, "fingerprint must not be null"); + Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL); String sql = """ SELECT @@ -422,7 +424,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem */ @Override 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 = ?"; diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteSchemaInitializationAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteSchemaInitializationAdapter.java index 52d3e39..28ac472 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteSchemaInitializationAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteSchemaInitializationAdapter.java @@ -62,6 +62,11 @@ import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitiali * Domain-/Application-Typen erscheinen keine JDBC- oder SQLite-Typen. */ 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); @@ -71,7 +76,7 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti /** Alle erwarteten Spalten der Tabelle {@code document_record}. */ private static final Set 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", "last_failure_instant", "last_success_instant", "created_at", "updated_at", "last_target_path", "last_target_file_name" @@ -79,7 +84,7 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti /** Alle erwarteten Spalten der Tabelle {@code processing_attempt}. */ private static final Set 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", "model_name", "prompt_identifier", "processed_page_count", "sent_character_count", "ai_raw_response", "ai_reasoning", "resolved_date", "date_source", @@ -286,8 +291,8 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti return DbState.FLYWAY_MANAGED; } // "Leer" = keine Tabellen vorhanden (unabhängig von Dateigröße) - boolean hasFachlicheTabellen = tables.contains("document_record") - || tables.contains("processing_attempt"); + boolean hasFachlicheTabellen = tables.contains(TABLE_DOCUMENT_RECORD) + || tables.contains(TABLE_PROCESSING_ATTEMPT); if (hasFachlicheTabellen) { return DbState.EXISTING_WITHOUT_FLYWAY; } @@ -320,25 +325,25 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti // Tabellen prüfen Set tabellen = readTableNames(meta); - if (!tabellen.contains("document_record")) { + if (!tabellen.contains(TABLE_DOCUMENT_RECORD)) { fehler.add("Tabelle 'document_record' fehlt"); } - if (!tabellen.contains("processing_attempt")) { + if (!tabellen.contains(TABLE_PROCESSING_ATTEMPT)) { fehler.add("Tabelle 'processing_attempt' fehlt"); } // Spalten prüfen – nur wenn Tabellen vorhanden - if (tabellen.contains("document_record")) { - pruefeSpaltenvollstaendigkeit(meta, "document_record", + if (tabellen.contains(TABLE_DOCUMENT_RECORD)) { + pruefeSpaltenvollstaendigkeit(meta, TABLE_DOCUMENT_RECORD, EXPECTED_COLUMNS_DOCUMENT_RECORD, fehler); } - if (tabellen.contains("processing_attempt")) { - pruefeSpaltenvollstaendigkeit(meta, "processing_attempt", + if (tabellen.contains(TABLE_PROCESSING_ATTEMPT)) { + pruefeSpaltenvollstaendigkeit(meta, TABLE_PROCESSING_ATTEMPT, EXPECTED_COLUMNS_PROCESSING_ATTEMPT, fehler); } // Indizes prüfen - if (tabellen.contains("document_record") && tabellen.contains("processing_attempt")) { + if (tabellen.contains(TABLE_DOCUMENT_RECORD) && tabellen.contains(TABLE_PROCESSING_ATTEMPT)) { Set vorhandeneIndizes = readIndexNames(meta); for (String erwartetIndex : EXPECTED_INDEXES) { if (!vorhandeneIndizes.contains(erwartetIndex)) { @@ -348,10 +353,10 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti } // Constraints prüfen (soweit per Metadata prüfbar) - if (tabellen.contains("document_record")) { + if (tabellen.contains(TABLE_DOCUMENT_RECORD)) { pruefeUniqueConstraintAufFingerprint(conn, fehler); } - if (tabellen.contains("processing_attempt")) { + if (tabellen.contains(TABLE_PROCESSING_ATTEMPT)) { pruefeForeignKeyAufDocumentRecord(conn, fehler); } @@ -399,10 +404,10 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti private void pruefeUniqueConstraintAufFingerprint(Connection conn, List fehler) throws SQLException { 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()) { String spalte = rs.getString("COLUMN_NAME"); - if ("fingerprint".equalsIgnoreCase(spalte)) { + if (COL_FINGERPRINT.equalsIgnoreCase(spalte)) { uniqueGefunden = true; break; } @@ -424,12 +429,12 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti private void pruefeForeignKeyAufDocumentRecord(Connection conn, List fehler) throws SQLException { 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()) { String pkTabelle = rs.getString("PKTABLE_NAME"); String fkSpalte = rs.getString("FKCOLUMN_NAME"); - if ("document_record".equalsIgnoreCase(pkTabelle) - && "fingerprint".equalsIgnoreCase(fkSpalte)) { + if (TABLE_DOCUMENT_RECORD.equalsIgnoreCase(pkTabelle) + && COL_FINGERPRINT.equalsIgnoreCase(fkSpalte)) { fkGefunden = true; break; } @@ -561,7 +566,7 @@ public class SqliteSchemaInitializationAdapter implements PersistenceSchemaIniti */ private static Set readIndexNames(DatabaseMetaData meta) throws SQLException { Set 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)) { while (rs.next()) { String indexName = rs.getString("INDEX_NAME"); diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteUnitOfWorkAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteUnitOfWorkAdapter.java index d318c0a..c455681 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteUnitOfWorkAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteUnitOfWorkAdapter.java @@ -24,6 +24,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint; * and processing attempt repositories. */ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort { + private static final String ROLLBACK_FAILED_MSG = "Rollback fehlgeschlagen: {}"; + private static final Logger logger = LogManager.getLogger(SqliteUnitOfWorkAdapter.class); @@ -57,7 +59,7 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort { connection.rollback(); logger.debug("Transaktion zurückgerollt (Dokumentfehler): {}", e.getMessage()); } catch (SQLException rollbackEx) { - logger.error("Rollback fehlgeschlagen: {}", rollbackEx.getMessage(), rollbackEx); + logger.error(ROLLBACK_FAILED_MSG, rollbackEx.getMessage(), rollbackEx); } throw e; } catch (RuntimeException e) { @@ -66,7 +68,7 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort { connection.rollback(); logger.debug("Transaktion zurückgerollt (Laufzeitfehler): {}", e.getMessage()); } 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); } catch (SQLException e) { @@ -75,7 +77,7 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort { connection.rollback(); logger.debug("Transaktion zurückgerollt (SQL-Fehler): {}", e.getMessage()); } 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); } diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/ai/AnthropicClaudeAdapterIntegrationTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/ai/AnthropicClaudeAdapterIntegrationTest.java index cec955a..8e8cb15 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/ai/AnthropicClaudeAdapterIntegrationTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/ai/AnthropicClaudeAdapterIntegrationTest.java @@ -214,10 +214,20 @@ class AnthropicClaudeAdapterIntegrationTest { * where log output is not relevant to the assertion. */ private static class NoOpProcessingLogger implements ProcessingLogger { - @Override public void info(String message, Object... args) {} - @Override public void debug(String message, Object... args) {} - @Override public void warn(String message, Object... args) {} - @Override public void error(String message, Object... args) {} - @Override public void debugSensitiveAiContent(String message, Object... args) {} + @Override public void info(String message, Object... args) { + // intentionally empty + } + @Override public void debug(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 + } } } diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteSchemaInitializationAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteSchemaInitializationAdapterTest.java index 7a2748a..e641e33 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteSchemaInitializationAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteSchemaInitializationAdapterTest.java @@ -1,6 +1,7 @@ package de.gecheckt.pdf.umbenenner.adapter.out.sqlite; 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 java.nio.file.Files; @@ -192,12 +193,14 @@ class SqliteSchemaInitializationAdapterTest { String jdbcUrl = jdbcUrl(dir, "fall3.db"); SqliteSchemaInitializationAdapter adapter = new SqliteSchemaInitializationAdapter(jdbcUrl); - // Erster Aufruf (Fall 1) - adapter.initializeSchema(); - // Zweiter Aufruf (Fall 3) – darf nicht werfen - adapter.initializeSchema(); - // Dritter Aufruf (Fall 3) – ebenfalls idempotent - adapter.initializeSchema(); + assertThatCode(() -> { + // Erster Aufruf (Fall 1) + adapter.initializeSchema(); + // Zweiter Aufruf (Fall 3) – darf nicht werfen + adapter.initializeSchema(); + // Dritter Aufruf (Fall 3) – ebenfalls idempotent + adapter.initializeSchema(); + }).doesNotThrowAnyException(); } @Test @@ -253,16 +256,19 @@ class SqliteSchemaInitializationAdapterTest { ds.setUrl(jdbcUrl); try (Connection conn = ds.getConnection()) { - assertThatThrownBy(() -> { - try (var ps = conn.prepareStatement(""" - 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) - """)) { - ps.executeUpdate(); - } - }).isInstanceOf(SQLException.class); + assertThatThrownBy(() -> insertOrphanedProcessingAttempt(conn)) + .isInstanceOf(SQLException.class); + } + } + + private static void insertOrphanedProcessingAttempt(Connection conn) throws SQLException { + try (var ps = conn.prepareStatement(""" + 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) + """)) { + ps.executeUpdate(); } } diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/targetfolder/FilesystemTargetFolderAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/targetfolder/FilesystemTargetFolderAdapterTest.java index 08f4069..5113436 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/targetfolder/FilesystemTargetFolderAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/targetfolder/FilesystemTargetFolderAdapterTest.java @@ -1,6 +1,7 @@ package de.gecheckt.pdf.umbenenner.adapter.out.targetfolder; 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 java.io.IOException; @@ -219,8 +220,8 @@ class FilesystemTargetFolderAdapterTest { @Test void tryDeleteTargetFile_fileDoesNotExist_doesNotThrow() { - // Must not throw even if the file is absent - adapter.tryDeleteTargetFile("nonexistent.pdf"); + assertThatCode(() -> adapter.tryDeleteTargetFile("nonexistent.pdf")) + .doesNotThrowAnyException(); } // ------------------------------------------------------------------------- diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/PromptSaveResult.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/PromptSaveResult.java index f07efe2..2d59c89 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/PromptSaveResult.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/PromptSaveResult.java @@ -18,6 +18,8 @@ public sealed interface PromptSaveResult PromptSaveResult.WriteFailed, PromptSaveResult.TargetDirectoryMissing, PromptSaveResult.AtomicMoveFailed { + String MESSAGE_NOT_NULL = "message must not be null"; + /** * Die Prompt-Datei wurde erfolgreich gespeichert. @@ -53,7 +55,7 @@ public sealed interface PromptSaveResult * @throws NullPointerException wenn {@code message} null ist */ 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 */ 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 */ public AtomicMoveFailed { - java.util.Objects.requireNonNull(message, "message must not be null"); + java.util.Objects.requireNonNull(message, MESSAGE_NOT_NULL); } } } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/modelcatalog/ModelCatalogResult.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/modelcatalog/ModelCatalogResult.java index 2a44d02..3514a64 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/modelcatalog/ModelCatalogResult.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/modelcatalog/ModelCatalogResult.java @@ -29,6 +29,8 @@ public sealed interface ModelCatalogResult ModelCatalogResult.EmptyList, ModelCatalogResult.IncompleteConfiguration, ModelCatalogResult.TechnicalFailure { + String PROVIDER_ID_NOT_NULL = "providerIdentifier must not be null"; + /** * 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 */ 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(loadedAt, "loadedAt must not be null"); if (models.isEmpty()) { @@ -88,7 +90,7 @@ public sealed interface ModelCatalogResult * @throws NullPointerException if any parameter is {@code null} */ 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"); } } @@ -118,7 +120,7 @@ public sealed interface ModelCatalogResult * @throws NullPointerException if any parameter is {@code null} */ 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"); } } @@ -153,7 +155,7 @@ public sealed interface ModelCatalogResult * @throws NullPointerException if any parameter is {@code null} */ 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(errorDetail, "errorDetail must not be null"); } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/AiResponseParser.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/AiResponseParser.java index e84e65d..02939dd 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/AiResponseParser.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/AiResponseParser.java @@ -38,6 +38,10 @@ import de.gecheckt.pdf.umbenenner.domain.model.ParsedAiResponse; * of {@link AiResponseValidator}. */ public final class AiResponseParser { + private static final String JSON_KEY_TITLE = "title"; + private static final String JSON_KEY_REASONING = "reasoning"; + + private AiResponseParser() { // Static utility – no instances @@ -81,19 +85,19 @@ public final class AiResponseParser { } // 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'"); } - String title = json.getString("title"); + String title = json.getString(JSON_KEY_TITLE); if (title.isBlank()) { return new AiResponseParsingFailure("BLANK_TITLE", "AI response field 'title' is blank"); } // 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'"); } - String reasoning = json.getString("reasoning"); + String reasoning = json.getString(JSON_KEY_REASONING); // Optional field: date String dateString = null; diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileCopyUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileCopyUseCase.java index 000cd09..d3be405 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileCopyUseCase.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileCopyUseCase.java @@ -70,6 +70,17 @@ public class DefaultManualFileCopyUseCase implements ManualFileCopyUseCase { private final ClockPort clock; 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. * @@ -127,86 +138,144 @@ public class DefaultManualFileCopyUseCase implements ManualFileCopyUseCase { logger.info("Manuelle Dateikopie angefordert: Fingerprint={}, Zielname={}", 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); - DocumentRecord record; if (lookupResult instanceof DocumentTerminalFinalFailure terminalFailure) { - record = terminalFailure.record(); + return new RecordLookupOutcome(terminalFailure.record(), null); } else if (lookupResult instanceof DocumentKnownProcessable known) { - record = known.record(); - ProcessingStatus status = record.overallStatus(); - if (status == ProcessingStatus.SUCCESS) { - // Defensiv: SUCCESS sollte über DocumentTerminalSuccess auflösen, nicht hier. + DocumentRecord record = known.record(); + if (record.overallStatus() == ProcessingStatus.SUCCESS) { logger.warn("Manuelle Dateikopie verweigert: Dokument bereits SUCCESS. Fingerprint={}", fingerprint.sha256Hex()); - return new ManualFileCopyInvalidState( + return new RecordLookupOutcome(null, new ManualFileCopyInvalidState( "Dokument ist bereits erfolgreich verarbeitet. Bitte die Umbenennung der " - + "Zieldatei verwenden."); + + "Zieldatei verwenden.")); } + return new RecordLookupOutcome(record, null); } else if (lookupResult instanceof DocumentTerminalSuccess) { logger.warn("Manuelle Dateikopie verweigert: Dokument bereits SUCCESS. Fingerprint={}", fingerprint.sha256Hex()); - return new ManualFileCopyInvalidState( + return new RecordLookupOutcome(null, new ManualFileCopyInvalidState( "Dokument ist bereits erfolgreich verarbeitet. Bitte die Umbenennung der " - + "Zieldatei verwenden."); + + "Zieldatei verwenden.")); } else if (lookupResult instanceof DocumentUnknown) { logger.warn("Manuelle Dateikopie verweigert: Dokument unbekannt. Fingerprint={}", fingerprint.sha256Hex()); - return new ManualFileCopyDocumentNotFound( - "Kein Dokument mit dem angegebenen Fingerprint gefunden."); + return new RecordLookupOutcome(null, new ManualFileCopyDocumentNotFound( + "Kein Dokument mit dem angegebenen Fingerprint gefunden.")); } else if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) { logger.warn("Manuelle Dateikopie fehlgeschlagen: Lookup-Fehler. Fingerprint={}, Ursache={}", fingerprint.sha256Hex(), failure.errorMessage()); - return new ManualFileCopyPersistenceFailure( - "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()); + return new RecordLookupOutcome(null, new ManualFileCopyPersistenceFailure( + "Persistenzfehler beim Laden des Dokumentstammsatzes: " + failure.errorMessage())); } + // 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 = targetFolderPort.resolveUniqueFilename(desiredFullName, fingerprint); - boolean noOpIdentical = false; - String appliedFileName; - if (resolutionResult instanceof ExistingIdenticalTargetFile identical) { - noOpIdentical = true; - appliedFileName = identical.existingFilename(); logger.info("Manuelle Dateikopie: Identische Datei bereits im Zielordner vorhanden. Fingerprint={}", fingerprint.sha256Hex()); + return new FilenameLookupOutcome(identical.existingFilename(), true, null); } else if (resolutionResult instanceof TargetFolderTechnicalFailure folderFailure) { logger.warn("Manuelle Dateikopie fehlgeschlagen: Zielordnerzugriff. Fingerprint={}, Ursache={}", fingerprint.sha256Hex(), folderFailure.errorMessage()); - return new ManualFileCopyFileSystemFailure( - "Zielordner nicht zugänglich: " + folderFailure.errorMessage()); + return new FilenameLookupOutcome(null, false, new ManualFileCopyFileSystemFailure( + "Zielordner nicht zugänglich: " + folderFailure.errorMessage())); } else if (resolutionResult instanceof ResolvedTargetFilename resolved) { - appliedFileName = resolved.resolvedFilename(); - } else { + return new FilenameLookupOutcome(resolved.resolvedFilename(), false, null); + } + 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( - "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) { - 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( - "Unbekanntes Kopier-Ergebnis: " + copyResult.getClass().getSimpleName()); - } - } - - // Schritt 4: Dokument-Stammsatz aktualisieren + /** + * Aktualisiert den Dokument-Stammsatz in der Persistenz und gibt das finale + * Operationsergebnis zurück. Bei einem Persistenzfehler nach erfolgter Zielkopie + * wird ein Best-Effort-Rollback der neu geschriebenen Datei durchgeführt. + * + * @param fingerprint der Fingerprint des Dokuments + * @param record der bisher gültige Dokument-Stammsatz + * @param appliedFileName der tatsächlich verwendete Zieldateiname + * @param noOpIdentical true, wenn keine neue Kopie geschrieben wurde + * @param desiredFullName der ursprünglich gewünschte Zieldateiname + * @return das finale Operationsergebnis + */ + private ManualFileCopyResult persistAndBuildResult(DocumentFingerprint fingerprint, + DocumentRecord record, + String appliedFileName, + boolean noOpIdentical, + String desiredFullName) { var now = clock.now(); DocumentRecord updatedRecord = new DocumentRecord( record.fingerprint(), @@ -248,17 +317,15 @@ public class DefaultManualFileCopyUseCase implements ManualFileCopyUseCase { "Persistenzfehler nach Kopie: " + errorMessage); } - boolean conflictSuffixApplied = !noOpIdentical && !appliedFileName.equals(desiredFullName); - if (noOpIdentical) { logger.info("Manuelle Dateikopie abgeschlossen ohne Schreibvorgang: identische Zieldatei {}.", appliedFileName); return new ManualFileCopyNoOpIdenticalTarget(appliedFileName); } + boolean conflictSuffixApplied = !appliedFileName.equals(desiredFullName); logger.info("Manuelle Dateikopie erfolgreich: {} (Suffix angewendet: {})", appliedFileName, conflictSuffixApplied); - return new ManualFileCopySuccess(appliedFileName, conflictSuffixApplied); } } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/editor/EditorValidationFinding.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/editor/EditorValidationFinding.java index 303765e..d4ba8cf 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/editor/EditorValidationFinding.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/editor/EditorValidationFinding.java @@ -24,6 +24,8 @@ public record EditorValidationFinding( Optional fieldKey, EditorValidationSeverity severity, String message) { + private static final String FIELD_KEY_NOT_NULL = "fieldKey must not be null"; + /** * Erstellt einen neuen Validierungsbefund. @@ -47,7 +49,7 @@ public record EditorValidationFinding( * @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#ERROR} */ 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); } @@ -59,7 +61,7 @@ public record EditorValidationFinding( * @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#WARNING} */ 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); } @@ -71,7 +73,7 @@ public record EditorValidationFinding( * @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#HINT} */ 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); } @@ -93,7 +95,7 @@ public record EditorValidationFinding( * @return ein neuer Befund mit Schweregrad {@link EditorValidationSeverity#INFO} */ 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); } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CorrectionOutcome.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CorrectionOutcome.java index 9bdfd89..0163ca9 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CorrectionOutcome.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CorrectionOutcome.java @@ -21,6 +21,8 @@ public sealed interface CorrectionOutcome permits CorrectionOutcome.Applied, CorrectionOutcome.Failed, CorrectionOutcome.NotAttempted { + String SUGGESTION_NOT_NULL = "suggestion must not be null"; + /** * 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 */ public Applied { - Objects.requireNonNull(suggestion, "suggestion must not be null"); + Objects.requireNonNull(suggestion, SUGGESTION_NOT_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 */ public Failed { - Objects.requireNonNull(suggestion, "suggestion must not be null"); + Objects.requireNonNull(suggestion, SUGGESTION_NOT_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 */ public NotAttempted { - Objects.requireNonNull(suggestion, "suggestion must not be null"); + Objects.requireNonNull(suggestion, SUGGESTION_NOT_NULL); Objects.requireNonNull(reason, "reason must not be null"); } } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CorrectionSuggestion.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CorrectionSuggestion.java index d10ddfa..ea9ff08 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CorrectionSuggestion.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CorrectionSuggestion.java @@ -21,6 +21,9 @@ public sealed interface CorrectionSuggestion permits CorrectionSuggestion.CreateDirectory, CorrectionSuggestion.CreatePromptFile, 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, @@ -53,8 +56,8 @@ public sealed interface CorrectionSuggestion * @throws IllegalArgumentException wenn {@code path} leer ist */ public CreateDirectory { - Objects.requireNonNull(path, "path must not be null"); - Objects.requireNonNull(descriptionForUser, "descriptionForUser must not be null"); + Objects.requireNonNull(path, PATH_NOT_NULL); + Objects.requireNonNull(descriptionForUser, DESCRIPTION_NOT_NULL); if (path.isBlank()) { throw new IllegalArgumentException("path must not be blank"); } @@ -93,8 +96,8 @@ public sealed interface CorrectionSuggestion * {@code maxTitleLength < 1} */ public CreatePromptFile { - Objects.requireNonNull(path, "path must not be null"); - Objects.requireNonNull(descriptionForUser, "descriptionForUser must not be null"); + Objects.requireNonNull(path, PATH_NOT_NULL); + Objects.requireNonNull(descriptionForUser, DESCRIPTION_NOT_NULL); if (path.isBlank()) { throw new IllegalArgumentException("path must not be blank"); } @@ -129,8 +132,8 @@ public sealed interface CorrectionSuggestion * @throws IllegalArgumentException wenn {@code path} leer ist */ public PrepareSqlitePath { - Objects.requireNonNull(path, "path must not be null"); - Objects.requireNonNull(descriptionForUser, "descriptionForUser must not be null"); + Objects.requireNonNull(path, PATH_NOT_NULL); + Objects.requireNonNull(descriptionForUser, DESCRIPTION_NOT_NULL); if (path.isBlank()) { throw new IllegalArgumentException("path must not be blank"); } diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java index f022044..271ecca 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java @@ -1652,6 +1652,7 @@ class BatchRunProcessingUseCaseTest { @Override public void debugSensitiveAiContent(String message, Object... args) { + // intentionally empty } @Override diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java index a742790..e94094f 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java @@ -300,8 +300,12 @@ class BatchRunProgressObservationTest { } private static final class NoOpLock implements RunLockPort { - @Override public void acquire() { } - @Override public void release() { } + @Override public void acquire() { + // intentionally empty + } + @Override public void release() { + // intentionally empty + } @Override public java.util.Optional tryAcquire() { return java.util.Optional.empty(); @@ -335,11 +339,21 @@ class BatchRunProgressObservationTest { } private static final class SilentLogger implements ProcessingLogger { - @Override public void info(String message, Object... args) { } - @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) { } + @Override public void info(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 debug(String message, Object... args) { + // intentionally empty + } + @Override public void debugSensitiveAiContent(String message, Object... args) { + // intentionally empty + } } private static final class RecordingObserver implements BatchRunProgressObserver { @@ -465,21 +479,31 @@ class BatchRunProgressObservationTest { @Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint f) { return new DocumentUnknown(); } - @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) { } - @Override public void deleteByFingerprint(DocumentFingerprint fingerprint) { } + @Override public void create(de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { + // intentionally empty + } + @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 { static final NoAttempts INSTANCE = new NoAttempts(); @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 findAllByFingerprint(DocumentFingerprint fingerprint) { return List.of(); } @Override public ProcessingAttempt findLatestProposalReadyAttempt( 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 { @@ -499,7 +523,9 @@ class BatchRunProgressObservationTest { return new ResolvedTargetFilename(baseFilename); } @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 { diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultDeleteDocumentHistoryUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultDeleteDocumentHistoryUseCaseTest.java index bb52e1f..9524923 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultDeleteDocumentHistoryUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultDeleteDocumentHistoryUseCaseTest.java @@ -83,17 +83,25 @@ class DefaultDeleteDocumentHistoryUseCaseTest { UnitOfWorkPort failingPort = operations -> operations.accept(new UnitOfWorkPort.TransactionOperations() { @Override - public void saveProcessingAttempt(ProcessingAttempt attempt) { } + public void saveProcessingAttempt(ProcessingAttempt attempt) { + // intentionally empty + } @Override - public void createDocumentRecord(DocumentRecord record) { } + public void createDocumentRecord(DocumentRecord record) { + // intentionally empty + } @Override - public void updateDocumentRecord(DocumentRecord record) { } + public void updateDocumentRecord(DocumentRecord record) { + // intentionally empty + } @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { throw new DocumentPersistenceException("Simulated DB error"); } @Override - public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } + public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { + // intentionally empty + } }); DefaultDeleteDocumentHistoryUseCase useCase = @@ -110,11 +118,21 @@ class DefaultDeleteDocumentHistoryUseCaseTest { private static UnitOfWorkPort noOpPort() { return operations -> operations.accept(new UnitOfWorkPort.TransactionOperations() { - @Override public void saveProcessingAttempt(ProcessingAttempt a) { } - @Override public void createDocumentRecord(DocumentRecord r) { } - @Override public void updateDocumentRecord(DocumentRecord r) { } - @Override public void resetDocumentByFingerprint(DocumentFingerprint fp) { } - @Override public void resetDocumentStatusForRetry(DocumentFingerprint fp) { } + @Override public void saveProcessingAttempt(ProcessingAttempt a) { + // intentionally empty + } + @Override public void createDocumentRecord(DocumentRecord r) { + // 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 resetByFingerprintFingerprints = new ArrayList<>(); final List resetStatusForRetryFingerprints = new ArrayList<>(); - @Override public void saveProcessingAttempt(ProcessingAttempt a) { } - @Override public void createDocumentRecord(DocumentRecord r) { } - @Override public void updateDocumentRecord(DocumentRecord r) { } + @Override public void saveProcessingAttempt(ProcessingAttempt a) { + // intentionally empty + } + @Override public void createDocumentRecord(DocumentRecord r) { + // intentionally empty + } + @Override public void updateDocumentRecord(DocumentRecord r) { + // intentionally empty + } @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultHistoryResetDocumentStatusUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultHistoryResetDocumentStatusUseCaseTest.java index 9ef3b3a..ed5eb16 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultHistoryResetDocumentStatusUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultHistoryResetDocumentStatusUseCaseTest.java @@ -84,13 +84,21 @@ class DefaultHistoryResetDocumentStatusUseCaseTest { UnitOfWorkPort failingPort = operations -> operations.accept(new UnitOfWorkPort.TransactionOperations() { @Override - public void saveProcessingAttempt(ProcessingAttempt attempt) { } + public void saveProcessingAttempt(ProcessingAttempt attempt) { + // intentionally empty + } @Override - public void createDocumentRecord(DocumentRecord record) { } + public void createDocumentRecord(DocumentRecord record) { + // intentionally empty + } @Override - public void updateDocumentRecord(DocumentRecord record) { } + public void updateDocumentRecord(DocumentRecord record) { + // intentionally empty + } @Override - public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } + public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { + // intentionally empty + } @Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { throw new DocumentPersistenceException("Simulated DB error"); @@ -111,11 +119,21 @@ class DefaultHistoryResetDocumentStatusUseCaseTest { private static UnitOfWorkPort noOpPort() { return operations -> operations.accept(new UnitOfWorkPort.TransactionOperations() { - @Override public void saveProcessingAttempt(ProcessingAttempt a) { } - @Override public void createDocumentRecord(DocumentRecord r) { } - @Override public void updateDocumentRecord(DocumentRecord r) { } - @Override public void resetDocumentByFingerprint(DocumentFingerprint fp) { } - @Override public void resetDocumentStatusForRetry(DocumentFingerprint fp) { } + @Override public void saveProcessingAttempt(ProcessingAttempt a) { + // intentionally empty + } + @Override public void createDocumentRecord(DocumentRecord r) { + // 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 resetStatusForRetryFingerprints = new ArrayList<>(); final List resetByFingerprintFingerprints = new ArrayList<>(); - @Override public void saveProcessingAttempt(ProcessingAttempt a) { } - @Override public void createDocumentRecord(DocumentRecord r) { } - @Override public void updateDocumentRecord(DocumentRecord r) { } + @Override public void saveProcessingAttempt(ProcessingAttempt a) { + // intentionally empty + } + @Override public void createDocumentRecord(DocumentRecord r) { + // intentionally empty + } + @Override public void updateDocumentRecord(DocumentRecord r) { + // intentionally empty + } @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileCopyUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileCopyUseCaseTest.java index c9e6e23..d6808b8 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileCopyUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileCopyUseCaseTest.java @@ -100,11 +100,21 @@ class DefaultManualFileCopyUseCaseTest { private static ProcessingLogger noOpLogger() { return new ProcessingLogger() { - @Override public void info(String msg, Object... args) { } - @Override public void debug(String msg, Object... args) { } - @Override public void debugSensitiveAiContent(String msg, Object... args) { } - @Override public void warn(String msg, Object... args) { } - @Override public void error(String msg, Object... args) { } + @Override public void info(String msg, Object... args) { + // intentionally empty + } + @Override public void debug(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) { return new DocumentRecordRepository() { @Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fp) { return result; } - @Override public void create(DocumentRecord r) { } - @Override public void update(DocumentRecord r) { } - @Override public void deleteByFingerprint(DocumentFingerprint fp) { } + @Override public void create(DocumentRecord r) { + // intentionally empty + } + @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() { @Override public String getTargetFolderLocator() { return "/zielordner"; } @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); return new ResolvedTargetFilename(baseName); } - @Override public void tryDeleteTargetFile(String name) { } + @Override public void tryDeleteTargetFile(String name) { + // intentionally empty + } }; DefaultManualFileCopyUseCase useCase = new DefaultManualFileCopyUseCase( @@ -545,11 +565,21 @@ class DefaultManualFileCopyUseCaseTest { // ------------------------------------------------------------------------- private static class NoOpTransactionOperations implements UnitOfWorkPort.TransactionOperations { - @Override public void saveProcessingAttempt(ProcessingAttempt attempt) { } - @Override public void createDocumentRecord(DocumentRecord record) { } - @Override public void updateDocumentRecord(DocumentRecord record) { } - @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } - @Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } + @Override public void saveProcessingAttempt(ProcessingAttempt attempt) { + // intentionally empty + } + @Override public void createDocumentRecord(DocumentRecord record) { + // 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 { @@ -559,10 +589,18 @@ class DefaultManualFileCopyUseCaseTest { this.captured = captured; } - @Override public void saveProcessingAttempt(ProcessingAttempt attempt) { } - @Override public void createDocumentRecord(DocumentRecord record) { } + @Override public void saveProcessingAttempt(ProcessingAttempt attempt) { + // intentionally empty + } + @Override public void createDocumentRecord(DocumentRecord record) { + // intentionally empty + } @Override public void updateDocumentRecord(DocumentRecord record) { captured.add(record); } - @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } - @Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } + @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { + // intentionally empty + } + @Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { + // intentionally empty + } } } diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileRenameUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileRenameUseCaseTest.java index 06d9fef..b600276 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileRenameUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultManualFileRenameUseCaseTest.java @@ -118,11 +118,21 @@ class DefaultManualFileRenameUseCaseTest { private static ProcessingLogger noOpLogger() { return new ProcessingLogger() { - @Override public void info(String msg, Object... args) { } - @Override public void debug(String msg, Object... args) { } - @Override public void debugSensitiveAiContent(String msg, Object... args) { } - @Override public void warn(String msg, Object... args) { } - @Override public void error(String msg, Object... args) { } + @Override public void info(String msg, Object... args) { + // intentionally empty + } + @Override public void debug(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) { return new DocumentRecordRepository() { @Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fp) { return result; } - @Override public void create(DocumentRecord r) { } - @Override public void update(DocumentRecord r) { } - @Override public void deleteByFingerprint(DocumentFingerprint fp) { } + @Override public void create(DocumentRecord r) { + // intentionally empty + } + @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() { @Override public String getTargetFolderLocator() { return "/zielordner"; } @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}); return new ResolvedTargetFilename(baseName); } - @Override public void tryDeleteTargetFile(String name) { } + @Override public void tryDeleteTargetFile(String name) { + // intentionally empty + } }; DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase( @@ -616,11 +636,21 @@ class DefaultManualFileRenameUseCaseTest { /** Führt keine Persistenzoperationen durch. */ private static class NoOpTransactionOperations implements UnitOfWorkPort.TransactionOperations { - @Override public void saveProcessingAttempt(ProcessingAttempt attempt) { } - @Override public void createDocumentRecord(DocumentRecord record) { } - @Override public void updateDocumentRecord(DocumentRecord record) { } - @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } - @Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } + @Override public void saveProcessingAttempt(ProcessingAttempt attempt) { + // intentionally empty + } + @Override public void createDocumentRecord(DocumentRecord record) { + // 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. */ @@ -631,10 +661,18 @@ class DefaultManualFileRenameUseCaseTest { this.captured = captured; } - @Override public void saveProcessingAttempt(ProcessingAttempt attempt) { } - @Override public void createDocumentRecord(DocumentRecord record) { } + @Override public void saveProcessingAttempt(ProcessingAttempt attempt) { + // intentionally empty + } + @Override public void createDocumentRecord(DocumentRecord record) { + // intentionally empty + } @Override public void updateDocumentRecord(DocumentRecord record) { captured.add(record); } - @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { } - @Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { } + @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { + // intentionally empty + } + @Override public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) { + // intentionally empty + } } } diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResetDocumentStatusUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResetDocumentStatusUseCaseTest.java index 5101334..b153f14 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResetDocumentStatusUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResetDocumentStatusUseCaseTest.java @@ -179,11 +179,21 @@ class DefaultResetDocumentStatusUseCaseTest { private static ProcessingLogger noOpLogger() { return new ProcessingLogger() { - @Override public void info(String msg, Object... args) { } - @Override public void debug(String msg, Object... args) { } - @Override public void debugSensitiveAiContent(String msg, Object... args) { } - @Override public void warn(String msg, Object... args) { } - @Override public void error(String msg, Object... args) { } + @Override public void info(String msg, Object... args) { + // intentionally empty + } + @Override public void debug(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 public void saveProcessingAttempt( - de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt attempt) { } + de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt attempt) { + // intentionally empty + } @Override public void createDocumentRecord( - de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { } + de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { + // intentionally empty + } @Override public void updateDocumentRecord( - de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { } + de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) { + // intentionally empty + } @Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCaseTest.java index 09d531b..6450bcd 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCaseTest.java @@ -128,11 +128,17 @@ class DefaultResolveHistoricalDocumentContextUseCaseTest { throw new DocumentPersistenceException("Verbindungsfehler", null); } @Override - public void create(DocumentRecord record) {} + public void create(DocumentRecord record) { + // intentionally empty + } @Override - public void update(DocumentRecord record) {} + public void update(DocumentRecord record) { + // intentionally empty + } @Override - public void deleteByFingerprint(DocumentFingerprint fingerprint) {} + public void deleteByFingerprint(DocumentFingerprint fingerprint) { + // intentionally empty + } }; var useCase = new DefaultResolveHistoricalDocumentContextUseCase(throwingRepo); @@ -151,11 +157,17 @@ class DefaultResolveHistoricalDocumentContextUseCaseTest { return result; } @Override - public void create(DocumentRecord record) {} + public void create(DocumentRecord record) { + // intentionally empty + } @Override - public void update(DocumentRecord record) {} + public void update(DocumentRecord record) { + // intentionally empty + } @Override - public void deleteByFingerprint(DocumentFingerprint fingerprint) {} + public void deleteByFingerprint(DocumentFingerprint fingerprint) { + // intentionally empty + } }; } diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalFileNameUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalFileNameUseCaseTest.java index f8a0440..5913ae0 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalFileNameUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalFileNameUseCaseTest.java @@ -104,11 +104,17 @@ class DefaultResolveHistoricalFileNameUseCaseTest { throw new DocumentPersistenceException("Verbindungsfehler", null); } @Override - public void create(DocumentRecord record) {} + public void create(DocumentRecord record) { + // intentionally empty + } @Override - public void update(DocumentRecord record) {} + public void update(DocumentRecord record) { + // intentionally empty + } @Override - public void deleteByFingerprint(DocumentFingerprint fingerprint) {} + public void deleteByFingerprint(DocumentFingerprint fingerprint) { + // intentionally empty + } }; var useCase = new DefaultResolveHistoricalFileNameUseCase(throwingRepo); @@ -127,11 +133,17 @@ class DefaultResolveHistoricalFileNameUseCaseTest { return result; } @Override - public void create(DocumentRecord record) {} + public void create(DocumentRecord record) { + // intentionally empty + } @Override - public void update(DocumentRecord record) {} + public void update(DocumentRecord record) { + // intentionally empty + } @Override - public void deleteByFingerprint(DocumentFingerprint fingerprint) {} + public void deleteByFingerprint(DocumentFingerprint fingerprint) { + // intentionally empty + } }; } diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java index f7a64d8..bb4836d 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java @@ -209,6 +209,14 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId; * */ 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 Path DEFAULT_CONFIG_PATH = Paths.get("config/application.properties"); @@ -942,7 +950,7 @@ public class BootstrapRunner { configPath.toAbsolutePath()); return new GuiStartupContext( GuiConfigurationEditorStateFactory.createBlankStartState(), - Optional.of("Konfigurationsdatei nicht gefunden: " + configPath.toAbsolutePath() + Optional.of(CONFIG_FILE_NOT_FOUND_PREFIX + configPath.toAbsolutePath() + "\nDie GUI startet ohne Konfigurationsdatei."), loader, writer, @@ -995,7 +1003,7 @@ public class BootstrapRunner { e.getMessage(), e); return new GuiStartupContext( GuiConfigurationEditorStateFactory.createBlankStartState(), - Optional.of("Konfiguration konnte nicht geladen werden: " + e.getMessage()), + Optional.of(CONFIG_LOAD_FAILED_PREFIX + e.getMessage()), loader, writer, modelCatalogPort, @@ -1074,7 +1082,7 @@ public class BootstrapRunner { } catch (RuntimeException e) { LOG.error("Scheduler-Tick: Unerwarteter Fehler: {}", e.getMessage(), e); return new BatchRunTriggerResult.Failed( - "Unerwarteter Fehler: " + e.getMessage(), + UNEXPECTED_ERROR_PREFIX + e.getMessage(), e.getClass().getSimpleName()); } }; @@ -1199,7 +1207,7 @@ public class BootstrapRunner { LOG.warn("GUI-Anwendungskontext: Konfiguration konnte nicht geladen werden: {}", e.getMessage()); 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) { LOG.warn("GUI-Anwendungskontext: Konfiguration nicht lauffähig: {}", e.getMessage()); guiApplicationRunContext = Optional.empty(); @@ -1329,7 +1337,7 @@ public class BootstrapRunner { } } catch (RuntimeException 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())); } } @@ -1360,7 +1368,7 @@ public class BootstrapRunner { Path configFilePath, de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver, 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(cancellationToken, "cancellationToken must not be null"); 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: {}", e.getMessage(), e); return GuiBatchRunLaunchOutcome.rejected( - "Konfiguration konnte nicht geladen werden: " + e.getMessage()); + CONFIG_LOAD_FAILED_PREFIX + e.getMessage()); } catch (InvalidStartConfigurationException e) { LOG.error("GUI-Verarbeitungslauf: Konfiguration ist nicht lauffähig: {}", e.getMessage()); return GuiBatchRunLaunchOutcome.rejected( @@ -1448,7 +1456,7 @@ public class BootstrapRunner { Set fingerprintFilter, de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver, 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(progressObserver, "progressObserver 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: {}", e.getMessage(), e); return GuiBatchRunLaunchOutcome.rejected( - "Konfiguration konnte nicht geladen werden: " + e.getMessage()); + CONFIG_LOAD_FAILED_PREFIX + e.getMessage()); } catch (InvalidStartConfigurationException e) { LOG.error("GUI-Mini-Verarbeitungslauf: Konfiguration ist nicht lauffähig: {}", e.getMessage()); return GuiBatchRunLaunchOutcome.rejected( @@ -1533,7 +1541,7 @@ public class BootstrapRunner { ResetDocumentStatusResult resetDocumentStatusForGui( Path configFilePath, Set fingerprints) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); Objects.requireNonNull(fingerprints, "fingerprints must not be null"); LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.", fingerprints.size(), configFilePath); @@ -1545,7 +1553,7 @@ public class BootstrapRunner { } if (!Files.exists(configFilePath)) { - String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath; + String msg = CONFIG_FILE_NOT_FOUND_PREFIX + configFilePath; LOG.error("GUI-Status-Reset: {}", msg); return allFailures(fingerprints, msg); } @@ -1587,16 +1595,16 @@ public class BootstrapRunner { } catch (ConfigurationLoadingException 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) { 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) { LOG.error("GUI-Status-Reset: SQLite-Initialisierung fehlgeschlagen: {}", e.getMessage(), e); return allFailures(fingerprints, "SQLite-Datenbank konnte nicht vorbereitet werden: " + e.getMessage()); } catch (RuntimeException 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())); } } @@ -1684,13 +1692,13 @@ public class BootstrapRunner { ManualFileRenameResult performGuiManualFileRename( Path configFilePath, 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"); LOG.info("GUI-Umbenennung: Anfrage für Fingerprint={}, Zielname={}.", request.fingerprint().sha256Hex(), request.desiredBaseFileName()); if (!Files.exists(configFilePath)) { - String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath; + String msg = CONFIG_FILE_NOT_FOUND_PREFIX + configFilePath; LOG.error("GUI-Umbenennung: {}", msg); return new de.gecheckt.pdf.umbenenner.application.port.in .ManualFileRenameFileSystemFailure(msg); @@ -1709,12 +1717,12 @@ public class BootstrapRunner { e.getMessage(), e); return new de.gecheckt.pdf.umbenenner.application.port.in .ManualFileRenamePersistenceFailure( - "Konfiguration konnte nicht geladen werden: " + e.getMessage()); + CONFIG_LOAD_FAILED_PREFIX + e.getMessage()); } catch (InvalidStartConfigurationException e) { LOG.error("GUI-Umbenennung: Konfiguration ist nicht lauffähig: {}", e.getMessage()); return new de.gecheckt.pdf.umbenenner.application.port.in .ManualFileRenamePersistenceFailure( - "Die Konfiguration ist nicht lauffähig: " + e.getMessage()); + CONFIG_NOT_RUNNABLE_PREFIX + e.getMessage()); } catch (DocumentPersistenceException e) { LOG.error("GUI-Umbenennung: SQLite-Initialisierung fehlgeschlagen: {}", e.getMessage(), e); @@ -1725,7 +1733,7 @@ public class BootstrapRunner { LOG.error("GUI-Umbenennung: Unerwarteter Fehler: {}", e.getMessage(), e); return new de.gecheckt.pdf.umbenenner.application.port.in .ManualFileRenameFileSystemFailure( - "Unerwarteter Fehler: " + UNEXPECTED_ERROR_PREFIX + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage())); @@ -1748,13 +1756,13 @@ public class BootstrapRunner { ManualFileCopyResult performGuiManualFileCopy( Path configFilePath, 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"); LOG.info("GUI-Dateikopie: Anfrage für Fingerprint={}, Zielname={}.", request.fingerprint().sha256Hex(), request.desiredBaseFileName()); if (!Files.exists(configFilePath)) { - String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath; + String msg = CONFIG_FILE_NOT_FOUND_PREFIX + configFilePath; LOG.error("GUI-Dateikopie: {}", msg); return new de.gecheckt.pdf.umbenenner.application.port.in .ManualFileCopyFileSystemFailure(msg); @@ -1773,12 +1781,12 @@ public class BootstrapRunner { e.getMessage(), e); return new de.gecheckt.pdf.umbenenner.application.port.in .ManualFileCopyPersistenceFailure( - "Konfiguration konnte nicht geladen werden: " + e.getMessage()); + CONFIG_LOAD_FAILED_PREFIX + e.getMessage()); } catch (InvalidStartConfigurationException e) { LOG.error("GUI-Dateikopie: Konfiguration ist nicht lauffähig: {}", e.getMessage()); return new de.gecheckt.pdf.umbenenner.application.port.in .ManualFileCopyPersistenceFailure( - "Die Konfiguration ist nicht lauffähig: " + e.getMessage()); + CONFIG_NOT_RUNNABLE_PREFIX + e.getMessage()); } catch (DocumentPersistenceException e) { LOG.error("GUI-Dateikopie: SQLite-Initialisierung fehlgeschlagen: {}", e.getMessage(), e); @@ -1789,7 +1797,7 @@ public class BootstrapRunner { LOG.error("GUI-Dateikopie: Unerwarteter Fehler: {}", e.getMessage(), e); return new de.gecheckt.pdf.umbenenner.application.port.in .ManualFileCopyFileSystemFailure( - "Unerwarteter Fehler: " + UNEXPECTED_ERROR_PREFIX + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage())); @@ -1814,8 +1822,8 @@ public class BootstrapRunner { Optional resolveHistoricalDocumentContextForGui( Path configFilePath, DocumentFingerprint fingerprint) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); - Objects.requireNonNull(fingerprint, "fingerprint must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); + Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL); if (!Files.exists(configFilePath)) { LOG.debug("Historischer Kontext: Konfigurationsdatei nicht gefunden: {}", configFilePath); @@ -1853,7 +1861,7 @@ public class BootstrapRunner { DefaultHistoryOverviewUseCase.HistoryOverviewResult loadHistoryOverviewForGui( Path configFilePath, 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"); try { migrateConfigurationIfNeeded(configFilePath); @@ -1883,8 +1891,8 @@ public class BootstrapRunner { Optional loadHistoryDetailsForGui( Path configFilePath, DocumentFingerprint fingerprint) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); - Objects.requireNonNull(fingerprint, "fingerprint must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); + Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL); try { migrateConfigurationIfNeeded(configFilePath); StartConfiguration config = loadAndValidateConfiguration(configFilePath); @@ -1913,8 +1921,8 @@ public class BootstrapRunner { void resetHistoryDocumentStatusForGui( Path configFilePath, DocumentFingerprint fingerprint) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); - Objects.requireNonNull(fingerprint, "fingerprint must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); + Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL); LOG.info("Historien-Status-Reset für Fingerprint: {}", fingerprint.sha256Hex()); try { migrateConfigurationIfNeeded(configFilePath); @@ -1945,8 +1953,8 @@ public class BootstrapRunner { void deleteDocumentHistoryForGui( Path configFilePath, DocumentFingerprint fingerprint) { - Objects.requireNonNull(configFilePath, "configFilePath must not be null"); - Objects.requireNonNull(fingerprint, "fingerprint must not be null"); + Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL); + Objects.requireNonNull(fingerprint, FINGERPRINT_NOT_NULL); LOG.info("Historien-Löschen für Fingerprint: {}", fingerprint.sha256Hex()); try { migrateConfigurationIfNeeded(configFilePath); diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/GuiConfigurationPropertiesWriter.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/GuiConfigurationPropertiesWriter.java index aea0919..bd1df95 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/GuiConfigurationPropertiesWriter.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/GuiConfigurationPropertiesWriter.java @@ -69,6 +69,7 @@ public final class GuiConfigurationPropertiesWriter implements GuiConfigurationF * Creates a new properties writer. */ public GuiConfigurationPropertiesWriter() { + // intentionally empty – no initialization required } /** diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/CliArgumentParser.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/CliArgumentParser.java index d52d247..0a221bd 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/CliArgumentParser.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/CliArgumentParser.java @@ -28,6 +28,9 @@ import java.util.Optional; * This class is stateless and safe for concurrent use once instantiated. */ public class CliArgumentParser { + private static final String OPTION_PREFIX = "Option "; + + private static final String OPTION_HEADLESS = "--headless"; private static final String OPTION_CONFIG = "--config"; @@ -82,21 +85,12 @@ public class CliArgumentParser { return new StartupArgumentsParseResult.Invalid( "Duplicate option: " + OPTION_CONFIG); } - if (i + 1 >= args.length) { - return new StartupArgumentsParseResult.Invalid( - "Option " + OPTION_CONFIG + " requires a path argument but none was provided"); - } - 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"); + StartupArgumentsParseResult validation = validateConfigPathToken(args, i); + if (validation != null) { + return validation; } configSeen = true; - configPath = Optional.of(pathToken); + configPath = Optional.of(args[i + 1]); i += 2; } default -> { @@ -108,4 +102,29 @@ public class CliArgumentParser { 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; + } } diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/EarlyLogDirectoryInitializer.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/EarlyLogDirectoryInitializer.java index f4f5475..1854db0 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/EarlyLogDirectoryInitializer.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/EarlyLogDirectoryInitializer.java @@ -46,27 +46,35 @@ public final class EarlyLogDirectoryInitializer { */ public static void applyFromArgs(String[] args) { try { - if (System.getProperty(SYSTEM_PROPERTY_KEY) != null - && !System.getProperty(SYSTEM_PROPERTY_KEY).isBlank()) { + if (isLogPropertyAlreadySet()) { return; } Path configPath = resolveConfigPath(args); if (configPath == null || !Files.isRegularFile(configPath)) { return; } - 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()); - } + applyLogDirectoryFromConfig(configPath); } catch (IOException | RuntimeException ignored) { // 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) { if (args != null) { for (int i = 0; i < args.length - 1; i++) { diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerConfigPathSemanticsTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerConfigPathSemanticsTest.java index 97d7266..54728fa 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerConfigPathSemanticsTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerConfigPathSemanticsTest.java @@ -450,8 +450,12 @@ class BootstrapRunnerConfigPathSemanticsTest { } private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort { - @Override public void acquire() {} - @Override public void release() {} + @Override public void acquire() { + // intentionally empty + } + @Override public void release() { + // intentionally empty + } @Override public java.util.Optional tryAcquire() { return java.util.Optional.empty(); @@ -460,7 +464,9 @@ class BootstrapRunnerConfigPathSemanticsTest { private static class MockSchemaInitializationPort implements de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort { - @Override public void initializeSchema() {} + @Override public void initializeSchema() { + // intentionally empty + } } private static class MockRunBatchProcessingUseCase diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerEdgeCasesTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerEdgeCasesTest.java index 87b578b..8fcfc65 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerEdgeCasesTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerEdgeCasesTest.java @@ -567,10 +567,14 @@ class BootstrapRunnerEdgeCasesTest { private static class MockRunLockPort implements RunLockPort { @Override - public void acquire() { } + public void acquire() { + // intentionally empty + } @Override - public void release() { } + public void release() { + // intentionally empty + } @Override public java.util.Optional tryAcquire() { diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerStartupDispatchTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerStartupDispatchTest.java index f947333..b9cfd9e 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerStartupDispatchTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerStartupDispatchTest.java @@ -256,8 +256,12 @@ class BootstrapRunnerStartupDispatchTest { // --- Shared mock helpers (mirroring BootstrapRunnerTest pattern) --- private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort { - @Override public void acquire() {} - @Override public void release() {} + @Override public void acquire() { + // intentionally empty + } + @Override public void release() { + // intentionally empty + } @Override public java.util.Optional tryAcquire() { return java.util.Optional.empty(); @@ -266,7 +270,9 @@ class BootstrapRunnerStartupDispatchTest { private static class MockSchemaInitializationPort implements de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort { - @Override public void initializeSchema() {} + @Override public void initializeSchema() { + // intentionally empty + } } private static class MockRunBatchProcessingUseCase diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java index 1d8c471..df2b4f5 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java @@ -546,10 +546,14 @@ class BootstrapRunnerTest { private static class MockRunLockPort implements RunLockPort { @Override - public void acquire() { } + public void acquire() { + // intentionally empty + } @Override - public void release() { } + public void release() { + // intentionally empty + } @Override public java.util.Optional tryAcquire() { diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapSmokeTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapSmokeTest.java index b316d41..fa21489 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapSmokeTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapSmokeTest.java @@ -189,8 +189,12 @@ class BootstrapSmokeTest { // ------------------------------------------------------------------------- private static class NoOpRunLockPort implements RunLockPort { - @Override public void acquire() { } - @Override public void release() { } + @Override public void acquire() { + // intentionally empty + } + @Override public void release() { + // intentionally empty + } @Override public java.util.Optional tryAcquire() { return java.util.Optional.empty(); @@ -198,6 +202,8 @@ class BootstrapSmokeTest { } private static class NoOpSchemaInitializationPort implements PersistenceSchemaInitializationPort { - @Override public void initializeSchema() { } + @Override public void initializeSchema() { + // intentionally empty + } } }