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

View File

@ -1,6 +1,5 @@
package de.neitzel.core.io; package de.neitzel.core.io;
import de.neitzel.core.util.FileUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.*; 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, * 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 * 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. * 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 * This class is useful for applications needing to process text files in specific encodings and ensures
* encoding compatibility. * encoding compatibility.
*/ */
@ -25,7 +24,7 @@ public class ConvertedEncodingFileReader extends InputStreamReader {
* This encoding is primarily used to determine whether a file needs conversion * 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. * to the target format or can be read directly in its existing format.
* The default value is set to "ISO-8859-15". * The default value is set to "ISO-8859-15".
* * <p>
* Modifying this variable requires careful consideration, as it affects * Modifying this variable requires careful consideration, as it affects
* the behavior of methods that rely on encoding validation, particularly * the behavior of methods that rely on encoding validation, particularly
* in the process of detecting UTF-8 files or converting them during file reading. * 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"; private static String checkEncoding = "ISO-8859-15";
/** /**
* Sets the encoding that will be used to check the file encoding for compatibility. * Constructs a ConvertedEncodingFileReader for reading a file with encoding conversion support.
* Throws an exception if the specified encoding is not valid or supported. * 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; * @param filename the path to the file to be read
* it must be a valid and supported Charset. * @param targetFormat the target encoding format to use for reading the file
* @throws IllegalCharsetNameException if the specified encoding is not valid or supported. * @throws IOException if an I/O error occurs while accessing or reading the specified file
*/ */
private static void setCheckEncoding(final String encoding) { public ConvertedEncodingFileReader(final String filename, final String targetFormat) throws IOException {
if (Charset.forName(encoding) != null) throw new IllegalCharsetNameException("Encoding " + encoding + " is not supported!"); this(new File(filename), targetFormat);
checkEncoding = encoding;
} }
/** /**
@ -60,19 +58,6 @@ public class ConvertedEncodingFileReader extends InputStreamReader {
super(createTargetFormatInputFileStream(file, targetFormat), targetFormat); 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. * 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. * 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); 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 lombok.extern.slf4j.Slf4j;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Utility class for handling file operations, such as encoding checks, content reading/writing, * A utility class for file-related operations. This class provides functionalities
* path manipulations, and file conversions. * for handling files and directories in an efficient manner.
*/ */
@Slf4j @Slf4j
public class FileUtils { 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. * Private constructor to prevent instantiation of the utility class.
* This utility class is not meant to be instantiated, as it only provides * 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 * Determines if the content of the given file is encoded in UTF-8.
* 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)
* *
* 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"); public static boolean isUTF8(final File file) throws IOException {
return isUTF8(file, DEFAULT_CHECK_ENCODING);
/** }
* 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;
/** /**
* Determines whether the given file is encoded in UTF-8. * 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). * 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 * 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). * 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. * 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 * 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. * 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. * Creates a universal file reader for the specified file and format.
* The method resolves the file using its name and the expected 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); 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. * 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. * 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); InputStreamReader result = new InputStreamReader(new FileInputStream(file), encoding);
if (skipBOM) { if (skipBOM) {
int BOM = result.read(); 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; return result;
} }
/** /**
* Retrieves the parent directory of the given file or directory path. * 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 * If the given path does not have a parent directory, it defaults to returning the
* current directory represented by ".". * 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. * 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); 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 # Ideen
# Bindings # Bindings
Bindngs kommen über ein spezielles Binding Control, das dann notwendige Bindings beinhaltet. 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 # FXMLComponent
Dient dem Laden einer Komponente und bekommt dazu das fxml, die Daten und ggf. auch eine ModelView. Dient dem Laden einer Komponente und bekommt dazu das fxml, die Daten und ggf. auch eine ModelView.
# ModelView generieren # ModelView generieren
Damit eine ModelView nicht ständig manuell generiert werden muss, ist hier ggf. etwas zu generieren? Damit eine ModelView nicht ständig manuell generiert werden muss, ist hier ggf. etwas zu generieren?
Ggf. eine eigenes Beschreibungssprache? Ggf. eine eigenes Beschreibungssprache?
# Aufbau einer Validierung
- gehört in das ViewModel
-

View File

@ -1,12 +1,20 @@
package de.neitzel.fx.component; 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.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.layout.Pane; 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.io.IOException;
import java.lang.reflect.Method;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -16,13 +24,35 @@ import java.util.Map;
/** /**
* ComponentLoader is responsible for loading JavaFX FXML components and binding * ComponentLoader is responsible for loading JavaFX FXML components and binding
* them to automatically generated ViewModels based on simple POJO models. * 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 { public class ComponentLoader {
private Map<String, Map<String, String>> nfxBindingMap = new HashMap<>(); 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 * Loads an FXML file and binds its elements to a generated ViewModel
* based on the given POJO model. * based on the given POJO model.
@ -32,83 +62,116 @@ public class ComponentLoader {
* @return the root JavaFX node loaded from FXML * @return the root JavaFX node loaded from FXML
*/ */
public Parent load(Object model, String fxmlPath) { public Parent load(Object model, String fxmlPath) {
try { return load(model, getClass().getResource(fxmlPath));
AutoViewModel<?> viewModel = new AutoViewModel<>(model); }
String cleanedUri = preprocessNfxAttributes(fxmlPath);
FXMLLoader loader = new FXMLLoader(new URL(cleanedUri)); public static <T extends Parent> T load(URL fxmlUrl, Object controller, String nothing) throws IOException {
loader.setControllerFactory(type -> new ComponentController(viewModel)); FXMLLoader loader = new FXMLLoader(fxmlUrl);
Parent root = loader.load(); loader.setController(controller);
processNfxBindings(root, viewModel, loader); 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; 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);
} }
} }
/** private static <T> void bindSafe(@NotNull Property<T> source, Property<?> target) {
* Processes all UI elements for NFX binding attributes and applies try {
* the appropriate bindings to the ViewModel. Property<T> targetCasted = (Property<T>) target;
* source.bind(targetCasted);
* @param root the root node of the loaded FXML hierarchy } catch (ClassCastException e) {
* @param viewModel the generated ViewModel to bind against log.error("⚠️ Typkonflikt beim Binding: %s ⇄ %s%n", source.getClass(), target.getClass(), e);
*/ }
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) { private static void evaluateBindings(List<BindingData> bindings, Map<String, Object> namespace) {
Object subModel = ((Property<?>) viewModel.getProperty(target)).getValue(); for (var binding : bindings) {
Parent subComponent = load(subModel, source); try {
if (node instanceof Pane pane) { Object source = resolveExpression(binding.getSource(), namespace);
pane.getChildren().setAll(subComponent); Object target = resolveExpression(binding.getTarget(), namespace);
}
}
});
}
/** if (source instanceof Property && target instanceof Property) {
* Recursively walks all nodes in the scene graph starting from the root, Property<?> sourceProp = (Property<?>) source;
* applying the given consumer to each node. Property<?> targetProp = (Property<?>) target;
*
* @param root the starting node Class<?> sourceType = getPropertyType(sourceProp);
* @param consumer the consumer to apply to each node Class<?> targetType = getPropertyType(targetProp);
*/
private void walkNodes(Parent root, java.util.function.Consumer<javafx.scene.Node> consumer) { boolean bindableForward = targetType.isAssignableFrom(sourceType);
consumer.accept(root); boolean bindableBackward = sourceType.isAssignableFrom(targetType);
if (root instanceof Pane pane) {
for (javafx.scene.Node child : pane.getChildren()) { switch (binding.getDirection().toLowerCase()) {
if (child instanceof Parent p) { case "bidirectional":
walkNodes(p, consumer); if (bindableForward && bindableBackward) {
bindBidirectionalSafe(sourceProp, targetProp);
} else { } 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);
} }
} }
} }
/** private static Class<?> getPropertyType(Property<?> prop) {
* Extracts custom NFX attributes from a node's properties map. try {
* These attributes are expected to be in the format "nfx:..." and hold string values. Method getter = prop.getClass().getMethod("get");
* return getter.getReturnType();
* @param node the node to inspect } catch (Exception e) {
* @return a map of NFX attribute names to values return Object.class; // Fallback
*/
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);
} }
}); }
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 // 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 * of {@link BindingData} objects. It extends the {@link Region} class and
* provides functionality to bind and monitor connections between source * provides functionality to bind and monitor connections between source
* and target properties. * and target properties.
* * <p>
* The primary purpose of this control is to maintain an observable list * The primary purpose of this control is to maintain an observable list
* of bindings, allowing developers to track or adjust the linked properties * of bindings, allowing developers to track or adjust the linked properties
* dynamically. * dynamically.
* * <p>
* The internal list of bindings is implemented as an {@link ObservableList}, * The internal list of bindings is implemented as an {@link ObservableList},
* allowing property change notifications to be easily monitored for UI * allowing property change notifications to be easily monitored for UI
* updates or other reactive behaviors. * updates or other reactive behaviors.
* * <p>
* This class serves as an organizational component and does not provide * This class serves as an organizational component and does not provide
* any user interaction by default. * 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 * Represents an observable list of {@link BindingData} objects contained within the
* {@link Binding} instance. This list is utilized to manage and monitor * {@link Binding} instance. This list is utilized to manage and monitor
* the bindings between source and target properties dynamically. * the bindings between source and target properties dynamically.
* * <p>
* The list is implemented as an {@link ObservableList}, allowing changes in the * 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 * collection to be observed and reacted to, such as triggering UI updates or
* responding to binding modifications. * responding to binding modifications.
* * <p>
* This field is initialized as an empty list using {@link FXCollections#observableArrayList()}. * 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 * It is declared as final to ensure its reference cannot be changed, while the
* contents of the list remain mutable. * contents of the list remain mutable.
*/ */
private final ObservableList<BindingData> bindings = FXCollections.observableArrayList(); 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. * Retrieves the observable list of {@code Binding} objects associated with this control.
* The returned list allows monitoring and management of the bindings maintained * The returned list allows monitoring and management of the bindings maintained
@ -49,19 +64,4 @@ public class Binding extends Region {
public ObservableList<BindingData> getBindings() { public ObservableList<BindingData> getBindings() {
return bindings; 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.Parent;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
public class FxmlComponent extends StackPane { public class FxmlComponent extends StackPane {
private final StringProperty fxml = new SimpleStringProperty(); private final StringProperty fxml = new SimpleStringProperty();
private final StringProperty direction = new SimpleStringProperty("unidirectional"); private final StringProperty direction = new SimpleStringProperty("unidirectional");
private final ObjectProperty<Object> data = new SimpleObjectProperty<>(); 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() { public FxmlComponent() {
fxml.addListener((obs, oldVal, newVal) -> load()); fxml.addListener((obs, oldVal, newVal) -> load());
data.addListener((obs, oldVal, newVal) -> injectData()); data.addListener((obs, oldVal, newVal) -> injectData());
@ -37,9 +26,7 @@ public class FxmlComponent extends StackPane {
private void load() { private void load() {
if (getFxml() == null || getFxml().isBlank()) return; if (getFxml() == null || getFxml().isBlank()) return;
try {
ComponentLoader loader = new ComponentLoader(); ComponentLoader loader = new ComponentLoader();
// Option: ControllerFactory verwenden, wenn nötig
Parent content = loader.load(getClass().getResource(getFxml())); Parent content = loader.load(getClass().getResource(getFxml()));
getChildren().setAll(content); getChildren().setAll(content);
@ -48,10 +35,6 @@ public class FxmlComponent extends StackPane {
if (controller != null && getData() != null) { if (controller != null && getData() != null) {
injectDataToController(controller, getData()); injectDataToController(controller, getData());
} }
} catch (IOException e) {
e.printStackTrace();
}
} }
private void injectData() { 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) { private void injectDataToController(Object controller, Object dataObject) {
// Daten-Objekt per Reflection zuweisen // Daten-Objekt per Reflection zuweisen
// Beispiel: Controller hat `setData(User data)` // Beispiel: Controller hat `setData(User data)`
@ -86,5 +85,25 @@ public class FxmlComponent extends StackPane {
} }
return null; 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. * within the JavaFX property system.
*/ */
private StringProperty direction = new SimpleStringProperty("unidirectional"); private StringProperty direction = new SimpleStringProperty("unidirectional");
/** /**
* Represents the source of the binding. This property holds a string value * Represents the source of the binding. This property holds a string value
* that specifies the originating object or identifier in the binding connection. * that specifies the originating object or identifier in the binding connection.