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:
+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();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user