Remove the replaced JavaFX units so the view library is the only fx API.

fx-example still calls those units, so it leaves the reactor until it is migrated.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-30 18:35:18 +01:00
co-authored by Cursor
parent 5c7da8673e
commit eaebe66b0d
14 changed files with 1 additions and 1697 deletions
@@ -1,226 +0,0 @@
package de.neitzel.fx.component;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleFloatProperty;
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;
/**
* AutoViewModel automatically exposes JavaFX properties for all readable/writable fields
* of a given POJO model. It creates appropriate Property instances and keeps them in a map
* for lookup by field name.
*
* @param <T> the type of the underlying model
*/
@Slf4j
public class AutoViewModel<T> {
/**
* The wrapped model instance.
* -- GETTER --
* Retrieves the model associated with this AutoViewModel.
*/
@Getter
private final T model;
/**
* Map of field name to JavaFX Property instance exposing that field's value.
*/
private final Map<String, Property<?>> properties = new HashMap<>();
/**
* Constructs an AutoViewModel for the provided model and initializes properties via reflection.
*
* @param model the POJO model whose getters/setters are used to create JavaFX properties
*/
public AutoViewModel(T model) {
this.model = model;
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)) {
String fieldName = getFieldName(getter);
Object value = invokeGetter(getter);
Property<?> prop = toProperty(value);
properties.put(fieldName, prop);
// Bind ViewModel → Model
prop.addListener((_obs, _oldVal, newVal) -> {
// use _obs and _oldVal to satisfy static analysis (they are intentionally retained)
if (_oldVal != null && _oldVal.equals(newVal)) {
// no-op: values unchanged
}
Method setter = findSetterFor(model.getClass(), fieldName, newVal != null ? newVal.getClass() : null);
if (setter != null) {
try {
setter.invoke(model, newVal);
} catch (Exception e) {
log.warn("Failed to invoke setter for field {}", fieldName, e);
}
}
});
}
}
}
/**
* 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
&& !method.getReturnType().equals(void.class)
&& (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")) {
return decapitalize(name.substring(3));
} else if (name.startsWith("is")) {
return decapitalize(name.substring(2));
}
return name;
}
// ========== 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) {
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);
if (value instanceof Boolean b) return new SimpleBooleanProperty(b);
if (value instanceof Double d) return new SimpleDoubleProperty(d);
if (value instanceof Float f) return new SimpleFloatProperty(f);
if (value instanceof Long l) return new SimpleLongProperty(l);
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()) {
if (m.getName().equals(setterName) && m.getParameterCount() == 1) {
if (valueType == null || m.getParameterTypes()[0].isAssignableFrom(valueType)) {
return m;
}
}
}
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);
}
}
@@ -1,31 +0,0 @@
package de.neitzel.fx.component;
import lombok.Getter;
/**
* Generic controller used by the ComponentLoader to bind FXML views
* to automatically generated ViewModels based on POJO models.
* <p>
* This controller provides access to the {@link AutoViewModel}
* which contains JavaFX properties derived from the model.
*/
public class ComponentController {
/**
* The automatically generated ViewModel that holds JavaFX properties
* for all model fields. It is used to create bindings between
* the view and the model.
*/
@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;
}
}
@@ -1,295 +0,0 @@
package de.neitzel.fx.component;
import de.neitzel.fx.component.controls.Binding;
import de.neitzel.fx.component.controls.FxmlComponent;
import de.neitzel.fx.component.model.BindingData;
import javafx.beans.property.Property;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* ComponentLoader is responsible for loading JavaFX FXML components and binding
* them to automatically generated ViewModels based on simple POJO models.
*/
@Slf4j
public class ComponentLoader {
/**
* 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;
/**
* Default constructor only
*/
public 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);
T root = loader.load();
Map<String, Object> namespace = loader.getNamespace();
// Nach allen BindingControls suchen:
List<Binding> bindingControls = collectAllNodes(root).stream()
.filter(n -> n instanceof Binding)
.map(n -> (Binding) n)
.toList();
for (Binding bc : bindingControls) {
evaluateBindings(bc.getBindings(), namespace);
}
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 {
Property<T> targetCasted = (Property<T>) target;
source.bindBidirectional(targetCasted);
} catch (ClassCastException e) {
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
}
}
/**
* 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 {
Property<T> targetCasted = (Property<T>) target;
source.bind(targetCasted);
} catch (ClassCastException e) {
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
}
}
/**
* 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 {
Object source = resolveExpression(binding.getSource(), namespace);
Object target = resolveExpression(binding.getTarget(), namespace);
if (source instanceof Property<?> sourceProp && target instanceof Property<?> targetProp) {
Class<?> sourceType = getPropertyType(sourceProp);
Class<?> targetType = getPropertyType(targetProp);
boolean bindableForward = targetType.isAssignableFrom(sourceType);
boolean bindableBackward = sourceType.isAssignableFrom(targetType);
switch (binding.getDirection().toLowerCase()) {
case "bidirectional":
if (bindableForward && bindableBackward) {
bindBidirectionalSafe(sourceProp, targetProp);
} else {
log.error("⚠️ Kann bidirektionales Binding nicht durchführen: Typen inkompatibel ({} ⇄ {})", sourceType, targetType);
}
break;
case "unidirectional":
default:
if (bindableForward) {
bindSafe(sourceProp, targetProp);
} else {
log.error("⚠️ Kann unidirektionales Binding nicht durchführen: {} → {} nicht zuweisbar", sourceType, targetType);
}
break;
}
}
} catch (Exception e) {
log.error("Fehler beim Binding: {} → {}", binding.getSource(), binding.getTarget(), e);
}
}
}
/**
* 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");
return getter.getReturnType();
} catch (Exception e) {
return Object.class; // Fallback
}
}
/**
* 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("\\.");
Object current = namespace.get(parts[0]);
for (int i = 1; i < parts.length; i++) {
String getter = "get" + Character.toUpperCase(parts[i].charAt(0)) + parts[i].substring(1);
current = current.getClass().getMethod(getter).invoke(current);
}
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);
if (root instanceof Parent parent && !(root instanceof FxmlComponent)) {
for (Node child : parent.getChildrenUnmodifiable()) {
nodes.addAll(collectAllNodes(child));
}
}
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);
FXMLLoader loader = new FXMLLoader(fxmlPath);
loader.setControllerFactory(type -> {
Objects.requireNonNull(type);
return new ComponentController(viewModel);
});
Parent root = loader.load();
controller = loader.getController();
return root;
} catch (IOException e) {
throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
}
}
/**
* 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);
}
/**
* Loads an FXML file and binds its elements to a generated ViewModel
* based on the given POJO model.
*
* @param model the data model (POJO) to bind to the UI
* @param fxmlPath the path to the FXML file
* @return the root JavaFX node loaded from FXML
*/
public Parent load(Object model, String fxmlPath) {
return load(model, getClass().getResource(fxmlPath));
}
/**
* Binds a JavaFX UI control to a ViewModel property according to the specified direction.
*
* @param node the UI node (e.g., TextField)
* @param vmProp the ViewModel property to bind to
* @param direction the direction of the binding (e.g., "bidirectional", "read")
*/
@SuppressWarnings("unused")
private void bindNodeToProperty(javafx.scene.Node node, Property<?> vmProp, String direction) {
if (node instanceof javafx.scene.control.TextField tf && vmProp instanceof javafx.beans.property.StringProperty sp) {
if ("bidirectional".equalsIgnoreCase(direction)) {
tf.textProperty().bindBidirectional(sp);
} else if ("read".equalsIgnoreCase(direction)) {
tf.textProperty().bind(sp);
}
}
// Additional control types (e.g., CheckBox, ComboBox) can be added here
}
}
@@ -1,67 +0,0 @@
package de.neitzel.fx.component.controls;
import de.neitzel.fx.component.model.BindingData;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
/**
* The BindingControl class represents a UI control that manages a list
* of {@link BindingData} objects. It extends the {@link Region} class and
* provides functionality to bind and monitor connections between source
* and target properties.
* <p>
* The primary purpose of this control is to maintain an observable list
* of bindings, allowing developers to track or adjust the linked properties
* dynamically.
* <p>
* The internal list of bindings is implemented as an {@link ObservableList},
* allowing property change notifications to be easily monitored for UI
* updates or other reactive behaviors.
* <p>
* This class serves as an organizational component and does not provide
* any user interaction by default.
*/
public class Binding extends Region {
/**
* Represents an observable list of {@link BindingData} objects contained within the
* {@link Binding} instance. This list is utilized to manage and monitor
* the bindings between source and target properties dynamically.
* <p>
* The list is implemented as an {@link ObservableList}, allowing changes in the
* collection to be observed and reacted to, such as triggering UI updates or
* responding to binding modifications.
* <p>
* This field is initialized as an empty list using {@link FXCollections#observableArrayList()}.
* It is declared as final to ensure its reference cannot be changed, while the
* contents of the list remain mutable.
*/
private final ObservableList<BindingData> bindings = FXCollections.observableArrayList();
/**
* Constructs a new instance of the BindingControl class.
* <p>
* This default constructor initializes the BindingControl without
* any pre-configured bindings. The instance will contain an empty
* {@link ObservableList} of {@link BindingData} objects, which can later
* be populated as needed.
* <p>
* The constructor does not perform additional setup or initialization,
* allowing the class to be extended or customized as necessary.
*/
public Binding() {
// Empty, the ComponentLoader is used to work on the bindings.
}
/**
* Retrieves the observable list of {@code Binding} objects associated with this control.
* The returned list allows monitoring and management of the bindings maintained
* by the {@code BindingControl}.
*
* @return an {@code ObservableList<Binding>} containing the bindings managed by this control
*/
public ObservableList<BindingData> getBindings() {
return bindings;
}
}
@@ -1,196 +0,0 @@
package de.neitzel.fx.component.controls;
import de.neitzel.fx.component.ComponentLoader;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import java.util.Arrays;
/**
* A reusable JavaFX component that loads and embeds an FXML-defined UI fragment.
* <p>
* The component exposes three properties:
* <ul>
* <li>fxml: the resource path to the FXML file to load (relative to the component class)</li>
* <li>direction: a textual hint that describes binding directionality (default "unidirectional")</li>
* <li>data: an arbitrary object that will be injected into the loaded controller if a suitable setter exists</li>
* </ul>
* <p>
* When the {@code fxml} property changes the FXML is loaded via {@link ComponentLoader}. After loading the
* controller instance available from the loader, this component attempts to inject the object from the
* {@code data} property by calling a matching {@code setData(...)} method via reflection.
*/
public class FxmlComponent extends StackPane {
private final StringProperty fxml = new SimpleStringProperty();
private final StringProperty direction = new SimpleStringProperty("unidirectional");
private final ObjectProperty<Object> data = new SimpleObjectProperty<>();
/**
* Creates a new instance of {@code FxmlComponent}.
* <p>
* The constructor registers listeners on the {@code fxml} and {@code data} properties to
* perform lazy loading and data injection when values change.
*/
public FxmlComponent() {
fxml.addListener((obs, oldVal, newVal) -> load());
data.addListener((obs, oldVal, newVal) -> injectData());
}
/**
* Loads the FXML specified by the {@link #fxml} property and replaces the current children
* with the loaded root node. If no FXML is configured or the path is blank, the method returns
* without performing any action.
*/
private void load() {
if (getFxml() == null || getFxml().isBlank()) return;
ComponentLoader loader = new ComponentLoader();
Parent content = loader.load(getClass().getResource(getFxml()));
getChildren().setAll(content);
Object controller = loader.getController();
if (controller != null && getData() != null) {
injectDataToController(controller, getData());
}
}
/**
* Attempts to inject the {@link #data} object into the controller of the currently loaded child node,
* if a controller is present and defines a single-argument method named {@code setData}.
*/
private void injectData() {
if (!getChildren().isEmpty() && getData() != null) {
Object controller = getControllerFromChild(getChildren().get(0));
if (controller != null) {
injectDataToController(controller, getData());
}
}
}
/**
* Returns the configured FXML resource path.
*
* @return the FXML path or {@code null} if not set
*/
public String getFxml() {
return fxml.get();
}
/**
* Sets the FXML resource path to load. Changing this value triggers a reload of the component content.
*
* @param fxml resource path relative to the component class
*/
public void setFxml(String fxml) {
this.fxml.set(fxml);
}
/**
* Returns the data object that will be injected into the loaded controller when available.
*
* @return the data object or {@code null}
*/
public Object getData() {
return data.get();
}
/**
* Sets the data object to be injected into the controller of the loaded FXML.
* Changing the value will attempt injection into the currently loaded controller.
*
* @param data the data object to inject
*/
public void setData(Object data) {
this.data.set(data);
}
/**
* Uses reflection to find a single-argument method named {@code setData} on the controller and
* invokes it with the provided {@code dataObject}.
*
* @param controller the controller instance to inject into
* @param dataObject the object to inject
*/
private void injectDataToController(Object controller, Object dataObject) {
// Daten-Objekt per Reflection zuweisen
// Beispiel: Controller hat `setData(User data)`
Arrays.stream(controller.getClass().getMethods())
.filter(m -> m.getName().equals("setData") && m.getParameterCount() == 1)
.findFirst()
.ifPresent(method -> {
try {
method.invoke(controller, dataObject);
} catch (Exception e) {
e.printStackTrace();
}
});
// Optional: automatisch Binding ausführen (s. u.)
}
/**
* Extracts the controller instance from a child node's properties map if present.
*
* @param node the child node to inspect
* @return the controller object stored under the property key {@code "fx:controller"} or {@code null}
*/
private Object getControllerFromChild(Node node) {
if (node.getProperties().containsKey("fx:controller")) {
return node.getProperties().get("fx:controller");
}
return null;
}
/**
* Returns the JavaFX property representing the configured FXML path.
*
* @return the FXML property
*/
public StringProperty fxmlProperty() {
return fxml;
}
/**
* Returns the JavaFX property representing the binding direction hint.
*
* @return the direction property
*/
public StringProperty directionProperty() {
return direction;
}
/**
* Returns the configured direction string.
*
* @return the direction hint (e.g., "unidirectional")
*/
public String getDirection() {
return direction.get();
}
/**
* Sets the direction hint used by higher-level binding logic.
*
* @param direction textual direction hint
*/
public void setDirection(String direction) {
this.direction.set(direction);
}
/**
* Returns the JavaFX property representing the data object used for injection.
*
* @return the data property
*/
public ObjectProperty<Object> dataProperty() {
return data;
}
}
@@ -1,107 +0,0 @@
package de.neitzel.fx.component.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* The Binding class represents a connection between a source and a target,
* with an associated direction. It is typically used to define a binding
* relationship that determines how data flows between these two entities.
* <p>
* The class provides three properties:
* - direction: Represents the type of the binding. Defaults to "unidirectional".
* - source: Represents the source of the binding.
* - target: Represents the target of the binding.
* <p>
* It uses JavaFX property types, allowing these properties to be observed
* for changes.
*/
public class BindingData {
/**
* Represents the direction of the binding in the {@code Binding} class.
* It determines whether the binding is unidirectional or bidirectional.
* The default value is "unidirectional".
* <p>
* This property is observed for changes, enabling dynamic updates
* within the JavaFX property system.
*/
private StringProperty direction = new SimpleStringProperty("unidirectional");
/**
* Represents the source of the binding. This property holds a string value
* that specifies the originating object or identifier in the binding connection.
* It can be observed for changes, allowing updates to the binding relationship
* when the source value is modified.
*/
private StringProperty source = new SimpleStringProperty();
/**
* Represents the target of the binding in the Binding class.
* This property holds the target value, which can be observed for changes.
*/
private StringProperty target = new SimpleStringProperty();
/**
* Default constructor only
*/
public BindingData() {
// default constructor only
}
/**
* Gets the current direction of the binding.
* The direction determines how data flows between
* the source and the target, and typically defaults to "unidirectional".
*
* @return the current direction of the binding
*/
public String getDirection() {
return direction.get();
}
/**
* Sets the direction of the binding.
*
* @param dir the new direction to set for the binding
*/
public void setDirection(String dir) {
direction.set(dir);
}
/**
* Gets the current source value of the binding.
*
* @return the source value as a String.
*/
public String getSource() {
return source.get();
}
/**
* Sets the value of the source property for this binding.
*
* @param source the new source value to be set
*/
public void setSource(String source) {
this.source.set(source);
}
/**
* Retrieves the current value of the target property.
*
* @return the value of the target property as a String.
*/
public String getTarget() {
return target.get();
}
/**
* Sets the target property of the binding.
*
* @param target the new value to set for the target property
*/
public void setTarget(String target) {
this.target.set(target);
}
}
@@ -1,138 +0,0 @@
package de.neitzel.fx.injectfx;
import de.neitzel.injection.ComponentData;
import de.neitzel.injection.ComponentScanner;
import lombok.Getter;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Manages the creation and storage of singleton instances for all @FXMLComponent classes.
* It ensures that each component is instantiated only once and resolves dependencies recursively.
*/
public class FXMLComponentInstances {
@Getter
/** Map holding instances of all @FXMLComponent classes, indexed by class and its unique superclasses/interfaces. */
private final Map<Class<?>, Object> instanceMap = new HashMap<>();
/**
* The InjectableComponents instance that provides information about instantiable components.
*/
private final ComponentScanner injectableScanner;
/**
* Constructs an FXMLComponentInstances manager and initializes all component instances.
*
* @param injectableScanner The InjectableComponents instance containing resolved component types.
*/
public FXMLComponentInstances(ComponentScanner injectableScanner) {
this.injectableScanner = injectableScanner;
createAllInstances();
}
/**
* Creates instances for all registered @FXMLComponent classes.
*/
private void createAllInstances() {
for (Class<?> componentClass : injectableScanner.getInstantiableComponents().keySet()) {
getInstance(componentClass); // Ensures each component is instantiated once
}
}
/**
* Retrieves an instance of a given component class, creating it if necessary.
*
* @param componentClass The class for which an instance is needed.
* @return An instance of the requested class.
*/
public Object getInstance(Class<?> componentClass) {
if (instanceMap.containsKey(componentClass)) {
return instanceMap.get(componentClass);
}
Class<?> concreteClass = injectableScanner.getInstantiableComponents().get(componentClass).getType();
if (concreteClass == null) {
throw new IllegalStateException("No concrete implementation found for: " + componentClass.getName());
}
Object instance = createInstance(concreteClass);
registerInstance(concreteClass, instance);
return instance;
}
/**
* Creates an instance of the given class by selecting the best constructor.
*
* @param concreteClass The class to instantiate.
* @return A new instance of the class.
*/
private Object createInstance(Class<?> concreteClass) {
try {
Constructor<?> bestConstructor = findBestConstructor(concreteClass);
if (bestConstructor == null) {
throw new IllegalStateException("No suitable constructor found for: " + concreteClass.getName());
}
Class<?>[] paramTypes = bestConstructor.getParameterTypes();
Object[] params = Arrays.stream(paramTypes)
.map(this::getInstance) // Recursively resolve dependencies
.toArray();
return bestConstructor.newInstance(params);
} catch (Exception e) {
throw new RuntimeException("Failed to instantiate " + concreteClass.getName(), e);
}
}
/**
* Finds the best constructor for a given class by checking which one has resolvable parameters.
*
* @param concreteClass The class to analyze.
* @return The best constructor or {@code null} if none can be used.
*/
private Constructor<?> findBestConstructor(Class<?> concreteClass) {
Constructor<?>[] constructors = concreteClass.getConstructors();
// Prefer constructors with all parameters resolvable
for (Constructor<?> constructor : constructors) {
if (canUseConstructor(constructor)) {
return constructor;
}
}
return null;
}
/**
* Checks whether a given constructor can be used by ensuring all parameters are known.
*
* @param constructor The constructor to check.
* @return {@code true} if all parameters can be resolved, otherwise {@code false}.
*/
private boolean canUseConstructor(Constructor<?> constructor) {
return Arrays.stream(constructor.getParameterTypes())
.allMatch(injectableScanner.getInstantiableComponents()::containsKey);
}
/**
* Registers an instance in the map for all unique interfaces and superclasses it implements.
*
* @param concreteClass The concrete component class.
* @param instance The instance to store.
*/
private void registerInstance(Class<?> concreteClass, Object instance) {
instanceMap.put(concreteClass, instance);
for (Map.Entry<Class<?>, ComponentData> entry : injectableScanner.getInstantiableComponents().entrySet()) {
if (entry.getValue().getType().equals(concreteClass)) {
instanceMap.put(entry.getKey(), instance);
}
}
}
}
@@ -1,95 +0,0 @@
package de.neitzel.fx.injectfx;
import javafx.util.Callback;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
/**
* The InjectingControllerFactory is responsible for creating controller instances for JavaFX that
* support dependency injection. It uses a parameter map to resolve and supply dependencies to
* controller constructors dynamically during instantiation.
*
* <p>This class analyzes available constructors of a requested controller class at runtime and
* selects the first constructor whose parameter types are all present in the internal parameter map.
* The corresponding instances are retrieved and passed to the constructor to create the controller.
*
* <p>It implements the {@link javafx.util.Callback} interface to integrate with the JavaFX
* {@link javafx.fxml.FXMLLoader} controller factory mechanism.
*/
public class InjectingControllerFactory implements Callback<Class<?>, Object> {
/**
* A map that stores class-to-object mappings used for dependency injection
* in controller instantiation. Each key represents a parameter type and the
* associated value is the instance to be injected for that type.
*/
private final Map<Class<?>, Object> parameterMap = new HashMap<>();
/**
* Default constructor only
*/
public InjectingControllerFactory() {
// default constructor only
}
/**
* Adds a mapping between a class and its corresponding object instance
* to the parameter map used for dependency injection.
*
* @param clazz The class type to be associated with the provided object instance.
* @param object The object instance to be injected for the specified class type.
*/
public void addInjectingData(Class<?> clazz, Object object) {
parameterMap.put(clazz, object);
}
/**
* Creates an instance of a controller class using a constructor that matches the dependencies
* defined in the parameter map. The method dynamically analyzes the constructors of the given
* class and attempts to instantiate the class by injecting required dependencies.
*
* @param controllerClass the class of the controller to be instantiated
* @return an instance of the specified controller class
* @throws RuntimeException if an error occurs while creating the controller instance or if no suitable constructor is found
*/
@Override
public Object call(Class<?> controllerClass) {
try {
Optional<Constructor<?>> bestConstructor = Stream.of(controllerClass.getConstructors())
.filter(constructor -> canBeInstantiated(constructor, parameterMap))
.findFirst();
if (bestConstructor.isPresent()) {
Constructor<?> constructor = bestConstructor.get();
Object[] parameters = Stream.of(constructor.getParameterTypes())
.map(parameterMap::get)
.toArray();
return constructor.newInstance(parameters);
} else {
throw new IllegalStateException("Kein passender Konstruktor gefunden für " + controllerClass.getName());
}
} catch (Exception e) {
throw new RuntimeException("Fehler beim Erstellen der Controller-Instanz für " + controllerClass.getName(), e);
}
}
/**
* Determines if a given constructor can be instantiated using the provided parameter map.
* This method checks if the parameter map contains entries for all parameter types required
* by the specified constructor.
*
* @param constructor The constructor to be evaluated for instantiability.
* @param parameterMap A map where keys are parameter types and values are the corresponding
* instances available for injection.
* @return {@code true} if the constructor can be instantiated with the given parameter map;
* {@code false} otherwise.
*/
private boolean canBeInstantiated(Constructor<?> constructor, Map<Class<?>, Object> parameterMap) {
return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey);
}
}
@@ -1,134 +0,0 @@
package de.neitzel.fx.injectfx;
import de.neitzel.injection.ComponentScanner;
import javafx.fxml.FXMLLoader;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.URL;
/**
* The InjectingFXMLLoader class provides a custom implementation of JavaFX's FXMLLoader
* that supports dependency injection. It facilitates the loading of FXML files while
* dynamically injecting dependencies into controllers during the loading process.
* <p>
* This class utilizes the InjectingControllerFactory to enable seamless integration
* of dependency injection with JavaFX controllers. Dependencies can be added to the
* controller factory, and the loader will use this factory to instantiate controllers
* with the appropriate dependencies.
* <p>
* Features of this loader include:
* - Support for dependency injection into JavaFX controllers by using a custom factory.
* - Adding custom dependency mappings at runtime.
* - Scanning and initializing injectable components from a specified package.
*/
@Slf4j
public class InjectingFXMLLoader {
/**
* Represents an instance of the {@link InjectingControllerFactory}, which is used for creating
* and managing controller instances with dependency injection capabilities in a JavaFX application.
* <p>
* This factory facilitates the injection of dependencies by dynamically resolving and supplying
* required objects during controller instantiation. It plays a critical role in enabling seamless
* integration of dependency injection with JavaFX's {@link FXMLLoader}.
* <p>
* The `controllerFactory` is used internally by the {@link InjectingFXMLLoader} to provide a consistent
* and extensible mechanism for controller creation while maintaining loose coupling and enhancing testability.
* <p>
* Key responsibilities:
* - Manages a mapping of classes to their injectable instances required for controller instantiation.
* - Dynamically analyzes and invokes appropriate constructors for controllers based on the availability
* of dependencies.
* - Ensures that controllers are created with required dependencies, preventing manual resolution of injections.
* <p>
* This variable is initialized in the {@link InjectingFXMLLoader} and can be extended with additional
* mappings at runtime using relevant methods.
*/
private final InjectingControllerFactory controllerFactory;
/**
* Default constructor for the InjectingFXMLLoader class.
* This initializes the loader with a new instance of the {@link InjectingControllerFactory},
* which is used to provide dependency injection capabilities for JavaFX controllers.
* <p>
* The {@link InjectingControllerFactory} allows for the registration and dynamic injection
* of dependencies into controllers when they are instantiated during the FXML loading process.
*/
public InjectingFXMLLoader() {
controllerFactory = new InjectingControllerFactory();
}
/**
* Constructs a new instance of the InjectingFXMLLoader class with a specified
* InjectingControllerFactory for dependency injection. This enables the FXMLLoader
* to use the provided controller factory to instantiate controllers with their
* required dependencies during the FXML loading process.
*
* @param controllerFactory The controller factory responsible for creating controller
* instances with dependency injection support.
*/
public InjectingFXMLLoader(InjectingControllerFactory controllerFactory) {
this.controllerFactory = controllerFactory;
}
/**
* Constructs an instance of `InjectingFXMLLoader` and initializes the component scanner
* for dependency injection based on components found within a specified package.
* The scanned components are analyzed and their instances are prepared for injection
* using the internal `InjectingControllerFactory`.
*
* @param packageName The package name to be scanned for injectable components.
* Classes within the specified package will be identified
* as potential dependencies and made available for injection
* into JavaFX controllers during FXML loading.
*/
public InjectingFXMLLoader(String packageName) {
controllerFactory = new InjectingControllerFactory();
ComponentScanner scanner = new ComponentScanner(packageName);
FXMLComponentInstances instances = new FXMLComponentInstances(scanner);
addInjectingData(instances);
}
/**
* Adds all injectable data from the given {@code FXMLComponentInstances} to the controller factory.
* Iterates through the classes in the instance map and delegates adding each class-instance pair
* to the {@link #addInjectingData(Class, Object)} method.
*
* @param instances An {@code FXMLComponentInstances} object containing the mapping of classes
* to their respective singleton instances. This data represents the components
* available for dependency injection.
*/
private void addInjectingData(FXMLComponentInstances instances) {
for (var clazz : instances.getInstanceMap().keySet()) {
addInjectingData(clazz, instances.getInstance(clazz));
}
}
/**
* Adds a specific class-object mapping to the controller factory for dependency injection.
* This method allows the association of a given class type with its corresponding instance,
* enabling dependency injection into controllers during the FXML loading process.
*
* @param clazz The class type to be associated with the provided object instance.
* @param object The object instance to be injected for the specified class type.
*/
public void addInjectingData(Class<?> clazz, Object object) {
controllerFactory.addInjectingData(clazz, object);
}
/**
* Loads an FXML resource from the provided URL and injects dependencies into its controller
* using the configured {@code InjectingControllerFactory}.
*
* @param url the URL of the FXML file to be loaded
* @param <T> the type of the root element defined in the FXML
* @return the root element of the FXML file
* @throws IOException if an error occurs during the loading of the FXML file
*/
public <T> T load(URL url) throws IOException {
FXMLLoader loader = new FXMLLoader(url);
loader.setControllerFactory(controllerFactory);
return loader.load();
}
}
@@ -1,50 +0,0 @@
package de.neitzel.fx.mvvm;
/**
* Enum representing the direction of data binding between a JavaFX control and a ViewModel property.
* - READ: one-way binding from ViewModel to Control.
* - WRITE: one-way binding from Control to ViewModel.
* - BIDIRECTIONAL: two-way binding between Control and ViewModel.
*/
public enum BindDirection {
/**
* One-way binding from the ViewModel to the JavaFX control.
* The control reflects changes in the ViewModel but does not update it.
*/
READ,
/**
* One-way binding from the JavaFX control to the ViewModel.
* The ViewModel is updated when the control changes, but not vice versa.
*/
WRITE,
/**
* Two-way binding between the JavaFX control and the ViewModel.
* Changes in either are reflected in the other.
*/
BIDIRECTIONAL;
/**
* Parses a string to determine the corresponding BindDirection.
* Defaults to BIDIRECTIONAL if the input is null or empty.
*
* @param value the string representation of the direction
* @return the corresponding BindDirection enum constant
* @throws IllegalArgumentException if the string does not match any valid direction
*/
public static BindDirection fromString(String value) {
if (value == null || value.isEmpty()) {
return BIDIRECTIONAL;
}
for (BindDirection direction : BindDirection.values()) {
if (direction.name().equalsIgnoreCase(value)) {
return direction;
}
}
throw new IllegalArgumentException("Not a valid direction: " + value);
}
}
@@ -1,138 +0,0 @@
package de.neitzel.fx.mvvm;
import javafx.beans.property.Property;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.TextInputControl;
import java.io.InputStream;
/**
* 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.
*
* @param <T> the type of the model used in the ViewModel
*/
public class BindingAwareFXMLLoader<T> {
/**
* The model instance used to construct the GenericViewModel.
*/
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.
*
* @param fxml the input stream of the FXML file
* @return the root node of the loaded scene graph
* @throws Exception if loading the FXML or binding fails
*/
public Parent load(InputStream fxml) throws Exception {
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(param -> {
GenericViewController<T> controller = new GenericViewController<>();
controller.setModel(model);
return controller;
});
Parent root = loader.load(fxml);
GenericViewController<T> controller = loader.getController();
GenericViewModel<T> viewModel = controller.getViewModel();
// Traverse Nodes and evaluate binding info
bindNodesRecursively(root, viewModel);
return root;
}
/**
* Recursively traverses the scene graph and binds controls to properties
* in the ViewModel based on custom metadata in the node properties map.
*
* @param node the current node to inspect
* @param viewModel the ViewModel holding the properties
*/
private void bindNodesRecursively(Node node, GenericViewModel<T> viewModel) {
if (node instanceof Parent parent) {
for (Node child : parent.getChildrenUnmodifiable()) {
bindNodesRecursively(child, viewModel);
}
}
Object userData = node.getProperties().get("bind:property");
if (userData instanceof String propertyName) {
BindDirection direction = getDirection(node);
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
}
}
}
}
/**
* Binds two JavaFX properties according to the specified binding direction.
*
* @param controlProperty the property of the control (e.g. TextField.textProperty)
* @param modelProperty the property from the ViewModel
* @param direction the direction of the binding
* @param <V> the value type of the property
*/
private <V> void bind(Property<V> controlProperty, Property<V> modelProperty, BindDirection direction) {
switch (direction) {
case BIDIRECTIONAL -> controlProperty.bindBidirectional(modelProperty);
case READ -> controlProperty.bind(modelProperty);
case WRITE -> modelProperty.bind(controlProperty);
}
}
/**
* Retrieves the binding direction for a node based on its "bind:direction" property.
*
* @param node the node whose direction property is read
* @return the BindDirection specified or a default
*/
private BindDirection getDirection(final Node node) {
return BindDirection.fromString((String) node.getProperties().get("bind:direction"));
}
}
@@ -1,69 +0,0 @@
package de.neitzel.fx.mvvm;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;
/**
* A generic JavaFX controller that holds a GenericViewModel for the given model type.
* Intended for use with a custom FXMLLoader that performs automatic property bindings.
*
* @param <T> the type of the model
*/
public class GenericViewController<T> implements Initializable {
/**
* The GenericViewModel instance wrapping the underlying model.
*/
private GenericViewModel<T> viewModel;
/**
* Creates a new controller instance in an uninitialized state.
*
* <p>The constructor does not set a model. Call {@link #setModel(Object)} to
* attach a model instance before performing view bindings or accessing the view model via
* {@link #getViewModel()}.</p>
*/
public GenericViewController() {
// default constructor only ...
}
/**
* Retrieves the original model instance from the ViewModel.
*
* @return the model instance
*/
public T getModel() {
return viewModel.getModel();
}
/**
* Sets the model for this controller by wrapping it in a GenericViewModel.
*
* @param model the model to be used by the ViewModel
*/
public void setModel(T model) {
this.viewModel = new GenericViewModel<>(model);
}
/**
* Standard JavaFX initialize method. No-op, as actual binding is done externally via FXMLLoader extension.
*
* @param location the location used to resolve relative paths for the root object, or null if unknown
* @param resources the resources used to localize the root object, or null if not localized
*/
@Override
public void initialize(URL location, ResourceBundle resources) {
// Real binding happens in FXMLLoader extension
}
/**
* Returns the GenericViewModel used by this controller.
*
* @return the GenericViewModel instance
*/
public GenericViewModel<T> getViewModel() {
return viewModel;
}
}
@@ -1,150 +0,0 @@
package de.neitzel.fx.mvvm;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A generic ViewModel for JavaFX using reflection to automatically create properties for model attributes.
* Supports binding between JavaFX controls and the model via JavaFX properties.
*
* @param <T> the type of the underlying model
*/
public class GenericViewModel<T> {
/**
* The original model instance.
*/
private final T model;
/**
* A map holding JavaFX properties corresponding to the model's fields.
*/
private final Map<String, Property<?>> properties = new HashMap<>();
/**
* Constructs a GenericViewModel for the given model instance.
*
* @param model the model to wrap
*/
public GenericViewModel(T model) {
this.model = model;
initProperties();
}
/**
* Initializes JavaFX properties for all readable and writable model fields using reflection.
*/
private void initProperties() {
Class<?> clazz = model.getClass();
for (PropertyDescriptor pd : getPropertyDescriptors(clazz)) {
Method getter = pd.getReadMethod();
Method setter = pd.getWriteMethod();
String name = pd.getName();
if (getter != null && setter != null) {
Class<?> propertyType = pd.getPropertyType();
try {
Object value = getter.invoke(model);
Property<?> property = createJavaFXProperty(propertyType, value);
properties.put(name, property);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize property: " + name, e);
}
}
}
}
/**
* Retrieves all readable/writable property descriptors of the given class.
*
* @param clazz the class to introspect
* @return a list of property descriptors
*/
private List<PropertyDescriptor> getPropertyDescriptors(Class<?> clazz) {
try {
BeanInfo info = Introspector.getBeanInfo(clazz, Object.class);
return Arrays.asList(info.getPropertyDescriptors());
} catch (IntrospectionException e) {
throw new RuntimeException("Cannot introspect model class: " + clazz, e);
}
}
/**
* Creates an appropriate JavaFX Property instance for the given type and initial value.
*
* @param type the type of the property (e.g., String.class, int.class)
* @param initialValue the initial value of the property
* @return a new JavaFX Property instance
*/
private Property<?> createJavaFXProperty(Class<?> type, Object initialValue) {
if (type == String.class) return new SimpleStringProperty((String) initialValue);
if (type == int.class || type == Integer.class) return new SimpleIntegerProperty((Integer) initialValue);
if (type == double.class || type == Double.class) return new SimpleDoubleProperty((Double) initialValue);
if (type == boolean.class || type == Boolean.class) return new SimpleBooleanProperty((Boolean) initialValue);
if (type == long.class || type == Long.class) return new SimpleLongProperty((Long) initialValue);
// Fallback for unsupported types:
return new SimpleObjectProperty<>(initialValue);
}
/**
* Retrieves the JavaFX property associated with the given name and type.
*
* @param propertyClass the expected property type (e.g., StringProperty.class)
* @param name the name of the model field
* @param <P> the type of the Property
* @return the corresponding JavaFX property
* @throws IllegalArgumentException if the property doesn't exist or the type mismatches
*/
@SuppressWarnings("unchecked")
public <P extends Property<?>> P property(Class<P> propertyClass, String name) {
Property<?> prop = properties.get(name);
if (prop == null) {
throw new IllegalArgumentException("No property found for name: " + name);
}
if (!propertyClass.isInstance(prop)) {
throw new IllegalArgumentException("Property type mismatch: expected " + propertyClass.getSimpleName());
}
return (P) prop;
}
/**
* Writes the current values of all properties back into the model using their respective setters.
*/
public void save() {
for (PropertyDescriptor pd : getPropertyDescriptors(model.getClass())) {
String name = pd.getName();
Method setter = pd.getWriteMethod();
if (setter != null && properties.containsKey(name)) {
try {
Object value = properties.get(name).getValue();
setter.invoke(model, value);
} catch (Exception e) {
throw new RuntimeException("Failed to save property: " + name, e);
}
}
}
}
/**
* Returns the wrapped model instance.
*
* @return the model
*/
public T getModel() {
return model;
}
}
+1 -1
View File
@@ -18,7 +18,7 @@
<module>gson</module>
<module>net</module>
<module>log4j</module>
<module>fx-example</module>
<!-- fx-example still calls the removed JavaFX units and is not part of this reactor until it is migrated. -->
<module>quarkus</module>
</modules>