Files
pdf-umbenenner/docs/architecture/adapter-overview.md
T
2026-04-30 15:29:07 +02:00

19 KiB
Raw Blame History

Architektur-Übersicht: Adapter-Out, CLI & Bootstrap

Diese Datei beschreibt die drei Module pdf-umbenenner-adapter-out, pdf-umbenenner-adapter-in-cli und pdf-umbenenner-bootstrap: ihren Zweck, ihre Paketstruktur, die wichtigsten Klassen und die Verdrahtungslogik beim Programmstart. Sie richtet sich an Entwickler, die in einem dieser Module arbeiten wollen und noch keinen Überblick über das Projekt haben. Domain- und Application-Schicht (Port-Verträge, fachliche Domänenobjekte, Use-Case-Interfaces) sind nicht Gegenstand dieses Dokuments sie sind in docs/architecture/domain-overview.md beschrieben. GUI-interne Ports und die Struktur des GUI-Adapters finden sich in docs/architecture/gui-overview.md. Die hexagonale Abhängigkeitsrichtung ist strikt: Adapter kennen Domain und Application, nicht umgekehrt. Adapter dürfen außerdem nicht direkt voneinander abhängen.


1. Modulzweck

pdf-umbenenner-adapter-out

Enthält alle Outbound-Adapter-Implementierungen, also die konkreten technischen Lösungen für sämtliche Outbound-Ports der Application. Dazu gehören: Dateisystemzugriff, PDF-Textextraktion via PDFBox, SQLite-Persistenz (Schema, Repositories, Unit of Work), HTTP-Clients für zwei KI-Provider-Familien (OpenAI-kompatibel und Anthropic nativ), Properties-Konfiguration inklusive Legacy-Migration, dateibasierter Run-Lock sowie Systemuhr und SHA-256-Fingerprint.

pdf-umbenenner-adapter-in-cli

Schlanker Inbound-Adapter für den kopflosen Batch-Betrieb. Enthält genau eine Klasse (SchedulerBatchCommand), die den CLI-Einstiegspunkt bildet und ausschließlich über das Inbound-Port-Interface an die Application delegiert. Keine eigene Fachlogik.

pdf-umbenenner-bootstrap

Composition Root der Anwendung. Verantwortlich für: CLI-Argument-Parsing, Konfigurationsauflösung und -validierung, Aufbau des vollständigen Objektgraphen (manuell, ohne DI-Framework), Auswahl der aktiven KI-Adapter-Implementierung, Dispatch auf GUI- oder Headless-Pfad sowie Exit-Code-Ableitung. Bootstrap ist die einzige Stelle, an der alle Module zusammengeführt werden.


2. Paketstruktur

pdf-umbenenner-adapter-out

Wurzelpaket: de.gecheckt.pdf.umbenenner.adapter.out

Unterpaket Inhalt
.ai HTTP-Adapter für OpenAI-kompatible Schnittstelle und Anthropic Messages API
.clock Systemuhr-Adapter (Instant.now())
.configuration Properties-Laden, Multi-Provider-Parsing/-Validierung, Legacy-Migration
.fingerprint SHA-256-Inhalts-Fingerprint
.lock Dateibasierter Run-Lock
.modelcatalog HTTP-Modellabruf für den GUI-Konfigurationseditor
.pathcheck Pfadprüfung für den GUI-Editor
.pdfextraction PDFBox-3.x-Adapter: Textextraktion und Seitenanzahl
.prompt Prompt-Template-Lader
.resourcecreation Anlegen von Ordnern und Dateien (korrigierende technische Tests)
.sourcedocument Quellordner-Scanner (nicht rekursiv)
.sqlite Schema-Initialisierung, Repositories, Unit of Work
.targetcopy Zielkopie via Temp-Datei und atomarem Move
.targetfolder Kollisionsfreier Zieldateiname, Umbenennung bestehender Zieldateien
.validation API-Key-Auflösung aus Umgebungsvariablen (GUI-Editor)
.bootstrap.validation StartConfiguration-Validierung vor Prozessstart

pdf-umbenenner-adapter-in-cli

Wurzelpaket: de.gecheckt.pdf.umbenenner.adapter.in.cli

Enthält ausschließlich SchedulerBatchCommand sowie die zugehörige package-info.java.

pdf-umbenenner-bootstrap

Wurzelpaket: de.gecheckt.pdf.umbenenner.bootstrap

Unterpaket Inhalt
(Wurzel) PdfUmbenennerApplication (main), BootstrapRunner, AiProviderSelector
.adapter Bootstrap-interne Adapter: Log4jProcessingLogger, GuiConfigurationPropertiesWriter, AiModelCatalogDispatcher
.singleinstance SingleInstanceGuard Einzelinstanz-Schutz via Loopback-ServerSocket
.startup StartupMode, StartupArguments, CliArgumentParser

3. Schlüsselklassen

Die folgenden Klassen sind für das Verständnis der drei Module zentral. FQN-Kürzel: ... steht jeweils für das Wurzelpaket des Moduls.

Adapter-Out

KI-Adapter

  • ...ai.OpenAiHttpAdapter implementiert AiInvocationPort für OpenAI-kompatible Endpunkte. POST {baseUrl}/v1/chat/completions, Bearer-Authentifizierung, extrahiert choices[0].message.content, klassifiziert HTTP-Fehler und Timeouts als AiInvocationTechnicalFailure.

  • ...ai.AnthropicClaudeHttpAdapter implementiert AiInvocationPort für die native Anthropic Messages API. POST /v1/messages, Header x-api-key und anthropic-version, konkateniert text-Content-Blöcke aus dem Antwort-Array.

Beide Adapter liefern denselben Domain-Typ (NamingProposal) und enthalten keinerlei provider-spezifische Typen in öffentlichen Signaturen. Welche Implementierung aktiv ist, entscheidet ausschließlich der Bootstrap (→ AiProviderSelector).

Modell-Katalog (GUI)

  • ...modelcatalog.ClaudeModelCatalogAdapter AiModelCatalogPort für Claude, GET /v1/models mit x-api-key.

  • ...modelcatalog.OpenAiCompatibleModelCatalogAdapter AiModelCatalogPort für OpenAI-kompatibel, GET /v1/models mit Bearer.

PDF-Extraktion

  • ...pdfextraction.PdfTextExtractionPortAdapter PDFBox-3.x-Adapter. Alle technischen Fehler werden als PdfExtractionTechnicalError zurückgegeben; es werden keine Exceptions propagiert.

SQLite

  • ...sqlite.SqliteSchemaInitializationAdapter Flyway-basierte Schema-Initialisierung mit V1__initial_schema.sql. Drei-Fall-Strategie: leere Datenbank (Flyway führt das Skript vollständig aus), bestehender Datenbestand ohne Flyway-History (Schema-Prüfung, datiertes Backup, dann Baseline-Eintrag ohne Skriptausführung), regulärer Folgestart mit Flyway-History (idempotenter Lauf). Foreign-Key-Durchsetzung via SQLiteConfig.enforceForeignKeys(true) auf DataSource-Ebene, sodass jede neue Verbindung automatisch PRAGMA foreign_keys = ON erhält.

  • ...sqlite.SqliteUnitOfWorkAdapter implementiert UnitOfWorkPort. Setzt autoCommit=false, führt atomare Commits durch, rollt bei Fehlern zurück. Die innere TransactionOperations-Implementierung wurde um resetDocumentStatusForRetry(DocumentFingerprint) erweitert: setzt feldgenau overall_status = 'READY_FOR_AI', content_error_count = 0, transient_error_count = 0, last_failure_instant = NULL; alle anderen Felder und alle processing_attempt-Einträge bleiben unangetastet.

  • ...sqlite.SqliteDocumentRecordRepositoryAdapter Stammsatz pro SHA-256-Fingerprint (Gesamtstatus, Fehlerzähler, Zieldateiname usw.).

  • ...sqlite.SqliteProcessingAttemptRepositoryAdapter Versuchshistorie, referenziert über Fingerprint. Enthält u. a. Provider-Identifikator, Modellname, Prompt-Identifikator, KI-Rohantwort und finalen Zieldateinamen.

  • ...sqlite.SqliteHistoryQueryAdapter implementiert HistoryQueryPort. Kapselt alle lesenden Datenbankoperationen für den Historien-Tab: Übersicht (loadOverview mit Sortierung updated_at DESC, fingerprint ASC, LIMIT 501-Strategie, case-insensitive Freitextsuche via LOWER() mit Sonderzeichen-Escape für % und _), Stammsatz-Lookup (findRecordByFingerprint) und Versuchshistorie (findAttemptsByFingerprint).

Konfiguration

  • ...configuration.PropertiesConfigurationPortAdapter implementiert ConfigurationPort. Lädt config/application.properties (oder einen --config-Override), parst via MultiProviderConfigurationParser, löst API-Keys aus Umgebungsvariablen (OPENAI_COMPATIBLE_API_KEY, ANTHROPIC_API_KEY).

  • ...configuration.LegacyConfigurationMigrator erkennt alte Flat-Key-Konfigurationen (Schlüssel wie api.baseUrl, api.model), legt eine .bak-Sicherung an und überführt den Inhalt in das aktuelle Multi-Provider-Schema.

Prompt-Adapter

  • ...prompt.FilesystemPromptPortAdapter implementiert PromptPort. Lädt das Prompt-Template aus einer externen Datei und leitet den Identifikator aus dem Dateinamen ab. Die neue Methode savePrompt(String content) schreibt den Inhalt atomar: temporäre Datei im selben Verzeichnis anlegen (gleiche Partition), Inhalt in UTF-8 schreiben, dann ATOMIC_MOVE zur Zieldatei. Kein stiller Fallback bei AtomicMoveNotSupportedException. Der Pfad stammt aus der Adapter-internen Konfiguration, nicht aus dem Port-Aufruf.

Laufzeitinfrastruktur

  • ...lock.FilesystemRunLockPortAdapter Lock-Datei mit PID-Inhalt. Wirft RunLockUnavailableException, wenn die Datei bereits vorhanden ist. Release löscht die Datei (best-effort).

  • ...clock.SystemClockAdapter delegiert an Instant.now().

  • ...fingerprint.Sha256FingerprintAdapter SHA-256 über den Rohdatei-Inhalt. Fehler als FingerprintTechnicalError.

Zieldatei

  • ...targetcopy.FilesystemTargetFileCopyAdapter kopiert die Quelldatei zunächst in eine .tmp-Datei, dann atomarer Move (Fallback: Standard-Move). Die Quelldatei wird in keinem Fall verändert.

  • ...targetfolder.FilesystemTargetFolderAdapter ermittelt einen kollisionsfreien Zieldateinamen mit (1), (2)-Suffix. Erkennt inhaltsidentische Duplikate via SHA-256.

Validierung vor Prozessstart

  • ...bootstrap.validation.StartConfigurationValidator validiert die geladene StartConfiguration auf Pflichtfelder, Wertebereiche, URI-Syntax und Pfadbedingungen. Wird im Bootstrap-Headless-Pfad unmittelbar nach dem Laden der Konfiguration aufgerufen.

Adapter-In-CLI

  • ...adapter.in.cli.SchedulerBatchCommand einziger Inbound-Adapter für den Headless-Betrieb. Nimmt einen BatchRunContext entgegen, delegiert an BatchRunProcessingUseCase.execute() und gibt BatchRunOutcome zurück. Enthält keine eigene Fachlogik; die Verdrahtung mit dem Use-Case-Interface erfolgt ausschließlich im Bootstrap.

Bootstrap

  • ...bootstrap.PdfUmbenennerApplication main-Methode. Parst CLI-Argumente via CliArgumentParser, bricht bei ungültiger Verwendung mit Exit-Code 1 ab, delegiert an BootstrapRunner.run() und ruft abschließend System.exit() mit dem zurückgegebenen Code auf.

  • ...bootstrap.BootstrapRunner Herzstück der Verdrahtung. Baut den Objektgraph für Headless- und GUI-Pfad, dispatcht über StartupMode, enthält buildProductionBatchUseCase() und runHeadlessBatch() als zentrale Kompositionsmethoden, liefert den Exit-Code zurück.

  • ...bootstrap.AiProviderSelector einzige Stelle, an der AiProviderFamily auf eine konkrete AiInvocationPort-Implementierung abgebildet wird: OPENAI_COMPATIBLEOpenAiHttpAdapter, CLAUDEAnthropicClaudeHttpAdapter.

  • ...bootstrap.startup.CliArgumentParser parst --headless und --config <Pfad> zu einem typsicheren StartupArgumentsParseResult (sealed: Valid / Invalid).

  • ...bootstrap.singleinstance.SingleInstanceGuard bindet einen Loopback-ServerSocket auf Port 47832. Wirft AnotherInstanceRunningException, wenn der Port bereits belegt ist. Ein Shutdown-Hook gibt den Socket frei.

  • ...bootstrap.adapter.AiModelCatalogDispatcher Bootstrap-interner Dispatcher für die GUI. Routet AiModelCatalogPort-Aufrufe anhand des providerIdentifier an den Claude- oder OpenAI-kompatiblen Modell-Katalog-Adapter. Thread-safe.

  • ...bootstrap.ApplicationVersionProvider statische Hilfsklasse ohne Zustand. Liest Implementation-Version aus dem Paket-Manifest via getClass().getPackage().getImplementationVersion(). Fallback "dev" bei IDE-Start und ungepacktem Betrieb (kein Manifest-Eintrag vorhanden). Der aufgelöste Wert wird im GUI-Pfad in GuiStartupContext.applicationVersion eingesetzt.

  • ...bootstrap.adapter.Log4jProcessingLogger implementiert ProcessingLogger auf Basis von Log4j2. Unterdrückt sensitive KI-Inhalte, wenn AiContentSensitivity.PROTECT_SENSITIVE_CONTENT gesetzt ist.

  • ...bootstrap.adapter.GuiConfigurationPropertiesWriter schreibt die im GUI-Editor bearbeitete Konfiguration als normalisierte application.properties zurück auf das Dateisystem.


4. Verdrahtungslogik in Bootstrap

Die folgende Sequenz beschreibt den Ablauf von main() bis zum Start des eigentlichen Adapters. Der Objektgraph wird ausschließlich durch manuelle new-Aufrufe aufgebaut; es wird kein DI-Framework verwendet.

Argument-Parsing

  • PdfUmbenennerApplication.main()CliArgumentParser.parse(args)
  • Ergebnis Invalid → Exit-Code 1, keine weiteren Schritte

Einzelinstanz-Schutz

  • BootstrapRunner.run()SingleInstanceGuard.acquire()
  • AnotherInstanceRunningException → Exit-Code 1; im GUI-Modus zusätzlich ein Swing-Warndialog

Modus-Dispatch

  • BootstrapRunner.run() wertet startupArguments.mode() aus:
    • HEADLESSrunHeadlessBatch()
    • GUIstartGuiMode()

Konfigurationsauflösung (Headless-Pfad)

  • Prüfung, ob --config-Datei existiert (Fehler → Exit-Code 1)
  • LegacyConfigurationMigrator.migrateIfLegacy() bei erkannter Legacy-Form
  • PropertiesConfigurationPortAdapter lädt und parst die Properties
  • StartConfigurationValidator validiert die geladene StartConfiguration
  • Validierungsfehler → Exit-Code 1

KI-Provider-Auswahl

  • Innerhalb von buildProductionBatchUseCase(): multiProviderConfiguration().activeProviderFamily()AiProviderSelector.select(family, providerConfig)
  • Ergebnis: genau eine AiInvocationPort-Instanz

Objektgraph-Aufbau (Headless)

  • Erzeugte Instanzen (Reihenfolge nach Abhängigkeit): Sha256FingerprintAdapter, SqliteDocumentRecordRepositoryAdapter, SqliteProcessingAttemptRepositoryAdapter, SqliteUnitOfWorkAdapter, FilesystemTargetFolderAdapter, FilesystemTargetFileCopyAdapter, FilesystemPromptPortAdapter, SystemClockAdapter, SourceDocumentCandidatesPortAdapter, PdfTextExtractionPortAdapter, Log4jProcessingLogger
  • Application-Services (DocumentProcessingCoordinator, AiResponseValidator, AiNamingService) werden verdrahtet und in DefaultBatchRunProcessingUseCase eingebettet

CLI-Adapter

  • BootstrapRunner erzeugt SchedulerBatchCommand mit dem fertigen BatchRunProcessingUseCase

Exit-Code-Ableitung

  • BatchRunOutcome → 0 (Lauf technisch erfolgreich) oder 1 (harter Bootstrap-/Konfigurationsfehler)
  • PdfUmbenennerApplication ruft System.exit(exitCode) auf

GUI-Pfad

  • startGuiMode() baut via buildGuiStartupContext() einen GuiStartupContext: enthält AiModelCatalogDispatcher, EnvironmentApiKeyResolutionAdapter, TechnicalTestOrchestrator, GuiConfigurationPropertiesWriter
  • Bootstrap verdrahtet zusätzlich vier neue History-Use-Cases (DefaultHistoryOverviewUseCase, DefaultHistoryDetailsUseCase, DefaultHistoryResetDocumentStatusUseCase, DefaultDeleteDocumentHistoryUseCase) und den DefaultPromptEditorUseCase als anonyme Bridge-Implementierungen in den GuiStartupContext
  • ApplicationVersionProvider.resolveVersion() wird aufgerufen und der Wert in GuiStartupContext.applicationVersion gesetzt
  • Wenn eine Konfigurationsdatei beim Start bekannt ist, erzeugt Bootstrap zusätzlich einen vollständig verdrahteten GuiPromptEditorPort (kombiniert FilesystemPromptPortAdapter mit DefaultPromptEditorUseCase); ohne Konfiguration erhält der Context einen No-Op-Port
  • GuiAdapter.start(context) übernimmt; ab diesem Punkt liegt die Kontrolle beim GUI-Adapter
  • Im GUI-Pfad: keine SQLite-Schema-Initialisierung beim Start, kein Run-Lock-Erwerb, kein Batch-Use-Case; History-Operationen initialisieren die Schema-Verbindung ad-hoc pro Aufruf
  • GUI-interne Ports und deren Verbindung mit Outbound-Adaptern sind in docs/architecture/gui-overview.md beschrieben

5. Einstiegspunkte je Modul

pdf-umbenenner-adapter-out

  1. ...ai.OpenAiHttpAdapter zeigt das typische Adapter-Muster: Port-Interface implementieren, alle provider-spezifischen Details kapseln, ProviderConfiguration als einzige Konfigurationsquelle konsumieren. Danach AnthropicClaudeHttpAdapter zum Vergleich lesen.

  2. ...sqlite.SqliteSchemaInitializationAdapter erklärt das Datenbankschema, das alle SQLite-Adapter voraussetzen. Hier sieht man, welche Felder in document_record und processing_attempt existieren und wie Schema-Evolution additiv umgesetzt ist.

  3. ...configuration.PropertiesConfigurationPortAdapter Einstieg in die Konfigurationskette. Von hier aus MultiProviderConfigurationParser und LegacyConfigurationMigrator nachverfolgen.

pdf-umbenenner-adapter-in-cli

  1. ...adapter.in.cli.SchedulerBatchCommand komprimiertes Inbound-Adapter-Muster in einer einzigen Klasse. Zeigt, wie ein Inbound-Adapter ausschließlich über Port-Interfaces mit der Application kommuniziert.

  2. package-info.java beschreibt Abhängigkeitsrichtung und Verdrahtungsvertrag dieses Adapters.

  3. SchedulerBatchCommandTest zeigt, wie der Adapter ohne Bootstrap testbar ist.

pdf-umbenenner-bootstrap

  1. PdfUmbenennerApplication Startpunkt; die kurze Kette von main() bis System.exit() gibt einen ersten Überblick über die gesamte Startsequenz.

  2. BootstrapRunner Herzstück; buildProductionBatchUseCase() zeigt, wie der vollständige Objektgraph manuell aufgebaut wird. runHeadlessBatch() zeigt den Headless-Kontrollfluss.

  3. AiProviderSelector kleinste Klasse mit größter Hebelwirkung: hier liegt die einzige Stelle, an der die Provider-Auswahl aus der Konfiguration auf eine konkrete AiInvocationPort-Implementierung trifft.


Port-Verträge und Domain-Typen: docs/architecture/domain-overview.md GUI-interne Ports und GUI-Adapter-Struktur: docs/architecture/gui-overview.md