SonarQube: fix alle BLOCKER- und CRITICAL-Issues (S3252, S2479, S1186, S1192, S2699, S5783, S3776)
- S3252: GuiStatusRefreshTimeline nutzt Animation.INDEFINITE statt Timeline.INDEFINITE - S2479: Narrow-No-Break-Space (U+202F) in GuiTooltipTexts durch normales Leerzeichen ersetzt - S1186: 134 leere Stub-Methoden in 18 Test- und Produktionsdateien kommentiert - S1192: ~49 duplizierte String-Literale in ~25 Klassen als Konstanten extrahiert - S2699: fehlende Assertions in SqliteSchemaInitializationAdapterTest und FilesystemTargetFolderAdapterTest ergaenzt - S5783: Lambda-geprufte Ausnahme in SqliteSchemaInitializationAdapterTest in private Hilfsmethode extrahiert - S3776: kognitive Komplexitaet in 8 Methoden durch Methodenextraktion auf unter 15 gesenkt (EarlyLogDirectoryInitializer, CliArgumentParser, GuiConfigurationEditorWorkspace, GuiHistoryTab x2, GuiBatchRunTab x2, DefaultManualFileCopyUseCase) - Kompilierungsfehler behoben: private-Modifier in CorrectionOutcome-Interface entfernt, selbstreferenzielle Konstante in ModelCatalogResult korrigiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+34
-27
@@ -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();
|
||||
|
||||
+12
-8
@@ -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());
|
||||
}
|
||||
|
||||
+6
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -101,6 +101,9 @@ public record GuiStartupContext(
|
||||
Optional<String> applicationContextError,
|
||||
Optional<SchedulerControlUseCase> schedulerControlUseCase,
|
||||
Optional<ConfigurationFileLockPort> configurationFileLockPort) {
|
||||
private static final String NO_PROMPT_PORT_MSG = "Kein Prompt-Editor-Port in diesem Startkontext verfügbar.";
|
||||
private static final String NO_PORT_MSG = "Kein Port in diesem Startkontext.";
|
||||
|
||||
|
||||
/**
|
||||
* Creates a fully wired startup context.
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+6
-3
@@ -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
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -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 =
|
||||
|
||||
+7
-4
@@ -63,6 +63,9 @@ import javafx.scene.control.Alert;
|
||||
* </ol>
|
||||
*/
|
||||
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<DocumentFingerprint> fingerprintFilter) {
|
||||
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||
Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
|
||||
Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null");
|
||||
if (isRunning()) {
|
||||
return false;
|
||||
@@ -411,7 +414,7 @@ public final class GuiBatchRunCoordinator {
|
||||
*/
|
||||
public boolean startReprocessing(Path configFilePath,
|
||||
Set<DocumentFingerprint> fingerprintFilter) {
|
||||
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||
Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
|
||||
Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null");
|
||||
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<DocumentFingerprint> fingerprints) {
|
||||
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||
Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
|
||||
Objects.requireNonNull(fingerprints, "fingerprints must not be null");
|
||||
if (isRunning()) {
|
||||
return false;
|
||||
|
||||
+78
-68
@@ -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<DocumentFingerprint> f,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver o,
|
||||
|
||||
+9
-6
@@ -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";
|
||||
|
||||
+51
-40
@@ -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<ButtonType> choice = confirm.showAndWait();
|
||||
if (choice.isEmpty() || choice.get() != ButtonType.OK) return;
|
||||
|
||||
@@ -729,6 +716,27 @@ public final class GuiHistoryTab {
|
||||
});
|
||||
}
|
||||
|
||||
private static String buildResetConfirmationText(List<DocumentHistoryRow> selectedItems, long successCount) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Setzt den Status auf READY_FOR_AI zurück.\n");
|
||||
sb.append("Fehlerzähler und letzter Fehlerzeitpunkt werden gelöscht.\n");
|
||||
sb.append("Die Versuchshistorie bleibt vollständig erhalten.\n\n");
|
||||
if (selectedItems.size() == 1) {
|
||||
sb.append("Quelldatei: ").append(selectedItems.get(0).sourceFileName());
|
||||
} else {
|
||||
sb.append(selectedItems.size()).append(" Einträge werden zurückgesetzt.");
|
||||
}
|
||||
if (successCount > 0) {
|
||||
sb.append("\n\nHinweis: ").append(successCount)
|
||||
.append(" der ausgewählten Einträge ")
|
||||
.append(successCount == 1 ? "hat" : "haben")
|
||||
.append(" Status \"Erfolgreich\". ")
|
||||
.append(successCount == 1 ? "Dieser Eintrag wird" : "Diese Einträge werden")
|
||||
.append(" erneut verarbeitet.");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void handleDeleteAction() {
|
||||
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<ProcessingAttempt> 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);
|
||||
|
||||
+36
-12
@@ -119,9 +119,15 @@ class GuiBatchRunCoordinatorMiniRunTest {
|
||||
void startReset_invokesResetPortAndDispatchesResult() {
|
||||
AtomicReference<ResetDocumentStatusResult> 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<ResetDocumentStatusResult> 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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+21
-7
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+10
-6
@@ -95,6 +95,10 @@ import de.gecheckt.pdf.umbenenner.domain.model.AiRequestRepresentation;
|
||||
* </ul>
|
||||
*/
|
||||
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)
|
||||
|
||||
+17
-13
@@ -46,6 +46,10 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalog
|
||||
* </ul>
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
+17
-13
@@ -46,6 +46,10 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalog
|
||||
* </ul>
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -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);
|
||||
}
|
||||
|
||||
+6
-4
@@ -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<ProcessingAttempt> 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 = ?";
|
||||
|
||||
|
||||
+24
-19
@@ -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<String> EXPECTED_COLUMNS_DOCUMENT_RECORD = Set.of(
|
||||
"id", "fingerprint", "last_known_source_locator", "last_known_source_file_name",
|
||||
"id", COL_FINGERPRINT, "last_known_source_locator", "last_known_source_file_name",
|
||||
"overall_status", "content_error_count", "transient_error_count",
|
||||
"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<String> EXPECTED_COLUMNS_PROCESSING_ATTEMPT = Set.of(
|
||||
"id", "fingerprint", "run_id", "attempt_number", "started_at", "ended_at",
|
||||
"id", COL_FINGERPRINT, "run_id", "attempt_number", "started_at", "ended_at",
|
||||
"status", "failure_class", "failure_message", "retryable",
|
||||
"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<String> 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<String> 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<String> 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<String> 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<String> readIndexNames(DatabaseMetaData meta) throws SQLException {
|
||||
Set<String> names = new HashSet<>();
|
||||
for (String tabelle : new String[]{"document_record", "processing_attempt"}) {
|
||||
for (String tabelle : new String[]{TABLE_DOCUMENT_RECORD, TABLE_PROCESSING_ATTEMPT}) {
|
||||
try (ResultSet rs = meta.getIndexInfo(null, null, tabelle, false, false)) {
|
||||
while (rs.next()) {
|
||||
String indexName = rs.getString("INDEX_NAME");
|
||||
|
||||
+5
-3
@@ -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);
|
||||
}
|
||||
|
||||
+15
-5
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-16
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -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();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
+5
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -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");
|
||||
}
|
||||
|
||||
+8
-4
@@ -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;
|
||||
|
||||
+117
-50
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -24,6 +24,8 @@ public record EditorValidationFinding(
|
||||
Optional<String> 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);
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -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");
|
||||
}
|
||||
|
||||
+1
@@ -1652,6 +1652,7 @@ class BatchRunProcessingUseCaseTest {
|
||||
|
||||
@Override
|
||||
public void debugSensitiveAiContent(String message, Object... args) {
|
||||
// intentionally empty
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+39
-13
@@ -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<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> 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<ProcessingAttempt> 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 {
|
||||
|
||||
+36
-12
@@ -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<DocumentFingerprint> resetByFingerprintFingerprints = new ArrayList<>();
|
||||
final List<DocumentFingerprint> 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) {
|
||||
|
||||
+36
-12
@@ -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<DocumentFingerprint> resetStatusForRetryFingerprints = new ArrayList<>();
|
||||
final List<DocumentFingerprint> 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) {
|
||||
|
||||
+57
-19
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+57
-19
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-8
@@ -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) {
|
||||
|
||||
+18
-6
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+18
-6
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+41
-33
@@ -209,6 +209,14 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
* </ul>
|
||||
*/
|
||||
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<DocumentFingerprint> 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<DocumentFingerprint> fingerprints) {
|
||||
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||
Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
|
||||
Objects.requireNonNull(fingerprints, "fingerprints must not be null");
|
||||
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<HistoricalDocumentContext> 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<DefaultHistoryDetailsUseCase.HistoryDetailsResult> 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);
|
||||
|
||||
+1
@@ -69,6 +69,7 @@ public final class GuiConfigurationPropertiesWriter implements GuiConfigurationF
|
||||
* Creates a new properties writer.
|
||||
*/
|
||||
public GuiConfigurationPropertiesWriter() {
|
||||
// intentionally empty – no initialization required
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+32
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-10
@@ -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++) {
|
||||
|
||||
+9
-3
@@ -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<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> 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
|
||||
|
||||
+6
-2
@@ -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<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
|
||||
|
||||
+9
-3
@@ -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<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> 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
|
||||
|
||||
+6
-2
@@ -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<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
|
||||
|
||||
+9
-3
@@ -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<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user