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:
2026-04-20 21:57:06 +02:00
parent aa067a3165
commit 1bb7a42735
53 changed files with 7410 additions and 47 deletions
@@ -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}.
*
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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