M10 bis AP-003
This commit is contained in:
+17
-8
@@ -34,9 +34,9 @@ import org.apache.logging.log4j.Logger;
|
||||
*
|
||||
* <h2>Current scope</h2>
|
||||
* <p>
|
||||
* The adapter launches a minimal GUI shell that proves the GUI startup path works.
|
||||
* It does not provide a configuration editor, file operations, validation, provider
|
||||
* controls, or any other functionality beyond the technical startup proof.
|
||||
* The adapter launches the editor shell with the unloaded start state, an optional startup
|
||||
* notice, and a file-loading callback supplied by Bootstrap. File I/O and save behavior remain
|
||||
* outside the current GUI step.
|
||||
*/
|
||||
public class GuiAdapter {
|
||||
|
||||
@@ -84,13 +84,22 @@ public class GuiAdapter {
|
||||
* or if the platform is not supported
|
||||
*/
|
||||
public void start(Optional<String> startupNotice) {
|
||||
start(GuiStartupContext.blank(startupNotice));
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the JavaFX GUI with the supplied startup context.
|
||||
*
|
||||
* @param startupContext startup data for the GUI; must not be {@code null}
|
||||
*/
|
||||
public void start(GuiStartupContext startupContext) {
|
||||
LOG.info("GUI-Adapter: JavaFX-Start wird eingeleitet.");
|
||||
if (startupNotice.isPresent()) {
|
||||
Application.launch(PdfUmbenennerGuiApplication.class,
|
||||
PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX + startupNotice.get());
|
||||
} else {
|
||||
GuiStartupContextHolder.install(startupContext);
|
||||
try {
|
||||
Application.launch(PdfUmbenennerGuiApplication.class);
|
||||
} finally {
|
||||
GuiStartupContextHolder.clear();
|
||||
LOG.info("GUI-Adapter: JavaFX-Anwendung wurde beendet.");
|
||||
}
|
||||
LOG.info("GUI-Adapter: JavaFX-Anwendung wurde beendet.");
|
||||
}
|
||||
}
|
||||
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationTemplateFactory;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Builds the editor workspace shown after the JavaFX application starts.
|
||||
* <p>
|
||||
* The workspace owns the unloaded start state, the optional startup notice, the file-loading
|
||||
* callback and the visible section scaffold for the single-tab shell. It performs no save
|
||||
* operations and no validation logic.
|
||||
*/
|
||||
public final class GuiConfigurationEditorWorkspace {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(GuiConfigurationEditorWorkspace.class);
|
||||
private static final String WELCOME_TEXT =
|
||||
"Willkommen. Legen Sie mit „Neu“ eine Standardvorlage an oder öffnen Sie eine bestehende Konfiguration.";
|
||||
|
||||
private final BorderPane root = new BorderPane();
|
||||
private final Label statusLabel = new Label();
|
||||
private final Label configurationPathValueLabel = new Label();
|
||||
private final Label welcomeTitleLabel = new Label("Willkommen");
|
||||
private final Label welcomeTextLabel = new Label(WELCOME_TEXT);
|
||||
private final TabPane tabPane = new TabPane();
|
||||
private final VBox sectionsBox = new VBox(12);
|
||||
private final Button newButton = new Button("Neu");
|
||||
private final Button openButton = new Button("Öffnen");
|
||||
private final Button saveButton = new Button("Speichern");
|
||||
private final Button saveAsButton = new Button("Speichern unter");
|
||||
|
||||
private final GuiConfigurationFileLoader configurationFileLoader;
|
||||
private GuiConfigurationEditorState editorState;
|
||||
private boolean welcomeGuidanceVisible;
|
||||
|
||||
/**
|
||||
* Creates a new workspace with the unloaded start state.
|
||||
*
|
||||
* @param startupNotice optional startup notice to show in the header area
|
||||
*/
|
||||
public GuiConfigurationEditorWorkspace(Optional<String> startupNotice) {
|
||||
this(GuiStartupContext.blank(startupNotice));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new workspace with the supplied startup context.
|
||||
*
|
||||
* @param startupContext startup data from Bootstrap; must not be {@code null}
|
||||
*/
|
||||
public GuiConfigurationEditorWorkspace(GuiStartupContext startupContext) {
|
||||
GuiStartupContext effectiveContext = startupContext == null
|
||||
? GuiStartupContext.blank(Optional.empty())
|
||||
: startupContext;
|
||||
this.configurationFileLoader = effectiveContext.configurationFileLoader();
|
||||
this.editorState = effectiveContext.initialState();
|
||||
this.welcomeGuidanceVisible = editorState.isNewConfiguration();
|
||||
|
||||
configureRoot();
|
||||
configureHeader(effectiveContext.startupNotice());
|
||||
configureTabs();
|
||||
configureActionBar();
|
||||
configureActions();
|
||||
refreshView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root node used by the JavaFX scene.
|
||||
*
|
||||
* @return the root pane for the workspace
|
||||
*/
|
||||
public Parent root() {
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active editor state.
|
||||
*
|
||||
* @return the current editor state
|
||||
*/
|
||||
public GuiConfigurationEditorState editorState() {
|
||||
return editorState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently displayed configuration-path text.
|
||||
*
|
||||
* @return the header text for the loaded configuration path
|
||||
*/
|
||||
public String configurationPathText() {
|
||||
return configurationPathValueLabel.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the welcome text shown in the editor start state.
|
||||
*
|
||||
* @return the German welcome text
|
||||
*/
|
||||
public String welcomeText() {
|
||||
return welcomeTextLabel.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the welcome guidance is currently visible.
|
||||
*
|
||||
* @return {@code true} when the welcome text is shown, otherwise {@code false}
|
||||
*/
|
||||
public boolean isWelcomeGuidanceVisible() {
|
||||
return welcomeGuidanceVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "Neu" action button.
|
||||
*
|
||||
* @return the button used to switch to the standard template
|
||||
*/
|
||||
public Button newButton() {
|
||||
return newButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "Öffnen" action button.
|
||||
*
|
||||
* @return the button used for the open-file flow
|
||||
*/
|
||||
public Button openButton() {
|
||||
return openButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "Speichern" action button.
|
||||
*
|
||||
* @return the button used for future save wiring
|
||||
*/
|
||||
public Button saveButton() {
|
||||
return saveButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "Speichern unter" action button.
|
||||
*
|
||||
* @return the button used for future save-as wiring
|
||||
*/
|
||||
public Button saveAsButton() {
|
||||
return saveAsButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tab pane containing the single editor tab.
|
||||
*
|
||||
* @return the tab pane shown in the center area
|
||||
*/
|
||||
public TabPane tabPane() {
|
||||
return tabPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fixed section titles shown inside the single configuration tab.
|
||||
*
|
||||
* @return the section titles in visual order
|
||||
*/
|
||||
List<String> sectionTitles() {
|
||||
return List.of("Pfade", "Provider", "Verarbeitungslimits", "Tests", "Meldungen");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the explicit "Neu" action.
|
||||
* <p>
|
||||
* The workspace switches to the standard template and hides the welcome guidance.
|
||||
*/
|
||||
public void requestNewConfiguration() {
|
||||
LOG.info("GUI-Editor: Neue Standardvorlage wird angezeigt.");
|
||||
applyEditorState(GuiConfigurationTemplateFactory.createStandardTemplate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the explicit "Öffnen" action.
|
||||
* <p>
|
||||
* The file chooser is native to the platform and filters for {@code *.properties} files.
|
||||
*/
|
||||
public void requestOpenConfiguration() {
|
||||
Window owner = root.getScene() == null ? null : root.getScene().getWindow();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Konfiguration öffnen");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Properties-Dateien", "*.properties"));
|
||||
if (owner != null && editorState.hasLoadedFileSnapshot()) {
|
||||
Path currentPath = editorState.loadedFileSnapshot().orElseThrow().filePath();
|
||||
Path parent = currentPath.getParent();
|
||||
if (parent != null) {
|
||||
fileChooser.setInitialDirectory(parent.toFile());
|
||||
}
|
||||
}
|
||||
|
||||
java.io.File selectedFile = fileChooser.showOpenDialog(owner);
|
||||
if (selectedFile != null) {
|
||||
openConfigurationFile(selectedFile.toPath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the selected configuration file on a background thread.
|
||||
*
|
||||
* @param configFilePath the selected file path; must not be {@code null}
|
||||
*/
|
||||
void openConfigurationFile(Path configFilePath) {
|
||||
if (configurationFileLoader == null) {
|
||||
showError("Öffnen ist derzeit nicht verfügbar.");
|
||||
return;
|
||||
}
|
||||
|
||||
Thread worker = new Thread(() -> {
|
||||
try {
|
||||
GuiConfigurationEditorState loadedState = configurationFileLoader.load(configFilePath);
|
||||
Platform.runLater(() -> applyEditorState(loadedState));
|
||||
} catch (Exception exception) {
|
||||
Platform.runLater(() -> showError("Konfiguration konnte nicht geladen werden: "
|
||||
+ safeMessage(exception)));
|
||||
}
|
||||
}, "gui-config-loader");
|
||||
worker.setDaemon(true);
|
||||
worker.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the explicit "Speichern" action.
|
||||
* <p>
|
||||
* File writing is intentionally not implemented yet.
|
||||
*/
|
||||
public void requestSaveConfiguration() {
|
||||
LOG.info("GUI-Editor: Speichern-Aktion wurde ausgelöst, ist aber noch nicht implementiert.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the explicit "Speichern unter" action.
|
||||
* <p>
|
||||
* File writing is intentionally not implemented yet.
|
||||
*/
|
||||
public void requestSaveConfigurationAs() {
|
||||
LOG.info("GUI-Editor: Speichern-unter-Aktion wurde ausgelöst, ist aber noch nicht implementiert.");
|
||||
}
|
||||
|
||||
private void applyEditorState(GuiConfigurationEditorState newState) {
|
||||
this.editorState = newState;
|
||||
this.welcomeGuidanceVisible = false;
|
||||
statusLabel.setVisible(false);
|
||||
statusLabel.setManaged(false);
|
||||
refreshView();
|
||||
}
|
||||
|
||||
private void configureRoot() {
|
||||
root.getStyleClass().add("gui-editor-root");
|
||||
root.setPadding(new Insets(16));
|
||||
}
|
||||
|
||||
private void configureHeader(Optional<String> startupNotice) {
|
||||
Label titleLabel = new Label("PDF-Umbenenner");
|
||||
titleLabel.setStyle("-fx-font-size: 24px; -fx-font-weight: bold;");
|
||||
|
||||
Label subtitleLabel = new Label("Konfigurationseditor");
|
||||
subtitleLabel.setStyle("-fx-font-size: 12px; -fx-text-fill: #666666;");
|
||||
|
||||
VBox titleBox = new VBox(2, titleLabel, subtitleLabel);
|
||||
|
||||
Label pathCaption = new Label("Konfigurationspfad:");
|
||||
pathCaption.setStyle("-fx-font-weight: bold;");
|
||||
HBox pathRow = new HBox(8, pathCaption, configurationPathValueLabel);
|
||||
pathRow.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
statusLabel.setWrapText(true);
|
||||
statusLabel.setVisible(false);
|
||||
statusLabel.setManaged(false);
|
||||
statusLabel.setStyle(
|
||||
"-fx-padding: 8px 10px; -fx-background-color: #fff2cc; -fx-text-fill: #5c3b00; -fx-border-color: #e0b95b;");
|
||||
|
||||
VBox headerBox = new VBox(8, titleBox, pathRow, statusLabel, new Separator());
|
||||
headerBox.setPadding(new Insets(0, 0, 16, 0));
|
||||
root.setTop(headerBox);
|
||||
|
||||
startupNotice.ifPresent(this::showStatusMessage);
|
||||
}
|
||||
|
||||
private void configureTabs() {
|
||||
Tab editorTab = new Tab("Konfiguration");
|
||||
editorTab.setClosable(false);
|
||||
|
||||
sectionsBox.setSpacing(12);
|
||||
sectionsBox.setFillWidth(true);
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane(sectionsBox);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
scrollPane.setPadding(new Insets(0));
|
||||
editorTab.setContent(scrollPane);
|
||||
|
||||
tabPane.getTabs().add(editorTab);
|
||||
root.setCenter(tabPane);
|
||||
}
|
||||
|
||||
private void configureActionBar() {
|
||||
HBox actionBar = new HBox(10, newButton, openButton, saveButton, saveAsButton);
|
||||
actionBar.setAlignment(Pos.CENTER_LEFT);
|
||||
actionBar.setPadding(new Insets(16, 0, 0, 0));
|
||||
root.setBottom(actionBar);
|
||||
}
|
||||
|
||||
private void configureActions() {
|
||||
newButton.setOnAction(event -> requestNewConfiguration());
|
||||
openButton.setOnAction(event -> requestOpenConfiguration());
|
||||
saveButton.setOnAction(event -> requestSaveConfiguration());
|
||||
saveAsButton.setOnAction(event -> requestSaveConfigurationAs());
|
||||
}
|
||||
|
||||
private void refreshView() {
|
||||
refreshHeader();
|
||||
refreshSections();
|
||||
}
|
||||
|
||||
private void refreshHeader() {
|
||||
configurationPathValueLabel.setText(editorState.configurationPathText());
|
||||
}
|
||||
|
||||
private void refreshSections() {
|
||||
sectionsBox.getChildren().setAll(
|
||||
createWelcomeCard(),
|
||||
createPathsSection(),
|
||||
createProviderSection(),
|
||||
createProcessingLimitsSection(),
|
||||
createTestsSection(),
|
||||
createMessagesSection());
|
||||
}
|
||||
|
||||
private Node createWelcomeCard() {
|
||||
VBox card = createCardContainer();
|
||||
welcomeTitleLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;");
|
||||
welcomeTextLabel.setWrapText(true);
|
||||
welcomeTextLabel.setStyle("-fx-font-size: 13px;");
|
||||
|
||||
card.getChildren().addAll(welcomeTitleLabel, welcomeTextLabel);
|
||||
card.setVisible(welcomeGuidanceVisible);
|
||||
card.setManaged(welcomeGuidanceVisible);
|
||||
return card;
|
||||
}
|
||||
|
||||
private Node createPathsSection() {
|
||||
VBox card = createCardContainer();
|
||||
card.getChildren().addAll(
|
||||
sectionTitle("Pfade"),
|
||||
textLabel("Der Bereich ist vorbereitet. Geladene Pfade werden in einem späteren Schritt editierbar ergänzt."));
|
||||
return card;
|
||||
}
|
||||
|
||||
private Node createProviderSection() {
|
||||
VBox card = createCardContainer();
|
||||
card.getChildren().addAll(
|
||||
sectionTitle("Provider"),
|
||||
textLabel("Der Bereich ist vorbereitet. Provider-Auswahl und zugehörige Eingaben folgen in einem späteren Schritt."));
|
||||
return card;
|
||||
}
|
||||
|
||||
private Node createProcessingLimitsSection() {
|
||||
VBox card = createCardContainer();
|
||||
card.getChildren().addAll(
|
||||
sectionTitle("Verarbeitungslimits"),
|
||||
textLabel("Der Bereich ist vorbereitet. Eingaben für Limits werden in einem späteren Schritt ergänzt."));
|
||||
return card;
|
||||
}
|
||||
|
||||
private Node createTestsSection() {
|
||||
VBox card = createCardContainer();
|
||||
card.getChildren().addAll(
|
||||
sectionTitle("Tests"),
|
||||
textLabel("Der Bereich ist vorbereitet. Test- und Diagnoseaktionen werden in einem späteren Schritt ergänzt."));
|
||||
return card;
|
||||
}
|
||||
|
||||
private Node createMessagesSection() {
|
||||
VBox card = createCardContainer();
|
||||
card.getChildren().addAll(
|
||||
sectionTitle("Meldungen"),
|
||||
textLabel("Der Bereich ist vorbereitet. Sichtbare Meldungen und technische Hinweise folgen in einem späteren Schritt."));
|
||||
return card;
|
||||
}
|
||||
|
||||
private VBox createCardContainer() {
|
||||
VBox card = new VBox(8);
|
||||
card.setStyle(
|
||||
"-fx-padding: 12px; -fx-border-color: #d8d8d8; -fx-border-radius: 8px; -fx-background-radius: 8px; -fx-background-color: white;");
|
||||
card.setMaxWidth(Double.MAX_VALUE);
|
||||
VBox.setVgrow(card, Priority.NEVER);
|
||||
return card;
|
||||
}
|
||||
|
||||
private Label sectionTitle(String title) {
|
||||
Label label = new Label(title);
|
||||
label.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;");
|
||||
return label;
|
||||
}
|
||||
|
||||
private Label textLabel(String text) {
|
||||
Label label = new Label(text);
|
||||
label.setWrapText(true);
|
||||
return label;
|
||||
}
|
||||
|
||||
private void showStatusMessage(String message) {
|
||||
statusLabel.setText(message);
|
||||
statusLabel.setVisible(true);
|
||||
statusLabel.setManaged(true);
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
LOG.warn("GUI-Editor: {}", message);
|
||||
showStatusMessage(message);
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK);
|
||||
alert.setHeaderText("Konfiguration konnte nicht geladen werden");
|
||||
alert.show();
|
||||
}
|
||||
|
||||
private String safeMessage(Throwable exception) {
|
||||
return exception == null ? "unbekannter Fehler" : exception.getMessage() == null
|
||||
? exception.getClass().getSimpleName()
|
||||
: exception.getMessage();
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||
|
||||
/**
|
||||
* Loads a configuration file into a GUI editor state.
|
||||
* <p>
|
||||
* The interface allows Bootstrap to provide the concrete file-loading and migration logic
|
||||
* while the GUI only deals with editor states.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface GuiConfigurationFileLoader {
|
||||
|
||||
/**
|
||||
* Loads the configuration file at the given path.
|
||||
*
|
||||
* @param configFilePath the file to load; must not be {@code null}
|
||||
* @return the loaded editor state; never {@code null}
|
||||
*/
|
||||
GuiConfigurationEditorState load(Path configFilePath);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
/**
|
||||
* Runtime exception thrown when the GUI configuration cannot be loaded.
|
||||
*/
|
||||
public class GuiConfigurationLoadException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Creates a new load exception.
|
||||
*
|
||||
* @param message the exception message
|
||||
* @param cause the underlying cause
|
||||
*/
|
||||
public GuiConfigurationLoadException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new load exception.
|
||||
*
|
||||
* @param message the exception message
|
||||
*/
|
||||
public GuiConfigurationLoadException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
|
||||
|
||||
/**
|
||||
* Immutable startup data for the GUI adapter.
|
||||
* <p>
|
||||
* Carries the initial editor state, the optional startup notice and the file-loading callback
|
||||
* that the workspace uses for native open actions.
|
||||
*/
|
||||
public record GuiStartupContext(
|
||||
GuiConfigurationEditorState initialState,
|
||||
Optional<String> startupNotice,
|
||||
GuiConfigurationFileLoader configurationFileLoader) {
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
public GuiStartupContext {
|
||||
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
|
||||
startupNotice = startupNotice == null ? Optional.empty() : startupNotice;
|
||||
configurationFileLoader = Objects.requireNonNull(configurationFileLoader,
|
||||
"configurationFileLoader must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blank startup context with no loader side effects.
|
||||
*
|
||||
* @param startupNotice optional startup notice; {@code null} becomes empty
|
||||
* @return a startup context for the unloaded editor start
|
||||
*/
|
||||
public static GuiStartupContext blank(Optional<String> startupNotice) {
|
||||
return new GuiStartupContext(
|
||||
GuiConfigurationEditorStateFactory.createBlankStartState(),
|
||||
startupNotice,
|
||||
configFilePath -> GuiConfigurationEditorStateFactory.createBlankStartState());
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Process-local holder for the next GUI startup context.
|
||||
* <p>
|
||||
* JavaFX creates the application class reflectively, so Bootstrap installs the context here
|
||||
* immediately before the JavaFX launch call.
|
||||
*/
|
||||
final class GuiStartupContextHolder {
|
||||
|
||||
private static final AtomicReference<GuiStartupContext> CONTEXT = new AtomicReference<>();
|
||||
|
||||
private GuiStartupContextHolder() {
|
||||
// Utility class.
|
||||
}
|
||||
|
||||
static void install(GuiStartupContext context) {
|
||||
CONTEXT.set(context);
|
||||
}
|
||||
|
||||
static GuiStartupContext currentOrBlank() {
|
||||
return Optional.ofNullable(CONTEXT.get()).orElseGet(() -> GuiStartupContext.blank(Optional.empty()));
|
||||
}
|
||||
|
||||
static void clear() {
|
||||
CONTEXT.set(null);
|
||||
}
|
||||
}
|
||||
+15
-107
@@ -1,153 +1,61 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Minimal JavaFX {@link Application} subclass that establishes the GUI shell.
|
||||
* JavaFX application entry point for the PDF-Umbenenner GUI inbound adapter.
|
||||
* <p>
|
||||
* This class is the JavaFX lifecycle entry point launched by
|
||||
* {@link GuiAdapter#start(java.util.Optional)}. It creates the primary stage with a
|
||||
* minimal, neutral layout that proves the GUI startup path is technically functional.
|
||||
* The layout is deliberately structured as a {@link BorderPane} so that later milestones
|
||||
* can populate individual regions (header, center content, bottom message area) without
|
||||
* requiring an architectural restructuring.
|
||||
*
|
||||
* <h2>Startup notice</h2>
|
||||
* <p>
|
||||
* When Bootstrap forwards a startup notice (e.g. because the supplied {@code --config}
|
||||
* path was not found), the notice is passed as an application parameter prefixed with
|
||||
* {@link #STARTUP_NOTICE_ARG_PREFIX}. This class reads the parameter via
|
||||
* {@link Application#getParameters()} and displays it prominently in the center region
|
||||
* instead of the default placeholder.
|
||||
*
|
||||
* <h2>Current scope</h2>
|
||||
* <p>
|
||||
* The shell displays the application window with a neutral title and either a static
|
||||
* placeholder label or a startup notice. It contains no configuration editor, no file
|
||||
* operations, no validation, no provider controls, and no message area. These are the
|
||||
* responsibility of later milestones.
|
||||
*
|
||||
* <h2>Explicit non-goals</h2>
|
||||
* <ul>
|
||||
* <li>No configuration editor or editable input fields</li>
|
||||
* <li>No file operations (Neu, Oeffnen, Speichern, Speichern unter)</li>
|
||||
* <li>No validation or provider controls</li>
|
||||
* <li>No message area or technical tests</li>
|
||||
* <li>No welcome text in the final product sense</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Threading</h2>
|
||||
* <p>
|
||||
* The {@link #start(Stage)} method is called by the JavaFX runtime on the
|
||||
* JavaFX Application Thread. No blocking operations are performed during
|
||||
* stage setup.
|
||||
* The application starts the editor shell in a clean, unloaded state unless Bootstrap
|
||||
* has provided a preloaded startup context. The visible editor surface is delegated to
|
||||
* {@link GuiConfigurationEditorWorkspace}.
|
||||
*/
|
||||
public class PdfUmbenennerGuiApplication extends Application {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(PdfUmbenennerGuiApplication.class);
|
||||
|
||||
/**
|
||||
* Argument prefix used to forward a startup notice from {@link GuiAdapter} to this
|
||||
* application via {@link Application#launch(Class, String[])}.
|
||||
* <p>
|
||||
* When an argument beginning with this prefix is present in the raw parameter list,
|
||||
* the remainder of the argument string is treated as the notice text to display.
|
||||
*/
|
||||
static final String STARTUP_NOTICE_ARG_PREFIX = "--startup-notice=";
|
||||
|
||||
private static final String WINDOW_TITLE = "PDF-Umbenenner";
|
||||
private static final double DEFAULT_WIDTH = 800;
|
||||
private static final double DEFAULT_HEIGHT = 600;
|
||||
private static final double DEFAULT_WIDTH = 1100;
|
||||
private static final double DEFAULT_HEIGHT = 800;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the JavaFX application.
|
||||
* <p>
|
||||
* This no-argument constructor is required by the JavaFX runtime, which
|
||||
* instantiates the {@link Application} subclass reflectively.
|
||||
*/
|
||||
public PdfUmbenennerGuiApplication() {
|
||||
// Required by JavaFX runtime for reflective instantiation.
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and shows the primary stage with a minimal GUI shell.
|
||||
* <p>
|
||||
* The stage is set up with a {@link BorderPane} root layout. When a startup notice is
|
||||
* present (forwarded via application parameters by {@link GuiAdapter}), the notice is
|
||||
* displayed prominently in red in the center region. Otherwise a neutral placeholder
|
||||
* label is shown. The layout structure is chosen to allow incremental extension in later
|
||||
* milestones without requiring a root-level restructuring.
|
||||
* <p>
|
||||
* Start and shutdown events are logged via Log4j2 to satisfy the GUI logging requirements.
|
||||
* Initializes and shows the primary stage.
|
||||
*
|
||||
* @param primaryStage the primary stage provided by the JavaFX runtime; never {@code null}
|
||||
*/
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
LOG.info("GUI-Shell: JavaFX-Oberfläche wird initialisiert.");
|
||||
LOG.info("GUI: JavaFX-Oberfläche wird initialisiert.");
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
GuiStartupContext startupContext = GuiStartupContextHolder.currentOrBlank();
|
||||
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(startupContext);
|
||||
Scene scene = new Scene(workspace.root(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
|
||||
Optional<String> startupNotice = extractStartupNotice();
|
||||
if (startupNotice.isPresent()) {
|
||||
Label noticeLabel = new Label(startupNotice.get());
|
||||
noticeLabel.setWrapText(true);
|
||||
noticeLabel.setAlignment(Pos.CENTER);
|
||||
noticeLabel.setStyle("-fx-font-size: 13px; -fx-text-fill: #cc0000;");
|
||||
root.setCenter(noticeLabel);
|
||||
LOG.info("GUI-Shell: Starthinweis wird angezeigt.");
|
||||
} else {
|
||||
Label placeholderLabel = new Label("PDF-Umbenenner – GUI wird vorbereitet …");
|
||||
placeholderLabel.setStyle("-fx-font-size: 14px; -fx-text-fill: #666666;");
|
||||
root.setCenter(placeholderLabel);
|
||||
}
|
||||
|
||||
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
primaryStage.setTitle(WINDOW_TITLE);
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.setOnCloseRequest(event ->
|
||||
LOG.info("GUI-Shell: Fenster wird vom Benutzer geschlossen."));
|
||||
primaryStage.setOnCloseRequest(event -> LOG.info("GUI: Fenster wird vom Benutzer geschlossen."));
|
||||
primaryStage.show();
|
||||
|
||||
LOG.info("GUI-Shell: Hauptfenster erfolgreich angezeigt.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the startup notice from the application parameters, if present.
|
||||
* <p>
|
||||
* Searches the raw parameter list for an argument beginning with
|
||||
* {@link #STARTUP_NOTICE_ARG_PREFIX} and returns the remainder as the notice text.
|
||||
* Returns an empty Optional when no such argument is present.
|
||||
*
|
||||
* @return the startup notice text, or an empty Optional if no notice was forwarded
|
||||
*/
|
||||
private Optional<String> extractStartupNotice() {
|
||||
List<String> rawParams = getParameters().getRaw();
|
||||
return rawParams.stream()
|
||||
.filter(p -> p.startsWith(STARTUP_NOTICE_ARG_PREFIX))
|
||||
.map(p -> p.substring(STARTUP_NOTICE_ARG_PREFIX.length()))
|
||||
.findFirst();
|
||||
LOG.info("GUI: Hauptfenster erfolgreich angezeigt.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the JavaFX runtime when the application is stopping.
|
||||
* <p>
|
||||
* Logs the GUI shutdown event. No additional cleanup is required
|
||||
* for the minimal shell.
|
||||
* Logs the GUI shutdown event. No additional cleanup is required.
|
||||
*/
|
||||
@Override
|
||||
public void stop() {
|
||||
LOG.info("GUI-Shell: JavaFX-Anwendung wird beendet.");
|
||||
LOG.info("GUI: JavaFX-Anwendung wird beendet.");
|
||||
}
|
||||
}
|
||||
|
||||
+50
-6
@@ -18,7 +18,8 @@ import java.util.Optional;
|
||||
public record GuiConfigurationEditorState(
|
||||
Optional<GuiConfigurationFileSnapshot> loadedFileSnapshot,
|
||||
GuiConfigurationValues baselineValues,
|
||||
GuiConfigurationValues values) {
|
||||
GuiConfigurationValues values,
|
||||
Optional<String> pendingMigrationMessage) {
|
||||
|
||||
/**
|
||||
* Creates a new editor state.
|
||||
@@ -31,6 +32,7 @@ public record GuiConfigurationEditorState(
|
||||
loadedFileSnapshot = loadedFileSnapshot == null ? Optional.empty() : loadedFileSnapshot;
|
||||
baselineValues = Objects.requireNonNull(baselineValues, "baselineValues must not be null");
|
||||
values = Objects.requireNonNull(values, "values must not be null");
|
||||
pendingMigrationMessage = pendingMigrationMessage == null ? Optional.empty() : pendingMigrationMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +71,36 @@ public record GuiConfigurationEditorState(
|
||||
return loadedFileSnapshot.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration-path text shown in the GUI header.
|
||||
* <p>
|
||||
* When no file snapshot is loaded, the header must remain empty so the GUI does not
|
||||
* suggest a file is already associated with the editor state.
|
||||
*
|
||||
* @return the loaded configuration path as text, or an empty string when no file is loaded
|
||||
*/
|
||||
public String configurationPathText() {
|
||||
return loadedFileSnapshot.map(snapshot -> snapshot.filePath().toString()).orElse("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the editor holds a migration note for later display.
|
||||
*
|
||||
* @return {@code true} when a migration note is stored, otherwise {@code false}
|
||||
*/
|
||||
public boolean hasPendingMigrationMessage() {
|
||||
return pendingMigrationMessage.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored migration note text.
|
||||
*
|
||||
* @return the migration note text, or an empty string when no note is stored
|
||||
*/
|
||||
public String pendingMigrationMessageText() {
|
||||
return pendingMigrationMessage.orElse("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with different current editable values.
|
||||
*
|
||||
@@ -76,7 +108,7 @@ public record GuiConfigurationEditorState(
|
||||
* @return a new editor state containing the supplied values
|
||||
*/
|
||||
public GuiConfigurationEditorState withValues(GuiConfigurationValues values) {
|
||||
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values);
|
||||
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values, pendingMigrationMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +118,7 @@ public record GuiConfigurationEditorState(
|
||||
* @return a new editor state using the supplied baseline
|
||||
*/
|
||||
public GuiConfigurationEditorState withBaselineValues(GuiConfigurationValues baselineValues) {
|
||||
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values);
|
||||
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values, pendingMigrationMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +128,7 @@ public record GuiConfigurationEditorState(
|
||||
* @return a new editor state linked to the supplied snapshot
|
||||
*/
|
||||
public GuiConfigurationEditorState withLoadedFileSnapshot(GuiConfigurationFileSnapshot snapshot) {
|
||||
return new GuiConfigurationEditorState(Optional.of(snapshot), baselineValues, values);
|
||||
return new GuiConfigurationEditorState(Optional.of(snapshot), baselineValues, values, pendingMigrationMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +137,18 @@ public record GuiConfigurationEditorState(
|
||||
* @return a new editor state without a loaded file snapshot
|
||||
*/
|
||||
public GuiConfigurationEditorState withoutLoadedFileSnapshot() {
|
||||
return new GuiConfigurationEditorState(Optional.empty(), baselineValues, values);
|
||||
return new GuiConfigurationEditorState(Optional.empty(), baselineValues, values, pendingMigrationMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with a migration note that is kept for later display.
|
||||
*
|
||||
* @param migrationMessage the migration note; must not be {@code null}
|
||||
* @return a new editor state carrying the supplied migration note
|
||||
*/
|
||||
public GuiConfigurationEditorState withPendingMigrationMessage(String migrationMessage) {
|
||||
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values,
|
||||
Optional.of(migrationMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +159,7 @@ public record GuiConfigurationEditorState(
|
||||
public GuiConfigurationEditorState markClean() {
|
||||
return baselineValues.equals(values)
|
||||
? this
|
||||
: new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, baselineValues);
|
||||
: new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, baselineValues,
|
||||
pendingMigrationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
|
||||
/**
|
||||
* Creates editor states from loaded properties snapshots or from the empty start state.
|
||||
* <p>
|
||||
* The factory maps the raw `.properties` document into the GUI-side editor model without
|
||||
* introducing validation or effective-value resolution into the file load path.
|
||||
*/
|
||||
public final class GuiConfigurationEditorStateFactory {
|
||||
|
||||
private static final String PROP_SOURCE_FOLDER = "source.folder";
|
||||
private static final String PROP_TARGET_FOLDER = "target.folder";
|
||||
private static final String PROP_SQLITE_FILE = "sqlite.file";
|
||||
private static final String PROP_PROMPT_TEMPLATE_FILE = "prompt.template.file";
|
||||
private static final String PROP_RUNTIME_LOCK_FILE = "runtime.lock.file";
|
||||
private static final String PROP_LOG_DIRECTORY = "log.directory";
|
||||
private static final String PROP_LOG_LEVEL = "log.level";
|
||||
private static final String PROP_MAX_RETRIES_TRANSIENT = "max.retries.transient";
|
||||
private static final String PROP_MAX_PAGES = "max.pages";
|
||||
private static final String PROP_MAX_TEXT_CHARACTERS = "max.text.characters";
|
||||
private static final String PROP_LOG_AI_SENSITIVE = "log.ai.sensitive";
|
||||
private static final String PROP_ACTIVE_PROVIDER = "ai.provider.active";
|
||||
private static final String PROP_CLAUDE_BASE_URL = "ai.provider.claude.baseUrl";
|
||||
private static final String PROP_CLAUDE_MODEL = "ai.provider.claude.model";
|
||||
private static final String PROP_CLAUDE_TIMEOUT = "ai.provider.claude.timeoutSeconds";
|
||||
private static final String PROP_CLAUDE_API_KEY = "ai.provider.claude.apiKey";
|
||||
private static final String PROP_OPENAI_BASE_URL = "ai.provider.openai-compatible.baseUrl";
|
||||
private static final String PROP_OPENAI_MODEL = "ai.provider.openai-compatible.model";
|
||||
private static final String PROP_OPENAI_TIMEOUT = "ai.provider.openai-compatible.timeoutSeconds";
|
||||
private static final String PROP_OPENAI_API_KEY = "ai.provider.openai-compatible.apiKey";
|
||||
|
||||
private GuiConfigurationEditorStateFactory() {
|
||||
// Utility class.
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an editor state from a loaded properties snapshot.
|
||||
*
|
||||
* @param snapshot the loaded file snapshot; must not be {@code null}
|
||||
* @param pendingMigrationMessage optional migration note to retain for later display
|
||||
* @return an editor state representing the loaded configuration file
|
||||
*/
|
||||
public static GuiConfigurationEditorState fromPropertiesSnapshot(
|
||||
GuiConfigurationFileSnapshot snapshot,
|
||||
Optional<String> pendingMigrationMessage) {
|
||||
Properties properties = snapshot.properties();
|
||||
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations = new LinkedHashMap<>();
|
||||
providerConfigurations.put(AiProviderFamily.CLAUDE, new GuiProviderConfigurationState(
|
||||
propertyOrBlank(properties, PROP_CLAUDE_BASE_URL),
|
||||
propertyOrBlank(properties, PROP_CLAUDE_MODEL),
|
||||
propertyOrBlank(properties, PROP_CLAUDE_TIMEOUT),
|
||||
GuiProviderApiKeyState.unresolved(propertyOrBlank(properties, PROP_CLAUDE_API_KEY))));
|
||||
providerConfigurations.put(AiProviderFamily.OPENAI_COMPATIBLE, new GuiProviderConfigurationState(
|
||||
propertyOrBlank(properties, PROP_OPENAI_BASE_URL),
|
||||
propertyOrBlank(properties, PROP_OPENAI_MODEL),
|
||||
propertyOrBlank(properties, PROP_OPENAI_TIMEOUT),
|
||||
GuiProviderApiKeyState.unresolved(propertyOrBlank(properties, PROP_OPENAI_API_KEY))));
|
||||
|
||||
GuiConfigurationValues values = new GuiConfigurationValues(
|
||||
propertyOrBlank(properties, PROP_SOURCE_FOLDER),
|
||||
propertyOrBlank(properties, PROP_TARGET_FOLDER),
|
||||
propertyOrBlank(properties, PROP_SQLITE_FILE),
|
||||
propertyOrBlank(properties, PROP_PROMPT_TEMPLATE_FILE),
|
||||
propertyOrBlank(properties, PROP_RUNTIME_LOCK_FILE),
|
||||
propertyOrBlank(properties, PROP_LOG_DIRECTORY),
|
||||
propertyOrBlank(properties, PROP_LOG_LEVEL),
|
||||
propertyOrBlank(properties, PROP_MAX_RETRIES_TRANSIENT),
|
||||
propertyOrBlank(properties, PROP_MAX_PAGES),
|
||||
propertyOrBlank(properties, PROP_MAX_TEXT_CHARACTERS),
|
||||
propertyOrBlank(properties, PROP_LOG_AI_SENSITIVE),
|
||||
propertyOrBlank(properties, PROP_ACTIVE_PROVIDER),
|
||||
providerConfigurations);
|
||||
return new GuiConfigurationEditorState(Optional.of(snapshot), values, values, pendingMigrationMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the empty editor state used at application startup when no configuration is loaded.
|
||||
*
|
||||
* @return a blank editor state
|
||||
*/
|
||||
public static GuiConfigurationEditorState createBlankStartState() {
|
||||
return GuiConfigurationTemplateFactory.createBlankStartState();
|
||||
}
|
||||
|
||||
private static String propertyOrBlank(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+28
-1
@@ -44,7 +44,34 @@ public final class GuiConfigurationTemplateFactory {
|
||||
*/
|
||||
public static GuiConfigurationEditorState createStandardTemplate() {
|
||||
GuiConfigurationValues standardValues = createStandardValues();
|
||||
return new GuiConfigurationEditorState(Optional.empty(), standardValues, standardValues);
|
||||
return new GuiConfigurationEditorState(Optional.empty(), standardValues, standardValues, Optional.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the empty editor state used when the GUI starts without a loaded configuration.
|
||||
* <p>
|
||||
* This start state intentionally does not show the standard template yet. The template
|
||||
* is reserved for the explicit {@code Neu} action so the GUI starts without an implicit
|
||||
* draft and only shows the welcome guidance until the user requests a new configuration.
|
||||
*
|
||||
* @return a clean editor state without a loaded file snapshot and without template values
|
||||
*/
|
||||
public static GuiConfigurationEditorState createBlankStartState() {
|
||||
GuiConfigurationValues blankValues = new GuiConfigurationValues(
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
Map.of());
|
||||
return new GuiConfigurationEditorState(Optional.empty(), blankValues, blankValues, Optional.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+128
-35
@@ -1,15 +1,27 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationFileSnapshot;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationTemplateFactory;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.MultiProviderConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Label;
|
||||
|
||||
@@ -37,6 +49,8 @@ import org.junit.jupiter.api.TestMethodOrder;
|
||||
* <li>{@link GuiAdapter} can be constructed without triggering the JavaFX runtime.</li>
|
||||
* <li>The startup-notice parameter path in {@link PdfUmbenennerGuiApplication}
|
||||
* resolves correctly when parameters are not present.</li>
|
||||
* <li>The editor workspace exposes the empty start state and the explicit
|
||||
* {@code Neu} transition to the standard template.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Excluded from this scope</h2>
|
||||
@@ -169,43 +183,124 @@ class GuiAdapterSmokeTest {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Startup notice parameter extraction
|
||||
// Startup context
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Verifies that the startup-notice argument prefix constant in
|
||||
* {@link PdfUmbenennerGuiApplication} is non-blank and begins with {@code --}.
|
||||
* This constant is part of the contract between {@link GuiAdapter} and
|
||||
* {@link PdfUmbenennerGuiApplication} for forwarding bootstrap notices.
|
||||
* Verifies that a blank GUI startup context starts without a loaded configuration
|
||||
* while still carrying a startup notice when one is supplied.
|
||||
*/
|
||||
@Test
|
||||
@Order(4)
|
||||
void startupNoticeArgPrefix_isNonBlankAndStartsWithDoubleDash() {
|
||||
String prefix = PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX;
|
||||
assertNotNull(prefix, "STARTUP_NOTICE_ARG_PREFIX must not be null");
|
||||
assertFalse(prefix.isBlank(), "STARTUP_NOTICE_ARG_PREFIX must not be blank");
|
||||
assertTrue(prefix.startsWith("--"),
|
||||
"STARTUP_NOTICE_ARG_PREFIX must start with '--' to be recognizable as an application parameter");
|
||||
void startupContext_blankContextKeepsNoticeAndUsesEmptyState() {
|
||||
GuiStartupContext context = GuiStartupContext.blank(Optional.of("Hinweis"));
|
||||
|
||||
assertTrue(context.startupNotice().isPresent());
|
||||
assertEquals("Hinweis", context.startupNotice().orElseThrow());
|
||||
assertFalse(context.initialState().hasLoadedFileSnapshot());
|
||||
assertEquals("", context.initialState().configurationPathText());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Editor workspace structure
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Verifies that the startup-notice forwarding contract between {@link GuiAdapter}
|
||||
* and {@link PdfUmbenennerGuiApplication} is consistent: when a notice is present,
|
||||
* the argument is constructed by prepending the prefix to the notice text.
|
||||
* Verifies that the editor workspace starts without a loaded configuration, shows the
|
||||
* welcome guidance, and exposes the fixed GUI structure of the current shell.
|
||||
*
|
||||
* @throws Exception if the FX thread task fails or times out
|
||||
*/
|
||||
@Test
|
||||
@Order(5)
|
||||
void startupNotice_argIsConstructedByPrependingPrefix() {
|
||||
String noticeText = "Konfigurationsdatei nicht gefunden: /pfad/zur/datei.properties";
|
||||
String expectedArg = PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX + noticeText;
|
||||
void editorWorkspace_startStateShowsEmptyHeaderWelcomeGuidanceAndOneTab() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<Throwable> fxError = new AtomicReference<>();
|
||||
AtomicReference<GuiConfigurationEditorWorkspace> workspaceReference = new AtomicReference<>();
|
||||
|
||||
// Verify the concatenation produces a parseable argument string
|
||||
assertTrue(expectedArg.startsWith(PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX),
|
||||
"The constructed notice argument must start with the prefix");
|
||||
String extractedNotice = expectedArg.substring(
|
||||
PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX.length());
|
||||
assertEquals(noticeText, extractedNotice,
|
||||
"The notice text must be recoverable from the argument by stripping the prefix");
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(Optional.empty());
|
||||
workspaceReference.set(workspace);
|
||||
|
||||
assertEquals("", workspace.configurationPathText(),
|
||||
"The header path must stay empty before any configuration is loaded");
|
||||
assertTrue(workspace.isWelcomeGuidanceVisible(),
|
||||
"The welcome guidance must be visible in the unloaded start state");
|
||||
assertTrue(workspace.welcomeText().contains("Willkommen"),
|
||||
"The welcome text must be shown in German");
|
||||
assertNotNull(workspace.root(),
|
||||
"The workspace root must be available");
|
||||
assertEquals("Neu", workspace.newButton().getText(),
|
||||
"The 'Neu' button must be visible");
|
||||
assertEquals("Öffnen", workspace.openButton().getText(),
|
||||
"The 'Öffnen' button must be visible");
|
||||
assertEquals("Speichern", workspace.saveButton().getText(),
|
||||
"The 'Speichern' button must be visible");
|
||||
assertEquals("Speichern unter", workspace.saveAsButton().getText(),
|
||||
"The 'Speichern unter' button must be visible");
|
||||
assertEquals(1, workspace.tabPane().getTabs().size(),
|
||||
"Exactly one configuration tab must be present");
|
||||
assertEquals("Konfiguration", workspace.tabPane().getTabs().get(0).getText(),
|
||||
"The single tab must use the configuration label");
|
||||
assertEquals(
|
||||
"Pfade,Provider,Verarbeitungslimits,Tests,Meldungen",
|
||||
String.join(",", workspace.sectionTitles()),
|
||||
"The single tab must expose the fixed section structure in the documented order");
|
||||
} catch (Throwable t) {
|
||||
fxError.set(t);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(
|
||||
latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
|
||||
"FX thread task must complete within timeout");
|
||||
if (fxError.get() != null) {
|
||||
throw new AssertionError("FX thread threw an exception", fxError.get());
|
||||
}
|
||||
assertNotNull(workspaceReference.get(),
|
||||
"The workspace must have been created successfully");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
void editorWorkspace_newActionSwitchesToStandardTemplateWithoutPath() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<Throwable> fxError = new AtomicReference<>();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(Optional.empty());
|
||||
workspace.newButton().fire();
|
||||
workspace.saveButton().fire();
|
||||
workspace.saveAsButton().fire();
|
||||
|
||||
GuiConfigurationEditorState state = workspace.editorState();
|
||||
assertFalse(workspace.isWelcomeGuidanceVisible(),
|
||||
"The welcome guidance should disappear once the user creates a new template");
|
||||
assertEquals("", state.configurationPathText(),
|
||||
"Creating a new template must not imply a loaded file path");
|
||||
assertEquals("./work/local/source", state.values().sourceFolder(),
|
||||
"The standard template must populate the documented default source folder");
|
||||
assertEquals("./work/local/target", state.values().targetFolder(),
|
||||
"The standard template must populate the documented default target folder");
|
||||
assertEquals("INFO", state.values().logLevel(),
|
||||
"The standard template must remain intact after the wired placeholder actions");
|
||||
} catch (Throwable t) {
|
||||
fxError.set(t);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(
|
||||
latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
|
||||
"FX thread task must complete within timeout");
|
||||
if (fxError.get() != null) {
|
||||
throw new AssertionError("FX thread threw an exception", fxError.get());
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -222,7 +317,7 @@ class GuiAdapterSmokeTest {
|
||||
* bootstrap module covers the full launch path via the executable JAR.
|
||||
*/
|
||||
@Test
|
||||
@Order(6)
|
||||
@Order(8)
|
||||
void guiAdapter_startWithEmptyNotice_constructsNoNoticeArg() {
|
||||
// Verify the contract: when notice is empty, no prefix-based arg is constructed.
|
||||
Optional<String> emptyNotice = Optional.empty();
|
||||
@@ -230,16 +325,14 @@ class GuiAdapterSmokeTest {
|
||||
"When no notice is supplied, the Optional must be empty and no notice arg is constructed");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Asserts equality with a descriptive message; delegates to JUnit's assertEquals.
|
||||
*/
|
||||
private static void assertEquals(String expected, String actual, String message) {
|
||||
if (!expected.equals(actual)) {
|
||||
throw new AssertionError(message + " — expected: <" + expected + "> but was: <" + actual + ">");
|
||||
private static void waitFor(BooleanSupplier condition, long timeoutSeconds) throws InterruptedException {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds);
|
||||
while (System.nanoTime() < deadline) {
|
||||
if (condition.getAsBoolean()) {
|
||||
return;
|
||||
}
|
||||
Thread.sleep(20L);
|
||||
}
|
||||
throw new AssertionError("Condition did not become true within timeout");
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class GuiConfigurationEditorStateTest {
|
||||
@@ -53,6 +55,25 @@ class GuiConfigurationEditorStateTest {
|
||||
assertFalse(withSnapshot.isNewConfiguration());
|
||||
assertEquals(snapshot, withSnapshot.loadedFileSnapshot().orElseThrow());
|
||||
assertFalse(withSnapshot.isDirty());
|
||||
assertEquals(Path.of("config/application.properties").toString(), withSnapshot.configurationPathText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void configurationPathTextIsEmptyWhenNoFileIsLoaded() {
|
||||
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createBlankStartState();
|
||||
|
||||
assertEquals("", state.configurationPathText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingMigrationMessageCanBeStoredWithoutChangingDirtyState() {
|
||||
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
|
||||
GuiConfigurationEditorState migrated = state.withPendingMigrationMessage("Legacy import");
|
||||
|
||||
assertTrue(migrated.hasPendingMigrationMessage());
|
||||
assertEquals("Legacy import", migrated.pendingMigrationMessageText());
|
||||
assertFalse(migrated.isDirty());
|
||||
assertEquals(state.values(), migrated.values());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+26
@@ -22,6 +22,7 @@ class GuiConfigurationTemplateFactoryTest {
|
||||
assertFalse(state.isDirty());
|
||||
assertFalse(state.hasLoadedFileSnapshot());
|
||||
assertTrue(state.isNewConfiguration());
|
||||
assertFalse(state.hasPendingMigrationMessage());
|
||||
|
||||
GuiConfigurationValues values = state.values();
|
||||
assertEquals("./work/local/source", values.sourceFolder());
|
||||
@@ -57,6 +58,31 @@ class GuiConfigurationTemplateFactoryTest {
|
||||
assertEquals(GuiValueOrigin.UNKNOWN, openAi.apiKey().effectiveValueOrigin());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createBlankStartState_startsWithoutLoadedConfigurationAndWithoutTemplateValues() {
|
||||
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createBlankStartState();
|
||||
|
||||
assertFalse(state.isDirty());
|
||||
assertFalse(state.hasLoadedFileSnapshot());
|
||||
assertTrue(state.isNewConfiguration());
|
||||
assertEquals("", state.configurationPathText());
|
||||
|
||||
GuiConfigurationValues values = state.values();
|
||||
assertEquals("", values.sourceFolder());
|
||||
assertEquals("", values.targetFolder());
|
||||
assertEquals("", values.sqliteFile());
|
||||
assertEquals("", values.promptTemplateFile());
|
||||
assertEquals("", values.runtimeLockFile());
|
||||
assertEquals("", values.logDirectory());
|
||||
assertEquals("", values.logLevel());
|
||||
assertEquals("", values.maxRetriesTransient());
|
||||
assertEquals("", values.maxPages());
|
||||
assertEquals("", values.maxTextCharacters());
|
||||
assertEquals("", values.logAiSensitive());
|
||||
assertEquals("", values.activeProviderFamily());
|
||||
assertTrue(values.providerConfigurations().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void providerConfigurationMap_isImmutableFromOutside() {
|
||||
GuiConfigurationValues values = GuiConfigurationTemplateFactory.createStandardValues();
|
||||
|
||||
Reference in New Issue
Block a user