Added FileUtils / TempDirectory from EmailTool.

Moved FileUtils to io package.
This commit is contained in:
Konrad Neitzel
2025-07-01 15:38:07 +02:00
parent 2b60038774
commit 5aa75be6ba
9 changed files with 550 additions and 306 deletions
+85 -1
View File
@@ -1,15 +1,99 @@
# TODO
- SimpleListProperty auch nutzen bei Collections!
# Ideen
# Bindings
Bindngs kommen über ein spezielles Binding Control, das dann notwendige Bindings beinhaltet.
Da kann dann auch eine eigene Logik zur Erkennung des Bindings erfolgen oder zusätziche Informationen bezüglich notwendiger Elemente in dem ViewModel
Da kann dann auch eine eigene Logik zur Erkennung des Bindings erfolgen oder zusätziche Informationen bezüglich
notwendiger Elemente in dem ViewModel
## Bidirektional Converter für Bindings
```Java
StringConverter<Instant> converter = new StringConverter<>() {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
@Override
public String toString(Instant instant) {
return instant == null ? "" : formatter.format(instant);
}
@Override
public Instant fromString(String string) {
if (string == null || string.isBlank()) return null;
try {
LocalDateTime dateTime = LocalDateTime.parse(string, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return dateTime.atZone(ZoneId.systemDefault()).toInstant();
} catch (DateTimeParseException e) {
// Optional: Fehlermeldung anzeigen
return null;
}
}
};
// Binding einrichten:
Bindings.
bindBidirectional(textField.textProperty(),instantProperty,converter);
```
Overloads von bindBidirectional
bindBidirectional(Property<String>, Property<?>, Format)
bindBidirectional(Property<String>, Property<T>, StringConverter<T>)
bindBidirectional(Property<T>, Property<T>)
## Unidirektional mit Bindings.createStringBinding()
```Java
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
textField.
textProperty().
bind(Bindings.createStringBinding(
() ->{
Instant instant = instantProperty.get();
return instant ==null?"":formatter.
format(instant);
},
instantProperty
));
```
Bindings:
- BooleanBinding
- DoubleBinding
- FloatBinding
- IntegerBinding
- LongBinding
- StringBinding
- ObjectBinding
- NumberBinding
Bindings haben Methoden, die dann weitere Bindings erzeugen.
==> Parameter der Methode: Property, Observable und einer Methode, die ein Binding erstellt und das ViewModel<?>, das
dann genutzt werden kann, um weitere Properties zu holen ==> Erzeugung von neuen Properties.
Diese BindingCreators haben Namen, PropertyTyp und ein BindingType.
# FXMLComponent
Dient dem Laden einer Komponente und bekommt dazu das fxml, die Daten und ggf. auch eine ModelView.
# ModelView generieren
Damit eine ModelView nicht ständig manuell generiert werden muss, ist hier ggf. etwas zu generieren?
Ggf. eine eigenes Beschreibungssprache?
# Aufbau einer Validierung
- gehört in das ViewModel
-
@@ -1,12 +1,20 @@
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.beans.value.ObservableValue;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.Pane;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
@@ -16,13 +24,35 @@ import java.util.Map;
/**
* ComponentLoader is responsible for loading JavaFX FXML components and binding
* them to automatically generated ViewModels based on simple POJO models.
* <p>
* It parses custom NFX attributes in FXML to bind UI elements to properties in the ViewModel,
* and supports recursive loading of subcomponents.
*/
@Slf4j
public class ComponentLoader {
private Map<String, Map<String, String>> nfxBindingMap = new HashMap<>();
@Getter
private Object controller;
public Parent load(URL fxmlPath) {
return load(null, fxmlPath);
}
public Parent load(String fxmlPath) {
return load(null, fxmlPath);
}
public Parent load(Object model, URL fxmlPath) {
try {
AutoViewModel<?> viewModel = new AutoViewModel<>(model);
FXMLLoader loader = new FXMLLoader(fxmlPath);
loader.setControllerFactory(type -> 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 binds its elements to a generated ViewModel
* based on the given POJO model.
@@ -32,83 +62,116 @@ public class ComponentLoader {
* @return the root JavaFX node loaded from FXML
*/
public Parent load(Object model, String fxmlPath) {
return load(model, getClass().getResource(fxmlPath));
}
public static <T extends Parent> T load(URL fxmlUrl, Object controller, 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;
}
private static <T> void bindBidirectionalSafe(@NotNull Property<T> source, Property<?> target) {
try {
AutoViewModel<?> viewModel = new AutoViewModel<>(model);
String cleanedUri = preprocessNfxAttributes(fxmlPath);
FXMLLoader loader = new FXMLLoader(new URL(cleanedUri));
loader.setControllerFactory(type -> new ComponentController(viewModel));
Parent root = loader.load();
processNfxBindings(root, viewModel, loader);
return root;
} catch (IOException e) {
throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
Property<T> targetCasted = (Property<T>) target;
source.bindBidirectional(targetCasted);
} catch (ClassCastException e) {
log.error("⚠️ Typkonflikt beim Binding: %s ⇄ %s%n", source.getClass(), target.getClass(), e);
}
}
/**
* Processes all UI elements for NFX binding attributes and applies
* the appropriate bindings to the ViewModel.
*
* @param root the root node of the loaded FXML hierarchy
* @param viewModel the generated ViewModel to bind against
*/
private void processNfxBindings(Parent root, AutoViewModel<?> viewModel, FXMLLoader loader) {
walkNodes(root, node -> {
var nfx = lookupNfxAttributes(node, loader);
String target = nfx.get("nfx:target");
String direction = nfx.get("nfx:direction");
String source = nfx.get("nfx:source");
if (target != null && direction != null) {
Property<?> vmProp = viewModel.getProperty(target);
bindNodeToProperty(node, vmProp, direction);
}
if (source != null) {
Object subModel = ((Property<?>) viewModel.getProperty(target)).getValue();
Parent subComponent = load(subModel, source);
if (node instanceof Pane pane) {
pane.getChildren().setAll(subComponent);
}
}
});
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: %s ⇄ %s%n", source.getClass(), target.getClass(), e);
}
}
/**
* Recursively walks all nodes in the scene graph starting from the root,
* applying the given consumer to each node.
*
* @param root the starting node
* @param consumer the consumer to apply to each node
*/
private void walkNodes(Parent root, java.util.function.Consumer<javafx.scene.Node> consumer) {
consumer.accept(root);
if (root instanceof Pane pane) {
for (javafx.scene.Node child : pane.getChildren()) {
if (child instanceof Parent p) {
walkNodes(p, consumer);
} else {
consumer.accept(child);
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 && target instanceof Property) {
Property<?> sourceProp = (Property<?>) source;
Property<?> targetProp = (Property<?>) target;
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 (%s ⇄ %s)%n", sourceType, targetType);
}
break;
case "unidirectional":
default:
if (bindableForward) {
bindSafe(sourceProp, targetProp);
} else {
log.error("⚠️ Kann unidirektionales Binding nicht durchführen: %s → %s nicht zuweisbar%n", sourceType, targetType);
}
break;
}
}
} catch (Exception e) {
log.error("Fehler beim Binding: " + binding.getSource() + " → " + binding.getTarget(), e);
}
}
}
/**
* Extracts custom NFX attributes from a node's properties map.
* These attributes are expected to be in the format "nfx:..." and hold string values.
*
* @param node the node to inspect
* @return a map of NFX attribute names to values
*/
private Map<String, String> extractNfxAttributes(javafx.scene.Node node) {
Map<String, String> result = new HashMap<>();
node.getProperties().forEach((k, v) -> {
if (k instanceof String key && key.startsWith("nfx:") && v instanceof String value) {
result.put(key, value);
private static Class<?> getPropertyType(Property<?> prop) {
try {
Method getter = prop.getClass().getMethod("get");
return getter.getReturnType();
} catch (Exception e) {
return Object.class; // Fallback
}
}
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;
}
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 result;
}
return nodes;
}
/**
@@ -128,61 +191,4 @@ public class ComponentLoader {
}
// Additional control types (e.g., CheckBox, ComboBox) can be added here
}
private String preprocessNfxAttributes(String fxmlPath) {
try {
nfxBindingMap.clear();
var factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
var builder = factory.newDocumentBuilder();
var doc = builder.parse(getClass().getResourceAsStream(fxmlPath));
var all = doc.getElementsByTagName("*");
int autoId = 0;
for (int i = 0; i < all.getLength(); i++) {
var el = (org.w3c.dom.Element) all.item(i);
Map<String, String> nfxAttrs = new HashMap<>();
var attrs = el.getAttributes();
List<String> toRemove = new ArrayList<>();
for (int j = 0; j < attrs.getLength(); j++) {
var attr = (org.w3c.dom.Attr) attrs.item(j);
if (attr.getName().startsWith("nfx:")) {
nfxAttrs.put(attr.getName(), attr.getValue());
toRemove.add(attr.getName());
}
}
if (!nfxAttrs.isEmpty()) {
String fxid = el.getAttribute("fx:id");
if (fxid == null || fxid.isBlank()) {
fxid = "auto_id_" + (++autoId);
el.setAttribute("fx:id", fxid);
}
nfxBindingMap.put(fxid, nfxAttrs);
toRemove.forEach(el::removeAttribute);
}
}
// Speichere das bereinigte Dokument als temporäre Datei
File tempFile = File.createTempFile("cleaned_fxml", ".fxml");
tempFile.deleteOnExit();
var transformer = javax.xml.transform.TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes");
transformer.transform(new javax.xml.transform.dom.DOMSource(doc),
new javax.xml.transform.stream.StreamResult(tempFile));
return tempFile.toURI().toString();
} catch (Exception e) {
throw new RuntimeException("Preprocessing failed for: " + fxmlPath, e);
}
}
private Map<String, String> lookupNfxAttributes(javafx.scene.Node node, FXMLLoader loader) {
String fxid = loader.getNamespace().entrySet().stream()
.filter(e -> e.getValue() == node)
.map(Map.Entry::getKey)
.findFirst().orElse(null);
if (fxid == null) return Map.of();
return nfxBindingMap.getOrDefault(fxid, Map.of());
}
}
@@ -10,15 +10,15 @@ import javafx.scene.layout.Region;
* 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.
*/
@@ -28,17 +28,32 @@ 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
@@ -49,19 +64,4 @@ public class Binding extends Region {
public ObservableList<BindingData> getBindings() {
return bindings;
}
/**
* Constructs a new instance of the BindingControl class.
*
* 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.
*
* The constructor does not perform additional setup or initialization,
* allowing the class to be extended or customized as necessary.
*/
public Binding() {
// Absichtlich leer – wird später "ausgewertet"
}
}
@@ -9,27 +9,16 @@ import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import java.io.IOException;
import java.util.Arrays;
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<>();
public StringProperty fxmlProperty() { return fxml; }
public String getFxml() { return fxml.get(); }
public void setFxml(String fxml) { this.fxml.set(fxml); }
public StringProperty directionProperty() { return direction; }
public String getDirection() { return direction.get(); }
public void setDirection(String direction) { this.direction.set(direction); }
public ObjectProperty<Object> dataProperty() { return data; }
public Object getData() { return data.get(); }
public void setData(Object data) { this.data.set(data); }
public FxmlComponent() {
fxml.addListener((obs, oldVal, newVal) -> load());
data.addListener((obs, oldVal, newVal) -> injectData());
@@ -37,20 +26,14 @@ public class FxmlComponent extends StackPane {
private void load() {
if (getFxml() == null || getFxml().isBlank()) return;
try {
ComponentLoader loader = new ComponentLoader();
// Option: ControllerFactory verwenden, wenn nötig
Parent content = loader.load(getClass().getResource(getFxml()));
ComponentLoader loader = new ComponentLoader();
Parent content = loader.load(getClass().getResource(getFxml()));
getChildren().setAll(content);
getChildren().setAll(content);
Object controller = loader.getController();
if (controller != null && getData() != null) {
injectDataToController(controller, getData());
}
} catch (IOException e) {
e.printStackTrace();
Object controller = loader.getController();
if (controller != null && getData() != null) {
injectDataToController(controller, getData());
}
}
@@ -63,6 +46,22 @@ public class FxmlComponent extends StackPane {
}
}
public String getFxml() {
return fxml.get();
}
public void setFxml(String fxml) {
this.fxml.set(fxml);
}
public Object getData() {
return data.get();
}
public void setData(Object data) {
this.data.set(data);
}
private void injectDataToController(Object controller, Object dataObject) {
// Daten-Objekt per Reflection zuweisen
// Beispiel: Controller hat `setData(User data)`
@@ -86,5 +85,25 @@ public class FxmlComponent extends StackPane {
}
return null;
}
public StringProperty fxmlProperty() {
return fxml;
}
public StringProperty directionProperty() {
return direction;
}
public String getDirection() {
return direction.get();
}
public void setDirection(String direction) {
this.direction.set(direction);
}
public ObjectProperty<Object> dataProperty() {
return data;
}
}
@@ -26,6 +26,7 @@ public class BindingData {
* 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.