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

View File

@ -1,6 +1,6 @@
package de.neitzel.core.config;
import de.neitzel.core.util.FileUtils;
import de.neitzel.core.io.FileUtils;
import de.neitzel.core.util.Strings;
import lombok.extern.slf4j.Slf4j;
@ -28,7 +28,7 @@ public class Configuration {
* A {@link Properties} object that stores a set of key-value pairs.
* This variable can be used to manage configuration settings or other
* collections of properties within the application.
*
* <p>
* It provides methods to load, retrieve, and modify properties
* as necessary for the application's requirements.
*/
@ -62,7 +62,7 @@ public class Configuration {
*/
protected boolean getBooleanProperty(final String key, final boolean defaultValue) {
if (!properties.containsKey(key)) return defaultValue;
return getStringProperty(key, defaultValue ? "ja": "nein").equalsIgnoreCase("ja") || properties.getProperty(key).equalsIgnoreCase("true");
return getStringProperty(key, defaultValue ? "ja" : "nein").equalsIgnoreCase("ja") || properties.getProperty(key).equalsIgnoreCase("true");
}
/**
@ -92,18 +92,6 @@ public class Configuration {
return Strings.expandEnvironmentVariables(result);
}
/**
* Retrieves the value of a string property associated with the specified key,
* removes any surrounding quotes from the value, and returns the resultant string.
*
* @param key the key associated with the desired property
* @param defaultValue the default value to return if the property is not found or is null
* @return the string property without surrounding quotes, or the defaultValue if the property is not found
*/
protected String getStringPropertyWithoutQuotes(final String key, final String defaultValue) {
return Strings.removeQuotes(getStringProperty(key, defaultValue));
}
/**
* Retrieves the string value of a configuration property identified by the given key,
* removes surrounding quotes if present, and expands any environment variables found
@ -118,6 +106,18 @@ public class Configuration {
return Strings.expandEnvironmentVariables(result);
}
/**
* Retrieves the value of a string property associated with the specified key,
* removes any surrounding quotes from the value, and returns the resultant string.
*
* @param key the key associated with the desired property
* @param defaultValue the default value to return if the property is not found or is null
* @return the string property without surrounding quotes, or the defaultValue if the property is not found
*/
protected String getStringPropertyWithoutQuotes(final String key, final String defaultValue) {
return Strings.removeQuotes(getStringProperty(key, defaultValue));
}
/**
* Retrieves the integer value for the specified property key. If the key does
* not exist in the properties or the value is null/empty, the provided default
@ -146,7 +146,7 @@ public class Configuration {
if (value == null) {
properties.setProperty(key, "");
} else {
properties.setProperty(key, ""+value);
properties.setProperty(key, "" + value);
}
}
@ -251,7 +251,7 @@ public class Configuration {
* @param config the Configuration object whose properties will be merged into this instance
*/
public void merge(final Configuration config) {
for(Map.Entry<Object, Object> entry: config.properties.entrySet()) {
for (Map.Entry<Object, Object> entry : config.properties.entrySet()) {
properties.put(entry.getKey(), entry.getValue());
}
}
@ -261,7 +261,7 @@ public class Configuration {
*
* @param key the key to be removed from the properties map
*/
public void remove(final String key){
public void remove(final String key) {
if (properties.containsKey(key)) properties.remove(key);
}
}

View File

@ -1,6 +1,5 @@
package de.neitzel.core.io;
import de.neitzel.core.util.FileUtils;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
@ -12,7 +11,7 @@ import java.nio.charset.IllegalCharsetNameException;
* for handling file encoding conversion transparently. If a file is detected to be in UTF-8 encoding,
* this class converts it to the specified target encoding using a temporary file, then opens the reader
* with the converted encoding. If the file is already in the target encoding, it opens the reader directly.
*
* <p>
* This class is useful for applications needing to process text files in specific encodings and ensures
* encoding compatibility.
*/
@ -25,7 +24,7 @@ public class ConvertedEncodingFileReader extends InputStreamReader {
* This encoding is primarily used to determine whether a file needs conversion
* to the target format or can be read directly in its existing format.
* The default value is set to "ISO-8859-15".
*
* <p>
* Modifying this variable requires careful consideration, as it affects
* the behavior of methods that rely on encoding validation, particularly
* in the process of detecting UTF-8 files or converting them during file reading.
@ -33,17 +32,16 @@ public class ConvertedEncodingFileReader extends InputStreamReader {
private static String checkEncoding = "ISO-8859-15";
/**
* Sets the encoding that will be used to check the file encoding for compatibility.
* Throws an exception if the specified encoding is not valid or supported.
* Constructs a ConvertedEncodingFileReader for reading a file with encoding conversion support.
* This constructor takes the file path as a string and ensures the file's encoding is either
* converted to the specified target format or read directly if it matches the target format.
*
* @param encoding the name of the character encoding to set as the check encoding;
* it must be a valid and supported Charset.
* @throws IllegalCharsetNameException if the specified encoding is not valid or supported.
* @param filename the path to the file to be read
* @param targetFormat the target encoding format to use for reading the file
* @throws IOException if an I/O error occurs while accessing or reading the specified file
*/
private static void setCheckEncoding(final String encoding) {
if (Charset.forName(encoding) != null) throw new IllegalCharsetNameException("Encoding " + encoding + " is not supported!");
checkEncoding = encoding;
public ConvertedEncodingFileReader(final String filename, final String targetFormat) throws IOException {
this(new File(filename), targetFormat);
}
/**
@ -60,19 +58,6 @@ public class ConvertedEncodingFileReader extends InputStreamReader {
super(createTargetFormatInputFileStream(file, targetFormat), targetFormat);
}
/**
* Constructs a ConvertedEncodingFileReader for reading a file with encoding conversion support.
* This constructor takes the file path as a string and ensures the file's encoding is either
* converted to the specified target format or read directly if it matches the target format.
*
* @param filename the path to the file to be read
* @param targetFormat the target encoding format to use for reading the file
* @throws IOException if an I/O error occurs while accessing or reading the specified file
*/
public ConvertedEncodingFileReader(final String filename, final String targetFormat) throws IOException {
this(new File(filename), targetFormat);
}
/**
* Creates an input file stream for a given file, converting its encoding if necessary.
* If the file is not in UTF-8 encoding, a direct {@link FileInputStream} is returned for the file.
@ -100,4 +85,19 @@ public class ConvertedEncodingFileReader extends InputStreamReader {
return new FileInputStream(tempFile);
}
}
/**
* Sets the encoding that will be used to check the file encoding for compatibility.
* Throws an exception if the specified encoding is not valid or supported.
*
* @param encoding the name of the character encoding to set as the check encoding;
* it must be a valid and supported Charset.
* @throws IllegalCharsetNameException if the specified encoding is not valid or supported.
*/
private static void setCheckEncoding(final String encoding) {
if (Charset.forName(encoding) != null)
throw new IllegalCharsetNameException("Encoding " + encoding + " is not supported!");
checkEncoding = encoding;
}
}

View File

@ -1,19 +1,56 @@
package de.neitzel.core.util;
package de.neitzel.core.io;
import de.neitzel.core.util.ArrayUtils;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.stream.Collectors;
/**
* Utility class for handling file operations, such as encoding checks, content reading/writing,
* path manipulations, and file conversions.
* A utility class for file-related operations. This class provides functionalities
* for handling files and directories in an efficient manner.
*/
@Slf4j
public class FileUtils {
/**
* Defines a standardized timestamp format for debugging purposes, specifically used for naming
* or identifying files with precise timestamps. The format is "yyyy-MM-dd_HH_mm_ss_SSS", which
* includes:
* - Year in four digits (yyyy)
* - Month in two digits (MM)
* - Day of the month in two digits (dd)
* - Hour in 24-hour format with two digits (HH)
* - Minutes in two digits (mm)
* - Seconds in two digits (ss)
* - Milliseconds in three digits (SSS)
* <p>
* This ensures timestamps are sortable and easily identifiable.
*/
public static final SimpleDateFormat DEBUG_FILE_TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH_mm_ss_SSS");
/**
* Default encoding used for string checks and validations in the application.
* <p>
* This constant represents the `ISO-8859-15` encoding, which is a standardized
* character set encoding, commonly used in contexts where backward compatibility
* with `ISO-8859-1` is required, but with support for certain additional characters,
* such as the euro currency symbol ().
*/
public static final String DEFAULT_CHECK_ENCODING = "ISO-8859-15";
/**
* Specifies the default buffer size used for data processing operations.
* <p>
* This constant represents the size of the buffer in bytes, set to 1024,
* and is typically utilized in input/output operations to optimize performance
* by reducing the number of read/write calls.
*/
public static final int BUFFER_SIZE = 1024;
/**
* Private constructor to prevent instantiation of the utility class.
* This utility class is not meant to be instantiated, as it only provides
@ -27,39 +64,15 @@ public class FileUtils {
}
/**
* Defines a standardized timestamp format for debugging purposes, specifically used for naming
* or identifying files with precise timestamps. The format is "yyyy-MM-dd_HH_mm_ss_SSS", which
* includes:
* - Year in four digits (yyyy)
* - Month in two digits (MM)
* - Day of the month in two digits (dd)
* - Hour in 24-hour format with two digits (HH)
* - Minutes in two digits (mm)
* - Seconds in two digits (ss)
* - Milliseconds in three digits (SSS)
* Determines if the content of the given file is encoded in UTF-8.
*
* This ensures timestamps are sortable and easily identifiable.
* @param file The file to check for UTF-8 encoding. Must not be null.
* @return true if the file content is in UTF-8 encoding; false otherwise.
* @throws IOException If an I/O error occurs while reading the file.
*/
public static final SimpleDateFormat DEBUG_FILE_TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH_mm_ss_SSS");
/**
* Default encoding used for string checks and validations in the application.
*
* This constant represents the `ISO-8859-15` encoding, which is a standardized
* character set encoding, commonly used in contexts where backward compatibility
* with `ISO-8859-1` is required, but with support for certain additional characters,
* such as the euro currency symbol ().
*/
public static final String DEFAULT_CHECK_ENCODING = "ISO-8859-15";
/**
* Specifies the default buffer size used for data processing operations.
*
* This constant represents the size of the buffer in bytes, set to 1024,
* and is typically utilized in input/output operations to optimize performance
* by reducing the number of read/write calls.
*/
public static final int BUFFER_SIZE = 1024;
public static boolean isUTF8(final File file) throws IOException {
return isUTF8(file, DEFAULT_CHECK_ENCODING);
}
/**
* Determines whether the given file is encoded in UTF-8.
@ -97,7 +110,7 @@ public class FileUtils {
/**
* Checks if the provided file starts with a UTF-8 Byte Order Mark (BOM).
*
* <p>
* This method reads the first character of the file using a reader that assumes
* UTF-8 encoding and checks if it matches the Unicode Byte Order Mark (U+FEFF).
*
@ -114,20 +127,9 @@ public class FileUtils {
}
}
/**
* Determines if the content of the given file is encoded in UTF-8.
*
* @param file The file to check for UTF-8 encoding. Must not be null.
* @return true if the file content is in UTF-8 encoding; false otherwise.
* @throws IOException If an I/O error occurs while reading the file.
*/
public static boolean isUTF8(final File file) throws IOException {
return isUTF8(file, DEFAULT_CHECK_ENCODING);
}
/**
* Converts the content of a text file from one character encoding format to another.
*
* <p>
* This method reads the input text file using the specified source encoding and writes
* the content to the output text file in the specified target encoding.
*
@ -167,19 +169,6 @@ public class FileUtils {
}
}
/**
* Creates a universal file reader for the specified file name.
* This method initializes and returns an InputStreamReader to read
* the content of the given file.
*
* @param filename The name or path of the file to be read.
* @return An InputStreamReader for reading the specified file.
* @throws IOException If an I/O error occurs while creating the file reader.
*/
public static InputStreamReader createUniversalFileReader(final String filename) throws IOException {
return createUniversalFileReader(new File(filename));
}
/**
* Creates a universal file reader for the specified file and format.
* The method resolves the file using its name and the expected format,
@ -194,17 +183,6 @@ public class FileUtils {
return createUniversalFileReader(new File(filename), expectedFormat);
}
/**
* Creates a universal file reader for the specified file using the default encoding and configuration.
*
* @param file The file to be read. Must not be null.
* @return An InputStreamReader configured to read the specified file.
* @throws IOException If an I/O error occurs while creating the reader.
*/
public static InputStreamReader createUniversalFileReader(final File file) throws IOException {
return createUniversalFileReader(file, DEFAULT_CHECK_ENCODING, true);
}
/**
* Creates a universal file reader for the specified file with an expected format.
* This method wraps the given file in an InputStreamReader for consistent character stream manipulation.
@ -239,14 +217,14 @@ public class FileUtils {
InputStreamReader result = new InputStreamReader(new FileInputStream(file), encoding);
if (skipBOM) {
int BOM = result.read();
if (BOM != 0xFEFF) log.error ("Skipping BOM but value not 0xFEFF!");
if (BOM != 0xFEFF) log.error("Skipping BOM but value not 0xFEFF!");
}
return result;
}
/**
* Retrieves the parent directory of the given file or directory path.
*
* <p>
* If the given path does not have a parent directory, it defaults to returning the
* current directory represented by ".".
*
@ -282,6 +260,30 @@ public class FileUtils {
}
}
/**
* Creates a universal file reader for the specified file name.
* This method initializes and returns an InputStreamReader to read
* the content of the given file.
*
* @param filename The name or path of the file to be read.
* @return An InputStreamReader for reading the specified file.
* @throws IOException If an I/O error occurs while creating the file reader.
*/
public static InputStreamReader createUniversalFileReader(final String filename) throws IOException {
return createUniversalFileReader(new File(filename));
}
/**
* Creates a universal file reader for the specified file using the default encoding and configuration.
*
* @param file The file to be read. Must not be null.
* @return An InputStreamReader configured to read the specified file.
* @throws IOException If an I/O error occurs while creating the reader.
*/
public static InputStreamReader createUniversalFileReader(final File file) throws IOException {
return createUniversalFileReader(file, DEFAULT_CHECK_ENCODING, true);
}
/**
* Writes the given content to the specified file path. If the file already exists, it will be overwritten.
*
@ -294,4 +296,60 @@ public class FileUtils {
writer.write(content);
}
}
/**
* Deletes the specified file or directory. If the target is a directory, all its contents,
* including subdirectories and files, will be deleted recursively.
* If the target file or directory does not exist, the method immediately returns {@code true}.
*
* @param targetFile the {@code Path} of the file or directory to be deleted
* @return {@code true} if the target file or directory was successfully deleted,
* or if it does not exist; {@code false} if an error occurred during deletion
*/
public static boolean remove(Path targetFile) {
if (!Files.exists(targetFile)) {
return true;
}
if (Files.isDirectory(targetFile)) {
return removeDirectory(targetFile);
}
return targetFile.toFile().delete();
}
/**
* Deletes the specified directory and all its contents, including subdirectories and files.
* The method performs a recursive deletion, starting with the deepest entries in the directory tree.
* If the directory does not exist, the method immediately returns true.
*
* @param targetDir the {@code Path} of the directory to be deleted
* @return {@code true} if the directory and all its contents were successfully deleted
* or if the directory does not exist; {@code false} if an error occurred during deletion
*/
public static boolean removeDirectory(Path targetDir) {
if (!Files.exists(targetDir)) {
return true;
}
if (!Files.isDirectory(targetDir)) {
return false;
}
try {
Files.walk(targetDir)
.sorted((a, b) -> b.compareTo(a))
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (Exception ignored) {
}
});
} catch (IOException ignored) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,76 @@
package de.neitzel.core.io;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.stream.Stream;
/**
* A utility class for creating and managing a temporary directory.
* Instances of this class create a unique temporary directory on the filesystem
* that can safely be used during the runtime of a program.
* <p>
* The directory is automatically cleaned up when the instance is closed.
*/
public class TempDirectory implements AutoCloseable {
/**
* The filesystem path representing the temporary directory managed by this instance.
* This path is initialized when the TempDirectory object is created and points
* to a unique, newly created directory.
* <p>
* The directory can be used to safely store temporary runtime files. It is automatically
* deleted along with its content when the associated TempDirectory object is closed.
*/
private final Path directory;
/**
* Creates a temporary directory with a unique name on the filesystem.
* The directory will have a prefix specified by the user and is intended
* to serve as a temporary workspace during the runtime of the program.
*
* @param prefix the prefix to be used for the name of the temporary directory
* @throws IOException if an I/O error occurs when creating the directory
*/
public TempDirectory(String prefix) throws IOException {
this.directory = Files.createTempDirectory(prefix);
}
/**
* Retrieves the path of the temporary directory associated with this instance.
*
* @return the {@code Path} of the temporary directory
*/
public Path getDirectory() {
return directory;
}
/**
* Closes the temporary directory by cleaning up its contents and deleting the directory itself.
* <p>
* This method ensures that all files and subdirectories within the temporary directory are
* deleted in a reverse order, starting from the deepest leaf in the directory tree. If
* the directory does not exist, the method will not perform any actions.
* <p>
* If an error occurs while deleting any file or directory, a RuntimeException is thrown
* with the details of the failure.
*
* @throws IOException if an I/O error occurs while accessing the directory or its contents.
*/
@Override
public void close() throws IOException {
if (Files.exists(directory)) {
try (Stream<Path> walk = Files.walk(directory)) {
walk.sorted(Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
throw new RuntimeException("Failed to delete: " + path, e);
}
});
}
}
}
}

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
-

View File

@ -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) {
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 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;
} catch (IOException e) {
throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
}
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: %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);
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);
}
}
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 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);
/**
* 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);
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 {
consumer.accept(child);
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
}
});
return result;
}
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 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());
}
}

View File

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

View File

@ -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,9 +26,7 @@ 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()));
getChildren().setAll(content);
@ -48,10 +35,6 @@ public class FxmlComponent extends StackPane {
if (controller != null && getData() != null) {
injectDataToController(controller, getData());
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void injectData() {
@ -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;
}
}

View File

@ -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.