M12 vollständig abgeschlossen (AP-001 bis AP-008)
- AP-001: Prüf- und Korrektur-Kernobjekte (CheckpointId, CheckpointResult sealed interface, TechnicalTestReport mit Correction-Plan-Ableitung, CorrectionSuggestion sealed interface, PathCheckPort, ResourceCreationPort) - AP-002: Aktion "Validieren" als explizite, nicht schreibende Gesamtprüfung des aktuellen Editorzustands - AP-003: Provider-nahe technische Prüflogik für Endpoint, API-Key, Modellliste und Modellplausibilität — wiederverwendet den bestehenden Modellabruf-Port, kein zweiter HTTP-Pfad - AP-004: Windows-Pfadprüfung mit ausdrücklicher Unterstützung gemappter Laufwerksbuchstaben (FilesystemPathCheckAdapter) - AP-005: Aktion "Technische Tests ausführen" als vollständiger Gesamttest ohne Frühabbruch, Orchestrator sammelt Befunde aller Prüfblöcke - AP-006: Schreibende Korrekturhilfen mit gesammeltem Bestätigungsdialog, CorrectionExecutionService, FilesystemResourceCreationAdapter - AP-007: Automatische deutsche Standard-Prompt-Datei-Erzeugung, Default-Pfad neben der .properties-Datei, klare Fehlermeldung bei nicht beschreibbarem Zielpfad - AP-008: Regressionstests für Gesamttest ohne Frühabbruch, ungespeicherte Editorzustände, Korrekturdialog, Prompt-Erzeugung, Windows-Pfade Hexagonale Architektur durchgehend eingehalten, Domain und Application bleiben infrastrukturfrei. Threadingmodell konsequent umgesetzt. Naming-Regel und JavaDoc-Standard eingehalten. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+88
@@ -0,0 +1,88 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
/**
|
||||
* Eindeutiger Bezeichner für jeden definierten Prüfpunkt des technischen Gesamttests.
|
||||
* <p>
|
||||
* Jeder Wert entspricht genau einem Prüfpunkt, der im Rahmen der Aktion
|
||||
* „Technische Tests ausführen" durchlaufen wird. Die Reihenfolge der Konstanten
|
||||
* ist nicht verbindlich für die Ausführungsreihenfolge; sie dient nur der
|
||||
* Übersichtlichkeit.
|
||||
* <p>
|
||||
* Prüfpunkte sind unabhängig voneinander; ein Fehler in einem Prüfpunkt darf
|
||||
* nicht dazu führen, dass spätere Prüfpunkte übersprungen werden. Wenn ein
|
||||
* Prüfpunkt wegen fehlender Voraussetzungen nicht ausführbar ist (z. B.
|
||||
* API-Key-Test ohne bekannte Base-URL), ist das Ergebnis
|
||||
* {@link CheckpointResult.NotApplicable}, kein Fehler.
|
||||
*/
|
||||
public enum CheckpointId {
|
||||
|
||||
/**
|
||||
* Grundlegende Konfigurationsvalidierung – entspricht der lokalen Editorvalidierung.
|
||||
* Prüft formale Pflichtfelder und Werteformate ohne Dateisystem- oder Netzwerkkontakt.
|
||||
*/
|
||||
CONFIGURATION_BASIC_VALIDATION,
|
||||
|
||||
/**
|
||||
* Provider-Konfiguration prüfen: aktiver Provider bekannt, alle Pflichtfelder
|
||||
* des aktiven Providers formal ausgefüllt.
|
||||
*/
|
||||
PROVIDER_CONFIGURATION,
|
||||
|
||||
/**
|
||||
* Base-URL bzw. Endpunkt des aktiven Providers technisch erreichbar (Netzwerktest).
|
||||
*/
|
||||
BASE_URL_REACHABLE,
|
||||
|
||||
/**
|
||||
* API-Key vorhanden – mindestens eine Quelle (Umgebungsvariable oder Properties-Datei)
|
||||
* liefert einen nicht leeren Wert. Dieser Prüfpunkt trifft keine Aussage über die
|
||||
* Korrektheit des Schlüssels.
|
||||
*/
|
||||
API_KEY_PRESENT,
|
||||
|
||||
/**
|
||||
* API-Key technisch akzeptiert – Authentifizierung am Provider-Endpunkt erfolgreich.
|
||||
* Setzt voraus, dass {@link #API_KEY_PRESENT} bestanden wurde; andernfalls ist dieser
|
||||
* Prüfpunkt {@link CheckpointResult.NotApplicable}.
|
||||
*/
|
||||
API_KEY_ACCEPTED,
|
||||
|
||||
/**
|
||||
* Modellliste abrufbar – der Provider liefert eine nicht leere Liste verfügbarer Modelle.
|
||||
* Nutzt denselben Outbound-Port wie der automatische Modellabruf; keine zweite Implementierung.
|
||||
*/
|
||||
MODEL_LIST_AVAILABLE,
|
||||
|
||||
/**
|
||||
* Ausgewähltes Modell plausibel – der konfigurierte Modellname ist in der zuletzt
|
||||
* geladenen Modellliste vorhanden oder formal zulässig.
|
||||
* Setzt voraus, dass {@link #MODEL_LIST_AVAILABLE} bestanden wurde.
|
||||
*/
|
||||
SELECTED_MODEL_PLAUSIBLE,
|
||||
|
||||
/**
|
||||
* Prompt-Datei vorhanden und lesbar – die konfigurierte Prompt-Datei existiert im
|
||||
* Dateisystem und kann gelesen werden.
|
||||
*/
|
||||
PROMPT_FILE_PRESENT,
|
||||
|
||||
/**
|
||||
* Quellordner vorhanden und lesbar – der konfigurierte Quellordner existiert und
|
||||
* kann vom Prozess gelesen werden.
|
||||
*/
|
||||
SOURCE_FOLDER_PRESENT,
|
||||
|
||||
/**
|
||||
* Zielordner vorhanden oder anlegbar sowie schreibbar – der konfigurierte Zielordner
|
||||
* existiert und ist schreibbar, oder er ist noch nicht vorhanden, aber der Pfad ist
|
||||
* technisch anlegbar.
|
||||
*/
|
||||
TARGET_FOLDER_USABLE,
|
||||
|
||||
/**
|
||||
* SQLite-Datei bzw. SQLite-Pfad technisch nutzbar – der konfigurierte SQLite-Pfad
|
||||
* zeigt auf eine vorhandene Datei oder auf einen beschreibbaren Ordner, in dem die
|
||||
* Datei neu angelegt werden kann.
|
||||
*/
|
||||
SQLITE_PATH_USABLE
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Versiegeltes Ergebnis eines einzelnen Prüfpunkts des technischen Gesamttests.
|
||||
* <p>
|
||||
* Jeder Prüfpunkt liefert genau einen der drei möglichen Zustände:
|
||||
* <ul>
|
||||
* <li>{@link Success} – der Prüfpunkt wurde bestanden.</li>
|
||||
* <li>{@link Failure} – der Prüfpunkt wurde nicht bestanden (Fehler oder Warnung),
|
||||
* optional mit einem Korrekturvorschlag.</li>
|
||||
* <li>{@link NotApplicable} – der Prüfpunkt konnte wegen fehlender Voraussetzungen
|
||||
* nicht ausgeführt werden (z. B. API-Key-Test ohne vorhandenen API-Key).
|
||||
* Dies ist kein Fehler, sondern ein eigenständiger Zustand.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Alle Implementierungen sind immutable und enthalten keine JavaFX-Typen. Sie können
|
||||
* auf beliebigen Threads erzeugt und sicher an den JavaFX Application Thread übergeben werden.
|
||||
*/
|
||||
public sealed interface CheckpointResult
|
||||
permits CheckpointResult.Success,
|
||||
CheckpointResult.Failure,
|
||||
CheckpointResult.NotApplicable {
|
||||
|
||||
/**
|
||||
* Gibt den Bezeichner des Prüfpunkts zurück, zu dem dieses Ergebnis gehört.
|
||||
*
|
||||
* @return Prüfpunkt-Bezeichner; nie {@code null}
|
||||
*/
|
||||
CheckpointId checkpointId();
|
||||
|
||||
/**
|
||||
* Der Prüfpunkt wurde bestanden.
|
||||
*
|
||||
* @param checkpointId Bezeichner des bestandenen Prüfpunkts; nie {@code null}
|
||||
* @param message deutsche Bestätigungsmeldung; nie {@code null}
|
||||
*/
|
||||
record Success(
|
||||
CheckpointId checkpointId,
|
||||
String message) implements CheckpointResult {
|
||||
|
||||
/**
|
||||
* Erstellt ein Erfolgs-Ergebnis.
|
||||
*
|
||||
* @param checkpointId Prüfpunkt-Bezeichner; darf nicht {@code null} sein
|
||||
* @param message deutsche Bestätigungsmeldung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code checkpointId} oder {@code message} {@code null} sind
|
||||
*/
|
||||
public Success {
|
||||
Objects.requireNonNull(checkpointId, "checkpointId must not be null");
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Prüfpunkt wurde nicht bestanden.
|
||||
* <p>
|
||||
* Ein gescheiterter Prüfpunkt hat immer einen Schweregrad ({@link CheckpointSeverity})
|
||||
* und eine deutsche Fehlermeldung. Optional ist ein {@link CorrectionSuggestion}
|
||||
* beigefügt, wenn eine sichere technische Korrektur möglich ist.
|
||||
* <p>
|
||||
* Ein Failure mit Schweregrad {@link CheckpointSeverity#WARNING} markiert eine
|
||||
* riskante, aber formal zulässige Einstellung. Ein Failure mit
|
||||
* {@link CheckpointSeverity#ERROR} zeigt an, dass der Gesamtstand nicht lauffähig ist.
|
||||
*
|
||||
* @param checkpointId Bezeichner des nicht bestandenen Prüfpunkts; nie {@code null}
|
||||
* @param severity Schweregrad; nie {@code null}
|
||||
* @param message deutsche Fehlermeldung; nie {@code null}
|
||||
* @param correctionSuggestion optionaler Korrekturvorschlag; leer wenn keine Korrektur angeboten wird
|
||||
*/
|
||||
record Failure(
|
||||
CheckpointId checkpointId,
|
||||
CheckpointSeverity severity,
|
||||
String message,
|
||||
Optional<CorrectionSuggestion> correctionSuggestion) implements CheckpointResult {
|
||||
|
||||
/**
|
||||
* Erstellt ein Fehler-Ergebnis mit optionalem Korrekturvorschlag.
|
||||
*
|
||||
* @param checkpointId Prüfpunkt-Bezeichner; darf nicht {@code null} sein
|
||||
* @param severity Schweregrad; darf nicht {@code null} sein
|
||||
* @param message deutsche Fehlermeldung; darf nicht {@code null} sein
|
||||
* @param correctionSuggestion optionaler Vorschlag; {@code null} wird zu leerem Optional
|
||||
* @throws NullPointerException wenn {@code checkpointId}, {@code severity} oder {@code message} {@code null} sind
|
||||
*/
|
||||
public Failure {
|
||||
Objects.requireNonNull(checkpointId, "checkpointId must not be null");
|
||||
Objects.requireNonNull(severity, "severity must not be null");
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
correctionSuggestion = correctionSuggestion == null
|
||||
? Optional.empty()
|
||||
: correctionSuggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein Fehler-Ergebnis ohne Korrekturvorschlag.
|
||||
*
|
||||
* @param checkpointId Prüfpunkt-Bezeichner; darf nicht {@code null} sein
|
||||
* @param severity Schweregrad; darf nicht {@code null} sein
|
||||
* @param message deutsche Fehlermeldung; darf nicht {@code null} sein
|
||||
* @return ein neues Failure-Ergebnis ohne Korrekturvorschlag
|
||||
*/
|
||||
public static Failure of(CheckpointId checkpointId, CheckpointSeverity severity, String message) {
|
||||
return new Failure(checkpointId, severity, message, Optional.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein Fehler-Ergebnis mit einem Korrekturvorschlag.
|
||||
*
|
||||
* @param checkpointId Prüfpunkt-Bezeichner; darf nicht {@code null} sein
|
||||
* @param severity Schweregrad; darf nicht {@code null} sein
|
||||
* @param message deutsche Fehlermeldung; darf nicht {@code null} sein
|
||||
* @param suggestion Korrekturvorschlag; darf nicht {@code null} sein
|
||||
* @return ein neues Failure-Ergebnis mit Korrekturvorschlag
|
||||
*/
|
||||
public static Failure withCorrection(CheckpointId checkpointId, CheckpointSeverity severity,
|
||||
String message, CorrectionSuggestion suggestion) {
|
||||
Objects.requireNonNull(suggestion, "suggestion must not be null");
|
||||
return new Failure(checkpointId, severity, message, Optional.of(suggestion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob zu diesem Befund ein Korrekturvorschlag vorliegt.
|
||||
*
|
||||
* @return {@code true} wenn ein Korrekturvorschlag vorhanden ist
|
||||
*/
|
||||
public boolean hasCorrectionSuggestion() {
|
||||
return correctionSuggestion.isPresent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Prüfpunkt konnte wegen fehlender Voraussetzungen nicht ausgeführt werden.
|
||||
* <p>
|
||||
* Beispiel: Der Prüfpunkt {@link CheckpointId#API_KEY_ACCEPTED} ist nicht ausführbar,
|
||||
* wenn {@link CheckpointId#API_KEY_PRESENT} zuvor als Fehler bewertet wurde.
|
||||
* <p>
|
||||
* {@code NotApplicable} ist kein Fehler; er wird im Meldungsbereich neutral dargestellt
|
||||
* und wird nicht als Korrekturanlass behandelt.
|
||||
*
|
||||
* @param checkpointId Bezeichner des nicht ausgeführten Prüfpunkts; nie {@code null}
|
||||
* @param reason deutsche Begründung, warum der Prüfpunkt übersprungen wurde; nie {@code null}
|
||||
*/
|
||||
record NotApplicable(
|
||||
CheckpointId checkpointId,
|
||||
String reason) implements CheckpointResult {
|
||||
|
||||
/**
|
||||
* Erstellt ein Nicht-Anwendbar-Ergebnis.
|
||||
*
|
||||
* @param checkpointId Prüfpunkt-Bezeichner; darf nicht {@code null} sein
|
||||
* @param reason deutsche Begründung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code checkpointId} oder {@code reason} {@code null} sind
|
||||
*/
|
||||
public NotApplicable {
|
||||
Objects.requireNonNull(checkpointId, "checkpointId must not be null");
|
||||
Objects.requireNonNull(reason, "reason must not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
/**
|
||||
* Schweregrade für gescheiterte Prüfpunkte ({@link CheckpointResult.Failure}) des technischen Gesamttests.
|
||||
* <p>
|
||||
* Die Schweregrade sind analog zu den Stufen der editornahen Validierung
|
||||
* ({@code EditorValidationSeverity}), jedoch auf die Semantik des technischen Gesamttests
|
||||
* zugeschnitten:
|
||||
* <ul>
|
||||
* <li>{@link #WARNING} – riskante, aber technisch zulässige Einstellung. Das Speichern und
|
||||
* ein späterer headless-Lauf sind möglich, können aber unerwartetes Verhalten zeigen.</li>
|
||||
* <li>{@link #ERROR} – ungültige oder fehlende Konfiguration. Die Einstellung ist im
|
||||
* aktuellen Zustand nicht lauffähig.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Hinweise und neutrale Informationen werden als {@link CheckpointResult.Success} oder
|
||||
* {@link CheckpointResult.NotApplicable} modelliert, nicht als Failure mit diesem Enum.
|
||||
*/
|
||||
public enum CheckpointSeverity {
|
||||
|
||||
/**
|
||||
* Riskante, aber technisch zulässige Einstellung.
|
||||
* Speichern und ein späterer headless-Lauf bleiben möglich.
|
||||
*/
|
||||
WARNING,
|
||||
|
||||
/**
|
||||
* Ungültige oder fehlende Einstellung – Konfiguration ist im aktuellen Zustand nicht lauffähig.
|
||||
*/
|
||||
ERROR
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Gesamtergebnis der Ausführung eines bestätigten {@link CorrectionPlan}.
|
||||
* <p>
|
||||
* Enthält für jeden im Plan enthaltenen Korrekturvorschlag ein {@link CorrectionOutcome}.
|
||||
* Die Reihenfolge der Ergebnisse entspricht der Reihenfolge der Vorschläge im Plan.
|
||||
* <p>
|
||||
* Dieser Record ist immutable und enthält keine JavaFX-Typen.
|
||||
*
|
||||
* @param outcomes Ergebnisliste in Ausführungsreihenfolge; nie {@code null}
|
||||
*/
|
||||
public record CorrectionExecutionReport(List<CorrectionOutcome> outcomes) {
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Ausführungsbericht.
|
||||
*
|
||||
* @param outcomes Liste der Ausführungsergebnisse; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code outcomes} {@code null} ist
|
||||
*/
|
||||
public CorrectionExecutionReport {
|
||||
Objects.requireNonNull(outcomes, "outcomes must not be null");
|
||||
outcomes = List.copyOf(outcomes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob alle Korrekturen erfolgreich angewendet wurden.
|
||||
* <p>
|
||||
* Gibt {@code true} zurück, wenn alle Ergebnisse vom Typ {@link CorrectionOutcome.Applied}
|
||||
* sind und der Bericht mindestens einen Eintrag enthält.
|
||||
*
|
||||
* @return {@code true} wenn mindestens ein Eintrag vorhanden ist und alle angewendet wurden
|
||||
*/
|
||||
public boolean allApplied() {
|
||||
return !outcomes.isEmpty()
|
||||
&& outcomes.stream().allMatch(o -> o instanceof CorrectionOutcome.Applied);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob mindestens eine Korrektur gescheitert ist.
|
||||
*
|
||||
* @return {@code true} wenn mindestens ein {@link CorrectionOutcome.Failed} vorliegt
|
||||
*/
|
||||
public boolean hasFailures() {
|
||||
return outcomes.stream().anyMatch(o -> o instanceof CorrectionOutcome.Failed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob mindestens eine Korrektur nicht versucht wurde.
|
||||
*
|
||||
* @return {@code true} wenn mindestens ein {@link CorrectionOutcome.NotAttempted} vorliegt
|
||||
*/
|
||||
public boolean hasNotAttempted() {
|
||||
return outcomes.stream().anyMatch(o -> o instanceof CorrectionOutcome.NotAttempted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Gesamtzahl der Ergebnisse zurück.
|
||||
*
|
||||
* @return Anzahl der Ergebnisse; nie negativ
|
||||
*/
|
||||
public int size() {
|
||||
return outcomes.size();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Führt einen bestätigten {@link CorrectionPlan} aus, indem er jeden enthaltenen
|
||||
* {@link CorrectionSuggestion}-Vorschlag über den {@link ResourceCreationPort} ausführt.
|
||||
* <p>
|
||||
* Der Service iteriert alle Vorschläge im Plan und gibt pro Vorschlag ein
|
||||
* {@link CorrectionOutcome} an den {@link ResourceCreationPort} weiter. Das Gesamtergebnis
|
||||
* wird als {@link CorrectionExecutionReport} zurückgegeben.
|
||||
*
|
||||
* <h2>Kein Frühabbruch</h2>
|
||||
* <p>
|
||||
* Wenn eine Korrektur scheitert, laufen alle weiteren Korrekturen trotzdem weiter.
|
||||
* Ein einzelnes {@link CorrectionOutcome.Failed} führt nicht zum Abbruch.
|
||||
*
|
||||
* <h2>Aufrufkonvention</h2>
|
||||
* <p>
|
||||
* Dieser Service darf nur nach ausdrücklicher Benutzerbestätigung des
|
||||
* {@link CorrectionPlan} aufgerufen werden. Es darf keine stille Ausführung im
|
||||
* Hintergrund geben. Da die Ausführung I/O-intensiv sein kann, sollte der Aufruf
|
||||
* auf einem Hintergrund-Worker-Thread erfolgen.
|
||||
*
|
||||
* <h2>Thread-Safety</h2>
|
||||
* <p>
|
||||
* Diese Klasse ist zustandslos und thread-safe, sofern der injizierte
|
||||
* {@link ResourceCreationPort} ebenfalls thread-safe ist.
|
||||
*/
|
||||
public class CorrectionExecutionService {
|
||||
|
||||
private final ResourceCreationPort port;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Ausführungsservice.
|
||||
*
|
||||
* @param port der Port für schreibende Korrekturmaßnahmen; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code port} {@code null} ist
|
||||
*/
|
||||
public CorrectionExecutionService(ResourceCreationPort port) {
|
||||
this.port = Objects.requireNonNull(port, "port must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt alle Korrekturvorschläge im übergebenen Plan aus.
|
||||
* <p>
|
||||
* Iteriert die {@link CorrectionSuggestion}s des Plans und dispatcht jeden Vorschlag
|
||||
* an die passende Methode des {@link ResourceCreationPort}. Alle Ergebnisse werden
|
||||
* gesammelt und als {@link CorrectionExecutionReport} zurückgegeben. Ein Fehler bei
|
||||
* einem Vorschlag führt nicht zum Abbruch der Ausführung der nachfolgenden Vorschläge.
|
||||
* <p>
|
||||
* Wenn der Plan leer ist, wird ein leerer Bericht zurückgegeben.
|
||||
*
|
||||
* @param plan der zu ausführende Korrekturplan; darf nicht {@code null} sein
|
||||
* @return Bericht mit einem {@link CorrectionOutcome} pro Vorschlag; nie {@code null}
|
||||
* @throws NullPointerException wenn {@code plan} {@code null} ist
|
||||
*/
|
||||
public CorrectionExecutionReport execute(CorrectionPlan plan) {
|
||||
Objects.requireNonNull(plan, "plan must not be null");
|
||||
List<CorrectionOutcome> outcomes = new ArrayList<>(plan.size());
|
||||
|
||||
for (CorrectionSuggestion suggestion : plan.suggestions()) {
|
||||
CorrectionOutcome outcome = dispatch(suggestion);
|
||||
outcomes.add(outcome);
|
||||
}
|
||||
|
||||
return new CorrectionExecutionReport(outcomes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatcht einen einzelnen {@link CorrectionSuggestion} an die passende Methode
|
||||
* des {@link ResourceCreationPort}.
|
||||
*
|
||||
* @param suggestion der auszuführende Korrekturvorschlag; nie {@code null}
|
||||
* @return Ausführungsergebnis; nie {@code null}
|
||||
*/
|
||||
private CorrectionOutcome dispatch(CorrectionSuggestion suggestion) {
|
||||
return switch (suggestion) {
|
||||
case CorrectionSuggestion.CreateDirectory cd -> port.createDirectory(cd);
|
||||
case CorrectionSuggestion.CreatePromptFile cp -> port.createPromptFile(cp);
|
||||
case CorrectionSuggestion.PrepareSqlitePath ps -> port.prepareSqlitePath(ps);
|
||||
};
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Versiegeltes Ergebnis der Ausführung eines einzelnen {@link CorrectionSuggestion}-Vorschlags.
|
||||
* <p>
|
||||
* Nach Benutzerbestätigung eines {@link CorrectionPlan} wird jeder enthaltene Vorschlag
|
||||
* durch den {@link ResourceCreationPort} ausgeführt. Das Ergebnis jeder Ausführung wird
|
||||
* als eines der drei möglichen Zustände modelliert:
|
||||
* <ul>
|
||||
* <li>{@link Applied} – die Korrektur wurde erfolgreich durchgeführt.</li>
|
||||
* <li>{@link Failed} – die Korrektur wurde versucht, aber ist technisch gescheitert.</li>
|
||||
* <li>{@link NotAttempted} – die Korrektur wurde nicht versucht, obwohl sie im Plan enthalten war.
|
||||
* Typischer Grund: eine Voraussetzung war zur Laufzeit nicht erfüllt.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Alle Implementierungen sind immutable und enthalten keine JavaFX-Typen.
|
||||
*/
|
||||
public sealed interface CorrectionOutcome
|
||||
permits CorrectionOutcome.Applied,
|
||||
CorrectionOutcome.Failed,
|
||||
CorrectionOutcome.NotAttempted {
|
||||
|
||||
/**
|
||||
* Gibt den Korrekturvorschlag zurück, auf den sich dieses Ergebnis bezieht.
|
||||
*
|
||||
* @return Korrekturvorschlag; nie {@code null}
|
||||
*/
|
||||
CorrectionSuggestion suggestion();
|
||||
|
||||
/**
|
||||
* Die Korrektur wurde erfolgreich durchgeführt.
|
||||
*
|
||||
* @param suggestion der ausgeführte Korrekturvorschlag; nie {@code null}
|
||||
* @param message deutsche Bestätigungsmeldung; nie {@code null}
|
||||
*/
|
||||
record Applied(
|
||||
CorrectionSuggestion suggestion,
|
||||
String message) implements CorrectionOutcome {
|
||||
|
||||
/**
|
||||
* Erstellt ein Erfolgs-Ergebnis.
|
||||
*
|
||||
* @param suggestion Korrekturvorschlag; darf nicht {@code null} sein
|
||||
* @param message Bestätigungsmeldung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
*/
|
||||
public Applied {
|
||||
Objects.requireNonNull(suggestion, "suggestion must not be null");
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Korrektur wurde versucht, aber ist technisch gescheitert.
|
||||
*
|
||||
* @param suggestion der nicht erfolgreich ausgeführte Korrekturvorschlag; nie {@code null}
|
||||
* @param errorMessage deutsche Fehlerbeschreibung; nie {@code null}
|
||||
*/
|
||||
record Failed(
|
||||
CorrectionSuggestion suggestion,
|
||||
String errorMessage) implements CorrectionOutcome {
|
||||
|
||||
/**
|
||||
* Erstellt ein Fehler-Ergebnis.
|
||||
*
|
||||
* @param suggestion Korrekturvorschlag; darf nicht {@code null} sein
|
||||
* @param errorMessage Fehlerbeschreibung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
*/
|
||||
public Failed {
|
||||
Objects.requireNonNull(suggestion, "suggestion must not be null");
|
||||
Objects.requireNonNull(errorMessage, "errorMessage must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Korrektur wurde nicht versucht, obwohl sie im Plan enthalten war.
|
||||
* <p>
|
||||
* Typischer Grund: Eine Voraussetzung war zur Ausführungszeit nicht erfüllt
|
||||
* (z. B. übergeordneter Ordner nicht erreichbar). Dies ist kein technischer Fehler
|
||||
* des Korrekturprozesses selbst, sondern ein Hinweis auf eine unerfüllbare Bedingung.
|
||||
*
|
||||
* @param suggestion der nicht versuchte Korrekturvorschlag; nie {@code null}
|
||||
* @param reason deutsche Begründung; nie {@code null}
|
||||
*/
|
||||
record NotAttempted(
|
||||
CorrectionSuggestion suggestion,
|
||||
String reason) implements CorrectionOutcome {
|
||||
|
||||
/**
|
||||
* Erstellt ein Nicht-Versucht-Ergebnis.
|
||||
*
|
||||
* @param suggestion Korrekturvorschlag; darf nicht {@code null} sein
|
||||
* @param reason Begründung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
*/
|
||||
public NotAttempted {
|
||||
Objects.requireNonNull(suggestion, "suggestion must not be null");
|
||||
Objects.requireNonNull(reason, "reason must not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Gesammelter Korrekturplan, der alle schreibenden Korrekturmaßnahmen enthält,
|
||||
* die nach Benutzerbestätigung ausgeführt werden sollen.
|
||||
* <p>
|
||||
* Ein Korrekturplan wird aus den {@link CorrectionSuggestion}-Einträgen der
|
||||
* gescheiterten Prüfpunkte eines {@link TechnicalTestReport} abgeleitet. Er wird dem
|
||||
* Benutzer in einem gesammelten Bestätigungsdialog präsentiert, bevor eine schreibende
|
||||
* Maßnahme ausgeführt wird. Ohne ausdrückliche Bestätigung werden keine Korrekturen
|
||||
* vorgenommen.
|
||||
* <p>
|
||||
* Dieser Record ist immutable und enthält keine JavaFX-Typen.
|
||||
*
|
||||
* @param suggestions alle Korrekturvorschläge in Ausführungsreihenfolge; nie {@code null}
|
||||
*/
|
||||
public record CorrectionPlan(List<CorrectionSuggestion> suggestions) {
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Korrekturplan.
|
||||
*
|
||||
* @param suggestions Liste der Korrekturvorschläge; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code suggestions} {@code null} ist
|
||||
*/
|
||||
public CorrectionPlan {
|
||||
Objects.requireNonNull(suggestions, "suggestions must not be null");
|
||||
suggestions = List.copyOf(suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen leeren Korrekturplan ohne Maßnahmen.
|
||||
* <p>
|
||||
* Ein leerer Plan zeigt an, dass nach einem Gesamttest keine sicheren technischen
|
||||
* Korrekturen angeboten werden können.
|
||||
*
|
||||
* @return ein leerer Korrekturplan; nie {@code null}
|
||||
*/
|
||||
public static CorrectionPlan empty() {
|
||||
return new CorrectionPlan(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob dieser Plan mindestens einen Korrekturvorschlag enthält.
|
||||
*
|
||||
* @return {@code true} wenn mindestens ein Vorschlag vorhanden ist
|
||||
*/
|
||||
public boolean hasCorrections() {
|
||||
return !suggestions.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Anzahl der enthaltenen Korrekturvorschläge zurück.
|
||||
*
|
||||
* @return Anzahl der Vorschläge; nie negativ
|
||||
*/
|
||||
public int size() {
|
||||
return suggestions.size();
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Versiegelter Korrekturvorschlag für eine schreibende technische Korrekturmaßnahme.
|
||||
* <p>
|
||||
* Korrekturvorschläge beschreiben <em>was</em> korrigiert werden soll, aber noch nicht
|
||||
* <em>wie</em>. Die konkrete Ausführung übernimmt der {@link ResourceCreationPort}. Ein
|
||||
* Vorschlag wird dem Benutzer vor der Ausführung in einem gesammelten Bestätigungsdialog
|
||||
* angezeigt; ohne Bestätigung wird keine schreibende Änderung vorgenommen.
|
||||
* <p>
|
||||
* Nicht automatisch korrigierbare Probleme (falscher API-Key, unerreichbare Base-URL,
|
||||
* nicht verfügbare Modellliste) werden niemals als {@code CorrectionSuggestion} modelliert.
|
||||
* <p>
|
||||
* Alle Pfade werden als {@code String} übergeben, analog zur Konvention der übrigen
|
||||
* Outbound-Ports dieses Projekts. Der Adapter-Out ist für die Konvertierung in
|
||||
* {@code java.nio.file.Path} zuständig.
|
||||
*/
|
||||
public sealed interface CorrectionSuggestion
|
||||
permits CorrectionSuggestion.CreateDirectory,
|
||||
CorrectionSuggestion.CreatePromptFile,
|
||||
CorrectionSuggestion.PrepareSqlitePath {
|
||||
|
||||
/**
|
||||
* Gibt eine kurze deutsche Beschreibung der vorgeschlagenen Korrektur zurück,
|
||||
* die dem Benutzer im Bestätigungsdialog angezeigt wird.
|
||||
*
|
||||
* @return deutsche Beschreibung; nie {@code null}
|
||||
*/
|
||||
String descriptionForUser();
|
||||
|
||||
/**
|
||||
* Ein fehlender Ordner soll angelegt werden.
|
||||
* <p>
|
||||
* Anwendungsfälle: fehlender Zielordner.
|
||||
* Es werden nur Ordner angelegt, die noch nicht existieren und deren Elternpfad
|
||||
* erreichbar ist.
|
||||
*
|
||||
* @param path Pfad des anzulegenden Ordners als String; nie {@code null}
|
||||
* @param descriptionForUser deutsche Beschreibung für den Bestätigungsdialog; nie {@code null}
|
||||
*/
|
||||
record CreateDirectory(
|
||||
String path,
|
||||
String descriptionForUser) implements CorrectionSuggestion {
|
||||
|
||||
/**
|
||||
* Erstellt einen Vorschlag zum Anlegen eines Ordners.
|
||||
*
|
||||
* @param path Pfad des Ordners; darf nicht {@code null} oder leer sein
|
||||
* @param descriptionForUser deutsche Beschreibung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
* @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");
|
||||
if (path.isBlank()) {
|
||||
throw new IllegalArgumentException("path must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine fehlende Prompt-Datei soll mit einem deutschen Standardinhalt erzeugt werden.
|
||||
* <p>
|
||||
* Die Erzeugung erfolgt nur, wenn der Zielpfad beschreibbar ist. Der konkrete
|
||||
* Standardinhalt wird vom {@link ResourceCreationPort} bereitgestellt. Der
|
||||
* Standardpfad liegt im selben Ordner wie die {@code .properties}-Datei.
|
||||
*
|
||||
* @param path Pfad der anzulegenden Prompt-Datei als String; nie {@code null}
|
||||
* @param descriptionForUser deutsche Beschreibung für den Bestätigungsdialog; nie {@code null}
|
||||
*/
|
||||
record CreatePromptFile(
|
||||
String path,
|
||||
String descriptionForUser) implements CorrectionSuggestion {
|
||||
|
||||
/**
|
||||
* Erstellt einen Vorschlag zum Erzeugen einer deutschen Standard-Prompt-Datei.
|
||||
*
|
||||
* @param path Pfad der Prompt-Datei; darf nicht {@code null} oder leer sein
|
||||
* @param descriptionForUser deutsche Beschreibung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
* @throws IllegalArgumentException wenn {@code path} leer ist
|
||||
*/
|
||||
public CreatePromptFile {
|
||||
Objects.requireNonNull(path, "path must not be null");
|
||||
Objects.requireNonNull(descriptionForUser, "descriptionForUser must not be null");
|
||||
if (path.isBlank()) {
|
||||
throw new IllegalArgumentException("path must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein fehlender oder noch nicht vorbereiteter SQLite-Pfad soll nutzbar gemacht werden.
|
||||
* <p>
|
||||
* Konkret bedeutet das: Falls die SQLite-Datei noch nicht existiert, aber ihr
|
||||
* übergeordneter Ordner vorhanden oder anlegbar ist, wird der Ordner sichergestellt.
|
||||
* Eine leere SQLite-Datei wird nicht manuell erzeugt; das übernimmt das JDBC-Layer
|
||||
* beim ersten Datenbankzugriff.
|
||||
*
|
||||
* @param path Pfad der SQLite-Datei als String; nie {@code null}
|
||||
* @param descriptionForUser deutsche Beschreibung für den Bestätigungsdialog; nie {@code null}
|
||||
*/
|
||||
record PrepareSqlitePath(
|
||||
String path,
|
||||
String descriptionForUser) implements CorrectionSuggestion {
|
||||
|
||||
/**
|
||||
* Erstellt einen Vorschlag zur Vorbereitung des SQLite-Pfads.
|
||||
*
|
||||
* @param path Pfad der SQLite-Datei; darf nicht {@code null} oder leer sein
|
||||
* @param descriptionForUser deutsche Beschreibung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
* @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");
|
||||
if (path.isBlank()) {
|
||||
throw new IllegalArgumentException("path must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
/**
|
||||
* Liefert den deutschen Standardinhalt für neu erzeugte Prompt-Dateien.
|
||||
* <p>
|
||||
* Diese Klasse stellt einen brauchbaren Ausgangspunkt für die Prompt-Datei bereit,
|
||||
* der ohne weitere Anpassung funktioniert. Der Inhalt enthält die Anweisung an die KI,
|
||||
* aus einem bereits extrahierten Dokumenttext einen normierten deutschen Dateinamensvorschlag
|
||||
* zu erzeugen.
|
||||
* <p>
|
||||
* <strong>Abgrenzung:</strong> Diese Klasse enthält ausschließlich den Prompt-Text als
|
||||
* reine Zeichenkette. Kein Dateisystem-I/O, kein Template-Engine, keine Platzhalter
|
||||
* für den Dokumentinhalt (der Dokumenttext wird vom Aufrufer separat angefügt).
|
||||
* <p>
|
||||
* Der gelieferte Inhalt ist ein sinnvoller, funktionsfähiger Standard und nicht für
|
||||
* fachliche Weiterentwicklung oder Versionierung vorgesehen.
|
||||
*/
|
||||
public final class DefaultPromptTemplate {
|
||||
|
||||
private DefaultPromptTemplate() {
|
||||
// Utility-Klasse – keine Instanziierung
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den deutschen Standardinhalt für eine neu erzeugte Prompt-Datei zurück.
|
||||
* <p>
|
||||
* Der zurückgegebene Text enthält:
|
||||
* <ul>
|
||||
* <li>Eine Rollenanweisung an die KI (deutsches Dokumentenverwaltungssystem)</li>
|
||||
* <li>Das erwartete JSON-Ausgabeformat mit den Feldern {@code date}, {@code title} und {@code reasoning}</li>
|
||||
* <li>Benennungsregeln für Titel (maximal 20 Zeichen, deutsch, keine Sonderzeichen)</li>
|
||||
* <li>Hinweis auf das Datumsformat ({@code YYYY-MM-DD})</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Der Text enthält keinen Platzhalter für den Dokumentinhalt. Der Dokumenttext
|
||||
* wird vom {@link de.gecheckt.pdf.umbenenner.application.service.AiRequestComposer}
|
||||
* separat angehängt.
|
||||
*
|
||||
* @return der deutsche Standard-Prompt-Inhalt; nie {@code null}, nie leer
|
||||
*/
|
||||
public static String defaultContent() {
|
||||
return """
|
||||
Du bist ein Assistent für ein deutsches Dokumentenverwaltungssystem.
|
||||
Deine Aufgabe ist es, aus dem Inhalt einer bereits OCR-verarbeiteten PDF-Datei
|
||||
einen aussagekräftigen, kurzen und normierten Dateinamensvorschlag zu erstellen.
|
||||
|
||||
Antworte ausschließlich mit einem validen JSON-Objekt im folgenden Schema:
|
||||
{
|
||||
"date": "YYYY-MM-DD",
|
||||
"title": "Kurztitel auf Deutsch",
|
||||
"reasoning": "Kurze Begründung auf Deutsch"
|
||||
}
|
||||
|
||||
Regeln:
|
||||
- Das Feld "title" ist verpflichtend.
|
||||
- Das Feld "reasoning" ist verpflichtend.
|
||||
- Das Feld "date" ist optional. Wenn kein belastbares Datum aus dem Dokument eindeutig ableitbar ist, lass das Feld weg. Kein Datum erfinden.
|
||||
- Das Datumsformat ist YYYY-MM-DD (z.B. 2026-03-15).
|
||||
- Der Titel ist auf Deutsch, verständlich und eindeutig für den Dokumentinhalt.
|
||||
- Der Titel hat maximal 20 Zeichen (Basistitel ohne Suffix).
|
||||
- Keine generischen Bezeichner wie "Dokument", "Scan", "Datei", "PDF".
|
||||
- Keine Sonderzeichen außer Leerzeichen im Titel.
|
||||
- Eigennamen bleiben unverändert.
|
||||
- Umlaute und ß sind erlaubt.
|
||||
- Kein Text außerhalb des JSON-Objekts.
|
||||
""";
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
/**
|
||||
* Outbound-Port für technische Pfad- und Dateisystemprüfungen.
|
||||
* <p>
|
||||
* Dieser Port ist <strong>ausschließlich lesend</strong>. Er prüft den Zustand von Pfaden,
|
||||
* ohne Dateien, Ordner oder andere Ressourcen anzulegen, zu verändern oder zu löschen.
|
||||
* Schreibende Korrekturen sind über {@link ResourceCreationPort} zu initiieren.
|
||||
* <p>
|
||||
* <strong>Pfad-Konvention:</strong> Alle Pfade werden als {@code String} übergeben, analog
|
||||
* zur Konvention der übrigen Outbound-Ports dieses Projekts (z. B. {@code TargetFolderPort}).
|
||||
* Der Adapter-Out ist für die Konvertierung in plattformspezifische Pfadobjekte zuständig.
|
||||
* <p>
|
||||
* <strong>Windows- und Netzlaufwerke:</strong> Implementierungen müssen gemappte
|
||||
* Laufwerksbuchstaben wie {@code S:\} oder {@code H:\} im Windows-Kontext ausdrücklich
|
||||
* akzeptieren. Solche Pfade dürfen nicht allein deshalb abgelehnt werden, weil dahinter
|
||||
* technisch ein UNC-Pfad stehen könnte.
|
||||
* <p>
|
||||
* <strong>Fehlerbehandlung:</strong> Implementierungen werfen keine geprüften oder
|
||||
* ungeprüften Ausnahmen für erwartete Fehlerbedingungen (Pfad nicht vorhanden,
|
||||
* keine Leseberechtigung). Alle solchen Zustände werden als {@code boolean}-Ergebnis
|
||||
* oder über separate Methoden kommuniziert.
|
||||
*/
|
||||
public interface PathCheckPort {
|
||||
|
||||
/**
|
||||
* Prüft, ob der angegebene Pfad auf einen vorhandenen, lesbaren Ordner zeigt.
|
||||
*
|
||||
* @param path zu prüfender Pfad als String; darf nicht {@code null} oder leer sein
|
||||
* @return {@code true} wenn der Ordner existiert und gelesen werden kann
|
||||
*/
|
||||
boolean isDirectoryReadable(String path);
|
||||
|
||||
/**
|
||||
* Prüft, ob der angegebene Pfad auf einen vorhandenen, schreibbaren Ordner zeigt
|
||||
* oder ob dieser Ordner technisch anlegbar wäre.
|
||||
* <p>
|
||||
* Gibt {@code true} zurück, wenn:
|
||||
* <ul>
|
||||
* <li>der Ordner existiert und schreibbar ist, oder</li>
|
||||
* <li>der Ordner noch nicht existiert, aber sein Elternpfad erreichbar und
|
||||
* schreibbar ist (anlegbar).</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param path zu prüfender Pfad als String; darf nicht {@code null} oder leer sein
|
||||
* @return {@code true} wenn der Ordner vorhanden und schreibbar oder anlegbar ist
|
||||
*/
|
||||
boolean isDirectoryWritableOrCreatable(String path);
|
||||
|
||||
/**
|
||||
* Prüft, ob der angegebene Pfad auf eine vorhandene, lesbare Datei zeigt.
|
||||
*
|
||||
* @param path zu prüfender Pfad als String; darf nicht {@code null} oder leer sein
|
||||
* @return {@code true} wenn die Datei existiert und gelesen werden kann
|
||||
*/
|
||||
boolean isFileReadable(String path);
|
||||
|
||||
/**
|
||||
* Prüft, ob der angegebene Pfad als SQLite-Datenbankpfad technisch nutzbar ist.
|
||||
* <p>
|
||||
* Gibt {@code true} zurück, wenn:
|
||||
* <ul>
|
||||
* <li>die Datei existiert und les- und schreibbar ist, oder</li>
|
||||
* <li>die Datei noch nicht existiert, aber ihr übergeordneter Ordner vorhanden
|
||||
* und schreibbar ist (Datei wäre anlegbar).</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param path zu prüfender Pfad als String; darf nicht {@code null} oder leer sein
|
||||
* @return {@code true} wenn der SQLite-Pfad nutzbar oder anlegbar ist
|
||||
*/
|
||||
boolean isSqlitePathUsable(String path);
|
||||
}
|
||||
+496
@@ -0,0 +1,496 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ApiKeyOrigin;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogRequest;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
|
||||
|
||||
/**
|
||||
* Application-Service für die provider-nahen technischen Prüfpunkte des Gesamttests.
|
||||
* <p>
|
||||
* Dieser Service führt genau fünf providerbezogene Prüfpunkte aus:
|
||||
* <ul>
|
||||
* <li>{@link CheckpointId#BASE_URL_REACHABLE} – Endpoint technisch erreichbar</li>
|
||||
* <li>{@link CheckpointId#API_KEY_PRESENT} – API-Schlüssel in mindestens einer Quelle vorhanden</li>
|
||||
* <li>{@link CheckpointId#API_KEY_ACCEPTED} – Authentifizierung am Endpoint erfolgreich</li>
|
||||
* <li>{@link CheckpointId#MODEL_LIST_AVAILABLE} – Provider liefert eine Modellliste</li>
|
||||
* <li>{@link CheckpointId#SELECTED_MODEL_PLAUSIBLE} – konfiguriertes Modell in der Liste enthalten</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <strong>Port-Wiederverwendung:</strong> Der Service ruft den {@link AiModelCatalogPort}
|
||||
* exakt einmal auf und leitet aus dem Ergebnis alle fünf Prüfpunkte ab. Es findet keine
|
||||
* zweite HTTP-Implementierung statt.
|
||||
* <p>
|
||||
* <strong>API-Key-Vorrangregel:</strong> Der {@link ApiKeyResolutionPort} wird konsultiert,
|
||||
* damit auch reine Umgebungsvariablen-Setups korrekt als „API-Key vorhanden" bewertet werden.
|
||||
* Nur wenn der Deskriptor {@link ApiKeyOrigin#ABSENT} zurückliefert, gilt der Schlüssel als
|
||||
* fehlend. In diesem Fall werden alle Remote-Prüfpunkte als {@link CheckpointResult.NotApplicable}
|
||||
* markiert, ohne einen HTTP-Aufruf durchzuführen.
|
||||
* <p>
|
||||
* <strong>Mapping-Regeln für {@link ModelCatalogResult}-Varianten:</strong>
|
||||
* <ul>
|
||||
* <li>{@link ModelCatalogResult.Success}: alle fünf Prüfpunkte auswertbar; Modellplausibilität
|
||||
* anhand der zurückgegebenen Liste geprüft.</li>
|
||||
* <li>{@link ModelCatalogResult.EmptyList}: Endpoint und Key akzeptiert, aber keine Modellliste;
|
||||
* Modellplausibilität nicht prüfbar.</li>
|
||||
* <li>{@link ModelCatalogResult.IncompleteConfiguration}: Konfiguration unvollständig; kein
|
||||
* HTTP-Aufruf vom Adapter durchgeführt.</li>
|
||||
* <li>{@link ModelCatalogResult.TechnicalFailure}: abhängig vom Fehlerkategorie-String;
|
||||
* Authentifizierungsfehler, Verbindungsfehler, Serverfehler und ungültige Antworten
|
||||
* werden unterschiedlich auf die Prüfpunkte abgebildet.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <strong>Threading-Kontrakt:</strong> Die Methode {@link #runProviderChecks(EditorValidationInput)}
|
||||
* ist <em>synchron blockierend</em>. Sie darf nicht auf dem JavaFX Application Thread aufgerufen
|
||||
* werden. Der Aufrufer (GUI-Orchestrierung) ist verantwortlich, den Aufruf auf einem
|
||||
* Hintergrund-Worker-Thread auszuführen und die Ergebnisse via {@code Platform.runLater}
|
||||
* in die UI zu überführen. Dieser Service enthält kein {@code Platform.runLater} und
|
||||
* startet keine eigenen Threads.
|
||||
* <p>
|
||||
* <strong>Fehlerklasse-Konstanten (TechnicalFailure.errorCategory):</strong>
|
||||
* Die Adapter-Out-Implementierungen verwenden stabile Kategorie-Strings. Dieser Service
|
||||
* erkennt folgende Präfixe bzw. Werte (Groß-/Kleinschreibung ignoriert):
|
||||
* {@code AUTHENTICATION_FAILED}, {@code CONNECTION_FAILURE}, {@code ENDPOINT_NOT_FOUND},
|
||||
* {@code SERVER_ERROR}, {@code INVALID_RESPONSE}. Unbekannte Kategorien werden als
|
||||
* allgemeiner technischer Fehler behandelt.
|
||||
*/
|
||||
public class ProviderTechnicalTestService {
|
||||
|
||||
/** Fehlerkategorie-Konstante für Authentifizierungsfehler (case-insensitive Präfix-Erkennung). */
|
||||
static final String CATEGORY_AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED";
|
||||
/** Fehlerkategorie-Konstante für Verbindungsfehler. */
|
||||
static final String CATEGORY_CONNECTION_FAILURE = "CONNECTION_FAILURE";
|
||||
/** Fehlerkategorie-Konstante für nicht gefundenen Endpoint. */
|
||||
static final String CATEGORY_ENDPOINT_NOT_FOUND = "ENDPOINT_NOT_FOUND";
|
||||
/** Fehlerkategorie-Konstante für Serverfehler (5xx). */
|
||||
static final String CATEGORY_SERVER_ERROR = "SERVER_ERROR";
|
||||
/** Fehlerkategorie-Konstante für nicht parsierbare Antworten. */
|
||||
static final String CATEGORY_INVALID_RESPONSE = "INVALID_RESPONSE";
|
||||
|
||||
private static final int DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
|
||||
private final AiModelCatalogPort modelCatalogPort;
|
||||
private final ApiKeyResolutionPort apiKeyResolutionPort;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Service mit den erforderlichen Ports.
|
||||
*
|
||||
* @param modelCatalogPort Port für den Modellabruf; darf nicht {@code null} sein
|
||||
* @param apiKeyResolutionPort Port für die API-Key-Herkunftsauflösung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn einer der Parameter {@code null} ist
|
||||
*/
|
||||
public ProviderTechnicalTestService(AiModelCatalogPort modelCatalogPort,
|
||||
ApiKeyResolutionPort apiKeyResolutionPort) {
|
||||
this.modelCatalogPort = Objects.requireNonNull(modelCatalogPort, "modelCatalogPort must not be null");
|
||||
this.apiKeyResolutionPort = Objects.requireNonNull(apiKeyResolutionPort,
|
||||
"apiKeyResolutionPort must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt alle fünf provider-nahen technischen Prüfpunkte für den aktiven Provider aus.
|
||||
* <p>
|
||||
* Der aktive Provider wird aus {@code input.activeProviderIdentifier()} bestimmt.
|
||||
* Wenn der Bezeichner keiner bekannten Provider-Familie entspricht, werden alle fünf
|
||||
* Prüfpunkte als {@link CheckpointResult.Failure} mit Schweregrad ERROR zurückgegeben.
|
||||
* <p>
|
||||
* Diese Methode blockiert, bis das Ergebnis des Modellabrufs vorliegt oder ein
|
||||
* konfigurierter Timeout abläuft. Sie darf nicht auf dem JavaFX Application Thread
|
||||
* aufgerufen werden.
|
||||
*
|
||||
* @param input aktueller Editorzustand; darf nicht {@code null} sein
|
||||
* @return unveränderliche Liste mit genau fünf {@link CheckpointResult}-Einträgen
|
||||
* (in der Reihenfolge: API_KEY_PRESENT, BASE_URL_REACHABLE, API_KEY_ACCEPTED,
|
||||
* MODEL_LIST_AVAILABLE, SELECTED_MODEL_PLAUSIBLE); nie {@code null}
|
||||
* @throws NullPointerException wenn {@code input} {@code null} ist
|
||||
*/
|
||||
public List<CheckpointResult> runProviderChecks(EditorValidationInput input) {
|
||||
Objects.requireNonNull(input, "input must not be null");
|
||||
|
||||
Optional<AiProviderFamily> familyOpt = AiProviderFamily.fromIdentifier(
|
||||
input.activeProviderIdentifier());
|
||||
|
||||
if (familyOpt.isEmpty()) {
|
||||
String msg = "Aktiver Provider-Bezeichner unbekannt: \""
|
||||
+ input.activeProviderIdentifier() + "\". Provider-Prüfungen können nicht ausgeführt werden.";
|
||||
return List.of(
|
||||
CheckpointResult.Failure.of(CheckpointId.API_KEY_PRESENT, CheckpointSeverity.ERROR, msg),
|
||||
CheckpointResult.Failure.of(CheckpointId.BASE_URL_REACHABLE, CheckpointSeverity.ERROR, msg),
|
||||
CheckpointResult.Failure.of(CheckpointId.API_KEY_ACCEPTED, CheckpointSeverity.ERROR, msg),
|
||||
CheckpointResult.Failure.of(CheckpointId.MODEL_LIST_AVAILABLE, CheckpointSeverity.ERROR, msg),
|
||||
CheckpointResult.Failure.of(CheckpointId.SELECTED_MODEL_PLAUSIBLE, CheckpointSeverity.ERROR, msg)
|
||||
);
|
||||
}
|
||||
|
||||
AiProviderFamily family = familyOpt.get();
|
||||
// Den bereits im EditorValidationInput enthaltenen Descriptor verwenden.
|
||||
// EditorValidationInput enthält keinen rohen API-Key-String, sondern nur den
|
||||
// vom GUI-Adapter bereits aufgelösten Descriptor. Dieser spiegelt die
|
||||
// API-Key-Vorrangregel (ENV → Legacy-ENV → Property) wider.
|
||||
EffectiveApiKeyDescriptor apiKeyDescriptor = resolveApiKeyDescriptor(input, family);
|
||||
|
||||
// Prüfpunkt API_KEY_PRESENT: ohne HTTP-Aufruf
|
||||
CheckpointResult apiKeyPresentResult = checkApiKeyPresent(apiKeyDescriptor);
|
||||
|
||||
if (apiKeyDescriptor.isAbsent()) {
|
||||
// Kein API-Key → alle Remote-Prüfpunkte als NotApplicable markieren
|
||||
String reason = "Kein API-Schlüssel vorhanden. Remote-Prüfungen können nicht ausgeführt werden.";
|
||||
return List.of(
|
||||
apiKeyPresentResult,
|
||||
new CheckpointResult.NotApplicable(CheckpointId.BASE_URL_REACHABLE, reason),
|
||||
new CheckpointResult.NotApplicable(CheckpointId.API_KEY_ACCEPTED, reason),
|
||||
new CheckpointResult.NotApplicable(CheckpointId.MODEL_LIST_AVAILABLE, reason),
|
||||
new CheckpointResult.NotApplicable(CheckpointId.SELECTED_MODEL_PLAUSIBLE, reason)
|
||||
);
|
||||
}
|
||||
|
||||
// API-Key vorhanden → Modellabruf durchführen
|
||||
String configuredModel = resolveModelValue(input, family);
|
||||
ModelCatalogRequest catalogRequest = buildCatalogRequest(input, family, apiKeyDescriptor);
|
||||
|
||||
ModelCatalogResult catalogResult = modelCatalogPort.fetchAvailableModels(catalogRequest);
|
||||
|
||||
List<CheckpointResult> results = new ArrayList<>();
|
||||
results.add(apiKeyPresentResult);
|
||||
results.addAll(mapCatalogResultToCheckpoints(catalogResult, configuredModel));
|
||||
return List.copyOf(results);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ helpers
|
||||
|
||||
/**
|
||||
* Erzeugt das {@link CheckpointResult} für {@link CheckpointId#API_KEY_PRESENT}.
|
||||
*
|
||||
* @param descriptor Herkunftsdeskriptor des API-Schlüssels
|
||||
* @return Success wenn ein Schlüssel vorhanden ist, Failure ERROR sonst
|
||||
*/
|
||||
private CheckpointResult checkApiKeyPresent(EffectiveApiKeyDescriptor descriptor) {
|
||||
if (descriptor.isAbsent()) {
|
||||
return CheckpointResult.Failure.of(
|
||||
CheckpointId.API_KEY_PRESENT,
|
||||
CheckpointSeverity.ERROR,
|
||||
"Kein API-Schlüssel vorhanden. Weder Umgebungsvariable noch Properties-Datei liefert einen Wert.");
|
||||
}
|
||||
String sourceInfo = descriptor.isFromEnvironmentVariable()
|
||||
? "Umgebungsvariable " + descriptor.envVarName().orElse("(unbekannt)")
|
||||
: "Properties-Datei";
|
||||
return new CheckpointResult.Success(
|
||||
CheckpointId.API_KEY_PRESENT,
|
||||
"API-Schlüssel vorhanden (Quelle: " + sourceInfo + ").");
|
||||
}
|
||||
|
||||
/**
|
||||
* Bildet ein {@link ModelCatalogResult} auf die vier Remote-Prüfpunkte ab.
|
||||
*
|
||||
* @param result Ergebnis des Modellabrufs
|
||||
* @param configuredModel konfigurierter Modellname aus dem Editor
|
||||
* @return Liste mit genau vier Prüfpunkt-Ergebnissen
|
||||
*/
|
||||
private List<CheckpointResult> mapCatalogResultToCheckpoints(ModelCatalogResult result,
|
||||
String configuredModel) {
|
||||
return switch (result) {
|
||||
case ModelCatalogResult.Success success -> mapSuccess(success, configuredModel);
|
||||
case ModelCatalogResult.EmptyList emptyList -> mapEmptyList();
|
||||
case ModelCatalogResult.IncompleteConfiguration incomplete -> mapIncompleteConfiguration(incomplete);
|
||||
case ModelCatalogResult.TechnicalFailure failure -> mapTechnicalFailure(failure);
|
||||
};
|
||||
}
|
||||
|
||||
private List<CheckpointResult> mapSuccess(ModelCatalogResult.Success success, String configuredModel) {
|
||||
CheckpointResult baseUrl = new CheckpointResult.Success(
|
||||
CheckpointId.BASE_URL_REACHABLE, "Endpoint erreichbar.");
|
||||
CheckpointResult apiKeyAccepted = new CheckpointResult.Success(
|
||||
CheckpointId.API_KEY_ACCEPTED, "API-Schlüssel akzeptiert.");
|
||||
CheckpointResult modelList = new CheckpointResult.Success(
|
||||
CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
"Modellliste verfügbar (" + success.models().size() + " Modell(e)).");
|
||||
CheckpointResult modelPlausible = checkModelPlausible(success.models(), configuredModel);
|
||||
return List.of(baseUrl, apiKeyAccepted, modelList, modelPlausible);
|
||||
}
|
||||
|
||||
private List<CheckpointResult> mapEmptyList() {
|
||||
CheckpointResult baseUrl = new CheckpointResult.Success(
|
||||
CheckpointId.BASE_URL_REACHABLE, "Endpoint erreichbar.");
|
||||
CheckpointResult apiKeyAccepted = new CheckpointResult.Success(
|
||||
CheckpointId.API_KEY_ACCEPTED, "API-Schlüssel akzeptiert.");
|
||||
CheckpointResult modelList = CheckpointResult.Failure.of(
|
||||
CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
CheckpointSeverity.WARNING,
|
||||
"Provider liefert keine Modellliste.");
|
||||
CheckpointResult modelPlausible = new CheckpointResult.NotApplicable(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
"Keine Modellliste vorhanden, Modellplausibilität nicht prüfbar.");
|
||||
return List.of(baseUrl, apiKeyAccepted, modelList, modelPlausible);
|
||||
}
|
||||
|
||||
private List<CheckpointResult> mapIncompleteConfiguration(ModelCatalogResult.IncompleteConfiguration incomplete) {
|
||||
String reason = incomplete.missingReason();
|
||||
CheckpointResult baseUrl = new CheckpointResult.NotApplicable(
|
||||
CheckpointId.BASE_URL_REACHABLE,
|
||||
"Konfiguration unvollständig – kein Verbindungsversuch: " + reason);
|
||||
CheckpointResult apiKeyAccepted = new CheckpointResult.NotApplicable(
|
||||
CheckpointId.API_KEY_ACCEPTED,
|
||||
"Konfiguration unvollständig – Authentifizierung nicht prüfbar.");
|
||||
CheckpointResult modelList = CheckpointResult.Failure.of(
|
||||
CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
"Provider-Konfiguration unvollständig: " + reason);
|
||||
CheckpointResult modelPlausible = new CheckpointResult.NotApplicable(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
"Konfiguration unvollständig – Modellplausibilität nicht prüfbar.");
|
||||
return List.of(baseUrl, apiKeyAccepted, modelList, modelPlausible);
|
||||
}
|
||||
|
||||
private List<CheckpointResult> mapTechnicalFailure(ModelCatalogResult.TechnicalFailure failure) {
|
||||
String category = failure.errorCategory().toUpperCase();
|
||||
String detail = failure.errorDetail();
|
||||
|
||||
if (category.contains(CATEGORY_AUTHENTICATION_FAILED)) {
|
||||
return List.of(
|
||||
new CheckpointResult.Success(
|
||||
CheckpointId.BASE_URL_REACHABLE,
|
||||
"Endpoint hat geantwortet (Authentifizierungsfehler erhalten)."),
|
||||
CheckpointResult.Failure.of(
|
||||
CheckpointId.API_KEY_ACCEPTED,
|
||||
CheckpointSeverity.ERROR,
|
||||
"API-Schlüssel technisch nicht akzeptiert: " + detail),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
"Authentifizierung fehlgeschlagen – Modellliste nicht abrufbar."),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
"Authentifizierung fehlgeschlagen – Modellplausibilität nicht prüfbar.")
|
||||
);
|
||||
}
|
||||
|
||||
if (category.contains(CATEGORY_CONNECTION_FAILURE) || category.contains(CATEGORY_ENDPOINT_NOT_FOUND)) {
|
||||
String baseUrlMessage = category.contains(CATEGORY_ENDPOINT_NOT_FOUND)
|
||||
? "Endpoint nicht gefunden: " + detail
|
||||
: "Verbindung zum Endpoint fehlgeschlagen: " + detail;
|
||||
return List.of(
|
||||
CheckpointResult.Failure.of(
|
||||
CheckpointId.BASE_URL_REACHABLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
baseUrlMessage),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.API_KEY_ACCEPTED,
|
||||
"Endpoint nicht erreichbar – Authentifizierung nicht prüfbar."),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
"Endpoint nicht erreichbar – Modellliste nicht abrufbar."),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
"Endpoint nicht erreichbar – Modellplausibilität nicht prüfbar.")
|
||||
);
|
||||
}
|
||||
|
||||
if (category.contains(CATEGORY_SERVER_ERROR)) {
|
||||
return List.of(
|
||||
new CheckpointResult.Success(
|
||||
CheckpointId.BASE_URL_REACHABLE,
|
||||
"Endpoint hat geantwortet (Serverfehler erhalten)."),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.API_KEY_ACCEPTED,
|
||||
"Serverfehler – Authentifizierung nicht eindeutig prüfbar."),
|
||||
CheckpointResult.Failure.of(
|
||||
CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
CheckpointSeverity.WARNING,
|
||||
"Provider antwortet mit Serverfehler: " + detail),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
"Serverfehler – Modellplausibilität nicht prüfbar.")
|
||||
);
|
||||
}
|
||||
|
||||
if (category.contains(CATEGORY_INVALID_RESPONSE)) {
|
||||
return List.of(
|
||||
new CheckpointResult.Success(
|
||||
CheckpointId.BASE_URL_REACHABLE,
|
||||
"Endpoint hat geantwortet (Antwort nicht verarbeitbar)."),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.API_KEY_ACCEPTED,
|
||||
"Antwort nicht parsierbar – Authentifizierung nicht eindeutig prüfbar."),
|
||||
CheckpointResult.Failure.of(
|
||||
CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
"Antwort des Providers nicht verarbeitbar: " + detail),
|
||||
new CheckpointResult.NotApplicable(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
"Antwort nicht parsierbar – Modellplausibilität nicht prüfbar.")
|
||||
);
|
||||
}
|
||||
|
||||
// Unbekannte Fehlerkategorie
|
||||
String unknownMsg = "Unbekannter technischer Fehler beim Modellabruf: " + detail;
|
||||
return List.of(
|
||||
CheckpointResult.Failure.of(
|
||||
CheckpointId.BASE_URL_REACHABLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
unknownMsg),
|
||||
CheckpointResult.Failure.of(
|
||||
CheckpointId.API_KEY_ACCEPTED,
|
||||
CheckpointSeverity.ERROR,
|
||||
unknownMsg),
|
||||
CheckpointResult.Failure.of(
|
||||
CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
unknownMsg),
|
||||
CheckpointResult.Failure.of(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
unknownMsg)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob das konfigurierte Modell in der Modellliste enthalten ist.
|
||||
*
|
||||
* @param models verfügbare Modelle vom Provider
|
||||
* @param configuredModel konfigurierter Modellname aus dem Editor
|
||||
* @return Success wenn das Modell enthalten ist, Failure WARNING sonst
|
||||
*/
|
||||
private CheckpointResult checkModelPlausible(List<String> models, String configuredModel) {
|
||||
if (configuredModel.isBlank()) {
|
||||
return CheckpointResult.Failure.of(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
"Kein Modell konfiguriert.");
|
||||
}
|
||||
if (models.contains(configuredModel)) {
|
||||
return new CheckpointResult.Success(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
"Konfiguriertes Modell \"" + configuredModel + "\" in verfügbarer Liste gefunden.");
|
||||
}
|
||||
return CheckpointResult.Failure.of(
|
||||
CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
CheckpointSeverity.WARNING,
|
||||
"Konfiguriertes Modell \"" + configuredModel
|
||||
+ "\" nicht in verfügbarer Liste gefunden. Bitte Modellname prüfen.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den {@link ModelCatalogRequest} aus dem aktuellen Editorzustand auf.
|
||||
* <p>
|
||||
* Da {@link EditorValidationInput} keinen direkten API-Key-String enthält, sondern
|
||||
* nur einen bereits aufgelösten {@link EffectiveApiKeyDescriptor}, wird der Descriptor
|
||||
* aus dem Editorzustand direkt verwendet. Der Adapter-Out-Seitige Dispatcher erwartet
|
||||
* den Key entweder als ENV-Variable (die er selbst liest) oder als optionalen Wert
|
||||
* im Request. Da die Auflösung beim Service bereits über {@link ApiKeyResolutionPort}
|
||||
* erfolgt ist, wird für den Catalog-Request ein leerer Optional-Wert geliefert –
|
||||
* der Adapter verwendet dann intern seine eigene ENV-Variable-Auflösung.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @param family aktive Provider-Familie
|
||||
* @param apiKeyDesc bereits aufgelöster Herkunftsdeskriptor des API-Schlüssels
|
||||
* @return fertiger Request; nie {@code null}
|
||||
*/
|
||||
private ModelCatalogRequest buildCatalogRequest(EditorValidationInput input,
|
||||
AiProviderFamily family,
|
||||
EffectiveApiKeyDescriptor apiKeyDesc) {
|
||||
// EditorValidationInput enthält keinen direkten API-Key-String-Wert, nur den Descriptor.
|
||||
// Für den ModelCatalogRequest übergeben wir einen leeren Optional für den apiKey,
|
||||
// sodass der Adapter seine eigene ENV-Variable-Auflösung durchführt.
|
||||
// Der Adapter liefert dann IncompleteConfiguration, wenn auch er keinen Key findet –
|
||||
// was aber nicht passiert, da wir oben bereits geprüft haben, dass apiKeyDesc nicht ABSENT ist.
|
||||
Optional<String> apiKeyForRequest = Optional.empty();
|
||||
|
||||
String rawBaseUrl = resolveBaseUrlValue(input, family);
|
||||
Optional<String> baseUrl = rawBaseUrl.isBlank() ? Optional.empty() : Optional.of(rawBaseUrl);
|
||||
|
||||
int timeout = parseTimeoutOrDefault(resolveTimeoutValue(input, family));
|
||||
|
||||
return new ModelCatalogRequest(
|
||||
family.getIdentifier(),
|
||||
baseUrl,
|
||||
apiKeyForRequest,
|
||||
timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest den bereits aufgelösten {@link EffectiveApiKeyDescriptor} für die aktive Provider-Familie
|
||||
* direkt aus dem {@link EditorValidationInput}.
|
||||
* <p>
|
||||
* {@link EditorValidationInput} enthält keinen rohen API-Key-String, sondern nur den vom
|
||||
* GUI-Adapter bereits aufgelösten Descriptor. Der Descriptor spiegelt die Vorrangregel
|
||||
* (ENV → Legacy-ENV → Property) zum Zeitpunkt des letzten Editor-Refreshs wider.
|
||||
* <p>
|
||||
* Für Tests kann der Descriptor im Eingabeobjekt direkt gesetzt werden.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @param family aktive Provider-Familie
|
||||
* @return der Herkunftsdeskriptor; nie {@code null}
|
||||
*/
|
||||
private EffectiveApiKeyDescriptor resolveApiKeyDescriptor(EditorValidationInput input,
|
||||
AiProviderFamily family) {
|
||||
return switch (family) {
|
||||
case CLAUDE -> input.claudeApiKeyDescriptor();
|
||||
case OPENAI_COMPATIBLE -> input.openaiApiKeyDescriptor();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest den Base-URL-Wert für die angegebene Provider-Familie aus dem Editorzustand.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @param family aktive Provider-Familie
|
||||
* @return Base-URL-String; nie {@code null}, leer wenn nicht gesetzt
|
||||
*/
|
||||
private String resolveBaseUrlValue(EditorValidationInput input, AiProviderFamily family) {
|
||||
return switch (family) {
|
||||
case CLAUDE -> input.claudeBaseUrl();
|
||||
case OPENAI_COMPATIBLE -> input.openaiBaseUrl();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest den konfigurierten Modellnamen für die angegebene Provider-Familie aus dem Editorzustand.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @param family aktive Provider-Familie
|
||||
* @return Modellname; nie {@code null}, leer wenn nicht gesetzt
|
||||
*/
|
||||
private String resolveModelValue(EditorValidationInput input, AiProviderFamily family) {
|
||||
return switch (family) {
|
||||
case CLAUDE -> input.claudeModel();
|
||||
case OPENAI_COMPATIBLE -> input.openaiModel();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest den Timeout-Wert für die angegebene Provider-Familie aus dem Editorzustand.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @param family aktive Provider-Familie
|
||||
* @return Timeout-String; nie {@code null}, leer wenn nicht gesetzt
|
||||
*/
|
||||
private String resolveTimeoutValue(EditorValidationInput input, AiProviderFamily family) {
|
||||
return switch (family) {
|
||||
case CLAUDE -> input.claudeTimeoutSeconds();
|
||||
case OPENAI_COMPATIBLE -> input.openaiTimeoutSeconds();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst einen Timeout-String zu einem Integer. Liefert den Standard-Timeout, wenn der
|
||||
* String leer ist oder nicht als positive Ganzzahl parsierbar ist.
|
||||
*
|
||||
* @param raw roher Timeout-String
|
||||
* @return geparster Timeout in Sekunden (mindestens 1)
|
||||
*/
|
||||
private int parseTimeoutOrDefault(String raw) {
|
||||
try {
|
||||
int parsed = Integer.parseInt(raw.trim());
|
||||
return parsed > 0 ? parsed : DEFAULT_TIMEOUT_SECONDS;
|
||||
} catch (NumberFormatException e) {
|
||||
return DEFAULT_TIMEOUT_SECONDS;
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
/**
|
||||
* Outbound-Port für schreibende technische Korrekturhilfen.
|
||||
* <p>
|
||||
* Dieser Port ist <strong>schreibend</strong> und darf nur nach ausdrücklicher
|
||||
* Benutzerbestätigung eines {@link CorrectionPlan} aufgerufen werden. Es darf keine
|
||||
* stille Ausführung im Hintergrund geben.
|
||||
* <p>
|
||||
* <strong>Abgrenzung zu {@link PathCheckPort}:</strong> {@code PathCheckPort} ist
|
||||
* rein lesend; {@code ResourceCreationPort} ist rein schreibend. Beide Ports werden
|
||||
* niemals für dieselbe Aufgabe verwendet.
|
||||
* <p>
|
||||
* <strong>Pfad-Konvention:</strong> Alle Pfade werden als {@code String} übergeben,
|
||||
* analog zur Konvention der übrigen Outbound-Ports dieses Projekts. Der Adapter-Out
|
||||
* ist für die Konvertierung in plattformspezifische Pfadobjekte zuständig.
|
||||
* <p>
|
||||
* <strong>Fehlerbehandlung:</strong> Implementierungen werfen keine geprüften Ausnahmen.
|
||||
* Jede Methode gibt ein {@link CorrectionOutcome} zurück, das Erfolg, Scheitern oder
|
||||
* Nicht-Durchführbarkeit ausdrückt. Unerwartete technische Fehler werden als
|
||||
* {@link CorrectionOutcome.Failed} zurückgegeben.
|
||||
* <p>
|
||||
* <strong>Windows- und Netzlaufwerke:</strong> Implementierungen müssen gemappte
|
||||
* Laufwerksbuchstaben wie {@code S:\} oder {@code H:\} im Windows-Kontext unterstützen.
|
||||
*/
|
||||
public interface ResourceCreationPort {
|
||||
|
||||
/**
|
||||
* Legt den angegebenen Ordner an, einschließlich aller fehlenden übergeordneten Ordner.
|
||||
* <p>
|
||||
* Falls der Ordner bereits existiert, wird {@link CorrectionOutcome.Applied} mit einem
|
||||
* entsprechenden Hinweis zurückgegeben (idempotente Ausführung).
|
||||
*
|
||||
* @param suggestion der {@link CorrectionSuggestion.CreateDirectory}-Vorschlag; darf nicht {@code null} sein
|
||||
* @return Ergebnis der Ausführung; nie {@code null}
|
||||
*/
|
||||
CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory suggestion);
|
||||
|
||||
/**
|
||||
* Erzeugt eine neue Prompt-Datei mit einem deutschen Standardinhalt.
|
||||
* <p>
|
||||
* Der Standardinhalt wird von dieser Implementierung bereitgestellt. Die Datei wird
|
||||
* nur erzeugt, wenn sie noch nicht existiert und ihr übergeordneter Ordner beschreibbar ist.
|
||||
* Wenn der Pfad bereits eine Datei enthält, wird {@link CorrectionOutcome.NotAttempted}
|
||||
* zurückgegeben (kein stilless Überschreiben).
|
||||
*
|
||||
* @param suggestion der {@link CorrectionSuggestion.CreatePromptFile}-Vorschlag; darf nicht {@code null} sein
|
||||
* @return Ergebnis der Ausführung; nie {@code null}
|
||||
*/
|
||||
CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile suggestion);
|
||||
|
||||
/**
|
||||
* Bereitet den übergeordneten Ordner einer SQLite-Datei vor, sofern dieser noch nicht
|
||||
* existiert.
|
||||
* <p>
|
||||
* Eine leere SQLite-Datei wird nicht manuell erzeugt; das übernimmt der JDBC-Layer
|
||||
* beim ersten Datenbankzugriff. Diese Methode stellt lediglich sicher, dass der
|
||||
* übergeordnete Ordner vorhanden und schreibbar ist.
|
||||
*
|
||||
* @param suggestion der {@link CorrectionSuggestion.PrepareSqlitePath}-Vorschlag; darf nicht {@code null} sein
|
||||
* @return Ergebnis der Ausführung; nie {@code null}
|
||||
*/
|
||||
CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath suggestion);
|
||||
}
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationFinding;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationReport;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationSeverity;
|
||||
|
||||
/**
|
||||
* Orchestrator für den vollständigen technischen Gesamttest der GUI-Konfiguration.
|
||||
* <p>
|
||||
* Führt alle elf definierten Prüfpunkte in drei voneinander unabhängigen Blöcken aus:
|
||||
* <ol>
|
||||
* <li><strong>Lokale Validierung:</strong> Prüft den Editorzustand ohne I/O mithilfe des
|
||||
* {@link EditorConfigurationValidator}. Erzeugt Ergebnisse für
|
||||
* {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} und
|
||||
* {@link CheckpointId#PROVIDER_CONFIGURATION}.</li>
|
||||
* <li><strong>Pfadprüfungen:</strong> Prüft Quellordner, Zielordner, Prompt-Datei und
|
||||
* SQLite-Pfad über den {@link PathCheckPort}. Erzeugt Ergebnisse für
|
||||
* {@link CheckpointId#PROMPT_FILE_PRESENT}, {@link CheckpointId#SOURCE_FOLDER_PRESENT},
|
||||
* {@link CheckpointId#TARGET_FOLDER_USABLE} und {@link CheckpointId#SQLITE_PATH_USABLE}.</li>
|
||||
* <li><strong>Provider-Prüfungen:</strong> Prüft Endpoint, API-Key, Modellliste und
|
||||
* Modellplausibilität über den {@link ProviderTechnicalTestService}. Erzeugt Ergebnisse für
|
||||
* {@link CheckpointId#BASE_URL_REACHABLE}, {@link CheckpointId#API_KEY_PRESENT},
|
||||
* {@link CheckpointId#API_KEY_ACCEPTED}, {@link CheckpointId#MODEL_LIST_AVAILABLE}
|
||||
* und {@link CheckpointId#SELECTED_MODEL_PLAUSIBLE}.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* <strong>Kein Frühabbruch:</strong> Alle drei Prüfblöcke werden immer vollständig
|
||||
* ausgeführt, auch wenn ein Block eine Exception wirft. In diesem Fall werden die
|
||||
* betroffenen Checkpoints als {@link CheckpointResult.Failure} mit Schweregrad ERROR
|
||||
* und dem Präfix „Interner Fehler:" markiert. Der Gesamtbericht enthält immer genau
|
||||
* elf Einträge.
|
||||
* <p>
|
||||
* <strong>Threading-Kontrakt:</strong> Die Methode {@link #run(TechnicalTestRequest)}
|
||||
* ist synchron blockierend (der Provider-Prüfblock führt HTTP-Aufrufe durch). Sie darf
|
||||
* nicht auf dem JavaFX Application Thread aufgerufen werden. Der Aufrufer ist für die
|
||||
* Worker-Thread-Verwaltung und die Rückführung via {@code Platform.runLater} verantwortlich.
|
||||
* <p>
|
||||
* <strong>Prompt-Datei-Standardpfad:</strong> Wenn der Editorzustand keinen Prompt-Pfad
|
||||
* enthält, leitet der Orchestrator einen Standardpfad aus dem Konfigurationsdateipfad ab
|
||||
* ({@code <config-parent>/prompt.txt}). Ist auch kein Konfigurationsdateipfad gesetzt,
|
||||
* wird {@code config/prompt.txt} relativ zum Arbeitsverzeichnis verwendet.
|
||||
* <p>
|
||||
* Dieser Service enthält keine JavaFX-Typen, keine NIO-Pfadobjekte in Signaturen und
|
||||
* keine Infrastrukturabhängigkeiten jenseits der drei injizierten Abhängigkeiten.
|
||||
*/
|
||||
public class TechnicalTestOrchestrator {
|
||||
|
||||
private final EditorConfigurationValidator editorValidator;
|
||||
private final PathCheckPort pathCheckPort;
|
||||
private final ProviderTechnicalTestService providerTestService;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Orchestrator mit den drei erforderlichen Abhängigkeiten.
|
||||
*
|
||||
* @param editorValidator Lokaler Konfigurationsvalidator; darf nicht {@code null} sein
|
||||
* @param pathCheckPort Port für Dateisystem-Pfadprüfungen; darf nicht {@code null} sein
|
||||
* @param providerTestService Service für provider-nahe technische Prüfungen; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn einer der Parameter {@code null} ist
|
||||
*/
|
||||
public TechnicalTestOrchestrator(EditorConfigurationValidator editorValidator,
|
||||
PathCheckPort pathCheckPort,
|
||||
ProviderTechnicalTestService providerTestService) {
|
||||
this.editorValidator = Objects.requireNonNull(editorValidator, "editorValidator must not be null");
|
||||
this.pathCheckPort = Objects.requireNonNull(pathCheckPort, "pathCheckPort must not be null");
|
||||
this.providerTestService = Objects.requireNonNull(providerTestService, "providerTestService must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt den vollständigen technischen Gesamttest gegen den angegebenen Editorzustand aus.
|
||||
* <p>
|
||||
* Alle drei Prüfblöcke werden immer vollständig ausgeführt. Ein Fehler in einem Block
|
||||
* führt nicht dazu, dass ein anderer Block übersprungen wird. Der zurückgegebene Bericht
|
||||
* enthält immer genau elf {@link CheckpointResult}-Einträge.
|
||||
* <p>
|
||||
* <strong>Prompt-Datei-Standardpfad:</strong> Wenn der Editorzustand keinen Prompt-Pfad
|
||||
* enthält, wird als Standardpfad der Elternordner der Konfigurationsdatei gewählt
|
||||
* (aus {@link TechnicalTestRequest#configFilePath()}), konkret
|
||||
* {@code <config-parent>/prompt.txt}. Falls kein Konfigurationsdateipfad gesetzt ist,
|
||||
* lautet der Fallback {@code config/prompt.txt} relativ zum Arbeitsverzeichnis.
|
||||
* <p>
|
||||
* Wenn der Zielpfad der Prompt-Datei nicht beschreibbar ist, wird keine
|
||||
* {@link CorrectionSuggestion} erzeugt, sondern eine Failure-Meldung mit dem Hinweis,
|
||||
* die Datei manuell anzulegen.
|
||||
* <p>
|
||||
* <strong>Threading-Kontrakt:</strong> Diese Methode blockiert, bis alle Prüfungen
|
||||
* abgeschlossen sind. Sie darf nicht auf dem JavaFX Application Thread aufgerufen werden.
|
||||
*
|
||||
* @param request Eingabedaten für den Gesamttest; darf nicht {@code null} sein
|
||||
* @return vollständiger Gesamttestbericht mit genau elf Einträgen; nie {@code null}
|
||||
* @throws NullPointerException wenn {@code request} {@code null} ist
|
||||
*/
|
||||
public TechnicalTestReport run(TechnicalTestRequest request) {
|
||||
Objects.requireNonNull(request, "request must not be null");
|
||||
Instant startTime = Instant.now();
|
||||
EditorValidationInput input = request.validationInput();
|
||||
|
||||
List<CheckpointResult> results = new ArrayList<>(11);
|
||||
|
||||
// Block 1: Lokale Konfigurationsvalidierung (kein I/O)
|
||||
results.addAll(runLocalValidationBlock(input));
|
||||
|
||||
// Block 2: Pfadprüfungen (Dateisystem-I/O)
|
||||
results.addAll(runPathCheckBlock(input, request.configFilePath()));
|
||||
|
||||
// Block 3: Provider-nahe technische Prüfungen (Netzwerk-I/O)
|
||||
results.addAll(runProviderCheckBlock(input));
|
||||
|
||||
return new TechnicalTestReport(results, startTime);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Block 1: Lokale Konfigurationsvalidierung
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Führt die lokale Konfigurationsvalidierung durch und bildet das Ergebnis auf
|
||||
* {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} und
|
||||
* {@link CheckpointId#PROVIDER_CONFIGURATION} ab.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @return Liste mit genau zwei Einträgen
|
||||
*/
|
||||
private List<CheckpointResult> runLocalValidationBlock(EditorValidationInput input) {
|
||||
try {
|
||||
EditorValidationReport report = editorValidator.validate(input);
|
||||
return mapLocalValidationToCheckpoints(report);
|
||||
} catch (Exception e) {
|
||||
String errorMsg = "Interner Fehler bei der lokalen Konfigurationsvalidierung: " + e.getMessage();
|
||||
return List.of(
|
||||
CheckpointResult.Failure.of(CheckpointId.CONFIGURATION_BASIC_VALIDATION,
|
||||
CheckpointSeverity.ERROR, errorMsg),
|
||||
CheckpointResult.Failure.of(CheckpointId.PROVIDER_CONFIGURATION,
|
||||
CheckpointSeverity.ERROR, errorMsg)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bildet den {@link EditorValidationReport} auf die zwei lokalen Prüfpunkte ab.
|
||||
* <p>
|
||||
* Befunde ohne Feldbezug oder mit allgemeinen Feldbezügen werden
|
||||
* {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} zugeordnet. Provider-spezifische
|
||||
* Feldbefunde werden {@link CheckpointId#PROVIDER_CONFIGURATION} zugeordnet.
|
||||
*
|
||||
* @param report Validierungsergebnis
|
||||
* @return Liste mit genau zwei Einträgen
|
||||
*/
|
||||
private static List<CheckpointResult> mapLocalValidationToCheckpoints(EditorValidationReport report) {
|
||||
// Trennen: allgemeine Befunde vs. provider-spezifische Befunde
|
||||
List<EditorValidationFinding> generalFindings = report.findings().stream()
|
||||
.filter(f -> !isProviderSpecificField(f.fieldKey().orElse("")))
|
||||
.toList();
|
||||
List<EditorValidationFinding> providerFindings = report.findings().stream()
|
||||
.filter(f -> isProviderSpecificField(f.fieldKey().orElse("")))
|
||||
.toList();
|
||||
|
||||
CheckpointResult basicValidation = buildCheckpointFromFindings(
|
||||
CheckpointId.CONFIGURATION_BASIC_VALIDATION,
|
||||
generalFindings,
|
||||
"Konfiguration grundsätzlich gültig.");
|
||||
|
||||
CheckpointResult providerValidation = buildCheckpointFromFindings(
|
||||
CheckpointId.PROVIDER_CONFIGURATION,
|
||||
providerFindings,
|
||||
"Provider-Konfiguration vollständig.");
|
||||
|
||||
return List.of(basicValidation, providerValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Feldschlüssel zu einem provider-spezifischen Feld gehört.
|
||||
*
|
||||
* @param fieldKey Property-Schlüssel
|
||||
* @return {@code true} wenn es ein provider-spezifisches Feld ist
|
||||
*/
|
||||
private static boolean isProviderSpecificField(String fieldKey) {
|
||||
return fieldKey.startsWith("ai.provider.claude.")
|
||||
|| fieldKey.startsWith("ai.provider.openai-compatible.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt ein {@link CheckpointResult} aus einer Liste von Befunden.
|
||||
*
|
||||
* @param id Prüfpunkt-ID
|
||||
* @param findings Liste der relevanten Befunde
|
||||
* @param successMessage Meldung bei leerem Befund-Ergebnis
|
||||
* @return Success bei leerer Befund-Liste, Failure andernfalls
|
||||
*/
|
||||
private static CheckpointResult buildCheckpointFromFindings(CheckpointId id,
|
||||
List<EditorValidationFinding> findings,
|
||||
String successMessage) {
|
||||
if (findings.isEmpty()) {
|
||||
return new CheckpointResult.Success(id, successMessage);
|
||||
}
|
||||
|
||||
// Höchsten Schweregrad bestimmen
|
||||
boolean hasError = findings.stream()
|
||||
.anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
|
||||
CheckpointSeverity severity = hasError ? CheckpointSeverity.ERROR : CheckpointSeverity.WARNING;
|
||||
|
||||
// Befunde zusammenfassen
|
||||
String summary = findings.size() == 1
|
||||
? findings.get(0).message()
|
||||
: findings.size() + " Befunde: " + findings.get(0).message()
|
||||
+ (findings.size() > 1 ? " (und " + (findings.size() - 1) + " weitere)" : "");
|
||||
|
||||
return CheckpointResult.Failure.of(id, severity, summary);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Block 2: Pfadprüfungen
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Führt die Dateisystem-Pfadprüfungen für Prompt-Datei, Quellordner, Zielordner
|
||||
* und SQLite-Pfad durch.
|
||||
* <p>
|
||||
* Der {@code configFilePath} wird genutzt, um bei fehlendem Prompt-Pfad im Editorzustand
|
||||
* einen sinnvollen Standardpfad zu bestimmen ({@code <config-parent>/prompt.txt}).
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @param configFilePath Pfad der geladenen Konfigurationsdatei; leer wenn keine geladen
|
||||
* @return Liste mit genau vier Einträgen
|
||||
*/
|
||||
private List<CheckpointResult> runPathCheckBlock(EditorValidationInput input,
|
||||
String configFilePath) {
|
||||
try {
|
||||
List<CheckpointResult> results = new ArrayList<>(4);
|
||||
results.add(checkPromptFile(input.promptTemplateFile(), configFilePath));
|
||||
results.add(checkSourceFolder(input.sourceFolder()));
|
||||
results.add(checkTargetFolder(input.targetFolder()));
|
||||
results.add(checkSqlitePath(input.sqliteFile()));
|
||||
return results;
|
||||
} catch (Exception e) {
|
||||
String errorMsg = "Interner Fehler bei den Pfadprüfungen: " + e.getMessage();
|
||||
return List.of(
|
||||
CheckpointResult.Failure.of(CheckpointId.PROMPT_FILE_PRESENT,
|
||||
CheckpointSeverity.ERROR, errorMsg),
|
||||
CheckpointResult.Failure.of(CheckpointId.SOURCE_FOLDER_PRESENT,
|
||||
CheckpointSeverity.ERROR, errorMsg),
|
||||
CheckpointResult.Failure.of(CheckpointId.TARGET_FOLDER_USABLE,
|
||||
CheckpointSeverity.ERROR, errorMsg),
|
||||
CheckpointResult.Failure.of(CheckpointId.SQLITE_PATH_USABLE,
|
||||
CheckpointSeverity.ERROR, errorMsg)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft die Prompt-Datei auf Vorhandensein und Lesbarkeit.
|
||||
* <p>
|
||||
* <strong>Pfad-Auflösung:</strong> Wenn der konfigurierte Prompt-Pfad leer ist,
|
||||
* wird ein Standardpfad bestimmt:
|
||||
* <ul>
|
||||
* <li>Wenn {@code configFilePath} gesetzt ist: {@code <configFilePath-Elternordner>/prompt.txt}</li>
|
||||
* <li>Sonst: {@code config/prompt.txt} relativ zum Arbeitsverzeichnis</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <strong>Schreibbarkeits-Prüfung:</strong> Wenn der Zielpfad fehlt, wird geprüft, ob der
|
||||
* Elternordner beschreibbar wäre. Nur dann wird eine {@link CorrectionSuggestion.CreatePromptFile}
|
||||
* angeboten. Ist der Elternordner nicht beschreibbar, wird eine Failure ohne Korrekturvorschlag
|
||||
* zurückgegeben, aber mit einem Hinweis, die Datei manuell anzulegen.
|
||||
*
|
||||
* @param configuredPath konfigurierter Prompt-Pfad aus dem Editorzustand; kann leer sein
|
||||
* @param configFilePath Pfad der geladenen Konfigurationsdatei; leer wenn keine geladen
|
||||
* @return Prüfpunkt-Ergebnis
|
||||
*/
|
||||
private CheckpointResult checkPromptFile(String configuredPath, String configFilePath) {
|
||||
// Effektiven Prompt-Pfad bestimmen
|
||||
String effectivePath = resolvePromptPath(configuredPath, configFilePath);
|
||||
|
||||
if (pathCheckPort.isFileReadable(effectivePath)) {
|
||||
return new CheckpointResult.Success(CheckpointId.PROMPT_FILE_PRESENT,
|
||||
"Prompt-Datei vorhanden und lesbar: " + effectivePath);
|
||||
}
|
||||
|
||||
// Datei fehlt – Elternordner auf Beschreibbarkeit prüfen
|
||||
String parentPath = extractParentPath(effectivePath);
|
||||
boolean parentWritable = !parentPath.isBlank()
|
||||
&& pathCheckPort.isDirectoryWritableOrCreatable(parentPath);
|
||||
|
||||
if (parentWritable) {
|
||||
// Elternordner beschreibbar → Korrekturvorschlag anbieten
|
||||
CorrectionSuggestion suggestion = new CorrectionSuggestion.CreatePromptFile(
|
||||
effectivePath, "Prompt-Datei anlegen: " + effectivePath);
|
||||
return CheckpointResult.Failure.withCorrection(
|
||||
CheckpointId.PROMPT_FILE_PRESENT,
|
||||
CheckpointSeverity.ERROR,
|
||||
"Prompt-Datei nicht vorhanden oder nicht lesbar: " + effectivePath,
|
||||
suggestion);
|
||||
} else {
|
||||
// Elternordner nicht beschreibbar → kein Korrekturvorschlag, nur Hinweis
|
||||
return CheckpointResult.Failure.of(
|
||||
CheckpointId.PROMPT_FILE_PRESENT,
|
||||
CheckpointSeverity.ERROR,
|
||||
"Prompt-Datei fehlt und kann nicht automatisch erzeugt werden. "
|
||||
+ "Bitte manuell anlegen: " + effectivePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt den effektiven Prompt-Pfad aus dem konfigurierten Pfad und dem Konfigurationsdateipfad.
|
||||
* <p>
|
||||
* Wenn der konfigurierte Pfad nicht leer ist, wird dieser unverändert zurückgegeben.
|
||||
* Andernfalls wird ein Standardpfad aus dem Konfigurationsdateipfad abgeleitet:
|
||||
* {@code <configFilePath-Elternordner>/prompt.txt}. Falls auch der Konfigurationsdateipfad
|
||||
* leer ist, lautet der Fallback {@code config/prompt.txt}.
|
||||
*
|
||||
* @param configuredPath konfigurierter Prompt-Pfad; kann leer sein
|
||||
* @param configFilePath Pfad der geladenen Konfigurationsdatei; kann leer sein
|
||||
* @return effektiver Prompt-Pfad; nie {@code null}, nie leer
|
||||
*/
|
||||
static String resolvePromptPath(String configuredPath, String configFilePath) {
|
||||
if (!configuredPath.isBlank()) {
|
||||
return configuredPath;
|
||||
}
|
||||
// Standardpfad aus dem Konfigurationsdatei-Elternordner ableiten
|
||||
if (!configFilePath.isBlank()) {
|
||||
String parent = extractParentPath(configFilePath);
|
||||
if (!parent.isBlank()) {
|
||||
return parent + File.separator + "prompt.txt";
|
||||
}
|
||||
}
|
||||
// Absoluter Fallback
|
||||
return "config" + File.separator + "prompt.txt";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert den Elternpfad aus einem Dateipfad.
|
||||
* <p>
|
||||
* Gibt eine leere Zeichenkette zurück, wenn kein Elternpfad bestimmbar ist.
|
||||
*
|
||||
* @param filePath Dateipfad als String
|
||||
* @return Elternpfad oder leere Zeichenkette
|
||||
*/
|
||||
private static String extractParentPath(String filePath) {
|
||||
if (filePath == null || filePath.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
java.nio.file.Path path = Paths.get(filePath);
|
||||
java.nio.file.Path parent = path.getParent();
|
||||
return parent != null ? parent.toString() : "";
|
||||
} catch (InvalidPathException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft den Quellordner auf Vorhandensein und Lesbarkeit.
|
||||
*
|
||||
* @param path Pfad des Quellordners
|
||||
* @return Prüfpunkt-Ergebnis
|
||||
*/
|
||||
private CheckpointResult checkSourceFolder(String path) {
|
||||
if (path.isBlank()) {
|
||||
return CheckpointResult.Failure.of(CheckpointId.SOURCE_FOLDER_PRESENT,
|
||||
CheckpointSeverity.ERROR, "Quellordner: Kein Pfad konfiguriert.");
|
||||
}
|
||||
if (pathCheckPort.isDirectoryReadable(path)) {
|
||||
return new CheckpointResult.Success(CheckpointId.SOURCE_FOLDER_PRESENT,
|
||||
"Quellordner vorhanden und lesbar: " + path);
|
||||
}
|
||||
return CheckpointResult.Failure.of(CheckpointId.SOURCE_FOLDER_PRESENT,
|
||||
CheckpointSeverity.ERROR,
|
||||
"Quellordner nicht vorhanden oder nicht lesbar: " + path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft den Zielordner auf Vorhandensein oder Anlegbarkeit und Schreibbarkeit.
|
||||
* Bietet eine {@link CorrectionSuggestion.CreateDirectory} an, wenn der Ordner
|
||||
* fehlt, aber anlegbar wäre.
|
||||
*
|
||||
* @param path Pfad des Zielordners
|
||||
* @return Prüfpunkt-Ergebnis
|
||||
*/
|
||||
private CheckpointResult checkTargetFolder(String path) {
|
||||
if (path.isBlank()) {
|
||||
return CheckpointResult.Failure.of(CheckpointId.TARGET_FOLDER_USABLE,
|
||||
CheckpointSeverity.ERROR, "Zielordner: Kein Pfad konfiguriert.");
|
||||
}
|
||||
if (pathCheckPort.isDirectoryWritableOrCreatable(path)) {
|
||||
return new CheckpointResult.Success(CheckpointId.TARGET_FOLDER_USABLE,
|
||||
"Zielordner vorhanden/anlegbar und schreibbar: " + path);
|
||||
}
|
||||
// Ordner ist weder vorhanden/schreibbar noch anlegbar
|
||||
// Wenn der Ordner fehlt, könnte isDirectoryWritableOrCreatable false liefern weil
|
||||
// auch der Elternpfad fehlt. Trotzdem einen Korrekturvorschlag anbieten.
|
||||
CorrectionSuggestion suggestion = new CorrectionSuggestion.CreateDirectory(
|
||||
path, "Zielordner anlegen: " + path);
|
||||
return CheckpointResult.Failure.withCorrection(
|
||||
CheckpointId.TARGET_FOLDER_USABLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
"Zielordner nicht vorhanden oder nicht schreibbar: " + path,
|
||||
suggestion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob der SQLite-Pfad technisch nutzbar ist.
|
||||
* Bietet eine {@link CorrectionSuggestion.PrepareSqlitePath} an, wenn der Pfad
|
||||
* noch nicht nutzbar, aber vorbereitbar wäre.
|
||||
*
|
||||
* @param path Pfad der SQLite-Datei
|
||||
* @return Prüfpunkt-Ergebnis
|
||||
*/
|
||||
private CheckpointResult checkSqlitePath(String path) {
|
||||
if (path.isBlank()) {
|
||||
return CheckpointResult.Failure.of(CheckpointId.SQLITE_PATH_USABLE,
|
||||
CheckpointSeverity.ERROR, "SQLite-Pfad: Kein Pfad konfiguriert.");
|
||||
}
|
||||
if (pathCheckPort.isSqlitePathUsable(path)) {
|
||||
return new CheckpointResult.Success(CheckpointId.SQLITE_PATH_USABLE,
|
||||
"SQLite-Pfad technisch nutzbar: " + path);
|
||||
}
|
||||
CorrectionSuggestion suggestion = new CorrectionSuggestion.PrepareSqlitePath(
|
||||
path, "SQLite-Pfad vorbereiten: " + path);
|
||||
return CheckpointResult.Failure.withCorrection(
|
||||
CheckpointId.SQLITE_PATH_USABLE,
|
||||
CheckpointSeverity.ERROR,
|
||||
"SQLite-Pfad nicht nutzbar: " + path,
|
||||
suggestion);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Block 3: Provider-nahe technische Prüfungen
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Führt die provider-nahen technischen Prüfungen über den {@link ProviderTechnicalTestService} aus.
|
||||
* <p>
|
||||
* Der Service liefert genau fünf Ergebnisse in der Reihenfolge:
|
||||
* API_KEY_PRESENT, BASE_URL_REACHABLE, API_KEY_ACCEPTED, MODEL_LIST_AVAILABLE, SELECTED_MODEL_PLAUSIBLE.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @return Liste mit genau fünf Einträgen
|
||||
*/
|
||||
private List<CheckpointResult> runProviderCheckBlock(EditorValidationInput input) {
|
||||
try {
|
||||
return providerTestService.runProviderChecks(input);
|
||||
} catch (Exception e) {
|
||||
String errorMsg = "Interner Fehler bei den Provider-Prüfungen: " + e.getMessage();
|
||||
return List.of(
|
||||
CheckpointResult.Failure.of(CheckpointId.API_KEY_PRESENT,
|
||||
CheckpointSeverity.ERROR, errorMsg),
|
||||
CheckpointResult.Failure.of(CheckpointId.BASE_URL_REACHABLE,
|
||||
CheckpointSeverity.ERROR, errorMsg),
|
||||
CheckpointResult.Failure.of(CheckpointId.API_KEY_ACCEPTED,
|
||||
CheckpointSeverity.ERROR, errorMsg),
|
||||
CheckpointResult.Failure.of(CheckpointId.MODEL_LIST_AVAILABLE,
|
||||
CheckpointSeverity.ERROR, errorMsg),
|
||||
CheckpointResult.Failure.of(CheckpointId.SELECTED_MODEL_PLAUSIBLE,
|
||||
CheckpointSeverity.ERROR, errorMsg)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis eines vollständigen technischen Gesamttests.
|
||||
* <p>
|
||||
* Enthält die {@link CheckpointResult}-Einträge aller durchlaufenen Prüfpunkte in
|
||||
* Ausführungsreihenfolge. Jeder definierte Prüfpunkt ist vertreten – entweder als
|
||||
* {@link CheckpointResult.Success}, {@link CheckpointResult.Failure} oder
|
||||
* {@link CheckpointResult.NotApplicable}.
|
||||
* <p>
|
||||
* Der Gesamttest bricht bei einem Fehler <em>nicht</em> ab; alle Prüfpunkte werden
|
||||
* vollständig durchlaufen.
|
||||
* <p>
|
||||
* Dieser Record ist immutable und enthält keine JavaFX-Typen.
|
||||
*
|
||||
* @param results Prüfpunkt-Ergebnisse in Ausführungsreihenfolge; nie {@code null}
|
||||
* @param evaluatedAt Zeitpunkt, zu dem der Gesamttest gestartet wurde; nie {@code null}
|
||||
*/
|
||||
public record TechnicalTestReport(
|
||||
List<CheckpointResult> results,
|
||||
Instant evaluatedAt) {
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Gesamttestbericht.
|
||||
*
|
||||
* @param results Ergebnisliste; darf nicht {@code null} sein
|
||||
* @param evaluatedAt Startzeitpunkt des Tests; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
*/
|
||||
public TechnicalTestReport {
|
||||
Objects.requireNonNull(results, "results must not be null");
|
||||
Objects.requireNonNull(evaluatedAt, "evaluatedAt must not be null");
|
||||
results = List.copyOf(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob mindestens ein Prüfpunkt mit Schweregrad {@link CheckpointSeverity#ERROR}
|
||||
* gescheitert ist.
|
||||
* <p>
|
||||
* Wenn {@code true}, gilt die Konfiguration im aktuellen Zustand als nicht lauffähig.
|
||||
*
|
||||
* @return {@code true} wenn mindestens ein Fehler-Prüfpunkt vorliegt
|
||||
*/
|
||||
public boolean hasErrors() {
|
||||
return results.stream()
|
||||
.filter(r -> r instanceof CheckpointResult.Failure)
|
||||
.map(r -> (CheckpointResult.Failure) r)
|
||||
.anyMatch(f -> f.severity() == CheckpointSeverity.ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob mindestens ein Prüfpunkt mit Schweregrad {@link CheckpointSeverity#WARNING}
|
||||
* gescheitert ist.
|
||||
*
|
||||
* @return {@code true} wenn mindestens ein Warn-Prüfpunkt vorliegt
|
||||
*/
|
||||
public boolean hasWarnings() {
|
||||
return results.stream()
|
||||
.filter(r -> r instanceof CheckpointResult.Failure)
|
||||
.map(r -> (CheckpointResult.Failure) r)
|
||||
.anyMatch(f -> f.severity() == CheckpointSeverity.WARNING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob mindestens ein Prüfpunkt einen {@link CorrectionSuggestion} enthält.
|
||||
* <p>
|
||||
* Wenn {@code true}, kann aus diesem Bericht ein nicht leerer {@link CorrectionPlan}
|
||||
* abgeleitet werden.
|
||||
*
|
||||
* @return {@code true} wenn mindestens ein korrigierbarer Befund vorliegt
|
||||
*/
|
||||
public boolean hasCorrectableFindings() {
|
||||
return results.stream()
|
||||
.filter(r -> r instanceof CheckpointResult.Failure)
|
||||
.map(r -> (CheckpointResult.Failure) r)
|
||||
.anyMatch(CheckpointResult.Failure::hasCorrectionSuggestion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leitet einen {@link CorrectionPlan} aus den korrigierbaren Prüfpunkt-Fehlern ab.
|
||||
* <p>
|
||||
* Enthält alle {@link CorrectionSuggestion}-Einträge der gescheiterten Prüfpunkte
|
||||
* in Berichtsreihenfolge.
|
||||
*
|
||||
* @return abgeleiteter Korrekturplan; nie {@code null}; leer wenn keine Korrekturen möglich sind
|
||||
*/
|
||||
public CorrectionPlan deriveCorrectionPlan() {
|
||||
List<CorrectionSuggestion> suggestions = results.stream()
|
||||
.filter(r -> r instanceof CheckpointResult.Failure)
|
||||
.map(r -> (CheckpointResult.Failure) r)
|
||||
.filter(CheckpointResult.Failure::hasCorrectionSuggestion)
|
||||
.map(f -> f.correctionSuggestion().orElseThrow())
|
||||
.toList();
|
||||
return new CorrectionPlan(suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Gesamtzahl der Prüfpunkt-Ergebnisse zurück.
|
||||
*
|
||||
* @return Anzahl der Ergebnisse; nie negativ
|
||||
*/
|
||||
public int size() {
|
||||
return results.size();
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
|
||||
|
||||
/**
|
||||
* Eingabedaten für einen vollständigen technischen Gesamttest der GUI-Konfiguration.
|
||||
* <p>
|
||||
* Enthält den aktuellen Editorzustand als {@link EditorValidationInput} (alle String-Werte
|
||||
* so wie sie im Editor vorliegen). Der technische Gesamttest arbeitet ausschließlich auf
|
||||
* diesen Werten; er liest keine Konfigurationsdatei vom Dateisystem und speichert nichts.
|
||||
* <p>
|
||||
* Der optionale Pfad zur Konfigurationsdatei ({@code configFilePath}) ermöglicht es dem
|
||||
* Gesamttest, bei der automatischen Prompt-Erzeugung den Standardpfad relativ zur
|
||||
* Konfigurationsdatei zu bestimmen. Er ist leer, wenn keine Konfigurationsdatei geladen ist.
|
||||
* <p>
|
||||
* Dieser Record enthält keine JavaFX-Typen und keine Infrastrukturabhängigkeiten.
|
||||
*
|
||||
* @param validationInput aktueller Editorzustand; nie {@code null}
|
||||
* @param configFilePath optionaler Pfad der geladenen Konfigurationsdatei als String;
|
||||
* leer wenn keine Datei geladen ist
|
||||
*/
|
||||
public record TechnicalTestRequest(
|
||||
EditorValidationInput validationInput,
|
||||
String configFilePath) {
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Gesamttest-Anforderung.
|
||||
*
|
||||
* @param validationInput aktueller Editorzustand; darf nicht {@code null} sein
|
||||
* @param configFilePath Pfad der Konfigurationsdatei; {@code null} wird zu leerem String
|
||||
* @throws NullPointerException wenn {@code validationInput} {@code null} ist
|
||||
*/
|
||||
public TechnicalTestRequest {
|
||||
Objects.requireNonNull(validationInput, "validationInput must not be null");
|
||||
configFilePath = configFilePath == null ? "" : configFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Anforderung ohne geladene Konfigurationsdatei.
|
||||
*
|
||||
* @param validationInput aktueller Editorzustand; darf nicht {@code null} sein
|
||||
* @return eine neue Anforderung ohne Konfigurationsdateipfad
|
||||
*/
|
||||
public static TechnicalTestRequest of(EditorValidationInput validationInput) {
|
||||
return new TechnicalTestRequest(validationInput, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob ein Konfigurationsdateipfad gesetzt ist.
|
||||
*
|
||||
* @return {@code true} wenn ein nicht leerer Pfad vorhanden ist
|
||||
*/
|
||||
public boolean hasConfigFilePath() {
|
||||
return !configFilePath.isBlank();
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Typen und Port-Verträge für den technischen Gesamttest der GUI-Konfiguration.
|
||||
* <p>
|
||||
* Dieses Package enthält ausschließlich:
|
||||
* <ul>
|
||||
* <li>Eingabe- und Ergebnismodelle für den vollständigen Gesamttest ({@code TechnicalTestRequest},
|
||||
* {@code TechnicalTestReport}, {@code CheckpointResult}, {@code CheckpointId})</li>
|
||||
* <li>Korrekturmodelle für schreibende Korrekturhilfen ({@code CorrectionSuggestion},
|
||||
* {@code CorrectionPlan}, {@code CorrectionOutcome}, {@code CorrectionExecutionReport})</li>
|
||||
* <li>Outbound-Port-Verträge für Pfadprüfungen ({@code PathCheckPort}) und schreibende
|
||||
* Korrekturen ({@code ResourceCreationPort})</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Abgrenzungen:
|
||||
* <ul>
|
||||
* <li>Dieses Package enthält <strong>keine</strong> konkreten Implementierungen; diese
|
||||
* leben im Adapter-Out-Modul.</li>
|
||||
* <li>Keine JavaFX-, NIO-Framework-, HTTP- oder JDBC-Bibliothekstypen. Standard-JDK-Typen
|
||||
* wie {@code java.nio.file.Path} sind ebenfalls nicht in Port-Signaturen erlaubt;
|
||||
* die Ports verwenden {@code String} als plattformneutralen Pfadtyp.</li>
|
||||
* <li>Die Gesamttest-Orchestrierung und die Bestätigungslogik liegen in späteren
|
||||
* Arbeitspaketen; dieses Package definiert nur die Verträge.</li>
|
||||
* <li>Die automatische Hintergrundvalidierung (Öffnen/Bearbeiten) sowie die explizite
|
||||
* Aktion „Validieren" (nicht schreibend, lokal) sind im Package
|
||||
* {@code validation.editor} definiert und bleiben dort unverändert.</li>
|
||||
* </ul>
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||
Reference in New Issue
Block a user