feature/github-copilot-consolidation #1

Merged
konrad merged 10 commits from feature/github-copilot-consolidation into main 2025-12-14 13:29:43 +01:00
5 changed files with 236 additions and 42 deletions
Showing only changes of commit 3178dc2723 - Show all commits

View File

@ -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 <T> the type of the underlying model
*/
@SuppressWarnings("unused")
@Slf4j
public class AutoViewModel<T> {
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<T> {
initProperties();
}
/**
* Initializes a mapping of field names to JavaFX properties for the associated model object.
* <p>
* 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:
* <p>
* 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.
* <p>
* 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<T> {
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<T> {
}
}
/**
* 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<T> {
&& (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<T> {
// ========== 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<T> {
return new SimpleObjectProperty<>(value);
}
/**
* Finds a setter method in a specified class that corresponds to the given field name and value type.
* <p>
* 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<T> {
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;
}
}

View File

@ -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;
}
}

View File

@ -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 <T> 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 extends Parent> 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 <T> the type of the source property's value
*/
@SuppressWarnings("unchecked")
private static <T> void bindBidirectionalSafe(@NotNull Property<T> 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 <T> the type of the source property's value
*/
@SuppressWarnings("unchecked")
private static <T> void bindSafe(@NotNull Property<T> 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<BindingData> bindings, Map<String, Object> 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<String, Object> 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<Node> collectAllNodes(Node root) {
List<Node> 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);
}

View File

@ -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<Object> data = new SimpleObjectProperty<>();
/**

View File

@ -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<T> {
*/
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<T> {
if (userData instanceof String propertyName) {
BindDirection direction = getDirection(node);
if (node instanceof TextInputControl control) {
Property<String> prop = viewModel.property(StringProperty.class, propertyName);
bind(control.textProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.Label label) {
Property<String> prop = viewModel.property(StringProperty.class, propertyName);
bind(label.textProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.CheckBox checkBox) {
Property<Boolean> prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName);
bind(checkBox.selectedProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.Slider slider) {
Property<Number> 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<java.time.LocalDate> prop = (Property<java.time.LocalDate>) viewModel.property(javafx.beans.property.ObjectProperty.class, propertyName);
bind(datePicker.valueProperty(), prop, direction);
switch (node) {
case TextInputControl control -> {
Property<String> prop = viewModel.property(StringProperty.class, propertyName);
bind(control.textProperty(), prop, direction);
}
case javafx.scene.control.Label label -> {
Property<String> prop = viewModel.property(StringProperty.class, propertyName);
bind(label.textProperty(), prop, direction);
}
case javafx.scene.control.CheckBox checkBox -> {
Property<Boolean> prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName);
bind(checkBox.selectedProperty(), prop, direction);
}
case javafx.scene.control.Slider slider -> {
Property<Number> prop = viewModel.property(javafx.beans.property.DoubleProperty.class, propertyName);
bind(slider.valueProperty(), prop, direction);
}
case javafx.scene.control.DatePicker datePicker -> {
@SuppressWarnings("unchecked")
Property<java.time.LocalDate> prop =
(Property<java.time.LocalDate>) viewModel.property(
javafx.beans.property.ObjectProperty.class,
propertyName
);
bind(datePicker.valueProperty(), prop, direction);
}
default -> {
// ignore unsupported nodes
}
}
}
}