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:
+168
-8
@@ -29,6 +29,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiVisibleProviderSectio
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
|
||||
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;
|
||||
@@ -116,6 +117,18 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
private final Button openButton = new Button("Öffnen");
|
||||
private final Button saveButton = new Button("Speichern");
|
||||
private final Button saveAsButton = new Button("Speichern unter");
|
||||
/**
|
||||
* The "Validieren" button in the "Tests" section.
|
||||
* Package-private to allow CSS-ID and state assertions in smoke tests.
|
||||
*/
|
||||
final Button validateButton = new Button("Validieren");
|
||||
|
||||
/**
|
||||
* The "Technische Tests ausführen" button in the "Tests" section.
|
||||
* Disabled while a test run is in progress to prevent concurrent runs.
|
||||
* Package-private to allow CSS-ID and state assertions in smoke tests.
|
||||
*/
|
||||
final Button technicalTestsButton = new Button("Technische Tests ausführen");
|
||||
|
||||
private static final Path DEFAULT_SAVE_PATH = Paths.get("config/application.properties");
|
||||
|
||||
@@ -226,6 +239,19 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
*/
|
||||
final GuiModelCatalogCoordinator modelCatalogCoordinator;
|
||||
|
||||
/**
|
||||
* Coordinator that manages asynchronous execution of the "Technische Tests ausführen" action.
|
||||
* Package-private to allow thread-factory substitution in smoke tests.
|
||||
*/
|
||||
final GuiTechnicalTestCoordinator technicalTestCoordinator;
|
||||
|
||||
/**
|
||||
* Coordinator that manages the correction confirmation dialog and asynchronous execution
|
||||
* of corrective actions after a technical test run.
|
||||
* Package-private to allow dialog-supplier substitution in smoke tests.
|
||||
*/
|
||||
final GuiCorrectionDialogCoordinator correctionDialogCoordinator;
|
||||
|
||||
/**
|
||||
* Mutable list of pending message entries produced by the model catalogue coordinator
|
||||
* and the editor validation. Entries are appended on the JavaFX Application Thread.
|
||||
@@ -302,6 +328,25 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
effectiveContext.modelCatalogPort(), pendingMessages);
|
||||
this.modelCatalogCoordinator.postResultCallback = this::refreshAfterValidation;
|
||||
|
||||
CorrectionExecutionService correctionService = effectiveContext.correctionExecutionService();
|
||||
this.correctionDialogCoordinator = new GuiCorrectionDialogCoordinator(
|
||||
correctionService,
|
||||
pendingMessages,
|
||||
ignored -> refreshAfterValidation());
|
||||
|
||||
this.technicalTestCoordinator = new GuiTechnicalTestCoordinator(
|
||||
effectiveContext.technicalTestOrchestrator(),
|
||||
this::buildValidationInput,
|
||||
() -> editorState.loadedFileSnapshot()
|
||||
.map(snapshot -> snapshot.filePath().toString())
|
||||
.orElse(""),
|
||||
pendingMessages,
|
||||
report -> {
|
||||
technicalTestsButton.setDisable(false);
|
||||
refreshAfterValidation();
|
||||
correctionDialogCoordinator.offerCorrections(report);
|
||||
});
|
||||
|
||||
this.unsavedChangesGuard = new GuiUnsavedChangesGuard(
|
||||
triggerLabel -> showUnsavedChangesDialog(triggerLabel));
|
||||
|
||||
@@ -1363,14 +1408,56 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
// Tests / Meldungen sections (structural placeholders)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Builds the "Tests" section containing the explicit validation action button.
|
||||
* <p>
|
||||
* The "Validieren" button triggers a full in-memory validation of the current editor
|
||||
* state without writing anything to disk. It produces the same set of local findings
|
||||
* as the automatic background validation, but additionally appends an INFO message to
|
||||
* the central message area that confirms the action was executed and reports the number
|
||||
* of findings found.
|
||||
* <p>
|
||||
* Later technical overall-check actions (e.g. provider endpoint tests, path checks) will
|
||||
* be added to this section as separate buttons in subsequent iterations. The validation
|
||||
* action itself remains local and non-writing at all times.
|
||||
*
|
||||
* @return the card node for the "Tests" section
|
||||
*/
|
||||
private Node createTestsSection() {
|
||||
VBox card = createCardContainer();
|
||||
|
||||
validateButton.setId("validate-button");
|
||||
validateButton.setOnAction(e -> runValidationAction());
|
||||
|
||||
technicalTestsButton.setId("technical-tests-button");
|
||||
technicalTestsButton.setOnAction(e -> runTechnicalTestsAction());
|
||||
|
||||
HBox buttonRow = new HBox(8, validateButton, technicalTestsButton);
|
||||
buttonRow.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
card.getChildren().addAll(
|
||||
sectionTitle("Tests"),
|
||||
textLabel("Technische Tests und Diagnoseaktionen werden in einem späteren Schritt ergänzt."));
|
||||
buttonRow);
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the "Technische Tests ausführen" action triggered by the user.
|
||||
* <p>
|
||||
* Disables the button for the duration of the test run to prevent concurrent starts.
|
||||
* Delegates to {@link GuiTechnicalTestCoordinator#triggerTechnicalTests()}, which
|
||||
* reads the current editor state, starts a background worker thread, and re-enables
|
||||
* the button after the result has been applied to the message area.
|
||||
* <p>
|
||||
* This method does not write anything to disk and does not execute any corrective actions.
|
||||
* Must be called on the FX Application Thread.
|
||||
*/
|
||||
private void runTechnicalTestsAction() {
|
||||
LOG.info("Aktion Technische Tests ausführen gestartet.");
|
||||
technicalTestsButton.setDisable(true);
|
||||
technicalTestCoordinator.triggerTechnicalTests();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the "Meldungen" section containing the central message area.
|
||||
* <p>
|
||||
@@ -1449,16 +1536,22 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Runs the editor validation against the current editor state and stores the result
|
||||
* in {@link #lastValidationResult}, {@link #pendingMessages} and {@link #pendingFieldFindings}.
|
||||
* Builds an {@link EditorValidationInput} from the current editor state.
|
||||
* <p>
|
||||
* This method is pure in-memory (no I/O) and may be called on the JavaFX Application Thread.
|
||||
* Path existence checks, SQLite roundtrips and network calls are explicitly excluded from
|
||||
* this validation and belong to later technical overall checks.
|
||||
* Reads the active provider family and per-provider fields from the current
|
||||
* {@link GuiConfigurationValues}, resolves API key descriptors via the injected
|
||||
* {@link ApiKeyResolutionPort}, and returns the assembled input. This method is
|
||||
* pure in-memory (no I/O) and may be called on the JavaFX Application Thread.
|
||||
* <p>
|
||||
* This method is used both by the synchronous local validation and by the asynchronous
|
||||
* technical overall test to extract the current GUI state before handing it to a
|
||||
* background worker thread.
|
||||
* <p>
|
||||
* Must be called on the FX Application Thread.
|
||||
*
|
||||
* @return assembled validation input representing the current editor state; never {@code null}
|
||||
*/
|
||||
private void runEditorValidation() {
|
||||
EditorValidationInput buildValidationInput() {
|
||||
GuiConfigurationValues values = editorState.values();
|
||||
GuiProviderConfigurationState claudeState = Optional.ofNullable(
|
||||
values.providerConfiguration(AiProviderFamily.CLAUDE))
|
||||
@@ -1474,7 +1567,7 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
apiKeyResolutionPort.resolve(AiProviderFamily.OPENAI_COMPATIBLE,
|
||||
openaiState.apiKey().propertyValue());
|
||||
|
||||
EditorValidationInput input = new EditorValidationInput(
|
||||
return new EditorValidationInput(
|
||||
values.activeProviderFamily(),
|
||||
values.sourceFolder(),
|
||||
values.targetFolder(),
|
||||
@@ -1491,7 +1584,20 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
openaiState.model(),
|
||||
openaiState.timeoutSeconds(),
|
||||
openaiKeyDescriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the editor validation against the current editor state and stores the result
|
||||
* in {@link #lastValidationResult}, {@link #pendingMessages} and {@link #pendingFieldFindings}.
|
||||
* <p>
|
||||
* This method is pure in-memory (no I/O) and may be called on the JavaFX Application Thread.
|
||||
* Path existence checks, SQLite roundtrips and network calls are explicitly excluded from
|
||||
* this validation and belong to later technical overall checks.
|
||||
* <p>
|
||||
* Must be called on the FX Application Thread.
|
||||
*/
|
||||
private void runEditorValidation() {
|
||||
EditorValidationInput input = buildValidationInput();
|
||||
EditorValidationReport report = editorValidator.validate(input);
|
||||
|
||||
// Translate to GUI types
|
||||
@@ -1523,6 +1629,60 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
refreshAfterValidation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the explicit "Validieren" action triggered by the user.
|
||||
* <p>
|
||||
* This method re-runs the same in-memory validation logic as the automatic background
|
||||
* check ({@link #runEditorValidation()}) against the current editor state, without
|
||||
* writing anything to disk or making any network or filesystem calls. It is therefore
|
||||
* safe to call on the FX Application Thread at any time.
|
||||
* <p>
|
||||
* In addition to updating the findings shown by the automatic validation, this action
|
||||
* appends a dedicated INFO message to the central message area to confirm to the user
|
||||
* that the action was explicitly executed and to report the number of findings found.
|
||||
* The message uses a distinct source tag so that it can be replaced on subsequent
|
||||
* executions without removing messages from other sources.
|
||||
* <p>
|
||||
* Differences from the automatic background validation:
|
||||
* <ul>
|
||||
* <li>This method is user-initiated and always produces the confirmation INFO message.</li>
|
||||
* <li>The automatic validation runs silently on field changes and file loads.</li>
|
||||
* <li>Remote checks (provider endpoint, API key acceptance, model list) are not part of
|
||||
* this action; they belong to the technical overall-check action.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Must be called on the FX Application Thread.
|
||||
*/
|
||||
private void runValidationAction() {
|
||||
LOG.info("Aktion Validieren ausgeführt.");
|
||||
|
||||
// Re-run in-memory validation; this updates pendingMessages, pendingFieldFindings
|
||||
// and lastValidationResult identically to the automatic background check.
|
||||
runEditorValidation();
|
||||
|
||||
// Replace any previous action-confirmation message; preserve all other messages.
|
||||
pendingMessages.removeIf(m -> m.source().isPresent()
|
||||
&& "Validierung-Aktion".equals(m.source().get()));
|
||||
|
||||
int findingCount = lastValidationResult.fieldFindings().size();
|
||||
String confirmationText;
|
||||
if (findingCount == 0) {
|
||||
confirmationText = "Aktion Validieren wurde ausgeführt. Keine Befunde.";
|
||||
} else {
|
||||
confirmationText = "Aktion Validieren wurde ausgeführt. "
|
||||
+ findingCount + " Befund" + (findingCount == 1 ? "" : "e") + " gefunden.";
|
||||
}
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
GuiMessageSeverity.INFO, confirmationText, "Validierung-Aktion"));
|
||||
|
||||
lastValidationResult = new GuiEditorValidationResult(
|
||||
List.copyOf(pendingMessages),
|
||||
List.copyOf(pendingFieldFindings),
|
||||
java.time.Instant.now());
|
||||
|
||||
refreshAfterValidation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an {@link EditorValidationSeverity} to the corresponding {@link GuiMessageSeverity}.
|
||||
*
|
||||
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.ConfirmationDialogContent;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionReport;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionPlan;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
|
||||
import javafx.application.Platform;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Koordiniert den gesammelten Bestätigungsdialog und die anschließende Ausführung
|
||||
* schreibender Korrekturmaßnahmen nach einem technischen Gesamttest.
|
||||
* <p>
|
||||
* Der Koordinator empfängt einen {@link TechnicalTestReport}, prüft ob korrigierbare
|
||||
* Befunde vorliegen, leitet daraus einen {@link CorrectionPlan} ab, zeigt dem Benutzer
|
||||
* einen gesammelten Bestätigungsdialog und führt die Korrekturen bei Bestätigung über
|
||||
* den {@link CorrectionExecutionService} auf einem Hintergrund-Worker-Thread aus.
|
||||
* Ergebnisse werden in die geteilte {@code pendingMessages}-Liste eingehängt.
|
||||
*
|
||||
* <h2>Ablauf</h2>
|
||||
* <ol>
|
||||
* <li>Bericht erhält: prüfen ob {@code hasCorrectableFindings()}.</li>
|
||||
* <li>Wenn keine korrigierbaren Befunde: kein Dialog, keine Aktion.</li>
|
||||
* <li>Wenn korrigierbare Befunde: {@link CorrectionPlan} ableiten.</li>
|
||||
* <li>Dialog auf FX-Thread anzeigen.</li>
|
||||
* <li>Bei Bestätigung: Korrekturen auf Worker-Thread ausführen.</li>
|
||||
* <li>Ergebnisse via {@code Platform.runLater} auf FX-Thread zurückführen.</li>
|
||||
* <li>Meldungen in {@code pendingMessages} einhängen (Replace-Semantik).</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Threading-Kontrakt</h2>
|
||||
* <p>
|
||||
* {@link #offerCorrections(TechnicalTestReport)} muss auf dem JavaFX Application Thread
|
||||
* aufgerufen werden. I/O (Ausführung der Korrekturen) läuft auf einem dedizierten
|
||||
* Daemon-Hintergrund-Thread. UI-Updates erfolgen ausschließlich via
|
||||
* {@code Platform.runLater}.
|
||||
*
|
||||
* <h2>Keine stillen Korrekturen</h2>
|
||||
* <p>
|
||||
* Ohne ausdrückliche Benutzerbestätigung werden keine schreibenden Änderungen ausgeführt.
|
||||
* Bei Dialog-Abbruch bleibt der Zustand unverändert.
|
||||
*
|
||||
* <h2>Anti-Scope</h2>
|
||||
* <p>
|
||||
* Dieser Koordinator führt keine Provider-nahen Korrekturen, keine Änderung fachlich
|
||||
* riskanter Werte und keine automatischen Laufstarts durch.
|
||||
*/
|
||||
public final class GuiCorrectionDialogCoordinator {
|
||||
|
||||
/** Quell-Tag für Einträge in {@code pendingMessages}, die von diesem Koordinator stammen. */
|
||||
static final String SOURCE_TAG = "Korrekturen";
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(GuiCorrectionDialogCoordinator.class);
|
||||
|
||||
private final CorrectionExecutionService correctionExecutionService;
|
||||
private final List<GuiMessageEntry> pendingMessages;
|
||||
private final Consumer<Void> refreshCallback;
|
||||
|
||||
/**
|
||||
* Funktion, die dem Benutzer den Bestätigungsdialog zeigt.
|
||||
* <p>
|
||||
* Erhält den {@link ConfirmationDialogContent} und gibt {@code true} zurück, wenn der
|
||||
* Benutzer „Fortfahren" wählt, andernfalls {@code false}. Standard: echter Alert.
|
||||
* Paket-privat für Test-Substitution.
|
||||
*/
|
||||
Function<ConfirmationDialogContent, Boolean> dialogSupplier;
|
||||
|
||||
/**
|
||||
* Factory für den Hintergrund-Worker-Thread.
|
||||
* Standard: Daemon-Thread namens {@code gui-correction-worker}.
|
||||
* Paket-privat für Test-Substitution.
|
||||
*/
|
||||
Function<Runnable, Thread> correctionThreadFactory;
|
||||
|
||||
/**
|
||||
* Verbraucher zur Rückführung des Ergebnisses auf den FX-Thread.
|
||||
* Standard: {@code Platform.runLater}. Paket-privat für Test-Substitution.
|
||||
*/
|
||||
java.util.function.Consumer<Runnable> resultDelivery;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Koordinator.
|
||||
*
|
||||
* @param correctionExecutionService Service für die Ausführung von Korrekturen; darf nicht {@code null} sein
|
||||
* @param pendingMessages geteilte veränderliche Nachrichtenliste; darf nicht {@code null} sein
|
||||
* @param refreshCallback Callback nach Anwendung der Ergebnisse (z. B. View-Aktualisierung);
|
||||
* darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn einer der Parameter {@code null} ist
|
||||
*/
|
||||
public GuiCorrectionDialogCoordinator(CorrectionExecutionService correctionExecutionService,
|
||||
List<GuiMessageEntry> pendingMessages,
|
||||
Consumer<Void> refreshCallback) {
|
||||
this.correctionExecutionService = Objects.requireNonNull(correctionExecutionService,
|
||||
"correctionExecutionService must not be null");
|
||||
this.pendingMessages = Objects.requireNonNull(pendingMessages,
|
||||
"pendingMessages must not be null");
|
||||
this.refreshCallback = Objects.requireNonNull(refreshCallback,
|
||||
"refreshCallback must not be null");
|
||||
|
||||
this.dialogSupplier = this::showConfirmationDialog;
|
||||
this.correctionThreadFactory = task -> {
|
||||
Thread t = new Thread(task, "gui-correction-worker");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
};
|
||||
this.resultDelivery = Platform::runLater;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft den Bericht auf korrigierbare Befunde, zeigt bei Bedarf den Bestätigungsdialog
|
||||
* und führt die Korrekturen nach Bestätigung asynchron aus.
|
||||
* <p>
|
||||
* Wenn der Bericht keine korrigierbaren Befunde enthält ({@code hasCorrectableFindings()
|
||||
* == false}), wird kein Dialog angezeigt und keine Aktion ausgeführt.
|
||||
* <p>
|
||||
* Muss auf dem JavaFX Application Thread aufgerufen werden.
|
||||
*
|
||||
* @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
|
||||
*/
|
||||
public void offerCorrections(TechnicalTestReport report) {
|
||||
Objects.requireNonNull(report, "report must not be null");
|
||||
|
||||
if (!report.hasCorrectableFindings()) {
|
||||
LOG.debug("Gesamttest: Keine korrigierbaren Befunde – kein Bestätigungsdialog.");
|
||||
return;
|
||||
}
|
||||
|
||||
CorrectionPlan plan = report.deriveCorrectionPlan();
|
||||
if (!plan.hasCorrections()) {
|
||||
LOG.debug("Gesamttest: Korrekturplan ist leer – kein Bestätigungsdialog.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Gesamttest: {} korrigierbare Befunde. Bestätigungsdialog wird angezeigt.", plan.size());
|
||||
|
||||
ConfirmationDialogContent dialogContent = ConfirmationDialogContent.fromPlan(plan);
|
||||
boolean confirmed = dialogSupplier.apply(dialogContent);
|
||||
|
||||
if (!confirmed) {
|
||||
LOG.info("Bestätigungsdialog: Benutzer hat Korrekturen abgelehnt. Keine Änderungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Bestätigungsdialog: Benutzer hat Korrekturen bestätigt. Ausführung startet.");
|
||||
Runnable task = () -> {
|
||||
CorrectionExecutionReport executionReport = correctionExecutionService.execute(plan);
|
||||
resultDelivery.accept(() -> {
|
||||
applyResult(executionReport);
|
||||
refreshCallback.accept(null);
|
||||
});
|
||||
};
|
||||
|
||||
Thread worker = correctionThreadFactory.apply(task);
|
||||
worker.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet das Ergebnis der Korrekturausführung auf die geteilte Nachrichtenliste an.
|
||||
* <p>
|
||||
* Entfernt alle vorherigen Einträge mit Quelle {@link #SOURCE_TAG} und fügt für jedes
|
||||
* {@link CorrectionOutcome} einen neuen Eintrag hinzu. Zusätzlich wird eine Zusammenfassung
|
||||
* angehängt.
|
||||
* <p>
|
||||
* Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
|
||||
*
|
||||
* @param report das Ausführungsergebnis; darf nicht {@code null} sein
|
||||
*/
|
||||
private void applyResult(CorrectionExecutionReport report) {
|
||||
// Alte Einträge mit Source-Tag entfernen (Replace-Semantik)
|
||||
pendingMessages.removeIf(msg -> SOURCE_TAG.equals(msg.source().orElse("")));
|
||||
|
||||
long appliedCount = 0;
|
||||
long failedCount = 0;
|
||||
long notAttemptedCount = 0;
|
||||
|
||||
for (CorrectionOutcome outcome : report.outcomes()) {
|
||||
switch (outcome) {
|
||||
case CorrectionOutcome.Applied applied -> {
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
GuiMessageSeverity.INFO,
|
||||
"Korrektur angewendet: " + applied.suggestion().descriptionForUser()
|
||||
+ " – " + applied.message(),
|
||||
SOURCE_TAG));
|
||||
appliedCount++;
|
||||
LOG.info("Korrektur angewendet: {} – {}", applied.suggestion().descriptionForUser(),
|
||||
applied.message());
|
||||
}
|
||||
case CorrectionOutcome.Failed failed -> {
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
GuiMessageSeverity.ERROR,
|
||||
"Korrektur fehlgeschlagen: " + failed.suggestion().descriptionForUser()
|
||||
+ " (" + failed.errorMessage() + ")",
|
||||
SOURCE_TAG));
|
||||
failedCount++;
|
||||
LOG.warn("Korrektur fehlgeschlagen: {} – {}", failed.suggestion().descriptionForUser(),
|
||||
failed.errorMessage());
|
||||
}
|
||||
case CorrectionOutcome.NotAttempted notAttempted -> {
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
GuiMessageSeverity.HINT,
|
||||
"Korrektur nicht durchgeführt: " + notAttempted.suggestion().descriptionForUser()
|
||||
+ " – " + notAttempted.reason(),
|
||||
SOURCE_TAG));
|
||||
notAttemptedCount++;
|
||||
LOG.info("Korrektur nicht durchgeführt: {} – {}", notAttempted.suggestion().descriptionForUser(),
|
||||
notAttempted.reason());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zusammenfassung
|
||||
String summary = "Korrekturausführung abgeschlossen: "
|
||||
+ appliedCount + " angewendet, "
|
||||
+ failedCount + " fehlgeschlagen, "
|
||||
+ notAttemptedCount + " nicht versucht.";
|
||||
pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, summary, SOURCE_TAG));
|
||||
LOG.info("Korrekturausführung abgeschlossen: {} angewendet, {} fehlgeschlagen, {} nicht versucht.",
|
||||
appliedCount, failedCount, notAttemptedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt den echten JavaFX-Bestätigungsdialog und gibt die Benutzerentscheidung zurück.
|
||||
* <p>
|
||||
* Muss auf dem JavaFX Application Thread aufgerufen werden. Standard-Fokus liegt auf
|
||||
* dem Abbrechen-Button (kein versehentliches Bestätigen durch Enter).
|
||||
*
|
||||
* @param content der Dialoginhalt; darf nicht {@code null} sein
|
||||
* @return {@code true} wenn der Benutzer „Fortfahren" wählt, sonst {@code false}
|
||||
*/
|
||||
private boolean showConfirmationDialog(ConfirmationDialogContent content) {
|
||||
javafx.scene.control.ButtonType proceedButton =
|
||||
new javafx.scene.control.ButtonType("Fortfahren",
|
||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||
javafx.scene.control.ButtonType cancelButton =
|
||||
new javafx.scene.control.ButtonType("Abbrechen",
|
||||
javafx.scene.control.ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(
|
||||
javafx.scene.control.Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle(content.title());
|
||||
alert.setHeaderText(content.introText());
|
||||
|
||||
StringBuilder correctionListText = new StringBuilder();
|
||||
for (String line : content.correctionLines()) {
|
||||
correctionListText.append("• ").append(line).append("\n");
|
||||
}
|
||||
correctionListText.append("\nFortfahren?");
|
||||
alert.setContentText(correctionListText.toString());
|
||||
|
||||
alert.getButtonTypes().setAll(proceedButton, cancelButton);
|
||||
|
||||
// Standard-Fokus: Abbrechen
|
||||
javafx.scene.Node cancelNode = alert.getDialogPane().lookupButton(cancelButton);
|
||||
if (cancelNode instanceof javafx.scene.control.Button btn) {
|
||||
btn.setDefaultButton(true);
|
||||
}
|
||||
javafx.scene.Node proceedNode = alert.getDialogPane().lookupButton(proceedButton);
|
||||
if (proceedNode instanceof javafx.scene.control.Button btn) {
|
||||
btn.setDefaultButton(false);
|
||||
}
|
||||
|
||||
java.util.Optional<javafx.scene.control.ButtonType> result = alert.showAndWait();
|
||||
return result.isPresent() && result.get() == proceedButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine unveränderliche Momentaufnahme der aktuell ausstehenden Nachrichten zurück.
|
||||
* <p>
|
||||
* Ausschließlich für Tests gedacht.
|
||||
*
|
||||
* @return unveränderliche Kopie der Nachrichtenliste; nie {@code null}
|
||||
*/
|
||||
public List<GuiMessageEntry> pendingMessagesSnapshot() {
|
||||
return List.copyOf(pendingMessages);
|
||||
}
|
||||
}
|
||||
+99
-17
@@ -9,18 +9,29 @@ 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.EffectiveApiKeyDescriptor;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator;
|
||||
|
||||
/**
|
||||
* Immutable startup data for the GUI adapter.
|
||||
* <p>
|
||||
* Carries the initial editor state, the optional startup notice, the file-loading callback,
|
||||
* the file-writing callback that the workspace uses for native save actions, the
|
||||
* {@link AiModelCatalogPort} used to retrieve available AI model lists on demand, and the
|
||||
* {@link AiModelCatalogPort} used to retrieve available AI model lists on demand, the
|
||||
* {@link ApiKeyResolutionPort} used by the editor validation to determine the effective
|
||||
* API key provenance from environment variables.
|
||||
* API key provenance from environment variables, the {@link ProviderTechnicalTestService}
|
||||
* used to execute provider-specific technical checks, the {@link PathCheckPort}
|
||||
* used to verify filesystem path accessibility for configuration values, the
|
||||
* {@link TechnicalTestOrchestrator} used by the "Technische Tests ausführen" action, and the
|
||||
* {@link CorrectionExecutionService} used to execute corrective actions after a
|
||||
* technical test run has been confirmed by the user.
|
||||
* <p>
|
||||
* All ports are supplied by Bootstrap so that the GUI adapter does not need to know about
|
||||
* provider-specific HTTP details or adapter wiring.
|
||||
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to
|
||||
* know about provider-specific HTTP details or adapter wiring.
|
||||
*/
|
||||
public record GuiStartupContext(
|
||||
GuiConfigurationEditorState initialState,
|
||||
@@ -28,17 +39,25 @@ public record GuiStartupContext(
|
||||
GuiConfigurationFileLoader configurationFileLoader,
|
||||
GuiConfigurationFileWriter configurationFileWriter,
|
||||
AiModelCatalogPort modelCatalogPort,
|
||||
ApiKeyResolutionPort apiKeyResolutionPort) {
|
||||
ApiKeyResolutionPort apiKeyResolutionPort,
|
||||
ProviderTechnicalTestService providerTechnicalTestService,
|
||||
PathCheckPort pathCheckPort,
|
||||
TechnicalTestOrchestrator technicalTestOrchestrator,
|
||||
CorrectionExecutionService correctionExecutionService) {
|
||||
|
||||
/**
|
||||
* Creates a startup context.
|
||||
*
|
||||
* @param initialState initial editor state; must not be {@code null}
|
||||
* @param startupNotice optional startup notice; {@code null} becomes empty
|
||||
* @param configurationFileLoader file-loading callback; must not be {@code null}
|
||||
* @param configurationFileWriter file-writing callback; must not be {@code null}
|
||||
* @param modelCatalogPort port for retrieving AI model lists; must not be {@code null}
|
||||
* @param apiKeyResolutionPort port for resolving API key provenance; must not be {@code null}
|
||||
* @param initialState initial editor state; must not be {@code null}
|
||||
* @param startupNotice optional startup notice; {@code null} becomes empty
|
||||
* @param configurationFileLoader file-loading callback; must not be {@code null}
|
||||
* @param configurationFileWriter file-writing callback; must not be {@code null}
|
||||
* @param modelCatalogPort port for retrieving AI model lists; must not be {@code null}
|
||||
* @param apiKeyResolutionPort port for resolving API key provenance; must not be {@code null}
|
||||
* @param providerTechnicalTestService service for provider-specific technical checks; must not be {@code null}
|
||||
* @param pathCheckPort port for filesystem path accessibility checks; must not be {@code null}
|
||||
* @param technicalTestOrchestrator orchestrator for the full technical test run; must not be {@code null}
|
||||
* @param correctionExecutionService service for executing confirmed corrective actions; must not be {@code null}
|
||||
*/
|
||||
public GuiStartupContext {
|
||||
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
|
||||
@@ -51,14 +70,30 @@ public record GuiStartupContext(
|
||||
"modelCatalogPort must not be null");
|
||||
apiKeyResolutionPort = Objects.requireNonNull(apiKeyResolutionPort,
|
||||
"apiKeyResolutionPort must not be null");
|
||||
providerTechnicalTestService = Objects.requireNonNull(providerTechnicalTestService,
|
||||
"providerTechnicalTestService must not be null");
|
||||
pathCheckPort = Objects.requireNonNull(pathCheckPort,
|
||||
"pathCheckPort must not be null");
|
||||
technicalTestOrchestrator = Objects.requireNonNull(technicalTestOrchestrator,
|
||||
"technicalTestOrchestrator must not be null");
|
||||
correctionExecutionService = Objects.requireNonNull(correctionExecutionService,
|
||||
"correctionExecutionService must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blank startup context with no loader or writer side effects, a no-op model
|
||||
* catalogue port, and a no-op API key resolution port.
|
||||
* catalogue port, a no-op API key resolution port, a no-op provider technical test service,
|
||||
* a no-op path check port, a no-op technical test orchestrator, and a no-op
|
||||
* correction execution service.
|
||||
* <p>
|
||||
* The no-op model catalogue port always returns {@code IncompleteConfiguration}.
|
||||
* The no-op API key resolution port always returns {@code ABSENT}.
|
||||
* The no-op provider technical test service uses the no-op ports above.
|
||||
* The no-op path check port always returns {@code false} for all checks.
|
||||
* The no-op technical test orchestrator returns a report where all checkpoints are
|
||||
* {@link de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointResult.NotApplicable}.
|
||||
* The no-op correction execution service uses a no-op {@link ResourceCreationPort} that always
|
||||
* returns {@link de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted}.
|
||||
* This is safe for environments where no Bootstrap wiring is present, such as isolated
|
||||
* GUI tests.
|
||||
*
|
||||
@@ -66,15 +101,62 @@ public record GuiStartupContext(
|
||||
* @return a startup context for the unloaded editor start
|
||||
*/
|
||||
public static GuiStartupContext blank(Optional<String> startupNotice) {
|
||||
de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort noOpCatalogPort =
|
||||
request -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
|
||||
.ModelCatalogResult.IncompleteConfiguration(
|
||||
request.providerIdentifier(),
|
||||
"Kein Modellkatalog in diesem Startkontext verfügbar.");
|
||||
ApiKeyResolutionPort noOpApiKeyPort = (family, propertyValue) -> EffectiveApiKeyDescriptor.absent();
|
||||
ProviderTechnicalTestService noOpTestService =
|
||||
new ProviderTechnicalTestService(noOpCatalogPort, noOpApiKeyPort);
|
||||
PathCheckPort noOpPathCheckPort = new PathCheckPort() {
|
||||
@Override
|
||||
public boolean isDirectoryReadable(String path) { return false; }
|
||||
@Override
|
||||
public boolean isDirectoryWritableOrCreatable(String path) { return false; }
|
||||
@Override
|
||||
public boolean isFileReadable(String path) { return false; }
|
||||
@Override
|
||||
public boolean isSqlitePathUsable(String path) { return false; }
|
||||
};
|
||||
TechnicalTestOrchestrator noOpOrchestrator = new TechnicalTestOrchestrator(
|
||||
new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(),
|
||||
noOpPathCheckPort,
|
||||
noOpTestService);
|
||||
ResourceCreationPort noOpResourceCreationPort = new ResourceCreationPort() {
|
||||
@Override
|
||||
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
|
||||
createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
|
||||
.CorrectionSuggestion.CreateDirectory suggestion) {
|
||||
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
|
||||
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext.");
|
||||
}
|
||||
@Override
|
||||
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
|
||||
createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
|
||||
.CorrectionSuggestion.CreatePromptFile suggestion) {
|
||||
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
|
||||
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext.");
|
||||
}
|
||||
@Override
|
||||
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
|
||||
prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
|
||||
.CorrectionSuggestion.PrepareSqlitePath suggestion) {
|
||||
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
|
||||
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext.");
|
||||
}
|
||||
};
|
||||
CorrectionExecutionService noOpCorrectionService = new CorrectionExecutionService(noOpResourceCreationPort);
|
||||
return new GuiStartupContext(
|
||||
GuiConfigurationEditorStateFactory.createBlankStartState(),
|
||||
startupNotice,
|
||||
configFilePath -> GuiConfigurationEditorStateFactory.createBlankStartState(),
|
||||
(values, path) -> GuiConfigurationSaveResult.saved(path),
|
||||
request -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
|
||||
.ModelCatalogResult.IncompleteConfiguration(
|
||||
request.providerIdentifier(),
|
||||
"Kein Modellkatalog in diesem Startkontext verfügbar."),
|
||||
(family, propertyValue) -> EffectiveApiKeyDescriptor.absent());
|
||||
noOpCatalogPort,
|
||||
noOpApiKeyPort,
|
||||
noOpTestService,
|
||||
noOpPathCheckPort,
|
||||
noOpOrchestrator,
|
||||
noOpCorrectionService);
|
||||
}
|
||||
}
|
||||
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointId;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestRequest;
|
||||
import javafx.application.Platform;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Koordiniert die asynchrone Ausführung der Aktion „Technische Tests ausführen"
|
||||
* für den GUI-Konfigurationseditor.
|
||||
* <p>
|
||||
* Dieser Koordinator ist verantwortlich für:
|
||||
* <ul>
|
||||
* <li>Lesen des aktuellen GUI-Editorzustands (via {@link Supplier}).</li>
|
||||
* <li>Aufbau eines {@link TechnicalTestRequest} aus dem aktuellen Zustand.</li>
|
||||
* <li>Ausführung des {@link TechnicalTestOrchestrator} auf einem dedizierten
|
||||
* Daemon-Hinterground-Thread namens {@code gui-technical-test}.</li>
|
||||
* <li>Rückführung des {@link TechnicalTestReport} auf den JavaFX Application Thread
|
||||
* via {@code Platform.runLater}.</li>
|
||||
* <li>Einhängen der Ergebnisse als {@link GuiMessageEntry}-Einträge in die geteilte
|
||||
* {@code pendingMessages}-Liste (Quelle: „Technische-Tests").</li>
|
||||
* <li>Ersetzen vorheriger Test-Einträge (Replace-Semantik) bei jedem neuen Aufruf.</li>
|
||||
* <li>Weitergabe des vollständigen Berichts an den {@code postResultCallback}, damit
|
||||
* spätere Arbeitsschritte (z. B. Korrekturhilfen) auf das Ergebnis zugreifen können.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <strong>Threading-Kontrakt:</strong> {@link #triggerTechnicalTests()} darf nur auf dem
|
||||
* JavaFX Application Thread aufgerufen werden. Hintergrund-Worker-Threads dürfen nur über
|
||||
* den injizierten {@code resultDelivery}-Verbraucher mit der UI interagieren, der in der
|
||||
* Produktion {@code Platform.runLater} kapselt.
|
||||
* <p>
|
||||
* <strong>Kein implizites Speichern:</strong> Der Koordinator liest den aktuellen GUI-Zustand
|
||||
* und führt den Test aus, ohne die Konfigurationsdatei zu schreiben oder den Dirty-Zustand
|
||||
* des Editors zu ändern.
|
||||
* <p>
|
||||
* <strong>Anti-Scope:</strong> Dieser Koordinator führt keine schreibenden Korrekturen durch.
|
||||
* Korrekturvorschläge werden als Bestandteil des {@link TechnicalTestReport} zurückgegeben,
|
||||
* sind aber nicht ausführbar. Die Ausführung ist einem späteren Arbeitsschritt vorbehalten.
|
||||
* <p>
|
||||
* Die Worker-Thread-Factory und die Result-Delivery-Funktion sind injizierbar, damit Tests
|
||||
* deterministisch ohne echten Hintergrund-Thread laufen können.
|
||||
*/
|
||||
public final class GuiTechnicalTestCoordinator {
|
||||
|
||||
/** Quell-Tag für Einträge in {@code pendingMessages}, die von diesem Koordinator stammen. */
|
||||
static final String SOURCE_TAG = "Technische-Tests";
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(GuiTechnicalTestCoordinator.class);
|
||||
|
||||
private final TechnicalTestOrchestrator orchestrator;
|
||||
private final Supplier<EditorValidationInput> inputProvider;
|
||||
private final Supplier<String> configFilePathProvider;
|
||||
private final List<GuiMessageEntry> pendingMessages;
|
||||
private final Consumer<TechnicalTestReport> postResultCallback;
|
||||
|
||||
/**
|
||||
* Factory für den Hintergrund-Worker-Thread. Paket-privat für Test-Substitution.
|
||||
* Standard: Daemon-Thread namens {@code gui-technical-test}.
|
||||
*/
|
||||
Function<Runnable, Thread> testThreadFactory;
|
||||
|
||||
/**
|
||||
* Verbraucher zur Rückführung des Ergebnisses. In der Produktion kapselt er {@code Platform.runLater}.
|
||||
* In Tests kann er durch einen direkten Aufruf ersetzt werden, damit das Ergebnis sofort
|
||||
* auf dem Worker-Thread angewendet wird, ohne die FX-Warteschlange zu entwässern.
|
||||
* Paket-privat für Test-Substitution.
|
||||
*/
|
||||
java.util.function.Consumer<Runnable> resultDelivery = Platform::runLater;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Koordinator.
|
||||
*
|
||||
* @param orchestrator Orchestrator für den vollständigen Gesamttest; darf nicht {@code null} sein
|
||||
* @param inputProvider Lieferant des aktuellen {@link EditorValidationInput}; darf nicht {@code null} sein
|
||||
* @param configFilePathProvider Lieferant des aktuell geladenen Konfigurationsdateipfads als String;
|
||||
* gibt eine leere Zeichenkette zurück wenn keine Datei geladen ist;
|
||||
* darf nicht {@code null} sein
|
||||
* @param pendingMessages geteilte veränderliche Nachrichtenliste; darf nicht {@code null} sein
|
||||
* @param postResultCallback Callback nach erfolgreicher Ergebnisanwendung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn einer der Parameter {@code null} ist
|
||||
*/
|
||||
public GuiTechnicalTestCoordinator(TechnicalTestOrchestrator orchestrator,
|
||||
Supplier<EditorValidationInput> inputProvider,
|
||||
Supplier<String> configFilePathProvider,
|
||||
List<GuiMessageEntry> pendingMessages,
|
||||
Consumer<TechnicalTestReport> postResultCallback) {
|
||||
this.orchestrator = Objects.requireNonNull(orchestrator, "orchestrator must not be null");
|
||||
this.inputProvider = Objects.requireNonNull(inputProvider, "inputProvider must not be null");
|
||||
this.configFilePathProvider = Objects.requireNonNull(configFilePathProvider, "configFilePathProvider must not be null");
|
||||
this.pendingMessages = Objects.requireNonNull(pendingMessages, "pendingMessages must not be null");
|
||||
this.postResultCallback = Objects.requireNonNull(postResultCallback, "postResultCallback must not be null");
|
||||
this.testThreadFactory = task -> {
|
||||
Thread t = new Thread(task, "gui-technical-test");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst die asynchrone Ausführung des vollständigen technischen Gesamttests aus.
|
||||
* <p>
|
||||
* Liest den aktuellen Editorzustand und den Konfigurationsdateipfad, baut einen
|
||||
* {@link TechnicalTestRequest} und startet den {@link TechnicalTestOrchestrator} auf
|
||||
* einem Hintergrund-Worker-Thread. Das Ergebnis wird via {@code resultDelivery} an den
|
||||
* JavaFX Application Thread zurückgegeben.
|
||||
* <p>
|
||||
* Der Konfigurationsdateipfad wird genutzt, um bei fehlender Prompt-Datei-Konfiguration
|
||||
* einen sinnvollen Standardpfad ({@code <config-parent>/prompt.txt}) zu bestimmen.
|
||||
* <p>
|
||||
* Muss auf dem JavaFX Application Thread aufgerufen werden.
|
||||
*/
|
||||
public void triggerTechnicalTests() {
|
||||
EditorValidationInput input = inputProvider.get();
|
||||
String configFilePath = configFilePathProvider.get();
|
||||
TechnicalTestRequest request = new TechnicalTestRequest(input, configFilePath);
|
||||
|
||||
LOG.info("GUI-Gesamttest: Technische Tests ausführen gestartet.");
|
||||
|
||||
Runnable task = () -> {
|
||||
TechnicalTestReport report = orchestrator.run(request);
|
||||
resultDelivery.accept(() -> {
|
||||
applyResult(report);
|
||||
postResultCallback.accept(report);
|
||||
});
|
||||
};
|
||||
|
||||
Thread worker = testThreadFactory.apply(task);
|
||||
worker.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet das Ergebnis des vollständigen Gesamttests auf die geteilte Nachrichtenliste an.
|
||||
* <p>
|
||||
* Entfernt alle vorherigen Einträge mit Quelle {@link #SOURCE_TAG} und fügt für jeden
|
||||
* Checkpoint-Ergebnis einen neuen Eintrag hinzu. Zusätzlich wird eine Zusammenfassung
|
||||
* angehängt.
|
||||
* <p>
|
||||
* Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
|
||||
*
|
||||
* @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
|
||||
*/
|
||||
private void applyResult(TechnicalTestReport report) {
|
||||
// Alte Einträge mit Source-Tag entfernen (Replace-Semantik)
|
||||
pendingMessages.removeIf(msg -> SOURCE_TAG.equals(msg.source().orElse("")));
|
||||
|
||||
long successCount = 0;
|
||||
long failureErrorCount = 0;
|
||||
long failureWarnCount = 0;
|
||||
long notApplicableCount = 0;
|
||||
|
||||
for (CheckpointResult result : report.results()) {
|
||||
String label = labelFor(result.checkpointId());
|
||||
switch (result) {
|
||||
case CheckpointResult.Success success -> {
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
GuiMessageSeverity.INFO,
|
||||
label + ": OK – " + success.message(),
|
||||
SOURCE_TAG));
|
||||
successCount++;
|
||||
LOG.info("GUI-Gesamttest: {} → OK", label);
|
||||
}
|
||||
case CheckpointResult.Failure failure -> {
|
||||
GuiMessageSeverity severity = failure.severity() ==
|
||||
de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointSeverity.ERROR
|
||||
? GuiMessageSeverity.ERROR : GuiMessageSeverity.WARNING;
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
severity,
|
||||
label + ": " + failure.message(),
|
||||
SOURCE_TAG));
|
||||
if (severity == GuiMessageSeverity.ERROR) {
|
||||
failureErrorCount++;
|
||||
LOG.warn("GUI-Gesamttest: {} → FEHLER: {}", label, failure.message());
|
||||
} else {
|
||||
failureWarnCount++;
|
||||
LOG.warn("GUI-Gesamttest: {} → WARNUNG: {}", label, failure.message());
|
||||
}
|
||||
}
|
||||
case CheckpointResult.NotApplicable notApplicable -> {
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
GuiMessageSeverity.HINT,
|
||||
label + ": nicht anwendbar – " + notApplicable.reason(),
|
||||
SOURCE_TAG));
|
||||
notApplicableCount++;
|
||||
LOG.info("GUI-Gesamttest: {} → nicht anwendbar: {}", label, notApplicable.reason());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zusammenfassung
|
||||
long totalFindings = failureErrorCount + failureWarnCount;
|
||||
String summary = buildSummaryMessage(report.results().size(),
|
||||
successCount, failureErrorCount, failureWarnCount, notApplicableCount);
|
||||
pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, summary, SOURCE_TAG));
|
||||
LOG.info("GUI-Gesamttest abgeschlossen. {} Befunde ({} Erfolg, {} Fehler, {} Warnung, {} nicht anwendbar).",
|
||||
totalFindings, successCount, failureErrorCount, failureWarnCount, notApplicableCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das deutsche Label für einen Prüfpunkt zurück.
|
||||
*
|
||||
* @param id der Prüfpunkt-Bezeichner; darf nicht {@code null} sein
|
||||
* @return das deutsche Label; nie {@code null}
|
||||
*/
|
||||
static String labelFor(CheckpointId id) {
|
||||
return switch (id) {
|
||||
case CONFIGURATION_BASIC_VALIDATION -> "Konfiguration grundsätzlich validierbar";
|
||||
case PROVIDER_CONFIGURATION -> "Provider-Konfiguration prüfbar";
|
||||
case BASE_URL_REACHABLE -> "Basis-URL/Endpoint erreichbar";
|
||||
case API_KEY_PRESENT -> "API-Schlüssel vorhanden";
|
||||
case API_KEY_ACCEPTED -> "API-Schlüssel technisch akzeptiert";
|
||||
case MODEL_LIST_AVAILABLE -> "Modellliste abrufbar";
|
||||
case SELECTED_MODEL_PLAUSIBLE -> "Ausgewähltes Modell plausibel";
|
||||
case PROMPT_FILE_PRESENT -> "Prompt-Datei vorhanden und lesbar";
|
||||
case SOURCE_FOLDER_PRESENT -> "Quellordner vorhanden und lesbar";
|
||||
case TARGET_FOLDER_USABLE -> "Zielordner vorhanden oder anlegbar sowie schreibbar";
|
||||
case SQLITE_PATH_USABLE -> "SQLite-Pfad technisch nutzbar";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die deutsche Zusammenfassungsmeldung des Gesamttests.
|
||||
*
|
||||
* @param total Gesamtzahl der Prüfpunkte
|
||||
* @param successCount Anzahl der erfolgreichen Prüfpunkte
|
||||
* @param errorCount Anzahl der fehlgeschlagenen Prüfpunkte (Schweregrad ERROR)
|
||||
* @param warningCount Anzahl der Warnungs-Prüfpunkte
|
||||
* @param notApplicable Anzahl der nicht-anwendbaren Prüfpunkte
|
||||
* @return deutsche Zusammenfassungsmeldung; nie {@code null}
|
||||
*/
|
||||
private static String buildSummaryMessage(long total, long successCount, long errorCount,
|
||||
long warningCount, long notApplicable) {
|
||||
long findings = errorCount + warningCount;
|
||||
String base = "Gesamttest abgeschlossen. " + total + " Prüfpunkte: "
|
||||
+ successCount + " Erfolg, "
|
||||
+ errorCount + " Fehler, "
|
||||
+ warningCount + " Warnung, "
|
||||
+ notApplicable + " nicht anwendbar.";
|
||||
if (findings == 0) {
|
||||
return base + " Keine Befunde.";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine unveränderliche Momentaufnahme der aktuell ausstehenden Nachrichten zurück.
|
||||
* <p>
|
||||
* Ausschließlich für Tests gedacht.
|
||||
*
|
||||
* @return unveränderliche Kopie der Nachrichtenliste; nie {@code null}
|
||||
*/
|
||||
public List<GuiMessageEntry> pendingMessagesSnapshot() {
|
||||
return List.copyOf(pendingMessages);
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionPlan;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
|
||||
|
||||
/**
|
||||
* Inhalt des gesammelten Bestätigungsdialogs für schreibende Korrekturmaßnahmen.
|
||||
* <p>
|
||||
* Bevor schreibende Korrekturen aus einem {@link CorrectionPlan} ausgeführt werden,
|
||||
* zeigt die GUI diesen Inhalt in einem einmaligen Bestätigungsdialog. Der Benutzer
|
||||
* kann die Korrekturen bestätigen oder ablehnen; ohne Bestätigung werden keine
|
||||
* Änderungen vorgenommen.
|
||||
* <p>
|
||||
* Dieser Record liegt bewusst im GUI-Modul, da er ausschließlich für die
|
||||
* Darstellung im Bestätigungsdialog der JavaFX-Oberfläche genutzt wird. Er enthält
|
||||
* selbst keine JavaFX-Typen und kann auf beliebigen Threads erzeugt werden.
|
||||
* <p>
|
||||
* Die Beschreibungszeilen ({@link #correctionLines}) entsprechen den
|
||||
* {@link CorrectionSuggestion#descriptionForUser()}-Texten der im Plan enthaltenen
|
||||
* Vorschläge in Reihenfolge.
|
||||
*
|
||||
* @param title deutscher Dialogtitel; nie {@code null}
|
||||
* @param introText einleitender deutschsprachiger Text; nie {@code null}
|
||||
* @param correctionLines Liste der deutschen Beschreibungszeilen, eine pro Korrekturmaßnahme;
|
||||
* nie {@code null}
|
||||
*/
|
||||
public record ConfirmationDialogContent(
|
||||
String title,
|
||||
String introText,
|
||||
List<String> correctionLines) {
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Bestätigungsdialog-Inhalt.
|
||||
*
|
||||
* @param title Dialogtitel; darf nicht {@code null} sein
|
||||
* @param introText einleitender Text; darf nicht {@code null} sein
|
||||
* @param correctionLines Beschreibungszeilen; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
*/
|
||||
public ConfirmationDialogContent {
|
||||
Objects.requireNonNull(title, "title must not be null");
|
||||
Objects.requireNonNull(introText, "introText must not be null");
|
||||
Objects.requireNonNull(correctionLines, "correctionLines must not be null");
|
||||
correctionLines = List.copyOf(correctionLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt den Bestätigungsdialog-Inhalt aus einem {@link CorrectionPlan}.
|
||||
* <p>
|
||||
* Die Beschreibungszeilen werden aus den
|
||||
* {@link CorrectionSuggestion#descriptionForUser()}-Texten der Vorschläge im Plan
|
||||
* in Reihenfolge übernommen.
|
||||
*
|
||||
* @param plan Korrekturplan; darf nicht {@code null} sein
|
||||
* @return ein neuer Dialoginhalt; nie {@code null}
|
||||
* @throws NullPointerException wenn {@code plan} {@code null} ist
|
||||
*/
|
||||
public static ConfirmationDialogContent fromPlan(CorrectionPlan plan) {
|
||||
Objects.requireNonNull(plan, "plan must not be null");
|
||||
List<String> lines = plan.suggestions().stream()
|
||||
.map(CorrectionSuggestion::descriptionForUser)
|
||||
.toList();
|
||||
return new ConfirmationDialogContent(
|
||||
"Korrekturen bestätigen",
|
||||
"Folgende technische Korrekturen werden durchgeführt:",
|
||||
lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob der Dialoginhalt mindestens eine Beschreibungszeile enthält.
|
||||
*
|
||||
* @return {@code true} wenn mindestens eine Korrekturmaßnahme beschrieben ist
|
||||
*/
|
||||
public boolean hasCorrections() {
|
||||
return !correctionLines.isEmpty();
|
||||
}
|
||||
}
|
||||
+2
@@ -25,6 +25,8 @@
|
||||
* <li>The consolidated validation result that feeds both the central message area and
|
||||
* field-near error display
|
||||
* ({@link de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiEditorValidationResult}).</li>
|
||||
* <li>The confirmation dialog content for collected write-corrective actions
|
||||
* ({@link de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.ConfirmationDialogContent}).</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Most classes in this package are intentionally free of JavaFX controls so they can be used
|
||||
|
||||
Reference in New Issue
Block a user