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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 21:27:59 +02:00
parent 14da7ee789
commit b7f9184344
49 changed files with 974 additions and 511 deletions
@@ -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)
@@ -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());
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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 = ?";
@@ -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");
@@ -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);
}
@@ -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
}
}
}
@@ -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();
}
}
@@ -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();
}
// -------------------------------------------------------------------------