From 5aa75be6ba406013c11863abc7e702666d1313a7 Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Tue, 1 Jul 2025 15:38:07 +0200 Subject: [PATCH] Added FileUtils / TempDirectory from EmailTool. Moved FileUtils to io package. --- .../de/neitzel/core/config/Configuration.java | 68 ++--- .../core/io/ConvertedEncodingFileReader.java | 66 ++--- .../neitzel/core/{util => io}/FileUtils.java | 192 ++++++++----- .../de/neitzel/core/io/TempDirectory.java | 76 ++++++ fx/ideas.md | 86 +++++- .../neitzel/fx/component/ComponentLoader.java | 258 +++++++++--------- .../fx/component/controls/Binding.java | 40 +-- .../fx/component/controls/FxmlComponent.java | 69 +++-- .../fx/component/model/BindingData.java | 1 + 9 files changed, 550 insertions(+), 306 deletions(-) rename core/src/main/java/de/neitzel/core/{util => io}/FileUtils.java (78%) create mode 100644 core/src/main/java/de/neitzel/core/io/TempDirectory.java diff --git a/core/src/main/java/de/neitzel/core/config/Configuration.java b/core/src/main/java/de/neitzel/core/config/Configuration.java index 94d9f3f..010b8e3 100644 --- a/core/src/main/java/de/neitzel/core/config/Configuration.java +++ b/core/src/main/java/de/neitzel/core/config/Configuration.java @@ -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. - * + *

* It provides methods to load, retrieve, and modify properties * as necessary for the application's requirements. */ @@ -56,20 +56,20 @@ public class Configuration { * Retrieves a boolean property value associated with the specified key. If the key does not exist in the properties, * the provided default value is returned. The method also supports interpreting specific string values as true. * - * @param key the key used to retrieve the boolean property + * @param key the key used to retrieve the boolean property * @param defaultValue the default value returned if the key is not found in the properties * @return the boolean value associated with the key, or the defaultValue if the key does not exist */ 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"); } /** * Retrieves the value of the specified property as a trimmed string. * If the property is not found, the default value is returned. * - * @param key the key of the property to retrieve + * @param key the key of the property to retrieve * @param defaultValue the default value to return if the property is not found * @return the trimmed string value of the property, or the default value if the property is not found */ @@ -82,34 +82,22 @@ public class Configuration { * Retrieves a string property value associated with the specified key, applies * environment variable expansion on the value, and returns the processed result. * - * @param key the key identifying the property to retrieve. + * @param key the key identifying the property to retrieve. * @param defaultValue the default value to use if the property is not found. * @return the processed property value with expanded environment variables, or - * the defaultValue if the property is not found. + * the defaultValue if the property is not found. */ protected String getStringPropertyWithEnv(final String key, final String defaultValue) { String result = getStringProperty(key, defaultValue); 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 * within the string. If the property is not found, a default value is used. * - * @param key the key identifying the configuration property + * @param key the key identifying the configuration property * @param defaultValue the default value to use if the property is not found * @return the processed string property with quotes removed and environment variables expanded */ @@ -118,16 +106,28 @@ 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 * value is returned. * - * @param key the property key to retrieve the value for + * @param key the property key to retrieve the value for * @param defaultValue the default value to return if the key is not present * or the value is null/empty * @return the integer value associated with the key, or the defaultValue if - * the key does not exist or its value is null/empty + * the key does not exist or its value is null/empty */ protected Integer getIntegerProperty(final String key, final Integer defaultValue) { if (!properties.containsKey(key)) return defaultValue; @@ -139,14 +139,14 @@ public class Configuration { * Sets an integer property in the properties object. If the provided value is null, * an empty string will be stored as the property's value. * - * @param key the key under which the property will be stored + * @param key the key under which the property will be stored * @param value the integer value to be stored; if null, an empty string will be used */ protected void setIntegerProperty(final String key, final Integer value) { if (value == null) { properties.setProperty(key, ""); } else { - properties.setProperty(key, ""+value); + properties.setProperty(key, "" + value); } } @@ -154,11 +154,11 @@ public class Configuration { * Retrieves a LocalDate value from the properties based on the provided key. * If the key does not exist or the value is invalid, a default value is returned. * - * @param key the key to look up the property in the properties map + * @param key the key to look up the property in the properties map * @param defaultValue the default LocalDate value to return if the key is not found * @param formatString the format string to parse the LocalDate value * @return the LocalDate value from the properties if available and valid, - * otherwise the defaultValue + * otherwise the defaultValue */ protected LocalDate getLocalDateProperty(final String key, final LocalDate defaultValue, final String formatString) { if (!properties.containsKey(key)) return defaultValue; @@ -172,8 +172,8 @@ public class Configuration { * Sets a property with the given key and a formatted LocalDate value. * If the provided value is null, the property will be set to an empty string. * - * @param key the key of the property to set - * @param value the LocalDate value to format and set as the property value + * @param key the key of the property to set + * @param value the LocalDate value to format and set as the property value * @param formatString the pattern string used to format the LocalDate value */ protected void setLocalDateProperty(final String key, final LocalDate value, final String formatString) { @@ -188,7 +188,7 @@ public class Configuration { * Sets a property with the specified key to the given value. If the value is null, * it defaults to an empty string. Logs the operation and updates the property. * - * @param key the key of the property to be set + * @param key the key of the property to be set * @param value the value to be associated with the specified key; defaults to an empty string if null */ public void setProperty(final String key, final String value) { @@ -211,9 +211,9 @@ public class Configuration { * specified location, it attempts to find the file alongside the JAR file of the application. * Reads the configuration with the provided encoding and an option to accept UTF-8 encoding. * - * @param fileName the name of the configuration file to be loaded - * @param encoding the encoding format to be used while reading the configuration file - * @param acceptUTF8 a boolean flag indicating whether to accept UTF-8 encoding + * @param fileName the name of the configuration file to be loaded + * @param encoding the encoding format to be used while reading the configuration file + * @param acceptUTF8 a boolean flag indicating whether to accept UTF-8 encoding */ public void load(final String fileName, final String encoding, final boolean acceptUTF8) { log.info("Reading Config: " + fileName + " with encoding: " + encoding + "accepting UTF-8: " + acceptUTF8); @@ -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 entry: config.properties.entrySet()) { + for (Map.Entry 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); } } diff --git a/core/src/main/java/de/neitzel/core/io/ConvertedEncodingFileReader.java b/core/src/main/java/de/neitzel/core/io/ConvertedEncodingFileReader.java index aa37570..ce6205c 100644 --- a/core/src/main/java/de/neitzel/core/io/ConvertedEncodingFileReader.java +++ b/core/src/main/java/de/neitzel/core/io/ConvertedEncodingFileReader.java @@ -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. - * + *

* This class is useful for applications needing to process text files in specific encodings and ensures * encoding compatibility. */ @@ -25,47 +24,19 @@ 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". - * + *

* 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. */ 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. - * - * @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; - } - - /** - * Constructs a ConvertedEncodingFileReader for a specified file and target encoding format. - * The class reads the provided file and ensures that its content is handled in the target encoding. - * If the file is not already in the target encoding, it converts the file's encoding - * transparently using a temporary file before reading it. - * - * @param file The file to be read. Must exist and be accessible. - * @param targetFormat The target character encoding format to which the file content should be converted. - * @throws IOException If the file does not exist, is inaccessible, or an error occurs during the encoding conversion process. - */ - public ConvertedEncodingFileReader(final File file, final String targetFormat) throws IOException { - 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 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 */ @@ -73,13 +44,27 @@ public class ConvertedEncodingFileReader extends InputStreamReader { this(new File(filename), targetFormat); } + /** + * Constructs a ConvertedEncodingFileReader for a specified file and target encoding format. + * The class reads the provided file and ensures that its content is handled in the target encoding. + * If the file is not already in the target encoding, it converts the file's encoding + * transparently using a temporary file before reading it. + * + * @param file The file to be read. Must exist and be accessible. + * @param targetFormat The target character encoding format to which the file content should be converted. + * @throws IOException If the file does not exist, is inaccessible, or an error occurs during the encoding conversion process. + */ + public ConvertedEncodingFileReader(final File file, final String targetFormat) throws IOException { + super(createTargetFormatInputFileStream(file, targetFormat), 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. * If the file is in UTF-8 encoding, it is converted to the specified target format using a temporary file, * and then an input stream for the temporary file is returned. * - * @param file the file for which the input stream is to be created + * @param file the file for which the input stream is to be created * @param targetFormat the desired target encoding format * @return a {@link FileInputStream} for the file or a temporary file with converted encoding * @throws IOException if the file does not exist or an error occurs during file operations @@ -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; + } } diff --git a/core/src/main/java/de/neitzel/core/util/FileUtils.java b/core/src/main/java/de/neitzel/core/io/FileUtils.java similarity index 78% rename from core/src/main/java/de/neitzel/core/util/FileUtils.java rename to core/src/main/java/de/neitzel/core/io/FileUtils.java index 6bb3219..5d99475 100644 --- a/core/src/main/java/de/neitzel/core/util/FileUtils.java +++ b/core/src/main/java/de/neitzel/core/io/FileUtils.java @@ -1,31 +1,21 @@ -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 { - /** - * Private constructor to prevent instantiation of the utility class. - * This utility class is not meant to be instantiated, as it only provides - * static utility methods for array-related operations. - * - * @throws UnsupportedOperationException always, to indicate that this class - * should not be instantiated. - */ - private FileUtils() { - throw new UnsupportedOperationException("Utility class"); - } - /** * 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 @@ -37,14 +27,14 @@ public class FileUtils { * - Minutes in two digits (mm) * - Seconds in two digits (ss) * - Milliseconds in three digits (SSS) - * + *

* 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. - * + *

* 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, @@ -54,17 +44,40 @@ public class FileUtils { /** * 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; + /** + * Private constructor to prevent instantiation of the utility class. + * This utility class is not meant to be instantiated, as it only provides + * static utility methods for array-related operations. + * + * @throws UnsupportedOperationException always, to indicate that this class + * should not be instantiated. + */ + private FileUtils() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * 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); + } + /** * Determines whether the given file is encoded in UTF-8. * - * @param file The file to be checked for UTF-8 encoding. + * @param file The file to be checked for UTF-8 encoding. * @param checkEncoding The character encoding to use while checking the file content. * @return true if the file is determined to be encoded in UTF-8; false otherwise. * @throws IOException If an I/O error occurs while reading the file. @@ -82,7 +95,7 @@ public class FileUtils { if ( (ArrayUtils.contains(buffer, (char) 0x00C2)) // Part of UTF-8 Characters 0xC2 0xZZ - || (ArrayUtils.contains(buffer, (char) 0x00C3))) { // Part of UTF-8 Characters 0xC3 0xZZ + || (ArrayUtils.contains(buffer, (char) 0x00C3))) { // Part of UTF-8 Characters 0xC3 0xZZ return true; } @@ -97,7 +110,7 @@ public class FileUtils { /** * Checks if the provided file starts with a UTF-8 Byte Order Mark (BOM). - * + *

* 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,26 +127,15 @@ 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. - * + *

* 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. * - * @param inFile The input text file to be converted. Must not be null. + * @param inFile The input text file to be converted. Must not be null. * @param sourceFormat The character encoding of the input file. Must not be null or empty. - * @param outFile The output text file to write the converted content to. Must not be null. + * @param outFile The output text file to write the converted content to. Must not be null. * @param targetFormat The character encoding to be used for the output file. Must not be null or empty. * @throws IOException If an I/O error occurs during reading or writing. */ @@ -167,25 +169,12 @@ 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, * returning an InputStreamReader for reading the file contents. * - * @param filename the name of the file to be read. + * @param filename the name of the file to be read. * @param expectedFormat the format expected for the file content. * @return an InputStreamReader for the specified file and format. * @throws IOException if an I/O error occurs while opening or reading the file. @@ -194,25 +183,14 @@ 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. * - * @param file The file to be read. Must not be null. + * @param file The file to be read. Must not be null. * @param expectedFormat The expected format of the file (e.g., encoding). Must not be null. * @return An InputStreamReader for the specified file, allowing the caller to read the file - * with the desired format applied. + * with the desired format applied. * @throws IOException If an I/O error occurs during the creation of the reader. */ public static InputStreamReader createUniversalFileReader(final File file, final String expectedFormat) throws IOException { @@ -223,9 +201,9 @@ public class FileUtils { * Creates an InputStreamReader for reading a file, considering the specified encoding format * and whether UTF-8 should be accepted. Handles potential BOM for UTF-8 encoded files. * - * @param file The file to be read. + * @param file The file to be read. * @param expectedFormat The expected encoding format of the file. - * @param acceptUTF8 Indicates whether UTF-8 encoding should be accepted if detected. + * @param acceptUTF8 Indicates whether UTF-8 encoding should be accepted if detected. * @return An InputStreamReader for the specified file and encoding. * @throws IOException If there is an error accessing the file or reading its content. */ @@ -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. - * + *

* If the given path does not have a parent directory, it defaults to returning the * current directory represented by ".". * @@ -282,10 +260,34 @@ 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. * - * @param path The path of the file to write to. Must not be null and must be writable. + * @param path The path of the file to write to. Must not be null and must be writable. * @param content The content to be written to the file. Must not be null. * @throws IOException If an I/O error occurs during writing to the file. */ @@ -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; + } + } diff --git a/core/src/main/java/de/neitzel/core/io/TempDirectory.java b/core/src/main/java/de/neitzel/core/io/TempDirectory.java new file mode 100644 index 0000000..f7ea1ed --- /dev/null +++ b/core/src/main/java/de/neitzel/core/io/TempDirectory.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 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); + } + }); + } + } + } +} diff --git a/fx/ideas.md b/fx/ideas.md index 8c180d5..8fa6cf0 100644 --- a/fx/ideas.md +++ b/fx/ideas.md @@ -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 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, Property, Format) +bindBidirectional(Property, Property, StringConverter) +bindBidirectional(Property, Property) + +## 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 +- \ No newline at end of file diff --git a/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java b/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java index 84f8bc2..9859156 100644 --- a/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java +++ b/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java @@ -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. - *

- * 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> 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 load(URL fxmlUrl, Object controller, String nothing) throws IOException { + FXMLLoader loader = new FXMLLoader(fxmlUrl); + loader.setController(controller); + T root = loader.load(); + + Map namespace = loader.getNamespace(); + + // Nach allen BindingControls suchen: + List 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 void bindBidirectionalSafe(@NotNull Property 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 targetCasted = (Property) 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 void bindSafe(@NotNull Property source, Property target) { + try { + Property targetCasted = (Property) 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 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 bindings, Map 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 extractNfxAttributes(javafx.scene.Node node) { - Map 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 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 collectAllNodes(Node root) { + List 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 nfxAttrs = new HashMap<>(); - var attrs = el.getAttributes(); - - List 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 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()); - } } diff --git a/fx/src/main/java/de/neitzel/fx/component/controls/Binding.java b/fx/src/main/java/de/neitzel/fx/component/controls/Binding.java index f4dc831..ffafadd 100644 --- a/fx/src/main/java/de/neitzel/fx/component/controls/Binding.java +++ b/fx/src/main/java/de/neitzel/fx/component/controls/Binding.java @@ -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. - * + *

* The primary purpose of this control is to maintain an observable list * of bindings, allowing developers to track or adjust the linked properties * dynamically. - * + *

* 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. - * + *

* 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. - * + *

* 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. - * + *

* 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 bindings = FXCollections.observableArrayList(); + /** + * 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() { + // 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 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" - } } \ No newline at end of file diff --git a/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java b/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java index a871db4..635910f 100644 --- a/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java +++ b/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java @@ -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 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 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 dataProperty() { + return data; + } } diff --git a/fx/src/main/java/de/neitzel/fx/component/model/BindingData.java b/fx/src/main/java/de/neitzel/fx/component/model/BindingData.java index 6c7ba64..8b65618 100644 --- a/fx/src/main/java/de/neitzel/fx/component/model/BindingData.java +++ b/fx/src/main/java/de/neitzel/fx/component/model/BindingData.java @@ -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.