M11 vollständig abgeschlossen (AP-001 bis AP-007)
- AP-001: Kernobjekte und Port-Verträge (ModelCatalog-Port, sealed Result-Typen, ApiKeyOrigin, GUI-Modell- und Meldungs-Records) - AP-002: Provider-ComboBox, exklusiver Providerbereich, zustandsbewahrender Providerwechsel - AP-003: HTTP-Adapter für Modellabruf (Claude, OpenAI-kompatibel) mit vollständigem Error-Mapping und Dispatcher im Bootstrap - AP-004: Automatischer Modellabruf bei Providerwechsel, Aktion "Modelle neu laden", Umschaltung zwischen Modell-ComboBox und Modell-Textfeld, Worker-Thread-Kapselung - AP-005: Automatische Editorvalidierung (Pflichtfelder, Warnschwellen max.text.characters, Plausibilitätshinweise max.pages, API-Key-Herkunftsauflösung mit Vorrangregel) - AP-006: Zentraler Meldungsbereich mit vier Severity-Stufen, feldnahe rote Fehlermeldungen, API-Key-Herkunftsanzeige - AP-007: Integrations- und Regressionstests, Timeout-Mapping-Tests, Replace-Semantik für wiederholte Modellabruf-Meldungen Hexagonale Architektur eingehalten, Application- und Domain-Schicht bleiben infrastrukturfrei. Threadingmodell konsequent umgesetzt. Naming-Regel und JavaDoc-Standard durchgängig beachtet. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+35
-4
@@ -20,7 +20,11 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileLoader;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileWriter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationLoadException;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.AiModelCatalogDispatcher;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.GuiConfigurationPropertiesWriter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.modelcatalog.ClaudeModelCatalogAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.modelcatalog.OpenAiCompatibleModelCatalogAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationFileSnapshot;
|
||||
@@ -616,13 +620,18 @@ public class BootstrapRunner {
|
||||
private GuiStartupContext buildGuiStartupContext(Optional<String> configPathOverride) {
|
||||
GuiConfigurationFileLoader loader = this::loadGuiConfigurationState;
|
||||
GuiConfigurationFileWriter writer = new GuiConfigurationPropertiesWriter();
|
||||
AiModelCatalogPort modelCatalogPort = buildModelCatalogDispatcher();
|
||||
de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort apiKeyResolutionPort =
|
||||
new de.gecheckt.pdf.umbenenner.adapter.out.validation.EnvironmentApiKeyResolutionAdapter();
|
||||
|
||||
if (configPathOverride.isEmpty()) {
|
||||
return new GuiStartupContext(
|
||||
GuiConfigurationEditorStateFactory.createBlankStartState(),
|
||||
Optional.empty(),
|
||||
loader,
|
||||
writer);
|
||||
writer,
|
||||
modelCatalogPort,
|
||||
apiKeyResolutionPort);
|
||||
}
|
||||
|
||||
Path configPath = Paths.get(configPathOverride.get());
|
||||
@@ -634,13 +643,16 @@ public class BootstrapRunner {
|
||||
Optional.of("Konfigurationsdatei nicht gefunden: " + configPath.toAbsolutePath()
|
||||
+ "\nDie GUI startet ohne Konfigurationsdatei."),
|
||||
loader,
|
||||
writer);
|
||||
writer,
|
||||
modelCatalogPort,
|
||||
apiKeyResolutionPort);
|
||||
}
|
||||
|
||||
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
||||
try {
|
||||
GuiConfigurationEditorState loadedState = loadGuiConfigurationState(configPath);
|
||||
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer);
|
||||
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
|
||||
modelCatalogPort, apiKeyResolutionPort);
|
||||
} catch (GuiConfigurationLoadException e) {
|
||||
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
|
||||
e.getMessage(), e);
|
||||
@@ -648,10 +660,29 @@ public class BootstrapRunner {
|
||||
GuiConfigurationEditorStateFactory.createBlankStartState(),
|
||||
Optional.of("Konfiguration konnte nicht geladen werden: " + e.getMessage()),
|
||||
loader,
|
||||
writer);
|
||||
writer,
|
||||
modelCatalogPort,
|
||||
apiKeyResolutionPort);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link AiModelCatalogPort} dispatcher for use in the GUI startup context.
|
||||
* <p>
|
||||
* Instantiates one stateless concrete adapter per supported provider family and wires
|
||||
* them into an {@link AiModelCatalogDispatcher}. The adapters are stateless: all
|
||||
* provider-specific values (API key, base URL, timeout) are supplied per call via the
|
||||
* {@link de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogRequest},
|
||||
* so no provider configuration needs to be resolved at this point.
|
||||
*
|
||||
* @return a ready-to-use model catalogue port; never {@code null}
|
||||
*/
|
||||
private AiModelCatalogPort buildModelCatalogDispatcher() {
|
||||
return new AiModelCatalogDispatcher(
|
||||
new ClaudeModelCatalogAdapter(),
|
||||
new OpenAiCompatibleModelCatalogAdapter());
|
||||
}
|
||||
|
||||
private GuiConfigurationEditorState loadGuiConfigurationState(Path configFilePath) {
|
||||
try {
|
||||
boolean legacyDetected = detectedLegacyConfiguration(configFilePath);
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogRequest;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;
|
||||
|
||||
/**
|
||||
* Composition-root dispatcher that routes {@link AiModelCatalogPort#fetchAvailableModels}
|
||||
* calls to the concrete adapter matching the provider identifier carried in the request.
|
||||
* <p>
|
||||
* This class is not a new architectural layer between the port contract and its adapters.
|
||||
* It is a Bootstrap-only wiring convenience that allows the GUI to call a single
|
||||
* {@link AiModelCatalogPort} instance regardless of which provider the user has currently
|
||||
* selected. The concrete adapters remain independent of each other and of this class.
|
||||
* <p>
|
||||
* <strong>Dispatching rule:</strong> The value of
|
||||
* {@link ModelCatalogRequest#providerIdentifier()} is matched case-sensitively against
|
||||
* the known provider identifiers:
|
||||
* <ul>
|
||||
* <li>{@code "claude"} — routed to the Claude adapter</li>
|
||||
* <li>{@code "openai-compatible"} — routed to the OpenAI-compatible adapter</li>
|
||||
* <li>any other value — returns {@link ModelCatalogResult.IncompleteConfiguration}
|
||||
* with a human-readable explanation</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <strong>Thread safety:</strong> The dispatcher holds only immutable references to
|
||||
* the two concrete adapters and is trivially thread-safe.
|
||||
*/
|
||||
public class AiModelCatalogDispatcher implements AiModelCatalogPort {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(AiModelCatalogDispatcher.class);
|
||||
|
||||
private static final String PROVIDER_CLAUDE = "claude";
|
||||
private static final String PROVIDER_OPENAI_COMPATIBLE = "openai-compatible";
|
||||
|
||||
private final AiModelCatalogPort claudeAdapter;
|
||||
private final AiModelCatalogPort openAiCompatibleAdapter;
|
||||
|
||||
/**
|
||||
* Creates a dispatcher wired with both supported adapter implementations.
|
||||
*
|
||||
* @param claudeAdapter the adapter for the Claude provider family;
|
||||
* must not be {@code null}
|
||||
* @param openAiCompatibleAdapter the adapter for the OpenAI-compatible provider family;
|
||||
* must not be {@code null}
|
||||
* @throws NullPointerException if either adapter is {@code null}
|
||||
*/
|
||||
public AiModelCatalogDispatcher(AiModelCatalogPort claudeAdapter,
|
||||
AiModelCatalogPort openAiCompatibleAdapter) {
|
||||
this.claudeAdapter = Objects.requireNonNull(claudeAdapter,
|
||||
"claudeAdapter must not be null");
|
||||
this.openAiCompatibleAdapter = Objects.requireNonNull(openAiCompatibleAdapter,
|
||||
"openAiCompatibleAdapter must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes the model catalogue request to the concrete adapter for the provider
|
||||
* identified by {@link ModelCatalogRequest#providerIdentifier()}.
|
||||
* <p>
|
||||
* If the provider identifier does not match any known provider, returns
|
||||
* {@link ModelCatalogResult.IncompleteConfiguration} without calling any adapter.
|
||||
* This keeps the GUI informed without any crash or exception.
|
||||
*
|
||||
* @param request all values needed to contact the provider; must not be {@code null}
|
||||
* @return the result from the matched adapter, or {@link ModelCatalogResult.IncompleteConfiguration}
|
||||
* when the provider identifier is unknown; never {@code null}
|
||||
* @throws NullPointerException if {@code request} is {@code null}
|
||||
*/
|
||||
@Override
|
||||
public ModelCatalogResult fetchAvailableModels(ModelCatalogRequest request) {
|
||||
Objects.requireNonNull(request, "request must not be null");
|
||||
|
||||
String provider = request.providerIdentifier();
|
||||
LOG.debug("Model catalogue dispatch: routing request for provider '{}'", provider);
|
||||
|
||||
return switch (provider) {
|
||||
case PROVIDER_CLAUDE -> claudeAdapter.fetchAvailableModels(request);
|
||||
case PROVIDER_OPENAI_COMPATIBLE -> openAiCompatibleAdapter.fetchAvailableModels(request);
|
||||
default -> {
|
||||
LOG.warn("Model catalogue dispatch: unknown provider identifier '{}'", provider);
|
||||
yield new ModelCatalogResult.IncompleteConfiguration(provider,
|
||||
"Unbekannter Provider: '" + provider + "'");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user