From 3178dc27239006a03cbb631c6ac17a1e3b9e92fd Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Sun, 30 Nov 2025 20:40:26 +0100 Subject: [PATCH] Refactorings ... --- .../neitzel/fx/component/AutoViewModel.java | 102 ++++++++++++++++-- .../fx/component/ComponentController.java | 12 ++- .../neitzel/fx/component/ComponentLoader.java | 97 ++++++++++++++++- .../fx/component/controls/FxmlComponent.java | 9 -- .../fx/mvvm/BindingAwareFXMLLoader.java | 58 ++++++---- 5 files changed, 236 insertions(+), 42 deletions(-) diff --git a/fx/src/main/java/de/neitzel/fx/component/AutoViewModel.java b/fx/src/main/java/de/neitzel/fx/component/AutoViewModel.java index 64718a0..3f34725 100644 --- a/fx/src/main/java/de/neitzel/fx/component/AutoViewModel.java +++ b/fx/src/main/java/de/neitzel/fx/component/AutoViewModel.java @@ -8,13 +8,13 @@ import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; /** * AutoViewModel automatically exposes JavaFX properties for all readable/writable fields @@ -23,14 +23,15 @@ import java.util.logging.Logger; * * @param the type of the underlying model */ -@SuppressWarnings("unused") +@Slf4j public class AutoViewModel { - private static final Logger LOGGER = Logger.getLogger(AutoViewModel.class.getName()); - /** * The wrapped model instance. + * -- GETTER -- + * Retrieves the model associated with this AutoViewModel. */ + @Getter private final T model; /** @@ -48,6 +49,22 @@ public class AutoViewModel { initProperties(); } + /** + * Initializes a mapping of field names to JavaFX properties for the associated model object. + *

+ * This method utilizes reflection to iterate through the public no-argument getters + * of the model's class to create corresponding JavaFX properties. It performs the following: + *

+ * 1. Identifies methods that adhere to the getter naming conventions (e.g., `getFieldName` or `isFieldName`). + * 2. Maps the field name derived from the getter method to a JavaFX property, creating an appropriate + * property type based on the getter's return type. + * 3. Adds a listener to each property to bind updates from the property back to the model object + * using the corresponding setter method, if available. This ensures bi-directional synchronization + * between the ViewModel and the underlying model. + *

+ * Exceptions that may occur during reflective operations (e.g., invoking methods) are caught + * and logged to avoid runtime failures. + */ private void initProperties() { for (Method getter : model.getClass().getMethods()) { if (isGetter(getter)) { @@ -68,7 +85,7 @@ public class AutoViewModel { try { setter.invoke(model, newVal); } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to invoke setter for field " + fieldName, e); + log.warn("Failed to invoke setter for field {}", fieldName, e); } } }); @@ -76,6 +93,14 @@ public class AutoViewModel { } } + /** + * Determines if a given method follows the JavaBean getter convention. + * The method must be public, take no parameters, have a non-void return type, + * and its name should start with either "get" or "is". + * + * @param method the method to be assessed + * @return true if the method adheres to the JavaBean getter convention, false otherwise + */ private boolean isGetter(Method method) { return Modifier.isPublic(method.getModifiers()) && method.getParameterCount() == 0 @@ -83,6 +108,14 @@ public class AutoViewModel { && (method.getName().startsWith("get") || method.getName().startsWith("is")); } + /** + * Derives the field name from a given Java method by following JavaBean naming conventions. + * The method's name is analyzed to strip the "get" or "is" prefix (if present), + * and the result is converted into a decapitalized field name. + * + * @param method the method whose name is to be processed to derive the field name + * @return the derived field name, or the original method name if no "get" or "is" prefix is found + */ private String getFieldName(Method method) { String name = method.getName(); if (name.startsWith("get")) { @@ -95,15 +128,32 @@ public class AutoViewModel { // ========== Hilfsmethoden ========== + /** + * Invokes the specified method on the model object and returns the result. + * If the method invocation fails, logs a warning and returns null. + * + * @param method the method to be invoked, typically a JavaBean getter + * @return the result of invoking the method, or null if an error occurs + */ private Object invokeGetter(Method method) { try { return method.invoke(model); } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to invoke getter: " + method.getName(), e); + log.warn("Failed to invoke getter: {}", method.getName(), e); return null; } } + /** + * Converts an object into an appropriate JavaFX {@link Property} based on its type. + * + * @param value the object to be converted into a JavaFX property; can be of type String, Integer, + * Boolean, Double, Float, Long, or any other object + * @return a JavaFX {@link Property} corresponding to the type of the input object, such as + * {@link SimpleStringProperty} for String, {@link SimpleIntegerProperty} for Integer, + * {@link SimpleBooleanProperty} for Boolean, and so on. If the type is not explicitly + * handled, a {@link SimpleObjectProperty} is returned wrapping the object + */ private Property toProperty(Object value) { if (value instanceof String s) return new SimpleStringProperty(s); if (value instanceof Integer i) return new SimpleIntegerProperty(i); @@ -114,6 +164,19 @@ public class AutoViewModel { return new SimpleObjectProperty<>(value); } + /** + * Finds a setter method in a specified class that corresponds to the given field name and value type. + *

+ * The method searches for a public method in the class whose name matches the JavaBean-style setter + * naming convention (e.g., "setFieldName"). The method must accept exactly one parameter, and if a + * value type is provided, the parameter type must be assignable from it. + * + * @param clazz the class to search for the method + * @param fieldName the name of the field for which the setter method is being sought + * @param valueType the expected type of the parameter for the setter method, or null if no specific + * type constraint is required + * @return the setter method if found, or null if no matching method is identified + */ private Method findSetterFor(Class clazz, String fieldName, Class valueType) { String setterName = "set" + capitalize(fieldName); for (Method m : clazz.getMethods()) { @@ -126,21 +189,38 @@ public class AutoViewModel { return null; } + /** + * Converts the first character of the given string to lowercase, leaving the rest of the string unchanged. + * If the input string is null or empty, it is returned as-is. + * + * @param str the string to be decapitalized + * @return the decapitalized string, or the original string if it is null or empty + */ private String decapitalize(String str) { if (str == null || str.isEmpty()) return str; return str.substring(0, 1).toLowerCase() + str.substring(1); } + /** + * Capitalizes the first letter of the given string and leaves the rest of the string unchanged. + * If the input string is null or empty, it is returned as is. + * + * @param str the string to be capitalized + * @return the input string with its first character converted to uppercase, or the original string if it is null or empty + */ private String capitalize(String str) { if (str == null || str.isEmpty()) return str; return str.substring(0, 1).toUpperCase() + str.substring(1); } + /** + * Retrieves the JavaFX property associated with the given field name. + * + * @param name the name of the field whose associated JavaFX property is to be returned + * @return the JavaFX {@link Property} associated with the specified field name, + * or null if no property exists for the given name + */ public Property getProperty(String name) { return properties.get(name); } - - public T getModel() { - return model; - } } diff --git a/fx/src/main/java/de/neitzel/fx/component/ComponentController.java b/fx/src/main/java/de/neitzel/fx/component/ComponentController.java index 9231af0..37d636f 100644 --- a/fx/src/main/java/de/neitzel/fx/component/ComponentController.java +++ b/fx/src/main/java/de/neitzel/fx/component/ComponentController.java @@ -1,7 +1,6 @@ package de.neitzel.fx.component; import lombok.Getter; -import lombok.RequiredArgsConstructor; /** * Generic controller used by the ComponentLoader to bind FXML views @@ -10,7 +9,6 @@ import lombok.RequiredArgsConstructor; * This controller provides access to the {@link AutoViewModel} * which contains JavaFX properties derived from the model. */ -@RequiredArgsConstructor public class ComponentController { /** @@ -20,4 +18,14 @@ public class ComponentController { */ @Getter private final AutoViewModel viewModel; + + /** + * Constructs a new ComponentController instance with the specified AutoViewModel. + * + * @param viewModel the AutoViewModel containing JavaFX properties derived from the model, + * used for binding the view to the model. + */ + public ComponentController(AutoViewModel viewModel) { + this.viewModel = viewModel; + } } diff --git a/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java b/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java index 6dd36aa..466fca2 100644 --- a/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java +++ b/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java @@ -7,7 +7,6 @@ import javafx.beans.property.Property; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.Parent; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @@ -26,7 +25,11 @@ import java.util.Objects; @Slf4j public class ComponentLoader { - @Getter + /** + * The controller object associated with the ComponentLoader. + * It is typically used to manage the behavior and interactions of the UI components + * defined in an FXML file. + */ private Object controller; /** @@ -36,6 +39,18 @@ public class ComponentLoader { // default constructor only } + /** + * Loads an FXML file from the specified URL, sets the provided controller, and processes bindings. + * This method parses the FXML file, initializes the specified controller, and evaluates binding controls + * defined within the FXML namespace. + * + * @param the type of the root node, which must extend {@code Parent} + * @param fxmlUrl the URL of the FXML file to be loaded + * @param controller the controller to associate with the FXML file + * @param nothing an unused parameter, included for compatibility + * @return the root node loaded from the FXML file + * @throws IOException if there is an error reading the FXML file + */ public static T load(URL fxmlUrl, Object controller, @SuppressWarnings("unused") String nothing) throws IOException { FXMLLoader loader = new FXMLLoader(fxmlUrl); loader.setController(controller); @@ -56,6 +71,15 @@ public class ComponentLoader { return root; } + /** + * Safely binds two properties bidirectionally while ensuring type compatibility. + * If the target property's type is not compatible with the source property's type, the binding will + * not be performed, and an error will be logged. + * + * @param source the source property to bind, must not be null + * @param target the target property to bind to; the type will be checked for compatibility at runtime + * @param the type of the source property's value + */ @SuppressWarnings("unchecked") private static void bindBidirectionalSafe(@NotNull Property source, Property target) { try { @@ -66,6 +90,15 @@ public class ComponentLoader { } } + /** + * Attempts to bind the source property to the target property in a type-safe manner. + * If the target property's type is incompatible with the source property's type, the binding operation + * will be skipped, and an error message will be logged. + * + * @param source the source property to bind, must not be null + * @param target the target property to which the source will be bound + * @param the type of the source property's value + */ @SuppressWarnings("unchecked") private static void bindSafe(@NotNull Property source, Property target) { try { @@ -76,6 +109,14 @@ public class ComponentLoader { } } + /** + * Evaluates and establishes bindings between source and target properties based on the provided + * bindings list and namespace. Supports unidirectional and bidirectional bindings, ensuring type + * compatibility before binding. Logs errors for unsuccessful binding attempts or exceptions. + * + * @param bindings a list of BindingData objects that define the source, target, and direction for the bindings + * @param namespace a map representing the namespace from which expressions in the bindings are resolved + */ private static void evaluateBindings(List bindings, Map namespace) { for (var binding : bindings) { try { @@ -114,6 +155,12 @@ public class ComponentLoader { } } + /** + * Determines the type of the value held by the specified property. + * + * @param prop the property whose value type is to be determined, must not be null + * @return the class of the property's value type; returns Object.class if the type cannot be determined + */ private static Class getPropertyType(Property prop) { try { Method getter = prop.getClass().getMethod("get"); @@ -123,6 +170,15 @@ public class ComponentLoader { } } + /** + * Resolves a dot-separated expression (e.g., "viewModel.username") by navigating through the provided namespace + * map and invoking the corresponding getter methods to access nested properties. + * + * @param expr the dot-separated expression to resolve, must not be null + * @param namespace a map containing the initial objects to resolve the expression from, must not be null + * @return the value resolved from the specified expression + * @throws Exception if an error occurs while invoking getter methods or resolving the expression + */ private static Object resolveExpression(@NotNull String expr, @NotNull Map namespace) throws Exception { // z.B. "viewModel.username" String[] parts = expr.split("\\."); @@ -134,6 +190,12 @@ public class ComponentLoader { return current; } + /** + * Recursively collects all nodes in a scene graph starting from the given root node. + * + * @param root the starting node from which all descendant nodes will be collected + * @return a list containing all nodes, including the root node and its descendants + */ private static @NotNull List collectAllNodes(Node root) { List nodes = new ArrayList<>(); nodes.add(root); @@ -145,10 +207,35 @@ public class ComponentLoader { return nodes; } + /** + * Returns the controller associated with the ComponentLoader. + * + * @return the controller object + */ + public Object getController() { + return controller; + } + + /** + * Loads an FXML file and returns the root JavaFX node without binding to a data model. + * This method uses the given {@code fxmlPath} to locate and load the FXML file. + * + * @param fxmlPath the URL of the FXML file to be loaded + * @return the root JavaFX node loaded from the specified FXML file + */ public Parent load(URL fxmlPath) { return load(null, fxmlPath); } + /** + * Loads an FXML file and binds its elements to a generated ViewModel + * based on the given POJO model and specified FXML path. + * + * @param model the data model (POJO) to bind to the UI + * @param fxmlPath the URL path to the FXML file + * @return the root JavaFX node loaded from FXML + * @throws RuntimeException if the FXML could not be loaded + */ public Parent load(Object model, URL fxmlPath) { try { AutoViewModel viewModel = new AutoViewModel<>(model); @@ -165,6 +252,12 @@ public class ComponentLoader { } } + /** + * Loads an FXML file and returns its root node. + * + * @param fxmlPath the relative path to the FXML file + * @return the root JavaFX node loaded from the FXML file + */ public Parent load(String fxmlPath) { return load(null, fxmlPath); } diff --git a/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java b/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java index 7b4f564..70c9617 100644 --- a/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java +++ b/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java @@ -27,19 +27,10 @@ import java.util.Arrays; */ public class FxmlComponent extends StackPane { - /** - * The FXML resource path to be loaded by this component. A value of {@code null} or blank disables loading. - */ private final StringProperty fxml = new SimpleStringProperty(); - /** - * Human-readable binding direction hint used by outer frameworks; defaults to "unidirectional". - */ private final StringProperty direction = new SimpleStringProperty("unidirectional"); - /** - * Optional data object that is injected into the controller of the loaded FXML when available. - */ private final ObjectProperty data = new SimpleObjectProperty<>(); /** diff --git a/fx/src/main/java/de/neitzel/fx/mvvm/BindingAwareFXMLLoader.java b/fx/src/main/java/de/neitzel/fx/mvvm/BindingAwareFXMLLoader.java index b4012bc..04c4a56 100644 --- a/fx/src/main/java/de/neitzel/fx/mvvm/BindingAwareFXMLLoader.java +++ b/fx/src/main/java/de/neitzel/fx/mvvm/BindingAwareFXMLLoader.java @@ -6,11 +6,9 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.TextInputControl; -import lombok.RequiredArgsConstructor; import java.io.InputStream; -@RequiredArgsConstructor /** * Custom FXMLLoader that binds JavaFX controls to a GenericViewModel using metadata from FXML. * It supports automatic binding setup based on properties defined in the FXML's node properties. @@ -24,6 +22,17 @@ public class BindingAwareFXMLLoader { */ private final T model; + /** + * Constructs a new instance of the BindingAwareFXMLLoader class. + * This loader is designed to load FXML files and automatically bind UI elements + * to the properties of the provided model using a binding-aware approach. + * + * @param model the model object to be used for binding to the UI components + */ + public BindingAwareFXMLLoader(T model) { + this.model = model; + } + /** * Loads an FXML file and performs automatic binding setup using the GenericViewModel. * @@ -68,22 +77,35 @@ public class BindingAwareFXMLLoader { if (userData instanceof String propertyName) { BindDirection direction = getDirection(node); - if (node instanceof TextInputControl control) { - Property prop = viewModel.property(StringProperty.class, propertyName); - bind(control.textProperty(), prop, direction); - } else if (node instanceof javafx.scene.control.Label label) { - Property prop = viewModel.property(StringProperty.class, propertyName); - bind(label.textProperty(), prop, direction); - } else if (node instanceof javafx.scene.control.CheckBox checkBox) { - Property prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName); - bind(checkBox.selectedProperty(), prop, direction); - } else if (node instanceof javafx.scene.control.Slider slider) { - Property prop = viewModel.property(javafx.beans.property.DoubleProperty.class, propertyName); - bind(slider.valueProperty(), prop, direction); - } else if (node instanceof javafx.scene.control.DatePicker datePicker) { - @SuppressWarnings("unchecked") - Property prop = (Property) viewModel.property(javafx.beans.property.ObjectProperty.class, propertyName); - bind(datePicker.valueProperty(), prop, direction); + switch (node) { + case TextInputControl control -> { + Property prop = viewModel.property(StringProperty.class, propertyName); + bind(control.textProperty(), prop, direction); + } + case javafx.scene.control.Label label -> { + Property prop = viewModel.property(StringProperty.class, propertyName); + bind(label.textProperty(), prop, direction); + } + case javafx.scene.control.CheckBox checkBox -> { + Property prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName); + bind(checkBox.selectedProperty(), prop, direction); + } + case javafx.scene.control.Slider slider -> { + Property prop = viewModel.property(javafx.beans.property.DoubleProperty.class, propertyName); + bind(slider.valueProperty(), prop, direction); + } + case javafx.scene.control.DatePicker datePicker -> { + @SuppressWarnings("unchecked") + Property prop = + (Property) viewModel.property( + javafx.beans.property.ObjectProperty.class, + propertyName + ); + bind(datePicker.valueProperty(), prop, direction); + } + default -> { + // ignore unsupported nodes + } } } }