Compare commits

...

11 Commits

Author SHA1 Message Date
0c5b92bd76 Merge pull request 'feature/github-copilot-consolidation' (#1) from feature/github-copilot-consolidation into main
Reviewed-on: #1
2025-12-14 13:29:42 +01:00
Konrad Neitzel
edf02e9816 Refactorings, creating module injection. 2025-12-02 18:23:05 +01:00
Konrad Neitzel
3178dc2723 Refactorings ... 2025-11-30 20:40:26 +01:00
Konrad Neitzel
5503dd192e Work in progress ... 2025-11-30 18:58:05 +01:00
Konrad Neitzel
9bbddaaae9 Merge remote-tracking branch 'origin/feature/github-copilot-consolidation' into feature/fx-component 2025-11-30 15:13:08 +01:00
Konrad Neitzel
9c55794be8 Merge branch 'main' into feature/fx-component 2025-11-30 15:12:24 +01:00
Konrad Neitzel
93de125e67 Add guidelines for Copilot usage and commit messages
Updated dependencies and use Java 25.
2025-11-29 22:47:48 +01:00
Konrad Neitzel
6c1338d171 Added JsonConfiguration 2025-07-01 15:50:03 +02:00
Konrad Neitzel
5aa75be6ba Added FileUtils / TempDirectory from EmailTool.
Moved FileUtils to io package.
2025-07-01 15:38:07 +02:00
Konrad Neitzel
2b60038774 Binding and FxmlComponent controls to allow bindings and loading of FXML Components. 2025-04-12 20:38:20 +02:00
Konrad Neitzel
be6a8c853e Updated Injection Classes. Unit Tests still required. 2025-04-11 21:55:18 +02:00
89 changed files with 6365 additions and 4565 deletions

15
.github/JavaDoc-rules.md vendored Normal file
View File

@ -0,0 +1,15 @@
JavaDoc should be included for:
[]: # - All public, protected and private classes and interfaces
[]: # - All public, protected and private methods and constructors
[]: # - Any method that overrides a superclass method
[]: # 3. If JavaDoc is missing, please add it following these guidelines:
[]: # - Use clear and concise language to describe the purpose and functionality.
[]: # - Include descriptions for all parameters using @param tags.
[]: # - Include a description of the return value using @return tags.
[]: # - Mention any exceptions that the method might throw using @throws tags.
[]: # - Follow standard JavaDoc formatting conventions.
[]: # 4. After adding JavaDoc, review it for clarity and completeness.
[]: # 5. Commit your changes with a message indicating that JavaDoc has been added or updated.
[]: #
[]: # By following these instructions, we can ensure that our codebase remains well-documented and easy to understand
for all contributors.

9
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,9 @@
When working on a prompt, please follow these instructions:
1. When creating Code then just create the code required to complete the task. Do not include any explanations,
comments, or other text.
2. After code changes, check if any JavaDoc is missing. Please see JavaDoc-rules.md for guidelines on adding JavaDoc.
3. Create Unit Tests for the code you created or modified. Ensure that the tests cover various scenarios and edge cases.
4. When writing commit messages, please follow the guidelines in git-commit-instructions.md to ensure clarity and
consistency.
5. Ensure that your code adheres to the project's coding standards and best practices.

9
.github/git-commit-instructions.md vendored Normal file
View File

@ -0,0 +1,9 @@
# Git Commit Message Guidelines
When creating a commit message, please follow these guidelines to ensure clarity and consistency across the project:
1. **Structure of Commit Message**:
- The commit message should consist of a header, a body (optional), and a footer (optional).
- The header should be a single line that summarizes the changes made.
- If necessary, the body can provide additional context or details about the changes.
- The footer can include references to issues or breaking changes.

1
.github/prompts/TEST.prompt.md vendored Normal file
View File

@ -0,0 +1 @@
Please just respond with "Hello, World!"

View File

@ -14,5 +14,5 @@
# KIND, either express or implied. See the License for the # KIND, either express or implied. See the License for the
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar

View File

@ -2,24 +2,32 @@
## A Java Utilities Library ## A Java Utilities Library
This repository is **not** a production-ready library, but rather a personal collection of small helpers, utilities, and ideas that I find useful or interesting as a Java developer. This repository is **not** a production-ready library, but rather a personal collection of small helpers, utilities, and
ideas that I find useful or interesting as a Java developer.
It serves primarily as a **knowledge base** and **inspiration pool** for my own development work. You're welcome to explore, copy, modify, or improve whatever you find helpful. It serves primarily as a **knowledge base** and **inspiration pool** for my own development work. You're welcome to
explore, copy, modify, or improve whatever you find helpful.
> ⚠️ Use at your own discretion — no guarantees of stability, backwards compatibility, or completeness. > ⚠️ Use at your own discretion — no guarantees of stability, backwards compatibility, or completeness.
--- ---
## License ## License
This is free and unencumbered software released into the public domain. Please see the [License](LICENSE.md) for details.
This is free and unencumbered software released into the public domain. Please see the [License](LICENSE.md) for
details.
--- ---
## Note on Content ## Note on Content
The content of this repository — including source code, documentation, and examples — was partially created with the assistance of ChatGPT (an AI model by OpenAI). The generated content has been reviewed, edited, and integrated into the overall context by me. The content of this repository — including source code, documentation, and examples — was partially created with the
assistance of ChatGPT (an AI model by OpenAI). The generated content has been reviewed, edited, and integrated into the
overall context by me.
Responsibility for all content lies entirely with me. Any generated content is subject to [OpenAIs Terms of Use](https://openai.com/policies/terms-of-use) and is — where possible — either in the public domain or usable under a license compatible with this project's license. Responsibility for all content lies entirely with me. Any generated content is subject
to [OpenAIs Terms of Use](https://openai.com/policies/terms-of-use) and is — where possible — either in the public
domain or usable under a license compatible with this project's license.
## Components ## Components
@ -41,10 +49,19 @@ Some JavaFX additions.
Some example programs that use the fx component Some example programs that use the fx component
### gson
TypeAdapter for gson.
### log4j ### log4j
Stuff for log4j (Version 1). Please use log4j2 if you want to use log4j! (Or log with slf4j as I do in all other modules!) Stuff for log4j (Version 1). Please use log4j2 if you want to use log4j! (Or log with slf4j as I do in all other
modules!)
### net ### net
Some network stuff using javax.mail as dependency. Some network stuff using javax.mail as dependency.
### quarkus
Some additions for quarkus that might be useful.

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -6,10 +6,10 @@ import java.util.Iterator;
* The ArgumentProvider class is a helper utility for iterating over an array * The ArgumentProvider class is a helper utility for iterating over an array
* of command-line arguments. It provides methods to check for available arguments * of command-line arguments. It provides methods to check for available arguments
* and retrieve them in sequence. * and retrieve them in sequence.
* * <p>
* This class is designed to work seamlessly with a command-line parser * This class is designed to work seamlessly with a command-line parser and handle
* and handle tokenized inputs efficiently. It implements the Iterator interface * tokenized inputs efficiently. It implements the {@link Iterator} interface for ease
* for ease of iteration. * of iteration and provides convenience methods for peeking ahead.
*/ */
public class ArgumentProvider implements Iterator<String> { public class ArgumentProvider implements Iterator<String> {
@ -19,18 +19,15 @@ public class ArgumentProvider implements Iterator<String> {
* and processing by the application logic. It is used within the {@code ArgumentProvider} * and processing by the application logic. It is used within the {@code ArgumentProvider}
* class to iterate through and retrieve arguments systematically. * class to iterate through and retrieve arguments systematically.
*/ */
private String[] arguments; private final String[] arguments;
/** /**
* Tracks the current position within an array of arguments. * Tracks the current position within an array of arguments.
* * This variable is used to index into the arguments array, enabling sequential access
* This variable is used to index into the arguments array, enabling * to command-line tokens. It is incremented as arguments are processed or retrieved
* sequential access to command-line tokens. It is incremented as arguments * using methods such as {@code next()} or {@code peek()} in the {@link ArgumentProvider}
* are processed or retrieved using methods such as {@code next()} or {@code peek()} * class. Its value starts at 0 and increases until it reaches the length of the provided
* in the {@link ArgumentProvider} class. * arguments array, at which point iteration ends.
*
* Its value starts at 0 and increases until it reaches the length of
* the provided arguments array, at which point iteration ends.
*/ */
private int current = 0; private int current = 0;
@ -43,21 +40,12 @@ public class ArgumentProvider implements Iterator<String> {
*/ */
public ArgumentProvider(final String[] arguments) { public ArgumentProvider(final String[] arguments) {
if (arguments == null) { if (arguments == null) {
this.arguments = new String[] {}; this.arguments = new String[]{};
} else { } else {
this.arguments = arguments; this.arguments = arguments;
} }
} }
/**
* Checks if there are more arguments available to iterate over.
*
* @return true if there are additional arguments available; false otherwise.
*/
public boolean hasNext() {
return current < arguments.length;
}
/** /**
* Determines if the specified number of additional arguments are available in the collection. * Determines if the specified number of additional arguments are available in the collection.
* *
@ -68,6 +56,26 @@ public class ArgumentProvider implements Iterator<String> {
return current + count <= arguments.length; return current + count <= arguments.length;
} }
/**
* Returns the next argument in the sequence without advancing the iterator.
* If there are no more arguments available, this method returns null.
*
* @return the next argument in the sequence or null if no arguments are available
*/
public String peek() {
if (!hasNext()) return null;
return arguments[current];
}
/**
* Checks if there are more arguments available to iterate over.
*
* @return true if there are additional arguments available; false otherwise.
*/
public boolean hasNext() {
return current < arguments.length;
}
/** /**
* Retrieves the next argument in the sequence. * Retrieves the next argument in the sequence.
* If no more arguments are available, returns {@code null}. * If no more arguments are available, returns {@code null}.
@ -81,15 +89,4 @@ public class ArgumentProvider implements Iterator<String> {
current++; current++;
return result; return result;
} }
/**
* Returns the next argument in the sequence without advancing the iterator.
* If there are no more arguments available, this method returns null.
*
* @return the next argument in the sequence or null if no arguments are available
*/
public String peek() {
if (!hasNext()) return null;
return arguments[current];
}
} }

View File

@ -13,7 +13,7 @@ import java.util.function.Consumer;
* The `Parameter` class defines a command-line parameter with associated metadata, * The `Parameter` class defines a command-line parameter with associated metadata,
* such as constraints on the number of values it accepts, descriptions, and aliases. * such as constraints on the number of values it accepts, descriptions, and aliases.
* It also allows specifying callbacks to handle parameter processing. * It also allows specifying callbacks to handle parameter processing.
* * <p>
* This class is intended to be used as part of a command-line parser, enabling * This class is intended to be used as part of a command-line parser, enabling
* structured handling of arguments provided via the command line. * structured handling of arguments provided via the command line.
*/ */
@ -23,14 +23,141 @@ import java.util.function.Consumer;
@Setter @Setter
public class Parameter { public class Parameter {
/**
* The `name` variable represents the primary identifier for a parameter.
* This string is used to uniquely identify a command-line parameter when parsing
* arguments. It serves as the key for registering and retrieving parameters in a
* command-line parser.
* <p>
* The value of `name` is case-insensitive and can be used alongside aliases to
* recognize and process parameters provided in the command-line input.
*/
private String name;
/**
* Specifies the minimum number of values required for this parameter.
* <p>
* This variable enforces a constraint on the number of arguments that must
* be provided for the parameter during command-line parsing. If the number of
* provided arguments is less than this value, an exception will be thrown during parsing.
* <p>
* It is primarily used in validation logic within the command-line parser
* to ensure the required input is received for proper processing of the parameter.
*/
private int minNumberValues;
/**
* Specifies the maximum number of values that the corresponding parameter can accept.
* <p>
* This field is used to validate and enforce constraints during command-line parsing.
* If more values than specified are provided for a parameter, the parser throws an exception
* to indicate a violation of this constraint.
* <p>
* A value of 0 implies that the parameter does not support any values, while a positive
* value indicates the exact maximum limit of acceptable values.
*/
private int maxNumberValues;
/**
* Indicates whether the parameter allows multiple entries.
* <p>
* If set to {@code true}, the parameter can be specified multiple times
* on the command-line. This allows the association of multiple values
* or options with the same parameter name.
* <p>
* If set to {@code false}, the parameter can be specified only once
* during command-line parsing.
*/
private boolean multipleEntries;
/**
* A concise, human-readable description of the parameter's purpose and functionality.
* <p>
* This description provides a brief summary to help users understand the
* essential role of the parameter within the command-line parser. It is typically
* displayed in usage instructions, help messages, or command summaries.
*/
private String shortDescription;
/**
* A detailed, extended description of the parameter's purpose and usage.
* <p>
* This description provides deeper context about what the parameter represents,
* how it fits into the overall command-line application, and any nuances
* associated with its use. It is displayed when help or additional documentation
* for a specific parameter is requested.
*/
private String longDescription;
/**
* A callback function that processes a list of string arguments for a specific parameter.
* The callback is invoked during command-line parsing when a parameter is recognized,
* passing the associated values of the parameter for further processing.
* <p>
* The provided {@link Consumer} implementation defines the logic to
* handle or validate the values supplied for a command-line parameter. The values
* passed are determined based on the parameter's configuration (e.g., minimum and
* maximum number of associated values).
* <p>
* This field is typically set for the `Parameter` class to enable custom handling
* of parameter-specific logic, such as invoking a help command or performing
* business logic with the values parsed from the command-line arguments.
*/
private Consumer<List<String>> callback;
/**
* Indicates whether the parameter is designated as the help command.
* <p>
* When set to {@code true}, this parameter is treated as the special help command
* within the command-line parser. A parameter marked as the help command typically
* provides users with information about available options or detailed descriptions
* of specific commands when invoked. If this flag is enabled, a custom callback
* method for displaying help context is automatically associated with the parameter.
*/
private boolean isHelpCommand;
/**
* Indicates whether the parameter is designated as the default parameter.
* <p>
* The default parameter is a special type of parameter that can accept arguments
* without an explicit prefix or identifier. It is used when a provided command-line
* argument does not match any defined parameter and does not start with a special
* character (e.g., '-') typically used to denote named parameters.
* <p>
* If this field is set to {@code true}, the parameter is treated as the default
* parameter. Only one parameter can be assigned this role within the parser.
* Attempting to assign multiple default parameters or a default parameter without
* accepting values (both `minNumberValues` and `maxNumberValues` set to 0) will
* result in an exception during configuration.
* <p>
* This property is utilized during the parsing process to determine whether an
* unmatched argument should be handled as a default parameter value, simplifying
* the handling of positional arguments or other unnamed input data.
*/
private boolean isDefaultParameter;
/**
* A list of alias names associated with a parameter.
* <p>
* This variable holds alternative names that can be used
* to reference the parameter in command-line input. Each alias
* functions as an equivalent to the primary parameter name.
* Aliases are case-insensitive when used within the command-line parser.
* <p>
* The aliases associated with a parameter allow greater flexibility
* and user convenience when specifying parameters during command-line execution.
*/
@Singular("alias")
private List<String> aliasList;
/** /**
* Represents a command-line parameter definition within the application. * Represents a command-line parameter definition within the application.
* * <p>
* This class encapsulates the metadata and configuration of a command-line parameter, * This class encapsulates the metadata and configuration of a command-line parameter,
* including its name, aliases, descriptions, value constraints, and behavior. Instances * including its name, aliases, descriptions, value constraints, and behavior. Instances
* of this class are used to define and manage the parameters that the `Parser` class * of this class are used to define and manage the parameters that the `Parser` class
* recognizes and processes. * recognizes and processes.
* * <p>
* A parameter can be configured as a default parameter or as a help command. The default * A parameter can be configured as a default parameter or as a help command. The default
* parameter serves as a catch-all for unmatched command-line inputs, while the help * parameter serves as a catch-all for unmatched command-line inputs, while the help
* command provides usage information for users. * command provides usage information for users.
@ -64,131 +191,4 @@ public class Parameter {
this.isDefaultParameter = isDefaultParameter; this.isDefaultParameter = isDefaultParameter;
this.aliasList = aliasList; this.aliasList = aliasList;
} }
/**
* The `name` variable represents the primary identifier for a parameter.
* This string is used to uniquely identify a command-line parameter when parsing
* arguments. It serves as the key for registering and retrieving parameters in a
* command-line parser.
*
* The value of `name` is case-insensitive and can be used alongside aliases to
* recognize and process parameters provided in the command-line input.
*/
private String name;
/**
* Specifies the minimum number of values required for this parameter.
*
* This variable enforces a constraint on the number of arguments that must
* be provided for the parameter during command-line parsing. If the number of
* provided arguments is less than this value, an exception will be thrown during parsing.
*
* It is primarily used in validation logic within the command-line parser
* to ensure the required input is received for proper processing of the parameter.
*/
private int minNumberValues;
/**
* Specifies the maximum number of values that the corresponding parameter can accept.
*
* This field is used to validate and enforce constraints during command-line parsing.
* If more values than specified are provided for a parameter, the parser throws an exception
* to indicate a violation of this constraint.
*
* A value of 0 implies that the parameter does not support any values, while a positive
* value indicates the exact maximum limit of acceptable values.
*/
private int maxNumberValues;
/**
* Indicates whether the parameter allows multiple entries.
*
* If set to {@code true}, the parameter can be specified multiple times
* on the command-line. This allows the association of multiple values
* or options with the same parameter name.
*
* If set to {@code false}, the parameter can be specified only once
* during command-line parsing.
*/
private boolean multipleEntries;
/**
* A concise, human-readable description of the parameter's purpose and functionality.
*
* This description provides a brief summary to help users understand the
* essential role of the parameter within the command-line parser. It is typically
* displayed in usage instructions, help messages, or command summaries.
*/
private String shortDescription;
/**
* A detailed, extended description of the parameter's purpose and usage.
*
* This description provides deeper context about what the parameter represents,
* how it fits into the overall command-line application, and any nuances
* associated with its use. It is displayed when help or additional documentation
* for a specific parameter is requested.
*/
private String longDescription;
/**
* A callback function that processes a list of string arguments for a specific parameter.
* The callback is invoked during command-line parsing when a parameter is recognized,
* passing the associated values of the parameter for further processing.
*
* The provided {@link Consumer} implementation defines the logic to
* handle or validate the values supplied for a command-line parameter. The values
* passed are determined based on the parameter's configuration (e.g., minimum and
* maximum number of associated values).
*
* This field is typically set for the `Parameter` class to enable custom handling
* of parameter-specific logic, such as invoking a help command or performing
* business logic with the values parsed from the command-line arguments.
*/
private Consumer<List<String>> callback;
/**
* Indicates whether the parameter is designated as the help command.
*
* When set to {@code true}, this parameter is treated as the special help command
* within the command-line parser. A parameter marked as the help command typically
* provides users with information about available options or detailed descriptions
* of specific commands when invoked. If this flag is enabled, a custom callback
* method for displaying help context is automatically associated with the parameter.
*/
private boolean isHelpCommand;
/**
* Indicates whether the parameter is designated as the default parameter.
*
* The default parameter is a special type of parameter that can accept arguments
* without an explicit prefix or identifier. It is used when a provided command-line
* argument does not match any defined parameter and does not start with a special
* character (e.g., '-') typically used to denote named parameters.
*
* If this field is set to {@code true}, the parameter is treated as the default
* parameter. Only one parameter can be assigned this role within the parser.
* Attempting to assign multiple default parameters or a default parameter without
* accepting values (both `minNumberValues` and `maxNumberValues` set to 0) will
* result in an exception during configuration.
*
* This property is utilized during the parsing process to determine whether an
* unmatched argument should be handled as a default parameter value, simplifying
* the handling of positional arguments or other unnamed input data.
*/
private boolean isDefaultParameter;
/**
* A list of alias names associated with a parameter.
*
* This variable holds alternative names that can be used
* to reference the parameter in command-line input. Each alias
* functions as an equivalent to the primary parameter name.
* Aliases are case-insensitive when used within the command-line parser.
*
* The aliases associated with a parameter allow greater flexibility
* and user convenience when specifying parameters during command-line execution.
*/
@Singular("alias")
private List<String> aliasList;
} }

View File

@ -2,7 +2,11 @@ package de.neitzel.core.commandline;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.*; import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* The Parser class is responsible for parsing and handling command-line arguments. * The Parser class is responsible for parsing and handling command-line arguments.
@ -15,11 +19,11 @@ public class Parser {
/** /**
* A map to store parameters where the key is a string representing * A map to store parameters where the key is a string representing
* the parameter name and the value is the corresponding {@link Parameter} object. * the parameter name and the value is the corresponding {@link Parameter} object.
* * <p>
* This map serves as a registry for defining, organizing, and accessing * This map serves as a registry for defining, organizing, and accessing
* command-line parameters. Each entry holds a parameter's metadata and * command-line parameters. Each entry holds a parameter's metadata and
* its associated configuration to facilitate effective command-line parsing. * its associated configuration to facilitate effective command-line parsing.
* * <p>
* Key considerations: * Key considerations:
* - The key represents the primary name of the parameter. * - The key represents the primary name of the parameter.
* - The value, encapsulated as a {@link Parameter} object, includes * - The value, encapsulated as a {@link Parameter} object, includes
@ -29,18 +33,18 @@ public class Parser {
/** /**
* The `defaultParameter` variable represents the default command-line parameter for a parser. * The `defaultParameter` variable represents the default command-line parameter for a parser.
* * <p>
* This parameter is used to capture arguments that do not explicitly match any named parameter * This parameter is used to capture arguments that do not explicitly match any named parameter
* or alias. It acts as a catch-all for unnamed command-line input, often simplifying the handling * or alias. It acts as a catch-all for unnamed command-line input, often simplifying the handling
* of positional arguments or other free-form input provided by the user. * of positional arguments or other free-form input provided by the user.
* * <p>
* It is essential to note: * It is essential to note:
* - Only one parameter can be designated as the default parameter in a command-line parser. * - Only one parameter can be designated as the default parameter in a command-line parser.
* - The parameter must allow values to be processed (e.g., its `minNumberValues` or `maxNumberValues` * - The parameter must allow values to be processed (e.g., its `minNumberValues` or `maxNumberValues`
* should not disallow input). * should not disallow input).
* - This parameter is automatically invoked if an input does not match any explicitly named * - This parameter is automatically invoked if an input does not match any explicitly named
* parameter or alias in the parser configuration. * parameter or alias in the parser configuration.
* * <p>
* When set to `null`, the parser does not handle unmatched arguments, and unrecognized inputs * When set to `null`, the parser does not handle unmatched arguments, and unrecognized inputs
* may result in an error or exception, depending on the parser's implementation. * may result in an error or exception, depending on the parser's implementation.
*/ */
@ -48,12 +52,12 @@ public class Parser {
/** /**
* Constructs a new instance of the `Parser` class. * Constructs a new instance of the `Parser` class.
* * <p>
* The `Parser` class is responsible for parsing and processing * The `Parser` class is responsible for parsing and processing
* command-line arguments. It interprets input data based on defined * command-line arguments. It interprets input data based on defined
* parameters and provides structured access to arguments, enabling * parameters and provides structured access to arguments, enabling
* streamlined application configuration and execution. * streamlined application configuration and execution.
* * <p>
* This constructor initializes the parser without any predefined * This constructor initializes the parser without any predefined
* configuration or parameters. Users can define parameters, * configuration or parameters. Users can define parameters,
* add handlers, and parse command-line arguments using available * add handlers, and parse command-line arguments using available
@ -71,7 +75,6 @@ public class Parser {
* and other metadata such as whether it is a help command or default parameter. * and other metadata such as whether it is a help command or default parameter.
* The parameter must fulfill specific constraints if defined as the default * The parameter must fulfill specific constraints if defined as the default
* parameter (e.g., accepting values). * parameter (e.g., accepting values).
*
* @throws IllegalArgumentException if the parameter is defined as a default parameter and * @throws IllegalArgumentException if the parameter is defined as a default parameter and
* does not accept values (both minNumberValues and maxNumberValues * does not accept values (both minNumberValues and maxNumberValues
* are set to 0). * are set to 0).
@ -81,7 +84,7 @@ public class Parser {
parameters.put(parameter.getName().toLowerCase(), parameter); parameters.put(parameter.getName().toLowerCase(), parameter);
// Add for all aliases // Add for all aliases
for (String alias: parameter.getAliasList()) { for (String alias : parameter.getAliasList()) {
parameters.put(alias.toLowerCase(), parameter); parameters.put(alias.toLowerCase(), parameter);
} }
@ -98,22 +101,39 @@ public class Parser {
} }
/** /**
* Checks whether a given parameter string matches a valid parameter within the defined constraints. * Handles the help command when invoked in the command-line interface, providing information about available parameters.
* If no arguments are provided, it lists all possible parameters and their short descriptions.
* If specific arguments are supplied, it provides the detailed description of the corresponding parameter.
* In case an unknown parameter is specified, it indicates so to the user.
* *
* @param param the parameter string to be checked. This can either be a key defined in the `parameters` map * @param arguments a list of strings representing the user-provided arguments. If empty or {@code null},
* or a potential default parameter if it does not start with a dash ("-"). * the method lists all available parameters with their short descriptions. If a parameter name
* @return {@code true} if the parameter is a valid entry in the `parameters` map or matches the default parameter; * is provided in the list, its detailed information is displayed. If an unrecognized parameter
* {@code false} otherwise, including when the parameter starts with a dash ("-"). * is provided, a corresponding message is shown.
*/ */
protected boolean isParameter(final String param) { public void helpCommandCallback(final List<String> arguments) {
if (parameters.containsKey(param)) return true; if (arguments == null || arguments.size() == 0) {
if (param.startsWith("-")) return false; System.out.println("Moegliche Parameter der Applikation:");
return defaultParameter != null; parameters.values().stream()
.distinct()
.sorted(Comparator.comparing(Parameter::getName))
.forEach(p -> System.out.println(p.getShortDescription()));
} else {
Parameter parameter = null;
if (parameters.containsKey(arguments.get(0))) parameter = parameters.get(arguments.get(0));
if (parameters.containsKey("-" + arguments.get(0))) parameter = parameters.get("-" + arguments.get(0));
if (parameter == null) {
System.out.println("Unbekannter Parameter: " + arguments.get(0));
} else {
System.out.println(parameter.getLongDescription());
}
}
} }
/** /**
* Parses an array of command-line arguments and processes them as defined by the application's parameters. * Parses an array of command-line arguments and processes them as defined by the application's parameters.
* * <p>
* The method iteratively checks each argument in the provided array, validates it against * The method iteratively checks each argument in the provided array, validates it against
* the registered parameters, and invokes the corresponding processing callback if applicable. * the registered parameters, and invokes the corresponding processing callback if applicable.
* Unrecognized parameters or missing required values will result in an {@code IllegalArgumentException}. * Unrecognized parameters or missing required values will result in an {@code IllegalArgumentException}.
@ -151,6 +171,20 @@ public class Parser {
} }
} }
/**
* Checks whether a given parameter string matches a valid parameter within the defined constraints.
*
* @param param the parameter string to be checked. This can either be a key defined in the `parameters` map
* or a potential default parameter if it does not start with a dash ("-").
* @return {@code true} if the parameter is a valid entry in the `parameters` map or matches the default parameter;
* {@code false} otherwise, including when the parameter starts with a dash ("-").
*/
protected boolean isParameter(final String param) {
if (parameters.containsKey(param)) return true;
if (param.startsWith("-")) return false;
return defaultParameter != null;
}
/** /**
* Retrieves a list of options by consuming arguments from the provided {@link ArgumentProvider} * Retrieves a list of options by consuming arguments from the provided {@link ArgumentProvider}
* within the range specified by the minimum and maximum values. * within the range specified by the minimum and maximum values.
@ -182,35 +216,4 @@ public class Parser {
return result; return result;
} }
/**
* Handles the help command when invoked in the command-line interface, providing information about available parameters.
* If no arguments are provided, it lists all possible parameters and their short descriptions.
* If specific arguments are supplied, it provides the detailed description of the corresponding parameter.
* In case an unknown parameter is specified, it indicates so to the user.
*
* @param arguments a list of strings representing the user-provided arguments. If empty or {@code null},
* the method lists all available parameters with their short descriptions. If a parameter name
* is provided in the list, its detailed information is displayed. If an unrecognized parameter
* is provided, a corresponding message is shown.
*/
public void helpCommandCallback(final List<String> arguments) {
if (arguments == null || arguments.size() == 0) {
System.out.println("Moegliche Parameter der Applikation:");
parameters.values().stream()
.distinct()
.sorted(Comparator.comparing(Parameter::getName))
.forEach(p -> System.out.println(p.getShortDescription() ));
} else {
Parameter parameter = null;
if (parameters.containsKey(arguments.get(0))) parameter = parameters.get(arguments.get(0));
if (parameters.containsKey("-" + arguments.get(0))) parameter = parameters.get("-" + arguments.get(0));
if (parameter == null) {
System.out.println("Unbekannter Parameter: " + arguments.get(0));
} else {
System.out.println(parameter.getLongDescription());
}
}
}
} }

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

@ -17,6 +17,19 @@ import java.io.InputStream;
@Slf4j @Slf4j
public class ImageScaler { public class ImageScaler {
/**
* Specifies the target image format for scaling operations.
* <p>
* This variable defines the file format that will be used when writing
* or encoding the scaled image. The format must be compatible with
* Java's ImageIO framework (e.g., "png", "jpg", "bmp").
* <p>
* In this case, the target format is set to "png", allowing images
* to be scaled and saved or returned as PNG files. It ensures
* consistent output format across the image scaling methods in the class.
*/
private static String TARGET_FORMAT = "png";
/** /**
* Private constructor to prevent instantiation of this utility class. * Private constructor to prevent instantiation of this utility class.
*/ */
@ -24,51 +37,9 @@ public class ImageScaler {
throw new UnsupportedOperationException("Utility class cannot be instantiated"); throw new UnsupportedOperationException("Utility class cannot be instantiated");
} }
/**
* Specifies the target image format for scaling operations.
*
* This variable defines the file format that will be used when writing
* or encoding the scaled image. The format must be compatible with
* Java's ImageIO framework (e.g., "png", "jpg", "bmp").
*
* In this case, the target format is set to "png", allowing images
* to be scaled and saved or returned as PNG files. It ensures
* consistent output format across the image scaling methods in the class.
*/
private static String TARGET_FORMAT="png";
/**
* Creates a scaled image from the given byte array representation and returns it as a byte array.
*
* If enforceSize is set to false, the given dimensions are used as maximum values for scaling.
* The resulting image may retain its aspect ratio and be smaller than the specified dimensions.
* If enforceSize is true, the resulting image will match the exact dimensions, potentially adding
* transparent areas on the top/bottom or right/left sides to maintain the original aspect ratio.
*
* @param originalImageBytes The byte array representing the original image.
* @param width The (maximum) width of the target image.
* @param height The (maximum) height of the target image.
* @param enforceSize A flag indicating whether the resulting image should strictly adhere to specified dimensions.
* If false, the image will retain its aspect ratio without empty borders.
* @return A byte array representing the scaled image, or null if the operation failed or the input was invalid.
*/
public static byte[] createScaledImage(final byte[] originalImageBytes, final int width, final int height, final boolean enforceSize) {
// Validation
if (originalImageBytes == null) return null;
if (originalImageBytes.length==0) return null;
try {
// Create the image from a byte array.
BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(originalImageBytes));
return createScaledImage(originalImage, width, height, enforceSize);
} catch (Exception ex) {
return null;
}
}
/** /**
* Scales an image provided as a byte array to the specified dimensions and returns an InputStream of the scaled image. * Scales an image provided as a byte array to the specified dimensions and returns an InputStream of the scaled image.
* * <p>
* The scaling behavior is determined by the enforceSize parameter: * The scaling behavior is determined by the enforceSize parameter:
* - If enforceSize is false, the provided dimensions are treated as maximum values, maintaining the original aspect ratio. * - If enforceSize is false, the provided dimensions are treated as maximum values, maintaining the original aspect ratio.
* - If enforceSize is true, the resulting image will match the exact dimensions, potentially with transparent borders to keep the original aspect ratio. * - If enforceSize is true, the resulting image will match the exact dimensions, potentially with transparent borders to keep the original aspect ratio.
@ -91,8 +62,37 @@ public class ImageScaler {
} }
/** /**
* Creates a scaled version of the given {@link BufferedImage} and returns it as a byte array. * Creates a scaled image from the given byte array representation and returns it as a byte array.
* <p>
* If enforceSize is set to false, the given dimensions are used as maximum values for scaling.
* The resulting image may retain its aspect ratio and be smaller than the specified dimensions.
* If enforceSize is true, the resulting image will match the exact dimensions, potentially adding
* transparent areas on the top/bottom or right/left sides to maintain the original aspect ratio.
* *
* @param originalImageBytes The byte array representing the original image.
* @param width The (maximum) width of the target image.
* @param height The (maximum) height of the target image.
* @param enforceSize A flag indicating whether the resulting image should strictly adhere to specified dimensions.
* If false, the image will retain its aspect ratio without empty borders.
* @return A byte array representing the scaled image, or null if the operation failed or the input was invalid.
*/
public static byte[] createScaledImage(final byte[] originalImageBytes, final int width, final int height, final boolean enforceSize) {
// Validation
if (originalImageBytes == null) return null;
if (originalImageBytes.length == 0) return null;
try {
// Create the image from a byte array.
BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(originalImageBytes));
return createScaledImage(originalImage, width, height, enforceSize);
} catch (Exception ex) {
return null;
}
}
/**
* Creates a scaled version of the given {@link BufferedImage} and returns it as a byte array.
* <p>
* The scaling behavior is determined by the enforceSize parameter: * The scaling behavior is determined by the enforceSize parameter:
* - If enforceSize is false, the provided dimensions are treated as maximum values, and the image * - If enforceSize is false, the provided dimensions are treated as maximum values, and the image
* will maintain its original aspect ratio. The resulting image may be smaller than the specified * will maintain its original aspect ratio. The resulting image may be smaller than the specified

View File

@ -1,9 +1,12 @@
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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.IllegalCharsetNameException;
@ -12,7 +15,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 +28,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 +36,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 +62,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 +89,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,64 @@
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.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
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 +72,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 +118,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 +135,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 +177,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 +191,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 +225,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 +268,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 +304,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

@ -38,17 +38,17 @@ public class StringEncoder {
int indexAmp = remaining.indexOf("&"); int indexAmp = remaining.indexOf("&");
if (indexAmp == -1) { if (indexAmp == -1) {
result.append(remaining); result.append(remaining);
remaining=""; remaining = "";
} else { } else {
// First get the elements till the & // First get the elements till the &
if (indexAmp > 0) { if (indexAmp > 0) {
result.append(remaining.substring(0, indexAmp)); result.append(remaining.substring(0, indexAmp));
remaining = remaining.substring(indexAmp); remaining = remaining.substring(indexAmp);
} }
int endSpecial=remaining.indexOf(";"); int endSpecial = remaining.indexOf(";");
if (endSpecial == -1) throw new IllegalArgumentException("String couldn't be decoded! (" + data + ")"); if (endSpecial == -1) throw new IllegalArgumentException("String couldn't be decoded! (" + data + ")");
String special = remaining.substring(0, endSpecial+1); String special = remaining.substring(0, endSpecial + 1);
remaining = remaining.substring(endSpecial+1); remaining = remaining.substring(endSpecial + 1);
result.append(decodeCharacter(special)); result.append(decodeCharacter(special));
} }
} }
@ -67,7 +67,7 @@ public class StringEncoder {
public static char decodeCharacter(final String data) { public static char decodeCharacter(final String data) {
if (!data.startsWith("&#")) throw new IllegalArgumentException("Data does not start with &# (" + data + ")"); if (!data.startsWith("&#")) throw new IllegalArgumentException("Data does not start with &# (" + data + ")");
if (!data.endsWith(";")) throw new IllegalArgumentException("Data does not end with ; (" + data + ")"); if (!data.endsWith(";")) throw new IllegalArgumentException("Data does not end with ; (" + data + ")");
return (char)Integer.parseInt(data.substring(2, data.length()-1)); return (char) Integer.parseInt(data.substring(2, data.length() - 1));
} }
/** /**
@ -83,10 +83,10 @@ public class StringEncoder {
if (data == null) return ""; if (data == null) return "";
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
for (int index=0; index < data.length(); index++) { for (int index = 0; index < data.length(); index++) {
char character = data.charAt(index); char character = data.charAt(index);
if (character < 32 || character > 127 || character == 38) { if (character < 32 || character > 127 || character == 38) {
result.append("&#" + (int)character + ";"); result.append("&#" + (int) character + ";");
} else { } else {
result.append(character); result.append(character);
} }

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

@ -22,6 +22,20 @@ public class ToneGenerator {
throw new UnsupportedOperationException("Utility class"); throw new UnsupportedOperationException("Utility class");
} }
/**
* Plays one or more specified tones for a given duration.
* Each tone is played sequentially in the order they are provided.
*
* @param duration the duration of each tone in milliseconds
* @param tones the names of the tones to play, specified as a variable-length argument
* @throws LineUnavailableException if the audio line cannot be opened or accessed
*/
public static void playTone(int duration, String... tones) throws LineUnavailableException {
for (String tone : tones) {
playTone(tone, duration);
}
}
/** /**
* Plays a tone based on a predefined tone name for a specified duration. * Plays a tone based on a predefined tone name for a specified duration.
* *
@ -64,18 +78,4 @@ public class ToneGenerator {
line.drain(); line.drain();
} }
} }
/**
* Plays one or more specified tones for a given duration.
* Each tone is played sequentially in the order they are provided.
*
* @param duration the duration of each tone in milliseconds
* @param tones the names of the tones to play, specified as a variable-length argument
* @throws LineUnavailableException if the audio line cannot be opened or accessed
*/
public static void playTone(int duration, String... tones) throws LineUnavailableException {
for (String tone : tones) {
playTone(tone, duration);
}
}
} }

View File

@ -7,34 +7,23 @@ import java.util.HashMap;
* with their corresponding frequencies in hertz (Hz). It allows users to * with their corresponding frequencies in hertz (Hz). It allows users to
* retrieve the frequency of a tone based on its standard notation name, * retrieve the frequency of a tone based on its standard notation name,
* such as "C3", "A4", or "G#5". * such as "C3", "A4", or "G#5".
* * <p>
* This class can be particularly useful in applications related to sound synthesis, * This class can be particularly useful in applications related to sound synthesis,
* music theory, signal processing, and other audio-related domains that require precise * music theory, signal processing, and other audio-related domains that require precise
* frequency information for specific tones. * frequency information for specific tones.
*/ */
public class ToneTable { public class ToneTable {
/**
* 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 ToneTable() {
throw new UnsupportedOperationException("Utility class");
}
/** /**
* A static map that associates written tone names with their corresponding frequencies in hertz (Hz). * A static map that associates written tone names with their corresponding frequencies in hertz (Hz).
* The keys represent tone names (e.g., "C4", "D#5"), and the values are their respective frequencies. * The keys represent tone names (e.g., "C4", "D#5"), and the values are their respective frequencies.
* This map serves as a reference for converting tone names into their precise frequency values, * This map serves as a reference for converting tone names into their precise frequency values,
* which are used in applications such as tone generation or audio playback. * which are used in applications such as tone generation or audio playback.
* * <p>
* The map is a crucial component of the ToneTable class, providing quick lookup of frequencies * The map is a crucial component of the ToneTable class, providing quick lookup of frequencies
* for predefined tone names. * for predefined tone names.
*/ */
private static final HashMap<String, Double> toneMap = new HashMap<>(); private static final HashMap<String, Double> toneMap = new HashMap<>();
static { static {
toneMap.put("C0", 16.35); toneMap.put("C0", 16.35);
toneMap.put("C#0", 17.32); toneMap.put("C#0", 17.32);
@ -191,6 +180,18 @@ public class ToneTable {
toneMap.put("B8", 7902.13); toneMap.put("B8", 7902.13);
} }
/**
* 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 ToneTable() {
throw new UnsupportedOperationException("Utility class");
}
/** /**
* Retrieves the frequency of the tone based on the given tone name. * Retrieves the frequency of the tone based on the given tone name.
* If the tone name exists in the tone map, its corresponding frequency is returned. * If the tone name exists in the tone map, its corresponding frequency is returned.

View File

@ -8,7 +8,14 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.*; import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Scanner;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
@ -89,6 +96,20 @@ public class Query<T> {
.execute(); .execute();
} }
/**
* Executes the SQL statement represented by the PreparedStatement created in
* the method {@code createPreparedStatement()}. This method is used to perform
* database operations such as updates or other actions that do not produce a
* result set.
*
* @throws SQLException if a database access error occurs or the SQL statement is invalid.
*/
public void execute() throws SQLException {
try (PreparedStatement ps = createPreparedStatement()) {
ps.execute();
}
}
/** /**
* Sets the query text for the current query instance. * Sets the query text for the current query instance.
* *
@ -100,6 +121,21 @@ public class Query<T> {
return this; return this;
} }
/**
* Creates and returns a PreparedStatement object configured with the query text
* and parameters defined for the current instance.
*
* @return a PreparedStatement object initialized with the query and parameter values
* @throws SQLException if a database access error occurs or the PreparedStatement cannot be created
*/
private PreparedStatement createPreparedStatement() throws SQLException {
PreparedStatement ps = connection.prepareStatement(queryText);
for (ParameterSetter setter : parameterSetters) {
setter.apply(ps);
}
return ps;
}
/** /**
* Sets the query resource by loading its content from the given resource path. * Sets the query resource by loading its content from the given resource path.
* The resource content is read and stored as the query text for the query object. * The resource content is read and stored as the query text for the query object.
@ -166,6 +202,22 @@ public class Query<T> {
return this; return this;
} }
/**
* Provides a managed stream to the given handler and ensures proper resource closing after use.
* A {@code Stream} is opened and passed to the specified handler as an argument.
* The stream will be automatically closed when the handler finishes execution or throws an exception.
*
* @param handler a function which receives a {@code Stream<T>} and processes it, returning a result of type {@code R}.
* @param <R> the type of the result produced by the handler.
* @return the result returned by the handler after processing the stream.
* @throws SQLException if an SQL error occurs during the creation or handling of the stream.
*/
public <R> R withStream(Function<Stream<T>, R> handler) throws SQLException {
try (Stream<T> stream = stream()) {
return handler.apply(stream);
}
}
/** /**
* Creates a {@link Stream} from the result set obtained by executing a prepared statement. * Creates a {@link Stream} from the result set obtained by executing a prepared statement.
* The result set is wrapped in a {@link TrimmingResultSet} to handle data trimming. * The result set is wrapped in a {@link TrimmingResultSet} to handle data trimming.
@ -179,35 +231,6 @@ public class Query<T> {
return streamFromResultSet(trimmingResultSet, factory); return streamFromResultSet(trimmingResultSet, factory);
} }
/**
* Creates and returns a PreparedStatement object configured with the query text
* and parameters defined for the current instance.
*
* @return a PreparedStatement object initialized with the query and parameter values
* @throws SQLException if a database access error occurs or the PreparedStatement cannot be created
*/
private PreparedStatement createPreparedStatement() throws SQLException {
PreparedStatement ps = connection.prepareStatement(queryText);
for (ParameterSetter setter : parameterSetters) {
setter.apply(ps);
}
return ps;
}
/**
* Executes the SQL statement represented by the PreparedStatement created in
* the method {@code createPreparedStatement()}. This method is used to perform
* database operations such as updates or other actions that do not produce a
* result set.
*
* @throws SQLException if a database access error occurs or the SQL statement is invalid.
*/
public void execute() throws SQLException {
try (PreparedStatement ps = createPreparedStatement()) {
ps.execute();
}
}
/** /**
* Converts a {@link ResultSet} into a {@link Stream} of objects created using the provided {@link Function} * Converts a {@link ResultSet} into a {@link Stream} of objects created using the provided {@link Function}
* factory. The stream ensures resources such as the {@code ResultSet} and its associated statement are * factory. The stream ensures resources such as the {@code ResultSet} and its associated statement are
@ -220,6 +243,7 @@ public class Query<T> {
private Stream<T> streamFromResultSet(final ResultSet rs, final Function<ResultSet, T> factory) { private Stream<T> streamFromResultSet(final ResultSet rs, final Function<ResultSet, T> factory) {
Iterator<T> iterator = new Iterator<>() { Iterator<T> iterator = new Iterator<>() {
private boolean hasNextChecked = false; private boolean hasNextChecked = false;
private boolean hasNext; private boolean hasNext;
/** /**
@ -272,22 +296,6 @@ public class Query<T> {
}); });
} }
/**
* Provides a managed stream to the given handler and ensures proper resource closing after use.
* A {@code Stream} is opened and passed to the specified handler as an argument.
* The stream will be automatically closed when the handler finishes execution or throws an exception.
*
* @param handler a function which receives a {@code Stream<T>} and processes it, returning a result of type {@code R}.
* @param <R> the type of the result produced by the handler.
* @return the result returned by the handler after processing the stream.
* @throws SQLException if an SQL error occurs during the creation or handling of the stream.
*/
public <R> R withStream(Function<Stream<T>, R> handler) throws SQLException {
try (Stream<T> stream = stream()) {
return handler.apply(stream);
}
}
/** /**
* Represents a functional interface that accepts three input arguments and allows for operations * Represents a functional interface that accepts three input arguments and allows for operations
* that can throw an {@link SQLException}. This can be useful in scenarios where SQL-related * that can throw an {@link SQLException}. This can be useful in scenarios where SQL-related

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@ public class ArrayUtils {
* @return true if the character is found in the array, false otherwise * @return true if the character is found in the array, false otherwise
*/ */
public static boolean contains(char[] array, char ch) { public static boolean contains(char[] array, char ch) {
for(int index=0; index < array.length; index++) { for (int index = 0; index < array.length; index++) {
if (array[index] == ch) return true; if (array[index] == ch) return true;
} }
return false; return false;
@ -41,7 +41,7 @@ public class ArrayUtils {
* @return true if the array contains the specified integer value, false otherwise * @return true if the array contains the specified integer value, false otherwise
*/ */
public static boolean contains(int[] array, int ch) { public static boolean contains(int[] array, int ch) {
for(int index=0; index < array.length; index++) { for (int index = 0; index < array.length; index++) {
if (array[index] == ch) return true; if (array[index] == ch) return true;
} }
return false; return false;
@ -55,7 +55,7 @@ public class ArrayUtils {
* @return true if the array contains the specified long value, false otherwise * @return true if the array contains the specified long value, false otherwise
*/ */
public static boolean contains(long[] array, long ch) { public static boolean contains(long[] array, long ch) {
for(int index=0; index < array.length; index++) { for (int index = 0; index < array.length; index++) {
if (array[index] == ch) return true; if (array[index] == ch) return true;
} }
return false; return false;
@ -70,7 +70,7 @@ public class ArrayUtils {
* @return true if the element is found in the array, false otherwise * @return true if the element is found in the array, false otherwise
*/ */
public static <T> boolean contains(T[] array, T ch) { public static <T> boolean contains(T[] array, T ch) {
for(int index=0; index < array.length; index++) { for (int index = 0; index < array.length; index++) {
if (Objects.equals(array[index], ch)) return true; if (Objects.equals(array[index], ch)) return true;
} }
return false; return false;

View File

@ -33,9 +33,9 @@ public class EnumUtil {
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
result.append("(|,|\\s"); result.append("(|,|\\s");
for (T flag: clazz.getEnumConstants()) { for (T flag : clazz.getEnumConstants()) {
result.append("|"); result.append("|");
for(char ch: flag.toString().toUpperCase().toCharArray()) { for (char ch : flag.toString().toUpperCase().toCharArray()) {
if (Character.isAlphabetic(ch)) { if (Character.isAlphabetic(ch)) {
result.append("["); result.append("[");
result.append(ch); result.append(ch);
@ -52,7 +52,7 @@ public class EnumUtil {
/** /**
* Parses a delimited string containing Enum constant names into a list of Enum constants. * Parses a delimited string containing Enum constant names into a list of Enum constants.
* * <p>
* This method takes an Enum class and a string of flags, splits the string by commas * This method takes an Enum class and a string of flags, splits the string by commas
* or whitespace, and converts each substring into an Enum constant of the given class. * or whitespace, and converts each substring into an Enum constant of the given class.
* The flag names are case-insensitive and will be converted to uppercase before matching * The flag names are case-insensitive and will be converted to uppercase before matching
@ -70,7 +70,7 @@ public class EnumUtil {
public static <T extends Enum<T>> List<T> parseFlags(final Class<T> clazz, final String flags) { public static <T extends Enum<T>> List<T> parseFlags(final Class<T> clazz, final String flags) {
List<T> result = new ArrayList<>(); List<T> result = new ArrayList<>();
if (flags != null) { if (flags != null) {
for (String flag: flags.split("[,\\s]")) { for (String flag : flags.split("[,\\s]")) {
if (!flag.isEmpty()) result.add(T.valueOf(clazz, flag.toUpperCase())); if (!flag.isEmpty()) result.add(T.valueOf(clazz, flag.toUpperCase()));
} }
} }

View File

@ -16,7 +16,7 @@ public class RegionalizationMessage {
* which is used to retrieve locale-specific objects and messages. * which is used to retrieve locale-specific objects and messages.
* It facilitates the process of internationalization by loading resources * It facilitates the process of internationalization by loading resources
* such as text and messages from specified bundles based on the provided locale. * such as text and messages from specified bundles based on the provided locale.
* * <p>
* This variable is initialized in constructors of the `RegionalizationMessage` class * This variable is initialized in constructors of the `RegionalizationMessage` class
* and is used internally by various methods to fetch localized messages. * and is used internally by various methods to fetch localized messages.
*/ */
@ -57,6 +57,21 @@ public class RegionalizationMessage {
return res.getString(key); return res.getString(key);
} }
/**
* Retrieves a localized and formatted message from a resource bundle based on a key.
* If the key is not found in the resource bundle, a default message is used.
* The message supports parameter substitution using the supplied arguments.
*
* @param key The key used to retrieve the localized message from the resource bundle.
* @param defaultMessage The default message to be used if the key is not found in the resource bundle.
* @param params The parameters to substitute into the message placeholders.
* @return A formatted string containing the localized message with substituted parameters.
*/
public String getFormattedMessage(final String key, final String defaultMessage, final Object... params) {
MessageFormat format = new MessageFormat(getMessage(key, defaultMessage));
return format.format(params);
}
/** /**
* Retrieves a localized message for the given key from the resource bundle. * Retrieves a localized message for the given key from the resource bundle.
* If the key is not found, the specified default message is returned. * If the key is not found, the specified default message is returned.
@ -72,19 +87,4 @@ public class RegionalizationMessage {
return defaultMessage; return defaultMessage;
} }
/**
* Retrieves a localized and formatted message from a resource bundle based on a key.
* If the key is not found in the resource bundle, a default message is used.
* The message supports parameter substitution using the supplied arguments.
*
* @param key The key used to retrieve the localized message from the resource bundle.
* @param defaultMessage The default message to be used if the key is not found in the resource bundle.
* @param params The parameters to substitute into the message placeholders.
* @return A formatted string containing the localized message with substituted parameters.
*/
public String getFormattedMessage(final String key, final String defaultMessage, final Object... params) {
MessageFormat format = new MessageFormat(getMessage(key, defaultMessage));
return format.format(params);
}
} }

View File

@ -7,6 +7,7 @@ import java.util.Locale;
/** /**
* A utility class for measuring elapsed time. The StopWatch can be started, stopped, and queried for the elapsed time in various formats. * A utility class for measuring elapsed time. The StopWatch can be started, stopped, and queried for the elapsed time in various formats.
*/ */
@SuppressWarnings("unused")
public class StopWatch { public class StopWatch {
/** /**
@ -14,12 +15,29 @@ public class StopWatch {
* duration between the start and the stop or the current time. * duration between the start and the stop or the current time.
*/ */
private Instant startTime; private Instant startTime;
/** /**
* Represents the stopping time of the stopwatch. This variable is set when the stopwatch is stopped and is used to calculate the elapsed * Represents the stopping time of the stopwatch. This variable is set when the stopwatch is stopped and is used to calculate the elapsed
* duration between the start and stop times. * duration between the start and stop times.
*/ */
private Instant stopTime; private Instant stopTime;
/**
* Creates a new StopWatch instance in the reset state.
*
* <p>The constructor does not start timing. Both {@link #startTime} and {@link #stopTime}
* are initialized to {@code null}. To begin measuring elapsed time, call {@link #start()}.
* After calling {@link #stop()}, retrieve the measured duration with {@link #getUsedTime()} or
* a human-readable string via {@link #getUsedTimeFormatted(boolean, boolean, boolean, boolean, boolean)}.
* </p>
*
* <p>Note: This class is not synchronized; synchronize externally if the same instance
* is accessed from multiple threads.</p>
*/
public StopWatch() {
// default constructor only ...
}
/** /**
* Starts the stopwatch by recording the current time as the starting time. If the stopwatch was previously stopped or not yet started, * Starts the stopwatch by recording the current time as the starting time. If the stopwatch was previously stopped or not yet started,
* this method resets the starting time to the current time and clears the stopping time. This method is typically called before measuring * this method resets the starting time to the current time and clears the stopping time. This method is typically called before measuring
@ -47,18 +65,15 @@ public class StopWatch {
} }
/** /**
* Calculates the amount of time elapsed between the stopwatch's starting time and either its stopping time or the current time if the * Provides a compact and human-readable formatted string representing the elapsed time measured by the stopwatch. The method formats the
* stopwatch is still running. * duration using a combination of days, hours, minutes, seconds, and milliseconds. This compact format removes additional verbosity in
* favor of a shorter, more concise representation.
* *
* @return the duration representing the elapsed time between the start and end points of the stopwatch. * @return a compact formatted string representing the elapsed time, including days, hours, minutes, seconds, and milliseconds as
* @throws IllegalStateException if the stopwatch has not been started before calling this method. * applicable
*/ */
public Duration getUsedTime() { public String getUsedTimeFormattedCompact() {
if (startTime == null) { return getUsedTimeFormatted(true, true, true, true, true);
throw new IllegalStateException("StopWatch has not been started.");
}
Instant end = (stopTime != null) ? stopTime : Instant.now();
return Duration.between(startTime, end);
} }
/** /**
@ -93,13 +108,13 @@ public class StopWatch {
if (includeDays && days > 0) { if (includeDays && days > 0) {
sb.append(days).append("d "); sb.append(days).append("d ");
} }
if (includeHours && (hours > 0 || sb.length() > 0)) { if (includeHours && (hours > 0 || !sb.isEmpty())) {
sb.append(hours).append("h "); sb.append(hours).append("h ");
} }
if (includeMinutes && (minutes > 0 || sb.length() > 0)) { if (includeMinutes && (minutes > 0 || !sb.isEmpty())) {
sb.append(minutes).append("m "); sb.append(minutes).append("m ");
} }
if (includeSeconds && (seconds > 0 || sb.length() > 0)) { if (includeSeconds && (seconds > 0 || !sb.isEmpty())) {
sb.append(seconds); sb.append(seconds);
} }
if (includeMillis) { if (includeMillis) {
@ -115,14 +130,17 @@ public class StopWatch {
} }
/** /**
* Provides a compact and human-readable formatted string representing the elapsed time measured by the stopwatch. The method formats the * Calculates the amount of time elapsed between the stopwatch's starting time and either its stopping time or the current time if the
* duration using a combination of days, hours, minutes, seconds, and milliseconds. This compact format removes additional verbosity in * stopwatch is still running.
* favor of a shorter, more concise representation.
* *
* @return a compact formatted string representing the elapsed time, including days, hours, minutes, seconds, and milliseconds as * @return the duration representing the elapsed time between the start and end points of the stopwatch.
* applicable * @throws IllegalStateException if the stopwatch has not been started before calling this method.
*/ */
public String getUsedTimeFormattedCompact() { public Duration getUsedTime() {
return getUsedTimeFormatted(true, true, true, true, true); if (startTime == null) {
throw new IllegalStateException("StopWatch has not been started.");
}
Instant end = (stopTime != null) ? stopTime : Instant.now();
return Duration.between(startTime, end);
} }
} }

View File

@ -8,6 +8,21 @@ import java.util.Map;
* Utility class providing common string manipulation methods. * Utility class providing common string manipulation methods.
*/ */
public class Strings { public class Strings {
/**
* A map that holds the system's environment variables. The keys represent
* environment variable names, and the values represent their corresponding
* values. It is initialized with the current system environment variables using
* {@link System#getenv()}.
* <p>
* This variable provides access to the underlying system environment, which can
* be used for various purposes such as configuration, path resolution, or
* dynamic value substitution.
* <p>
* Note: Modifications to this map will not affect the system environment, as it
* is a copy of the environment at the time of initialization.
*/
private static Map<String, String> environmentVariables = System.getenv();
/** /**
* 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
@ -20,21 +35,6 @@ public class Strings {
throw new UnsupportedOperationException("Utility class"); throw new UnsupportedOperationException("Utility class");
} }
/**
* A map that holds the system's environment variables. The keys represent
* environment variable names, and the values represent their corresponding
* values. It is initialized with the current system environment variables using
* {@link System#getenv()}.
*
* This variable provides access to the underlying system environment, which can
* be used for various purposes such as configuration, path resolution, or
* dynamic value substitution.
*
* Note: Modifications to this map will not affect the system environment, as it
* is a copy of the environment at the time of initialization.
*/
private static Map<String, String> environmentVariables = System.getenv();
/** /**
* Expands environment variable placeholders within the provided text. * Expands environment variable placeholders within the provided text.
* Placeholders in the format ${VARIABLE_NAME} are replaced with their * Placeholders in the format ${VARIABLE_NAME} are replaced with their
@ -63,7 +63,7 @@ public class Strings {
* @return true if the string is null or has a length of 0; false otherwise. * @return true if the string is null or has a length of 0; false otherwise.
*/ */
public static boolean isNullOrEmpty(final String string) { public static boolean isNullOrEmpty(final String string) {
return (string == null || string.length()==0); return (string == null || string.length() == 0);
} }
/** /**
@ -78,7 +78,7 @@ public class Strings {
public static String removeQuotes(final String value) { public static String removeQuotes(final String value) {
String trimmedValue = value.trim(); String trimmedValue = value.trim();
if (trimmedValue.length() > 1 && trimmedValue.startsWith("\"") && trimmedValue.endsWith("\"")) if (trimmedValue.length() > 1 && trimmedValue.startsWith("\"") && trimmedValue.endsWith("\""))
return trimmedValue.substring(1, trimmedValue.length()-1); return trimmedValue.substring(1, trimmedValue.length() - 1);
return value; return value;
} }
@ -131,11 +131,11 @@ public class Strings {
public static String increment(@NonNull final String element) { public static String increment(@NonNull final String element) {
if (element.isEmpty()) return "1"; if (element.isEmpty()) return "1";
String firstPart = element.substring(0, element.length()-1); String firstPart = element.substring(0, element.length() - 1);
char lastChar = element.charAt(element.length()-1); char lastChar = element.charAt(element.length() - 1);
if (lastChar == '9') return firstPart + 'A'; if (lastChar == '9') return firstPart + 'A';
if (lastChar == 'Z') return increment(firstPart) + '0'; if (lastChar == 'Z') return increment(firstPart) + '0';
return firstPart + (char)(lastChar+1); return firstPart + (char) (lastChar + 1);
} }
/** /**

View File

@ -8,7 +8,12 @@ import org.w3c.dom.Node;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.*; import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource; import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource; import javax.xml.transform.stream.StreamSource;
@ -25,6 +30,16 @@ import java.util.Date;
*/ */
@Slf4j @Slf4j
public class XmlUtils { public class XmlUtils {
/**
* A {@link DateTimeFormatter} instance used for formatting or parsing dates in the "yyyy-MM-dd" pattern.
* <p>
* This formatter adheres to the XML date format standard (ISO-8601). It can be used
* to ensure consistent date representations in XML or other similar text-based formats.
* <p>
* Thread-safe and immutable, this formatter can be shared across multiple threads.
*/
public static final DateTimeFormatter XML_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/** /**
* 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
@ -38,25 +53,16 @@ public class XmlUtils {
} }
/** /**
* A {@link DateTimeFormatter} instance used for formatting or parsing dates in the "yyyy-MM-dd" pattern. * Creates an XML element with the specified name and value, formatted as a date string.
* The date value is converted to an XML-compatible string format using a predefined formatter.
* *
* This formatter adheres to the XML date format standard (ISO-8601). It can be used * @param doc the XML document to which the element belongs; must not be null
* to ensure consistent date representations in XML or other similar text-based formats. * @param name the name of the element to be created; must not be null or empty
* * @param value the date value to be assigned to the created element; must not be null
* Thread-safe and immutable, this formatter can be shared across multiple threads. * @return the created XML element containing the specified name and formatted date value
*/ */
public static final DateTimeFormatter XML_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public static Element createElement(final Document doc, final String name, final Date value) {
return createElement(doc, name, XML_DATE_FORMATTER.format(value.toInstant()));
/**
* Creates a new element in the provided XML document with the specified name.
*
* @param doc The XML document in which the new element is to be created. Must not be null.
* @param name The name of the element to be created. Must not be null or empty.
* @return The newly created element with the given name. Never returns null.
*/
public static Element createElement(final Document doc, final String name) {
log.debug("Creating a new element " + name);
return doc.createElement(name);
} }
/** /**
@ -79,19 +85,6 @@ public class XmlUtils {
return element; return element;
} }
/**
* Creates an XML element with the specified name and value, formatted as a date string.
* The date value is converted to an XML-compatible string format using a predefined formatter.
*
* @param doc the XML document to which the element belongs; must not be null
* @param name the name of the element to be created; must not be null or empty
* @param value the date value to be assigned to the created element; must not be null
* @return the created XML element containing the specified name and formatted date value
*/
public static Element createElement(final Document doc, final String name, final Date value) {
return createElement(doc, name, XML_DATE_FORMATTER.format(value.toInstant()));
}
/** /**
* Creates an XML element with the specified name and optional value. * Creates an XML element with the specified name and optional value.
* *
@ -105,7 +98,7 @@ public class XmlUtils {
log.debug("Creating a new element " + name + " with value: " + value); log.debug("Creating a new element " + name + " with value: " + value);
Element element = doc.createElement(name); Element element = doc.createElement(name);
Node content = doc.createTextNode(value != null ? ""+value : ""); Node content = doc.createTextNode(value != null ? "" + value : "");
element.appendChild(content); element.appendChild(content);
return element; return element;
@ -127,6 +120,18 @@ public class XmlUtils {
return element; return element;
} }
/**
* Creates a new element in the provided XML document with the specified name.
*
* @param doc The XML document in which the new element is to be created. Must not be null.
* @param name The name of the element to be created. Must not be null or empty.
* @return The newly created element with the given name. Never returns null.
*/
public static Element createElement(final Document doc, final String name) {
log.debug("Creating a new element " + name);
return doc.createElement(name);
}
/** /**
* Creates a new XML element with a specified name and value, adds it to the specified parent element, * Creates a new XML element with a specified name and value, adds it to the specified parent element,
* and returns the created element. * and returns the created element.
@ -144,7 +149,6 @@ public class XmlUtils {
return element; return element;
} }
/** /**
* Formats a given XML string by applying proper indentation to enhance readability. * Formats a given XML string by applying proper indentation to enhance readability.
* The method uses a transformer to process the XML input, applying indentation * The method uses a transformer to process the XML input, applying indentation
@ -171,9 +175,8 @@ public class XmlUtils {
transformer.transform(xmlInput, xmlOutput); transformer.transform(xmlInput, xmlOutput);
return xmlOutput.getWriter().toString(); return xmlOutput.getWriter().toString();
} } catch (TransformerException e) {
catch(TransformerException e) { log.error("Error in XML: " + e.getMessage() + "\n" + xmlStream, e);
log.error("Error in XML: " + e.getMessage() + "\n"+xmlStream, e);
} }
return xmlStream; return xmlStream;
@ -205,8 +208,7 @@ public class XmlUtils {
String xmlString = stringWriter.toString(); String xmlString = stringWriter.toString();
log.info("MO Request: " + xmlString); log.info("MO Request: " + xmlString);
return xmlString; return xmlString;
} } catch (TransformerException e) {
catch(TransformerException e) {
log.error("Error in XML Transformation: " + e.getMessage(), e); log.error("Error in XML Transformation: " + e.getMessage(), e);
} }

View File

@ -1,4 +0,0 @@
package de.neitzel.core.inject.testcomponents.test1ok;
public class SuperClass {
}

View File

@ -1,4 +0,0 @@
package de.neitzel.core.inject.testcomponents.test1ok;
public interface TestInterface1_1 {
}

View File

@ -1,4 +0,0 @@
package de.neitzel.core.inject.testcomponents.test1ok;
public interface TestInterface1_2 {
}

View File

@ -1,12 +0,0 @@
package de.neitzel.core.inject.testcomponents.test1ok.sub;
import de.neitzel.core.inject.annotation.Component;
import de.neitzel.core.inject.testcomponents.test1ok.SuperClass;
import de.neitzel.core.inject.testcomponents.test1ok.TestInterface1_1;
import de.neitzel.core.inject.testcomponents.test1ok.TestInterface1_2;
@Component
public class TestComponent1_2 extends SuperClass implements TestInterface1_1, TestInterface1_2 {
public TestComponent1_2() {
}
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -2,20 +2,26 @@ package de.neitzel.encryption;
import com.idealista.fpe.config.Alphabet; import com.idealista.fpe.config.Alphabet;
public abstract class BaseAlphabet implements Alphabet { /**
/** * Base implementation of the {@link Alphabet} interface providing common helper methods
* Replaces illegal characters of a string with a given replacement character * for working with character sets and validating characters against the alphabet.
* @param text Text to check.
* @param replacement Replacement character.
* @return String in which all characters was replaced.
*/ */
public String replaceIllegalCharacters(String text, char replacement) { public interface BaseAlphabet extends Alphabet {
/**
* Replaces illegal characters of a string with a given replacement character.
*
* @param text Text to check; may not be null
* @param replacement Replacement character to use for characters not present in the alphabet
* @return a new string in which every invalid character was replaced with the provided replacement
* @throws IllegalArgumentException if {@code replacement} is not a valid character in the alphabet
*/
default String replaceIllegalCharacters(String text, char replacement) {
// Validate // Validate
if (!isValidCharacter(replacement)) throw new IllegalArgumentException("replacement"); if (!isValidCharacter(replacement)) throw new IllegalArgumentException("replacement");
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
for (char ch: text.toCharArray()) { for (char ch : text.toCharArray()) {
if (isValidCharacter(ch)){ if (isValidCharacter(ch)) {
result.append(ch); result.append(ch);
} else { } else {
result.append(replacement); result.append(replacement);
@ -26,10 +32,11 @@ public abstract class BaseAlphabet implements Alphabet {
/** /**
* Checks if a given Character is part of the alphabet. * Checks if a given Character is part of the alphabet.
*
* @param character Character to check. * @param character Character to check.
* @return true if character is valid, else false. * @return true if character is valid, else false.
*/ */
public boolean isValidCharacter(char character) { default boolean isValidCharacter(char character) {
// Compare with all characters // Compare with all characters
for (char ch : availableCharacters()) { for (char ch : availableCharacters()) {
if (ch == character) return true; if (ch == character) return true;

View File

@ -7,47 +7,51 @@ import com.idealista.fpe.config.LengthRange;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Random; import java.security.SecureRandom;
/** /**
* Helper class to deal with FF1Encryption using com.idealista:format-preserving-encryption:1.0.0. * Helper class to deal with FF1Encryption using com.idealista:format-preserving-encryption:1.0.0.
*/ */
@SuppressWarnings("unused")
public class FF1Encryption { public class FF1Encryption {
/** private static final SecureRandom RANDOM = new SecureRandom();
* Key to use for all encryption / unencryption
*/
private byte[] key;
/** /**
* Should to small strings be ignored? * Key to use for all encryption / unencryption.
*
* If this is set to true, small strings (less than 2 characters) will not be encrypted!
*/ */
private boolean ignoreToShortStrings; private final byte[] key;
/** /**
* tweak to use for encryption * Should too small strings be ignored?
* If this is set to true, small strings (less than {@code minLength} characters)
* will not be encrypted or decrypted and will be returned unchanged.
*/ */
private byte[] tweak; private final boolean ignoreToShortStrings;
/** /**
* Domain used for encryption * Tweak to use for encryption.
*/ */
private Domain domain; private final byte[] tweak;
/** /**
* Format preserving encryption to use. * Domain used for encryption.
*/ */
private FormatPreservingEncryption encryption; private final Domain domain;
/** /**
* Minimum length of a string. * Format preserving encryption implementation to use.
*/ */
private int minLength; private final FormatPreservingEncryption encryption;
/**
* Minimum length of a string for which encryption/decryption will be applied.
*/
private final int minLength;
/** /**
* Creates a new instance of FF1Encryption * Creates a new instance of FF1Encryption
*
* @param key AES key to use. * @param key AES key to use.
* @param tweak tweak to use for encryption / decryption * @param tweak tweak to use for encryption / decryption
* @param ignoreToShortStrings Ignore strings that are to short. * @param ignoreToShortStrings Ignore strings that are to short.
@ -58,6 +62,7 @@ public class FF1Encryption {
/** /**
* Creates a new instance of FF1Encryption * Creates a new instance of FF1Encryption
*
* @param key AES key to use. * @param key AES key to use.
* @param tweak tweak to use for encryption / decryption * @param tweak tweak to use for encryption / decryption
* @param ignoreToShortStrings Ignore strings that are to short. * @param ignoreToShortStrings Ignore strings that are to short.
@ -72,15 +77,45 @@ public class FF1Encryption {
this.domain = domain; this.domain = domain;
this.minLength = minLength; this.minLength = minLength;
encryption = FormatPreservingEncryptionBuilder this.encryption = FormatPreservingEncryptionBuilder
.ff1Implementation().withDomain(domain) .ff1Implementation()
.withDomain(domain)
.withDefaultPseudoRandomFunction(key) .withDefaultPseudoRandomFunction(key)
.withLengthRange(new LengthRange(minLength, maxLength)) .withLengthRange(new LengthRange(minLength, maxLength))
.build(); .build();
} }
/**
* Creates a new AES key woth the given length.
*
* @param length Length of the key in bits. Must be 128, 192 or 256 bits
* @return Byte array of the new key.
*/
public static byte[] createNewKey(int length) {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(length); // for example
return keyGen.generateKey().getEncoded();
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
/**
* Creates a new tweak of the given length.
*
* @param length Number of bytes the new teeak should have.
* @return byte array with the new tweak.
*/
public static byte[] createNewTweak(int length) {
byte[] key = new byte[length];
RANDOM.nextBytes(key);
return key;
}
/** /**
* Encrypts a given text. * Encrypts a given text.
*
* @param plainText Unencrypted text. * @param plainText Unencrypted text.
* @return Encrypted text. * @return Encrypted text.
*/ */
@ -97,6 +132,7 @@ public class FF1Encryption {
/** /**
* Decrypt a given text. * Decrypt a given text.
*
* @param cipherText Encrypted text. * @param cipherText Encrypted text.
* @return Decrypted text. * @return Decrypted text.
*/ */
@ -110,31 +146,4 @@ public class FF1Encryption {
// Return decrypted text. // Return decrypted text.
return encryption.decrypt(cipherText, tweak); return encryption.decrypt(cipherText, tweak);
} }
/**
* Creates a new AES key woth the given length.
* @param length Length of the key in bits. Must be 128, 192 or 256 bits
* @return Byte array of the new key.
*/
public static byte[] createNewKey(int length) {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(length); // for example
return keyGen.generateKey().getEncoded();
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
/**
* Creates a new tweak of the given length.
* @param length Number of bytes the new teeak should have.
* @return byte array with the new tweak.
*/
public static byte[] createNewTweak(int length) {
Random random = new Random();
byte[] key = new byte[length];
random.nextBytes(key);
return key;
}
} }

View File

@ -3,12 +3,14 @@ package de.neitzel.encryption;
/** /**
* Main characters of the Alphabet. * Main characters of the Alphabet.
*/ */
public class MainAlphabet extends BaseAlphabet { public class MainAlphabet implements BaseAlphabet {
/** /**
* All characters we want to use. * All characters that are part of the alphabet used by the MainDomain.
* This array includes lowercase letters, uppercase letters, digits, a set of
* special characters, and German-specific characters.
*/ */
private static final char[] ALL_CHARS = new char[] { private static final char[] ALL_CHARS = new char[]{
// lowercase characters // lowercase characters
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
@ -21,16 +23,38 @@ public class MainAlphabet extends BaseAlphabet {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
// special characters // special characters
'-', '_', '?', ' ', '#', '+', '/', '*', '!', '\"', '§', '$', '%', '&', '(', ')', '=', '@', '-', '_', '?', ' ', '#', '+', '/', '*', '!', '"', '§', '$', '%', '&', '(', ')', '=', '@',
'€', ',', ';', '.', ':', '<', '>', '|', '\'', '\\', '{', '}', '[', ']', '€', ',', ';', '.', ':', '<', '>', '|', '\'', '\\', '{', '}', '[', ']',
// german special characters // german special characters
'ä', 'Ä', 'ö', 'Ö', 'ü', 'Ü', 'ß' 'ä', 'Ä', 'ö', 'Ö', 'ü', 'Ü', 'ß'
}; };
/**
* Creates a new MainAlphabet instance.
*
* <p>This constructor performs no runtime initialization because the
* alphabet is represented by the static {@code ALL_CHARS} array. Use
* {@link #availableCharacters()} to obtain the supported characters and
* {@link #radix()} to obtain the alphabet size.</p>
*
* <p> Important usage notes: </p>
* <ul>
* <li>The array returned by {@link #availableCharacters()} is the internal
* static array; callers must not modify it.</li>
* <li>The alphabet intentionally contains letters (lower and upper case),
* digits, common punctuation and a set of German-specific characters
* to support locale-sensitive operations.</li>
* </ul>
*/
public MainAlphabet() {
// default constructor only ...
}
/** /**
* Gets the available characters. * Gets the available characters.
* @return *
* @return an array containing all supported characters in the alphabet
*/ */
@Override @Override
public char[] availableCharacters() { public char[] availableCharacters() {
@ -39,7 +63,8 @@ public class MainAlphabet extends BaseAlphabet {
/** /**
* Gets the radix of the alphabet. * Gets the radix of the alphabet.
* @return *
* @return the number of characters in the alphabet (radix)
*/ */
@Override @Override
public Integer radix() { public Integer radix() {

View File

@ -4,26 +4,60 @@ import com.idealista.fpe.config.Alphabet;
import com.idealista.fpe.config.Domain; import com.idealista.fpe.config.Domain;
import com.idealista.fpe.config.GenericTransformations; import com.idealista.fpe.config.GenericTransformations;
/**
* Domain implementation that provides the alphabet and transformation helpers
* used by the format-preserving encryption implementation in this project.
* <p>
* This class delegates character-to-index and index-to-character transformations
* to a {@link GenericTransformations} helper configured with the {@link MainAlphabet}.
*/
public class MainDomain implements Domain { public class MainDomain implements Domain {
/**
* The alphabet used by the domain for encoding/decoding characters.
*/
private Alphabet _alphabet; private Alphabet _alphabet;
/**
* Helper instance performing actual transformations between characters and indices.
*/
private GenericTransformations transformations; private GenericTransformations transformations;
/**
* Constructs a new {@code MainDomain} instance and initializes its alphabet and transformation helper.
*/
public MainDomain() { public MainDomain() {
_alphabet = new MainAlphabet(); _alphabet = new MainAlphabet();
transformations = new GenericTransformations(_alphabet.availableCharacters()); transformations = new GenericTransformations(_alphabet.availableCharacters());
} }
/**
* Returns the {@link Alphabet} implementation used by this domain.
*
* @return the alphabet used for transforming values in this domain
*/
@Override @Override
public Alphabet alphabet() { public Alphabet alphabet() {
return _alphabet; return _alphabet;
} }
/**
* Transforms a string of characters into an array of integer indices according to the configured alphabet.
*
* @param data the input string to transform; may not be null
* @return an integer array representing the indices of the characters in the given string
*/
@Override @Override
public int[] transform(String data) { public int[] transform(String data) {
return transformations.transform(data); return transformations.transform(data);
} }
/**
* Transforms an array of integer indices back into a string using the configured alphabet.
*
* @param data the array of indices to transform back into characters
* @return the resulting string produced from the given index array
*/
@Override @Override
public String transform(int[] data) { public String transform(int[] data) {
return transformations.transform(data); return transformations.transform(data);

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -3,9 +3,27 @@ package de.neitzel.fx.component.example;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
/**
* Simple address model used in the FX example component.
*/
@Getter @Getter
@Setter @Setter
public class Address { public class Address {
/**
* Street name and number.
*/
private String street; private String street;
/**
* City name.
*/
private String city; private String city;
/**
* Default constructor only
*/
public Address() {
// default constructor only
}
} }

View File

@ -6,8 +6,32 @@ import javafx.scene.Parent;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
/**
* Example JavaFX application demonstrating the {@link de.neitzel.fx.component.ComponentLoader} usage.
*/
public class ExampleApp extends Application { public class ExampleApp extends Application {
/**
* Default constructor only
*/
public ExampleApp() {
// default constructor only
}
/**
* Launches the JavaFX application.
*
* @param args command line arguments
*/
public static void main(String[] args) {
launch(args);
}
/**
* Initializes sample model data, loads the FX component and shows the primary stage.
*
* @param primaryStage the primary stage for this application
*/
@Override @Override
public void start(Stage primaryStage) throws Exception { public void start(Stage primaryStage) throws Exception {
// Beispielmodel initialisieren // Beispielmodel initialisieren
@ -29,8 +53,4 @@ public class ExampleApp extends Application {
primaryStage.setScene(scene); primaryStage.setScene(scene);
primaryStage.show(); primaryStage.show();
} }
public static void main(String[] args) {
launch();
}
} }

View File

@ -5,8 +5,17 @@ package de.neitzel.fx.component.example;
* taking care os Classloader Requirements of JavaFX. (Important when starting from inside NetBeans!) * taking care os Classloader Requirements of JavaFX. (Important when starting from inside NetBeans!)
*/ */
public class Main { public class Main {
/**
* Default constructor only
*/
public Main() {
// default constructor only
}
/** /**
* Additional main methode to start Application. * Additional main methode to start Application.
*
* @param args Commandline Arguments. * @param args Commandline Arguments.
*/ */
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -3,9 +3,27 @@ package de.neitzel.fx.component.example;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
/**
* Simple person model used in the FX example component.
*/
@Getter @Getter
@Setter @Setter
public class Person { public class Person {
/**
* The person's full name.
*/
private String name; private String name;
/**
* The address of the person.
*/
private Address address; private Address address;
/**
* Default constructor only
*/
public Person() {
// default constructor only
}
} }

View File

@ -6,8 +6,34 @@ import javafx.scene.Parent;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Minimal JavaFX application launcher used by the examples.
*
* <p>This class initializes the JavaFX runtime, loads {@code MainWindow.fxml} and shows
* the primary stage. It is intended as a small bootstrap used by IDEs or test runs
* to start the JavaFX UI for demonstration purposes.
*/
public class JavaFXApp extends Application { public class JavaFXApp extends Application {
private static final Logger LOGGER = Logger.getLogger(JavaFXApp.class.getName());
/**
* Default constructor only.
*
* <p>The constructor does not perform UI initialization. UI setup happens in {@link #start(Stage)}
* after JavaFX has set up the application thread.
*/
public JavaFXApp() {
// default constructor only
}
public static void main(String[] args) {
launch(args);
}
@Override @Override
public void start(Stage primaryStage) { public void start(Stage primaryStage) {
try { try {
@ -17,12 +43,7 @@ public class JavaFXApp extends Application {
primaryStage.setScene(new Scene(root)); primaryStage.setScene(new Scene(root));
primaryStage.show(); primaryStage.show();
} catch (Exception ex) { } catch (Exception ex) {
System.out.println("Exception: " + ex.getMessage()); LOGGER.log(Level.SEVERE, "Exception while starting JavaFXApp", ex);
ex.printStackTrace();
} }
} }
public static void main(String[] args) {
launch(args);
}
} }

View File

@ -5,8 +5,26 @@ package de.neitzel.fx.injectfx.example;
* taking care os Classloader Requirements of JavaFX. (Important when starting from inside NetBeans!) * taking care os Classloader Requirements of JavaFX. (Important when starting from inside NetBeans!)
*/ */
public class Main { public class Main {
/**
* Creates a new helper instance of {@code Main}.
*
* <p>This constructor is intentionally empty. The {@code Main} class serves as an alternative
* entry point to launch the JavaFX application via {@link JavaFXApp#main(String[])} in
* environments (IDEs, build tools) where directly starting a class that extends
* {@code javafx.application.Application} may fail due to classloader or JVM startup constraints.
*
* <p>Do not perform application initialization here; use the static {@code main} method to
* start the JavaFX runtime. This constructor exists to allow tooling and frameworks to
* instantiate the class reflectively if required.
*/
public Main() {
// default constructor only
}
/** /**
* Additional main methode to start Application. * Additional main methode to start Application.
*
* @param args Commandline Arguments. * @param args Commandline Arguments.
*/ */
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -8,13 +8,49 @@ import javafx.scene.control.TextField;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/**
* Controller for the MainWindow FXML view.
*
* <p>This class acts as the backing controller for the {@code MainWindow.fxml} UI. It demonstrates a
* minimal example of a JavaFX controller that:
* <ul>
* <li>receives UI element injection via {@link FXML} (the {@link #textField} field),</li>
* <li>initializes the view in {@link #initialize(URL, ResourceBundle)}, and</li>
* <li>handles user actions via {@link #onButtonClick(ActionEvent)}.</li>
* </ul>
*
* <p>Usage notes:
* <ul>
* <li>Fields annotated with {@link FXML} are injected by the {@code FXMLLoader} before
* {@link #initialize(URL, ResourceBundle)} is called do not expect them to be non-null
* in the constructor.</li>
* <li>All UI updates must run on the JavaFX Application Thread. Methods called by FXML
* (such as {@link #onButtonClick(ActionEvent)}) are executed on that thread by the
* JavaFX runtime.</li>
* <li>The controller keeps a simple click counter and updates {@link #textField} to reflect
* the current count using {@link #displayCounter()}.</li>
* </ul>
*/
public class MainWindow implements Initializable { public class MainWindow implements Initializable {
/**
* Counter for button clicks shown in the UI.
*/
private int counter = 0; private int counter = 0;
/**
* Text field UI control showing the click counter.
*/
@FXML @FXML
private TextField textField; private TextField textField;
/**
* Default constructor only
*/
public MainWindow() {
// default constructor only
}
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
displayCounter(); displayCounter();

View File

@ -2,9 +2,9 @@
<?import javafx.scene.control.TextField?> <?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?> <?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx" <AnchorPane xmlns:fx="http://javafx.com/fxml"
xmlns:fx="http://javafx.com/fxml"
xmlns:nfx="http://example.com/nfx" xmlns:nfx="http://example.com/nfx"
xmlns="http://javafx.com/javafx"
fx:controller="de.neitzel.fx.component.ComponentController" fx:controller="de.neitzel.fx.component.ComponentController"
prefWidth="300" prefHeight="100"> prefWidth="300" prefHeight="100">
<children> <children>

View File

@ -3,9 +3,11 @@
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?> <?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?> <?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="127.0" prefWidth="209.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="de.neitzel.fx.injectfx.example.MainWindow"> <AnchorPane xmlns:fx="http://javafx.com/fxml/1" prefHeight="127.0" prefWidth="209.0"
xmlns="http://javafx.com/javafx/17.0.2-ea" fx:controller="de.neitzel.fx.injectfx.example.MainWindow">
<children> <children>
<Button fx:id="button" layoutX="44.0" layoutY="70.0" mnemonicParsing="false" onAction="#onButtonClick" text="Click Me" /> <Button fx:id="button" layoutX="44.0" layoutY="70.0" mnemonicParsing="false" onAction="#onButtonClick"
<TextField fx:id="textField" layoutX="14.0" layoutY="24.0" /> text="Click Me"/>
<TextField fx:id="textField" layoutX="14.0" layoutY="24.0"/>
</children> </children>
</AnchorPane> </AnchorPane>

View File

@ -3,9 +3,9 @@
<?import javafx.scene.control.TextField?> <?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?> <?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?> <?import javafx.scene.layout.Pane?>
<AnchorPane xmlns="http://javafx.com/javafx" <AnchorPane xmlns:fx="http://javafx.com/fxml"
xmlns:fx="http://javafx.com/fxml"
xmlns:nfx="http://example.com/nfx" xmlns:nfx="http://example.com/nfx"
xmlns="http://javafx.com/javafx"
fx:controller="de.neitzel.fx.component.ComponentController" fx:controller="de.neitzel.fx.component.ComponentController"
prefWidth="300" prefHeight="180"> prefWidth="300" prefHeight="180">
<children> <children>

99
fx/ideas.md Normal file
View File

@ -0,0 +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
## Bidirektional Converter für Bindings
```Java
StringConverter<Instant> converter = new StringConverter<>() {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
@Override
public String toString(Instant instant) {
return instant == null ? "" : formatter.format(instant);
}
@Override
public Instant fromString(String string) {
if (string == null || string.isBlank()) return null;
try {
LocalDateTime dateTime = LocalDateTime.parse(string, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return dateTime.atZone(ZoneId.systemDefault()).toInstant();
} catch (DateTimeParseException e) {
// Optional: Fehlermeldung anzeigen
return null;
}
}
};
// Binding einrichten:
Bindings.
bindBidirectional(textField.textProperty(),instantProperty,converter);
```
Overloads von bindBidirectional
bindBidirectional(Property<String>, Property<?>, Format)
bindBidirectional(Property<String>, Property<T>, StringConverter<T>)
bindBidirectional(Property<T>, Property<T>)
## Unidirektional mit Bindings.createStringBinding()
```Java
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
textField.
textProperty().
bind(Bindings.createStringBinding(
() ->{
Instant instant = instantProperty.get();
return instant ==null?"":formatter.
format(instant);
},
instantProperty
));
```
Bindings:
- BooleanBinding
- DoubleBinding
- FloatBinding
- IntegerBinding
- LongBinding
- StringBinding
- ObjectBinding
- NumberBinding
Bindings haben Methoden, die dann weitere Bindings erzeugen.
==> Parameter der Methode: Property, Observable und einer Methode, die ein Binding erstellt und das ViewModel<?>, das
dann genutzt werden kann, um weitere Properties zu holen ==> Erzeugung von neuen Properties.
Diese BindingCreators haben Namen, PropertyTyp und ein BindingType.
# FXMLComponent
Dient dem Laden einer Komponente und bekommt dazu das fxml, die Daten und ggf. auch eine ModelView.
# ModelView generieren
Damit eine ModelView nicht ständig manuell generiert werden muss, ist hier ggf. etwas zu generieren?
Ggf. eine eigenes Beschreibungssprache?
# Aufbau einer Validierung
- gehört in das ViewModel
-

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@ -49,6 +49,11 @@
<artifactId>core</artifactId> <artifactId>core</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>de.neitzel.lib</groupId>
<artifactId>injection</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1,22 +1,70 @@
package de.neitzel.fx.component; package de.neitzel.fx.component;
import javafx.beans.property.*; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleFloatProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/**
* AutoViewModel automatically exposes JavaFX properties for all readable/writable fields
* of a given POJO model. It creates appropriate Property instances and keeps them in a map
* for lookup by field name.
*
* @param <T> the type of the underlying model
*/
@Slf4j
public class AutoViewModel<T> { public class AutoViewModel<T> {
/**
* The wrapped model instance.
* -- GETTER --
* Retrieves the model associated with this AutoViewModel.
*/
@Getter
private final T model; private final T model;
/**
* Map of field name to JavaFX Property instance exposing that field's value.
*/
private final Map<String, Property<?>> properties = new HashMap<>(); private final Map<String, Property<?>> properties = new HashMap<>();
/**
* Constructs an AutoViewModel for the provided model and initializes properties via reflection.
*
* @param model the POJO model whose getters/setters are used to create JavaFX properties
*/
public AutoViewModel(T model) { public AutoViewModel(T model) {
this.model = model; this.model = model;
initProperties(); initProperties();
} }
/**
* Initializes a mapping of field names to JavaFX properties for the associated model object.
* <p>
* This method utilizes reflection to iterate through the public no-argument getters
* of the model's class to create corresponding JavaFX properties. It performs the following:
* <p>
* 1. Identifies methods that adhere to the getter naming conventions (e.g., `getFieldName` or `isFieldName`).
* 2. Maps the field name derived from the getter method to a JavaFX property, creating an appropriate
* property type based on the getter's return type.
* 3. Adds a listener to each property to bind updates from the property back to the model object
* using the corresponding setter method, if available. This ensures bi-directional synchronization
* between the ViewModel and the underlying model.
* <p>
* Exceptions that may occur during reflective operations (e.g., invoking methods) are caught
* and logged to avoid runtime failures.
*/
private void initProperties() { private void initProperties() {
for (Method getter : model.getClass().getMethods()) { for (Method getter : model.getClass().getMethods()) {
if (isGetter(getter)) { if (isGetter(getter)) {
@ -27,13 +75,17 @@ public class AutoViewModel<T> {
properties.put(fieldName, prop); properties.put(fieldName, prop);
// Bind ViewModel Model // Bind ViewModel Model
prop.addListener((obs, oldVal, newVal) -> { prop.addListener((_obs, _oldVal, newVal) -> {
// use _obs and _oldVal to satisfy static analysis (they are intentionally retained)
if (_oldVal != null && _oldVal.equals(newVal)) {
// no-op: values unchanged
}
Method setter = findSetterFor(model.getClass(), fieldName, newVal != null ? newVal.getClass() : null); Method setter = findSetterFor(model.getClass(), fieldName, newVal != null ? newVal.getClass() : null);
if (setter != null) { if (setter != null) {
try { try {
setter.invoke(model, newVal); setter.invoke(model, newVal);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.warn("Failed to invoke setter for field {}", fieldName, e);
} }
} }
}); });
@ -41,16 +93,14 @@ public class AutoViewModel<T> {
} }
} }
public Property<?> getProperty(String name) { /**
return properties.get(name); * Determines if a given method follows the JavaBean getter convention.
} * The method must be public, take no parameters, have a non-void return type,
* and its name should start with either "get" or "is".
public T getModel() { *
return model; * @param method the method to be assessed
} * @return true if the method adheres to the JavaBean getter convention, false otherwise
*/
// ========== Hilfsmethoden ==========
private boolean isGetter(Method method) { private boolean isGetter(Method method) {
return Modifier.isPublic(method.getModifiers()) return Modifier.isPublic(method.getModifiers())
&& method.getParameterCount() == 0 && method.getParameterCount() == 0
@ -58,6 +108,14 @@ public class AutoViewModel<T> {
&& (method.getName().startsWith("get") || method.getName().startsWith("is")); && (method.getName().startsWith("get") || method.getName().startsWith("is"));
} }
/**
* Derives the field name from a given Java method by following JavaBean naming conventions.
* The method's name is analyzed to strip the "get" or "is" prefix (if present),
* and the result is converted into a decapitalized field name.
*
* @param method the method whose name is to be processed to derive the field name
* @return the derived field name, or the original method name if no "get" or "is" prefix is found
*/
private String getFieldName(Method method) { private String getFieldName(Method method) {
String name = method.getName(); String name = method.getName();
if (name.startsWith("get")) { if (name.startsWith("get")) {
@ -68,20 +126,57 @@ public class AutoViewModel<T> {
return name; return name;
} }
private String decapitalize(String str) { // ========== Hilfsmethoden ==========
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toLowerCase() + str.substring(1);
}
/**
* Invokes the specified method on the model object and returns the result.
* If the method invocation fails, logs a warning and returns null.
*
* @param method the method to be invoked, typically a JavaBean getter
* @return the result of invoking the method, or null if an error occurs
*/
private Object invokeGetter(Method method) { private Object invokeGetter(Method method) {
try { try {
return method.invoke(model); return method.invoke(model);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.warn("Failed to invoke getter: {}", method.getName(), e);
return null; return null;
} }
} }
/**
* Converts an object into an appropriate JavaFX {@link Property} based on its type.
*
* @param value the object to be converted into a JavaFX property; can be of type String, Integer,
* Boolean, Double, Float, Long, or any other object
* @return a JavaFX {@link Property} corresponding to the type of the input object, such as
* {@link SimpleStringProperty} for String, {@link SimpleIntegerProperty} for Integer,
* {@link SimpleBooleanProperty} for Boolean, and so on. If the type is not explicitly
* handled, a {@link SimpleObjectProperty} is returned wrapping the object
*/
private Property<?> toProperty(Object value) {
if (value instanceof String s) return new SimpleStringProperty(s);
if (value instanceof Integer i) return new SimpleIntegerProperty(i);
if (value instanceof Boolean b) return new SimpleBooleanProperty(b);
if (value instanceof Double d) return new SimpleDoubleProperty(d);
if (value instanceof Float f) return new SimpleFloatProperty(f);
if (value instanceof Long l) return new SimpleLongProperty(l);
return new SimpleObjectProperty<>(value);
}
/**
* Finds a setter method in a specified class that corresponds to the given field name and value type.
* <p>
* The method searches for a public method in the class whose name matches the JavaBean-style setter
* naming convention (e.g., "setFieldName"). The method must accept exactly one parameter, and if a
* value type is provided, the parameter type must be assignable from it.
*
* @param clazz the class to search for the method
* @param fieldName the name of the field for which the setter method is being sought
* @param valueType the expected type of the parameter for the setter method, or null if no specific
* type constraint is required
* @return the setter method if found, or null if no matching method is identified
*/
private Method findSetterFor(Class<?> clazz, String fieldName, Class<?> valueType) { private Method findSetterFor(Class<?> clazz, String fieldName, Class<?> valueType) {
String setterName = "set" + capitalize(fieldName); String setterName = "set" + capitalize(fieldName);
for (Method m : clazz.getMethods()) { for (Method m : clazz.getMethods()) {
@ -94,18 +189,38 @@ public class AutoViewModel<T> {
return null; return null;
} }
/**
* Converts the first character of the given string to lowercase, leaving the rest of the string unchanged.
* If the input string is null or empty, it is returned as-is.
*
* @param str the string to be decapitalized
* @return the decapitalized string, or the original string if it is null or empty
*/
private String decapitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toLowerCase() + str.substring(1);
}
/**
* Capitalizes the first letter of the given string and leaves the rest of the string unchanged.
* If the input string is null or empty, it is returned as is.
*
* @param str the string to be capitalized
* @return the input string with its first character converted to uppercase, or the original string if it is null or empty
*/
private String capitalize(String str) { private String capitalize(String str) {
if (str == null || str.isEmpty()) return str; if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1); return str.substring(0, 1).toUpperCase() + str.substring(1);
} }
private Property<?> toProperty(Object value) { /**
if (value instanceof String s) return new SimpleStringProperty(s); * Retrieves the JavaFX property associated with the given field name.
if (value instanceof Integer i) return new SimpleIntegerProperty(i); *
if (value instanceof Boolean b) return new SimpleBooleanProperty(b); * @param name the name of the field whose associated JavaFX property is to be returned
if (value instanceof Double d) return new SimpleDoubleProperty(d); * @return the JavaFX {@link Property} associated with the specified field name,
if (value instanceof Float f) return new SimpleFloatProperty(f); * or null if no property exists for the given name
if (value instanceof Long l) return new SimpleLongProperty(l); */
return new SimpleObjectProperty<>(value); public Property<?> getProperty(String name) {
return properties.get(name);
} }
} }

View File

@ -1,7 +1,6 @@
package de.neitzel.fx.component; package de.neitzel.fx.component;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor;
/** /**
* Generic controller used by the ComponentLoader to bind FXML views * Generic controller used by the ComponentLoader to bind FXML views
@ -10,7 +9,6 @@ import lombok.RequiredArgsConstructor;
* This controller provides access to the {@link AutoViewModel} * This controller provides access to the {@link AutoViewModel}
* which contains JavaFX properties derived from the model. * which contains JavaFX properties derived from the model.
*/ */
@RequiredArgsConstructor
public class ComponentController { public class ComponentController {
/** /**
@ -20,4 +18,14 @@ public class ComponentController {
*/ */
@Getter @Getter
private final AutoViewModel<?> viewModel; private final AutoViewModel<?> viewModel;
/**
* Constructs a new ComponentController instance with the specified AutoViewModel.
*
* @param viewModel the AutoViewModel containing JavaFX properties derived from the model,
* used for binding the view to the model.
*/
public ComponentController(AutoViewModel<?> viewModel) {
this.viewModel = viewModel;
}
} }

View File

@ -1,27 +1,266 @@
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.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.layout.Pane; 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.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
/** /**
* 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<>();
/**
* The controller object associated with the ComponentLoader.
* It is typically used to manage the behavior and interactions of the UI components
* defined in an FXML file.
*/
private Object controller;
/**
* Default constructor only
*/
public ComponentLoader() {
// default constructor only
}
/**
* Loads an FXML file from the specified URL, sets the provided controller, and processes bindings.
* This method parses the FXML file, initializes the specified controller, and evaluates binding controls
* defined within the FXML namespace.
*
* @param <T> the type of the root node, which must extend {@code Parent}
* @param fxmlUrl the URL of the FXML file to be loaded
* @param controller the controller to associate with the FXML file
* @param nothing an unused parameter, included for compatibility
* @return the root node loaded from the FXML file
* @throws IOException if there is an error reading the FXML file
*/
public static <T extends Parent> T load(URL fxmlUrl, Object controller, @SuppressWarnings("unused") String nothing) throws IOException {
FXMLLoader loader = new FXMLLoader(fxmlUrl);
loader.setController(controller);
T root = loader.load();
Map<String, Object> namespace = loader.getNamespace();
// Nach allen BindingControls suchen:
List<Binding> bindingControls = collectAllNodes(root).stream()
.filter(n -> n instanceof Binding)
.map(n -> (Binding) n)
.toList();
for (Binding bc : bindingControls) {
evaluateBindings(bc.getBindings(), namespace);
}
return root;
}
/**
* Safely binds two properties bidirectionally while ensuring type compatibility.
* If the target property's type is not compatible with the source property's type, the binding will
* not be performed, and an error will be logged.
*
* @param source the source property to bind, must not be null
* @param target the target property to bind to; the type will be checked for compatibility at runtime
* @param <T> the type of the source property's value
*/
@SuppressWarnings("unchecked")
private static <T> void bindBidirectionalSafe(@NotNull Property<T> source, Property<?> target) {
try {
Property<T> targetCasted = (Property<T>) target;
source.bindBidirectional(targetCasted);
} catch (ClassCastException e) {
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
}
}
/**
* Attempts to bind the source property to the target property in a type-safe manner.
* If the target property's type is incompatible with the source property's type, the binding operation
* will be skipped, and an error message will be logged.
*
* @param source the source property to bind, must not be null
* @param target the target property to which the source will be bound
* @param <T> the type of the source property's value
*/
@SuppressWarnings("unchecked")
private static <T> void bindSafe(@NotNull Property<T> source, Property<?> target) {
try {
Property<T> targetCasted = (Property<T>) target;
source.bind(targetCasted);
} catch (ClassCastException e) {
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
}
}
/**
* Evaluates and establishes bindings between source and target properties based on the provided
* bindings list and namespace. Supports unidirectional and bidirectional bindings, ensuring type
* compatibility before binding. Logs errors for unsuccessful binding attempts or exceptions.
*
* @param bindings a list of BindingData objects that define the source, target, and direction for the bindings
* @param namespace a map representing the namespace from which expressions in the bindings are resolved
*/
private static void evaluateBindings(List<BindingData> bindings, Map<String, Object> namespace) {
for (var binding : bindings) {
try {
Object source = resolveExpression(binding.getSource(), namespace);
Object target = resolveExpression(binding.getTarget(), namespace);
if (source instanceof Property<?> sourceProp && target instanceof Property<?> targetProp) {
Class<?> sourceType = getPropertyType(sourceProp);
Class<?> targetType = getPropertyType(targetProp);
boolean bindableForward = targetType.isAssignableFrom(sourceType);
boolean bindableBackward = sourceType.isAssignableFrom(targetType);
switch (binding.getDirection().toLowerCase()) {
case "bidirectional":
if (bindableForward && bindableBackward) {
bindBidirectionalSafe(sourceProp, targetProp);
} else {
log.error("⚠️ Kann bidirektionales Binding nicht durchführen: Typen inkompatibel ({} ⇄ {})", sourceType, targetType);
}
break;
case "unidirectional":
default:
if (bindableForward) {
bindSafe(sourceProp, targetProp);
} else {
log.error("⚠️ Kann unidirektionales Binding nicht durchführen: {} → {} nicht zuweisbar", sourceType, targetType);
}
break;
}
}
} catch (Exception e) {
log.error("Fehler beim Binding: {} → {}", binding.getSource(), binding.getTarget(), e);
}
}
}
/**
* Determines the type of the value held by the specified property.
*
* @param prop the property whose value type is to be determined, must not be null
* @return the class of the property's value type; returns Object.class if the type cannot be determined
*/
private static Class<?> getPropertyType(Property<?> prop) {
try {
Method getter = prop.getClass().getMethod("get");
return getter.getReturnType();
} catch (Exception e) {
return Object.class; // Fallback
}
}
/**
* Resolves a dot-separated expression (e.g., "viewModel.username") by navigating through the provided namespace
* map and invoking the corresponding getter methods to access nested properties.
*
* @param expr the dot-separated expression to resolve, must not be null
* @param namespace a map containing the initial objects to resolve the expression from, must not be null
* @return the value resolved from the specified expression
* @throws Exception if an error occurs while invoking getter methods or resolving the expression
*/
private static Object resolveExpression(@NotNull String expr, @NotNull Map<String, Object> namespace) throws Exception {
// z.B. "viewModel.username"
String[] parts = expr.split("\\.");
Object current = namespace.get(parts[0]);
for (int i = 1; i < parts.length; i++) {
String getter = "get" + Character.toUpperCase(parts[i].charAt(0)) + parts[i].substring(1);
current = current.getClass().getMethod(getter).invoke(current);
}
return current;
}
/**
* Recursively collects all nodes in a scene graph starting from the given root node.
*
* @param root the starting node from which all descendant nodes will be collected
* @return a list containing all nodes, including the root node and its descendants
*/
private static @NotNull List<Node> collectAllNodes(Node root) {
List<Node> nodes = new ArrayList<>();
nodes.add(root);
if (root instanceof Parent parent && !(root instanceof FxmlComponent)) {
for (Node child : parent.getChildrenUnmodifiable()) {
nodes.addAll(collectAllNodes(child));
}
}
return nodes;
}
/**
* Returns the controller associated with the ComponentLoader.
*
* @return the controller object
*/
public Object getController() {
return controller;
}
/**
* Loads an FXML file and returns the root JavaFX node without binding to a data model.
* This method uses the given {@code fxmlPath} to locate and load the FXML file.
*
* @param fxmlPath the URL of the FXML file to be loaded
* @return the root JavaFX node loaded from the specified FXML file
*/
public Parent load(URL fxmlPath) {
return load(null, fxmlPath);
}
/**
* Loads an FXML file and binds its elements to a generated ViewModel
* based on the given POJO model and specified FXML path.
*
* @param model the data model (POJO) to bind to the UI
* @param fxmlPath the URL path to the FXML file
* @return the root JavaFX node loaded from FXML
* @throws RuntimeException if the FXML could not be loaded
*/
public Parent load(Object model, URL fxmlPath) {
try {
AutoViewModel<?> viewModel = new AutoViewModel<>(model);
FXMLLoader loader = new FXMLLoader(fxmlPath);
loader.setControllerFactory(type -> {
Objects.requireNonNull(type);
return new ComponentController(viewModel);
});
Parent root = loader.load();
controller = loader.getController();
return root;
} catch (IOException e) {
throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
}
}
/**
* Loads an FXML file and returns its root node.
*
* @param fxmlPath the relative path to the FXML file
* @return the root JavaFX node loaded from the FXML file
*/
public Parent load(String fxmlPath) {
return load(null, fxmlPath);
}
/** /**
* Loads an FXML file and binds its elements to a generated ViewModel * Loads an FXML file and binds its elements to a generated ViewModel
@ -32,83 +271,7 @@ 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));
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);
}
}
/**
* 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);
}
}
});
}
/**
* Recursively walks all nodes in the scene graph starting from the root,
* applying the given consumer to each node.
*
* @param root the starting node
* @param consumer the consumer to apply to each node
*/
private void walkNodes(Parent root, java.util.function.Consumer<javafx.scene.Node> consumer) {
consumer.accept(root);
if (root instanceof Pane pane) {
for (javafx.scene.Node child : pane.getChildren()) {
if (child instanceof Parent p) {
walkNodes(p, consumer);
} else {
consumer.accept(child);
}
}
}
}
/**
* Extracts custom NFX attributes from a node's properties map.
* These attributes are expected to be in the format "nfx:..." and hold string values.
*
* @param node the node to inspect
* @return a map of NFX attribute names to values
*/
private Map<String, String> extractNfxAttributes(javafx.scene.Node node) {
Map<String, String> result = new HashMap<>();
node.getProperties().forEach((k, v) -> {
if (k instanceof String key && key.startsWith("nfx:") && v instanceof String value) {
result.put(key, value);
}
});
return result;
} }
/** /**
@ -118,6 +281,7 @@ public class ComponentLoader {
* @param vmProp the ViewModel property to bind to * @param vmProp the ViewModel property to bind to
* @param direction the direction of the binding (e.g., "bidirectional", "read") * @param direction the direction of the binding (e.g., "bidirectional", "read")
*/ */
@SuppressWarnings("unused")
private void bindNodeToProperty(javafx.scene.Node node, Property<?> vmProp, String direction) { private void bindNodeToProperty(javafx.scene.Node node, Property<?> vmProp, String direction) {
if (node instanceof javafx.scene.control.TextField tf && vmProp instanceof javafx.beans.property.StringProperty sp) { if (node instanceof javafx.scene.control.TextField tf && vmProp instanceof javafx.beans.property.StringProperty sp) {
if ("bidirectional".equalsIgnoreCase(direction)) { if ("bidirectional".equalsIgnoreCase(direction)) {
@ -128,61 +292,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

@ -0,0 +1,67 @@
package de.neitzel.fx.component.controls;
import de.neitzel.fx.component.model.BindingData;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
/**
* The BindingControl class represents a UI control that manages a list
* of {@link BindingData} objects. It extends the {@link Region} class and
* provides functionality to bind and monitor connections between source
* and target properties.
* <p>
* The primary purpose of this control is to maintain an observable list
* of bindings, allowing developers to track or adjust the linked properties
* dynamically.
* <p>
* The internal list of bindings is implemented as an {@link ObservableList},
* allowing property change notifications to be easily monitored for UI
* updates or other reactive behaviors.
* <p>
* This class serves as an organizational component and does not provide
* any user interaction by default.
*/
public class Binding extends Region {
/**
* Represents an observable list of {@link BindingData} objects contained within the
* {@link Binding} instance. This list is utilized to manage and monitor
* the bindings between source and target properties dynamically.
* <p>
* The list is implemented as an {@link ObservableList}, allowing changes in the
* collection to be observed and reacted to, such as triggering UI updates or
* responding to binding modifications.
* <p>
* This field is initialized as an empty list using {@link FXCollections#observableArrayList()}.
* It is declared as final to ensure its reference cannot be changed, while the
* contents of the list remain mutable.
*/
private final ObservableList<BindingData> bindings = FXCollections.observableArrayList();
/**
* Constructs a new instance of the BindingControl class.
* <p>
* This default constructor initializes the BindingControl without
* any pre-configured bindings. The instance will contain an empty
* {@link ObservableList} of {@link BindingData} objects, which can later
* be populated as needed.
* <p>
* The constructor does not perform additional setup or initialization,
* allowing the class to be extended or customized as necessary.
*/
public Binding() {
// Empty, the ComponentLoader is used to work on the bindings.
}
/**
* Retrieves the observable list of {@code Binding} objects associated with this control.
* The returned list allows monitoring and management of the bindings maintained
* by the {@code BindingControl}.
*
* @return an {@code ObservableList<Binding>} containing the bindings managed by this control
*/
public ObservableList<BindingData> getBindings() {
return bindings;
}
}

View File

@ -0,0 +1,196 @@
package de.neitzel.fx.component.controls;
import de.neitzel.fx.component.ComponentLoader;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import java.util.Arrays;
/**
* A reusable JavaFX component that loads and embeds an FXML-defined UI fragment.
* <p>
* The component exposes three properties:
* <ul>
* <li>fxml: the resource path to the FXML file to load (relative to the component class)</li>
* <li>direction: a textual hint that describes binding directionality (default "unidirectional")</li>
* <li>data: an arbitrary object that will be injected into the loaded controller if a suitable setter exists</li>
* </ul>
* <p>
* When the {@code fxml} property changes the FXML is loaded via {@link ComponentLoader}. After loading the
* controller instance available from the loader, this component attempts to inject the object from the
* {@code data} property by calling a matching {@code setData(...)} method via reflection.
*/
public class FxmlComponent extends StackPane {
private final StringProperty fxml = new SimpleStringProperty();
private final StringProperty direction = new SimpleStringProperty("unidirectional");
private final ObjectProperty<Object> data = new SimpleObjectProperty<>();
/**
* Creates a new instance of {@code FxmlComponent}.
* <p>
* The constructor registers listeners on the {@code fxml} and {@code data} properties to
* perform lazy loading and data injection when values change.
*/
public FxmlComponent() {
fxml.addListener((obs, oldVal, newVal) -> load());
data.addListener((obs, oldVal, newVal) -> injectData());
}
/**
* Loads the FXML specified by the {@link #fxml} property and replaces the current children
* with the loaded root node. If no FXML is configured or the path is blank, the method returns
* without performing any action.
*/
private void load() {
if (getFxml() == null || getFxml().isBlank()) return;
ComponentLoader loader = new ComponentLoader();
Parent content = loader.load(getClass().getResource(getFxml()));
getChildren().setAll(content);
Object controller = loader.getController();
if (controller != null && getData() != null) {
injectDataToController(controller, getData());
}
}
/**
* Attempts to inject the {@link #data} object into the controller of the currently loaded child node,
* if a controller is present and defines a single-argument method named {@code setData}.
*/
private void injectData() {
if (!getChildren().isEmpty() && getData() != null) {
Object controller = getControllerFromChild(getChildren().get(0));
if (controller != null) {
injectDataToController(controller, getData());
}
}
}
/**
* Returns the configured FXML resource path.
*
* @return the FXML path or {@code null} if not set
*/
public String getFxml() {
return fxml.get();
}
/**
* Sets the FXML resource path to load. Changing this value triggers a reload of the component content.
*
* @param fxml resource path relative to the component class
*/
public void setFxml(String fxml) {
this.fxml.set(fxml);
}
/**
* Returns the data object that will be injected into the loaded controller when available.
*
* @return the data object or {@code null}
*/
public Object getData() {
return data.get();
}
/**
* Sets the data object to be injected into the controller of the loaded FXML.
* Changing the value will attempt injection into the currently loaded controller.
*
* @param data the data object to inject
*/
public void setData(Object data) {
this.data.set(data);
}
/**
* Uses reflection to find a single-argument method named {@code setData} on the controller and
* invokes it with the provided {@code dataObject}.
*
* @param controller the controller instance to inject into
* @param dataObject the object to inject
*/
private void injectDataToController(Object controller, Object dataObject) {
// Daten-Objekt per Reflection zuweisen
// Beispiel: Controller hat `setData(User data)`
Arrays.stream(controller.getClass().getMethods())
.filter(m -> m.getName().equals("setData") && m.getParameterCount() == 1)
.findFirst()
.ifPresent(method -> {
try {
method.invoke(controller, dataObject);
} catch (Exception e) {
e.printStackTrace();
}
});
// Optional: automatisch Binding ausführen (s. u.)
}
/**
* Extracts the controller instance from a child node's properties map if present.
*
* @param node the child node to inspect
* @return the controller object stored under the property key {@code "fx:controller"} or {@code null}
*/
private Object getControllerFromChild(Node node) {
if (node.getProperties().containsKey("fx:controller")) {
return node.getProperties().get("fx:controller");
}
return null;
}
/**
* Returns the JavaFX property representing the configured FXML path.
*
* @return the FXML property
*/
public StringProperty fxmlProperty() {
return fxml;
}
/**
* Returns the JavaFX property representing the binding direction hint.
*
* @return the direction property
*/
public StringProperty directionProperty() {
return direction;
}
/**
* Returns the configured direction string.
*
* @return the direction hint (e.g., "unidirectional")
*/
public String getDirection() {
return direction.get();
}
/**
* Sets the direction hint used by higher-level binding logic.
*
* @param direction textual direction hint
*/
public void setDirection(String direction) {
this.direction.set(direction);
}
/**
* Returns the JavaFX property representing the data object used for injection.
*
* @return the data property
*/
public ObjectProperty<Object> dataProperty() {
return data;
}
}

View File

@ -0,0 +1,107 @@
package de.neitzel.fx.component.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* The Binding class represents a connection between a source and a target,
* with an associated direction. It is typically used to define a binding
* relationship that determines how data flows between these two entities.
* <p>
* The class provides three properties:
* - direction: Represents the type of the binding. Defaults to "unidirectional".
* - source: Represents the source of the binding.
* - target: Represents the target of the binding.
* <p>
* It uses JavaFX property types, allowing these properties to be observed
* for changes.
*/
public class BindingData {
/**
* Represents the direction of the binding in the {@code Binding} class.
* It determines whether the binding is unidirectional or bidirectional.
* The default value is "unidirectional".
* <p>
* This property is observed for changes, enabling dynamic updates
* within the JavaFX property system.
*/
private StringProperty direction = new SimpleStringProperty("unidirectional");
/**
* Represents the source of the binding. This property holds a string value
* that specifies the originating object or identifier in the binding connection.
* It can be observed for changes, allowing updates to the binding relationship
* when the source value is modified.
*/
private StringProperty source = new SimpleStringProperty();
/**
* Represents the target of the binding in the Binding class.
* This property holds the target value, which can be observed for changes.
*/
private StringProperty target = new SimpleStringProperty();
/**
* Default constructor only
*/
public BindingData() {
// default constructor only
}
/**
* Gets the current direction of the binding.
* The direction determines how data flows between
* the source and the target, and typically defaults to "unidirectional".
*
* @return the current direction of the binding
*/
public String getDirection() {
return direction.get();
}
/**
* Sets the direction of the binding.
*
* @param dir the new direction to set for the binding
*/
public void setDirection(String dir) {
direction.set(dir);
}
/**
* Gets the current source value of the binding.
*
* @return the source value as a String.
*/
public String getSource() {
return source.get();
}
/**
* Sets the value of the source property for this binding.
*
* @param source the new source value to be set
*/
public void setSource(String source) {
this.source.set(source);
}
/**
* Retrieves the current value of the target property.
*
* @return the value of the target property as a String.
*/
public String getTarget() {
return target.get();
}
/**
* Sets the target property of the binding.
*
* @param target the new value to set for the target property
*/
public void setTarget(String target) {
this.target.set(target);
}
}

View File

@ -1,7 +1,7 @@
package de.neitzel.fx.injectfx; package de.neitzel.fx.injectfx;
import de.neitzel.core.inject.ComponentData; import de.neitzel.injection.ComponentData;
import de.neitzel.core.inject.ComponentScanner; import de.neitzel.injection.ComponentScanner;
import lombok.Getter; import lombok.Getter;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
@ -19,7 +19,9 @@ public class FXMLComponentInstances {
/** Map holding instances of all @FXMLComponent classes, indexed by class and its unique superclasses/interfaces. */ /** Map holding instances of all @FXMLComponent classes, indexed by class and its unique superclasses/interfaces. */
private final Map<Class<?>, Object> instanceMap = new HashMap<>(); private final Map<Class<?>, Object> instanceMap = new HashMap<>();
/** The InjectableComponents instance that provides information about instantiable components. */ /**
* The InjectableComponents instance that provides information about instantiable components.
*/
private final ComponentScanner injectableScanner; private final ComponentScanner injectableScanner;
/** /**

View File

@ -13,27 +13,29 @@ import java.util.stream.Stream;
* support dependency injection. It uses a parameter map to resolve and supply dependencies to * support dependency injection. It uses a parameter map to resolve and supply dependencies to
* controller constructors dynamically during instantiation. * controller constructors dynamically during instantiation.
* *
* This class simplifies the process of injecting dependencies into JavaFX controllers by analyzing * <p>This class analyzes available constructors of a requested controller class at runtime and
* the constructors of the given controller classes at runtime. It selects the constructor that best * selects the first constructor whose parameter types are all present in the internal parameter map.
* matches the available dependencies in the parameter map and creates an instance of the controller. * The corresponding instances are retrieved and passed to the constructor to create the controller.
* *
* It implements the Callback interface to provide compatibility with the JavaFX FXMLLoader, allowing * <p>It implements the {@link javafx.util.Callback} interface to integrate with the JavaFX
* controllers with dependencies to be injected seamlessly during the FXML loading process. * {@link javafx.fxml.FXMLLoader} controller factory mechanism.
*/ */
public class InjectingControllerFactory implements Callback<Class<?>, Object> { public class InjectingControllerFactory implements Callback<Class<?>, Object> {
/** /**
* A map that stores class-to-object mappings used for dependency injection * A map that stores class-to-object mappings used for dependency injection
* in controller instantiation. This map is utilized to resolve and supply * in controller instantiation. Each key represents a parameter type and the
* the required dependencies for constructors during the creation of controller * associated value is the instance to be injected for that type.
* instances.
*
* Each key in the map represents a class type, and the corresponding value
* is the instance of that type. This allows the {@link InjectingControllerFactory}
* to use the stored instances to dynamically match and inject dependencies
* into controllers at runtime.
*/ */
private final Map<Class<?>, Object> parameterMap = new HashMap<>(); private final Map<Class<?>, Object> parameterMap = new HashMap<>();
/**
* Default constructor only
*/
public InjectingControllerFactory() {
// default constructor only
}
/** /**
* Adds a mapping between a class and its corresponding object instance * Adds a mapping between a class and its corresponding object instance
* to the parameter map used for dependency injection. * to the parameter map used for dependency injection.

View File

@ -1,6 +1,6 @@
package de.neitzel.fx.injectfx; package de.neitzel.fx.injectfx;
import de.neitzel.core.inject.ComponentScanner; import de.neitzel.injection.ComponentScanner;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -11,12 +11,12 @@ import java.net.URL;
* The InjectingFXMLLoader class provides a custom implementation of JavaFX's FXMLLoader * The InjectingFXMLLoader class provides a custom implementation of JavaFX's FXMLLoader
* that supports dependency injection. It facilitates the loading of FXML files while * that supports dependency injection. It facilitates the loading of FXML files while
* dynamically injecting dependencies into controllers during the loading process. * dynamically injecting dependencies into controllers during the loading process.
* * <p>
* This class utilizes the InjectingControllerFactory to enable seamless integration * This class utilizes the InjectingControllerFactory to enable seamless integration
* of dependency injection with JavaFX controllers. Dependencies can be added to the * of dependency injection with JavaFX controllers. Dependencies can be added to the
* controller factory, and the loader will use this factory to instantiate controllers * controller factory, and the loader will use this factory to instantiate controllers
* with the appropriate dependencies. * with the appropriate dependencies.
* * <p>
* Features of this loader include: * Features of this loader include:
* - Support for dependency injection into JavaFX controllers by using a custom factory. * - Support for dependency injection into JavaFX controllers by using a custom factory.
* - Adding custom dependency mappings at runtime. * - Adding custom dependency mappings at runtime.
@ -28,20 +28,20 @@ public class InjectingFXMLLoader {
/** /**
* Represents an instance of the {@link InjectingControllerFactory}, which is used for creating * Represents an instance of the {@link InjectingControllerFactory}, which is used for creating
* and managing controller instances with dependency injection capabilities in a JavaFX application. * and managing controller instances with dependency injection capabilities in a JavaFX application.
* * <p>
* This factory facilitates the injection of dependencies by dynamically resolving and supplying * This factory facilitates the injection of dependencies by dynamically resolving and supplying
* required objects during controller instantiation. It plays a critical role in enabling seamless * required objects during controller instantiation. It plays a critical role in enabling seamless
* integration of dependency injection with JavaFX's {@link FXMLLoader}. * integration of dependency injection with JavaFX's {@link FXMLLoader}.
* * <p>
* The `controllerFactory` is used internally by the {@link InjectingFXMLLoader} to provide a consistent * The `controllerFactory` is used internally by the {@link InjectingFXMLLoader} to provide a consistent
* and extensible mechanism for controller creation while maintaining loose coupling and enhancing testability. * and extensible mechanism for controller creation while maintaining loose coupling and enhancing testability.
* * <p>
* Key responsibilities: * Key responsibilities:
* - Manages a mapping of classes to their injectable instances required for controller instantiation. * - Manages a mapping of classes to their injectable instances required for controller instantiation.
* - Dynamically analyzes and invokes appropriate constructors for controllers based on the availability * - Dynamically analyzes and invokes appropriate constructors for controllers based on the availability
* of dependencies. * of dependencies.
* - Ensures that controllers are created with required dependencies, preventing manual resolution of injections. * - Ensures that controllers are created with required dependencies, preventing manual resolution of injections.
* * <p>
* This variable is initialized in the {@link InjectingFXMLLoader} and can be extended with additional * This variable is initialized in the {@link InjectingFXMLLoader} and can be extended with additional
* mappings at runtime using relevant methods. * mappings at runtime using relevant methods.
*/ */
@ -51,7 +51,7 @@ public class InjectingFXMLLoader {
* Default constructor for the InjectingFXMLLoader class. * Default constructor for the InjectingFXMLLoader class.
* This initializes the loader with a new instance of the {@link InjectingControllerFactory}, * This initializes the loader with a new instance of the {@link InjectingControllerFactory},
* which is used to provide dependency injection capabilities for JavaFX controllers. * which is used to provide dependency injection capabilities for JavaFX controllers.
* * <p>
* The {@link InjectingControllerFactory} allows for the registration and dynamic injection * The {@link InjectingControllerFactory} allows for the registration and dynamic injection
* of dependencies into controllers when they are instantiated during the FXML loading process. * of dependencies into controllers when they are instantiated during the FXML loading process.
*/ */
@ -100,7 +100,7 @@ public class InjectingFXMLLoader {
* available for dependency injection. * available for dependency injection.
*/ */
private void addInjectingData(FXMLComponentInstances instances) { private void addInjectingData(FXMLComponentInstances instances) {
for (var clazz: instances.getInstanceMap().keySet()) { for (var clazz : instances.getInstanceMap().keySet()) {
addInjectingData(clazz, instances.getInstance(clazz)); addInjectingData(clazz, instances.getInstance(clazz));
} }
} }

View File

@ -6,11 +6,9 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.control.TextInputControl; import javafx.scene.control.TextInputControl;
import lombok.RequiredArgsConstructor;
import java.io.InputStream; import java.io.InputStream;
@RequiredArgsConstructor
/** /**
* Custom FXMLLoader that binds JavaFX controls to a GenericViewModel using metadata from FXML. * Custom FXMLLoader that binds JavaFX controls to a GenericViewModel using metadata from FXML.
* It supports automatic binding setup based on properties defined in the FXML's node properties. * It supports automatic binding setup based on properties defined in the FXML's node properties.
@ -24,6 +22,17 @@ public class BindingAwareFXMLLoader<T> {
*/ */
private final T model; private final T model;
/**
* Constructs a new instance of the BindingAwareFXMLLoader class.
* This loader is designed to load FXML files and automatically bind UI elements
* to the properties of the provided model using a binding-aware approach.
*
* @param model the model object to be used for binding to the UI components
*/
public BindingAwareFXMLLoader(T model) {
this.model = model;
}
/** /**
* Loads an FXML file and performs automatic binding setup using the GenericViewModel. * Loads an FXML file and performs automatic binding setup using the GenericViewModel.
* *
@ -68,23 +77,36 @@ public class BindingAwareFXMLLoader<T> {
if (userData instanceof String propertyName) { if (userData instanceof String propertyName) {
BindDirection direction = getDirection(node); BindDirection direction = getDirection(node);
if (node instanceof TextInputControl control) { switch (node) {
case TextInputControl control -> {
Property<String> prop = viewModel.property(StringProperty.class, propertyName); Property<String> prop = viewModel.property(StringProperty.class, propertyName);
bind(control.textProperty(), prop, direction); bind(control.textProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.Label label) { }
case javafx.scene.control.Label label -> {
Property<String> prop = viewModel.property(StringProperty.class, propertyName); Property<String> prop = viewModel.property(StringProperty.class, propertyName);
bind(label.textProperty(), prop, direction); bind(label.textProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.CheckBox checkBox) { }
case javafx.scene.control.CheckBox checkBox -> {
Property<Boolean> prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName); Property<Boolean> prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName);
bind(checkBox.selectedProperty(), prop, direction); bind(checkBox.selectedProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.Slider slider) { }
case javafx.scene.control.Slider slider -> {
Property<Number> prop = viewModel.property(javafx.beans.property.DoubleProperty.class, propertyName); Property<Number> prop = viewModel.property(javafx.beans.property.DoubleProperty.class, propertyName);
bind(slider.valueProperty(), prop, direction); bind(slider.valueProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.DatePicker datePicker) { }
case javafx.scene.control.DatePicker datePicker -> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Property<java.time.LocalDate> prop = (Property<java.time.LocalDate>) viewModel.property(javafx.beans.property.ObjectProperty.class, propertyName); Property<java.time.LocalDate> prop =
(Property<java.time.LocalDate>) viewModel.property(
javafx.beans.property.ObjectProperty.class,
propertyName
);
bind(datePicker.valueProperty(), prop, direction); bind(datePicker.valueProperty(), prop, direction);
} }
default -> {
// ignore unsupported nodes
}
}
} }
} }

View File

@ -12,18 +12,21 @@ import java.util.ResourceBundle;
* @param <T> the type of the model * @param <T> the type of the model
*/ */
public class GenericViewController<T> implements Initializable { public class GenericViewController<T> implements Initializable {
/** /**
* The GenericViewModel instance wrapping the underlying model. * The GenericViewModel instance wrapping the underlying model.
*/ */
private GenericViewModel<T> viewModel; private GenericViewModel<T> viewModel;
/** /**
* Sets the model for this controller by wrapping it in a GenericViewModel. * Creates a new controller instance in an uninitialized state.
* *
* @param model the model to be used by the ViewModel * <p>The constructor does not set a model. Call {@link #setModel(Object)} to
* attach a model instance before performing view bindings or accessing the view model via
* {@link #getViewModel()}.</p>
*/ */
public void setModel(T model) { public GenericViewController() {
this.viewModel = new GenericViewModel<>(model); // default constructor only ...
} }
/** /**
@ -35,6 +38,15 @@ public class GenericViewController<T> implements Initializable {
return viewModel.getModel(); return viewModel.getModel();
} }
/**
* Sets the model for this controller by wrapping it in a GenericViewModel.
*
* @param model the model to be used by the ViewModel
*/
public void setModel(T model) {
this.viewModel = new GenericViewModel<>(model);
}
/** /**
* Standard JavaFX initialize method. No-op, as actual binding is done externally via FXMLLoader extension. * Standard JavaFX initialize method. No-op, as actual binding is done externally via FXMLLoader extension.
* *

View File

@ -1,6 +1,12 @@
package de.neitzel.fx.mvvm; package de.neitzel.fx.mvvm;
import javafx.beans.property.*; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import java.beans.BeanInfo; import java.beans.BeanInfo;
import java.beans.IntrospectionException; import java.beans.IntrospectionException;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -14,7 +14,7 @@ import java.util.Set;
/** /**
* A custom {@link TypeAdapter} implementation for selectively excluding specified fields during JSON serialization. * A custom {@link TypeAdapter} implementation for selectively excluding specified fields during JSON serialization.
* * <p>
* This adapter facilitates both serialization and deserialization of objects while providing functionality to remove specified * This adapter facilitates both serialization and deserialization of objects while providing functionality to remove specified
* fields from the serialized output. The set of excluded field names is provided during the adapter's initialization. * fields from the serialized output. The set of excluded field names is provided during the adapter's initialization.
* *
@ -24,15 +24,16 @@ public class ExcludeFieldsAdapter<T> extends TypeAdapter<T> {
/** /**
* The type of objects that this adapter handles for serialization and deserialization. * The type of objects that this adapter handles for serialization and deserialization.
* * <p>
* This is a generic class type parameter representing the class of objects that the * This is a generic class type parameter representing the class of objects that the
* {@link ExcludeFieldsAdapter} instance is designed to process. It ensures type * {@link ExcludeFieldsAdapter} instance is designed to process. It ensures type
* safety and guarantees the compatibility of the adapter with a specific object type. * safety and guarantees the compatibility of the adapter with a specific object type.
*/ */
private final Class<T> type; private final Class<T> type;
/** /**
* A set containing the names of fields to be excluded during JSON serialization. * A set containing the names of fields to be excluded during JSON serialization.
* * <p>
* This collection is used in the {@link ExcludeFieldsAdapter} to determine which fields should be removed from the * This collection is used in the {@link ExcludeFieldsAdapter} to determine which fields should be removed from the
* serialized JSON representation of an object. The fields to be excluded are specified during the initialization * serialized JSON representation of an object. The fields to be excluded are specified during the initialization
* of the adapter. * of the adapter.
@ -41,7 +42,7 @@ public class ExcludeFieldsAdapter<T> extends TypeAdapter<T> {
/** /**
* An instance of the Gson library used for JSON serialization and deserialization. * An instance of the Gson library used for JSON serialization and deserialization.
* * <p>
* This variable holds the Gson object that is utilized to handle the conversion processes within the * This variable holds the Gson object that is utilized to handle the conversion processes within the
* custom serialization and deserialization logic. If not explicitly provided during initialization, * custom serialization and deserialization logic. If not explicitly provided during initialization,
* it can be lazily initialized using a GsonBuilder. * it can be lazily initialized using a GsonBuilder.
@ -50,10 +51,10 @@ public class ExcludeFieldsAdapter<T> extends TypeAdapter<T> {
/** /**
* A {@link GsonBuilder} instance used for configuring and creating {@link Gson} objects within the adapter. * A {@link GsonBuilder} instance used for configuring and creating {@link Gson} objects within the adapter.
* * <p>
* This variable serves as a reference to a GsonBuilder that allows customization of the Gson instance, * This variable serves as a reference to a GsonBuilder that allows customization of the Gson instance,
* including registering custom type adapters, serializers, and deserializers, as well as adjusting serialization policies. * including registering custom type adapters, serializers, and deserializers, as well as adjusting serialization policies.
* * <p>
* The {@link #gsonBuilder} is primarily utilized when an existing {@link Gson} instance is not directly provided. * The {@link #gsonBuilder} is primarily utilized when an existing {@link Gson} instance is not directly provided.
* It ensures that the adapter can defer the creation of a Gson object until it is explicitly required. * It ensures that the adapter can defer the creation of a Gson object until it is explicitly required.
*/ */
@ -89,19 +90,6 @@ public class ExcludeFieldsAdapter<T> extends TypeAdapter<T> {
this.gson = null; this.gson = null;
} }
/**
* Lazily initializes and retrieves the {@link Gson} instance. If the instance is null, it creates a new {@link Gson}
* object using the {@link GsonBuilder} provided during the adapter's initialization.
*
* @return the {@link Gson} instance used for serialization and deserialization
*/
private Gson getGson() {
if (gson == null) {
gson = gsonBuilder.create();
}
return gson;
}
/** /**
* Serializes an object of type {@code T} into JSON format, excluding certain fields specified during the initialization of this adapter. * Serializes an object of type {@code T} into JSON format, excluding certain fields specified during the initialization of this adapter.
* The object is first converted into a {@link JsonObject}, then the fields listed in the excluded fields set are removed from the JSON * The object is first converted into a {@link JsonObject}, then the fields listed in the excluded fields set are removed from the JSON
@ -122,6 +110,19 @@ public class ExcludeFieldsAdapter<T> extends TypeAdapter<T> {
Streams.write(obj, out); Streams.write(obj, out);
} }
/**
* Lazily initializes and retrieves the {@link Gson} instance. If the instance is null, it creates a new {@link Gson}
* object using the {@link GsonBuilder} provided during the adapter's initialization.
*
* @return the {@link Gson} instance used for serialization and deserialization
*/
private Gson getGson() {
if (gson == null) {
gson = gsonBuilder.create();
}
return gson;
}
/** /**
* Reads a JSON input stream and deserializes it into an object of the specified type. * Reads a JSON input stream and deserializes it into an object of the specified type.
* *

View File

@ -1,6 +1,12 @@
package de.neitzel.gson.adapter; package de.neitzel.gson.adapter;
import com.google.gson.*; import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.time.Instant; import java.time.Instant;

View File

@ -0,0 +1,270 @@
package de.neitzel.gson.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import lombok.Builder;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
/**
* Represents a configuration utility class that manages application settings and allows
* loading and saving the configuration in JSON format. This class is built using Gson for
* JSON serialization and deserialization.
* <p>
* The class maintains application settings in memory and provides methods to store
* the configuration on the filesystem and retrieve it as needed.
*/
@Builder(builderClassName = "ConfigurationBuilder")
public class JsonConfiguration {
/**
* The name of the application.
* This variable is used to identify the application and is incorporated into various configurations,
* such as the naming of directories or files associated with the application's settings.
* It is finalized and set during the construction of the {@code JsonConfiguration} instance and cannot be changed afterward.
*/
private final String appName;
/**
* The filesystem path representing the home directory utilized by the application.
* This directory serves as the location where application-specific configuration files
* and data are stored. The path is initialized when the {@code JsonConfiguration} instance
* is constructed, based on the settings provided through the {@code JsonConfigurationBuilder}.
* It can be customized to a user-specified value or defaults to the user's home directory.
* <p>
* The {@code homeDir} variable is integral in determining the location of the main configuration
* file and other related resources used by the application.
*/
private final String homeDir;
/**
* A map that stores configuration settings as key-value pairs,
* where both the keys and values are represented as strings.
* <p>
* This map is used to manage dynamic or runtime settings for the configuration,
* allowing for flexible assignment and retrieval of values associated with specific
* configuration keys. It is initialized as a final instance, ensuring it cannot be
* reassigned after creation.
*/
private final Map<String, String> settings;
/**
* A Gson instance used for handling JSON serialization and deserialization within
* the JsonConfiguration class. This instance is immutable and customized with
* registered adapters during the initialization of the JsonConfiguration.
* <p>
* The Gson object provides functionality for converting Java objects to JSON
* and vice versa. It supports complex serialization and deserialization
* workflows by leveraging adapters specified during the configuration phase.
* <p>
* Adapters can be registered to customize the behavior of (de)serialization
* for specific types.
*/
private final Gson gson;
/**
* Stores a key-value pair into the configuration settings.
* The value is serialized into a JSON string before being stored.
*
* @param key the key under which the value will be stored
* @param value the object to be associated with the specified key
*/
public void set(String key, Object value) {
settings.put(key, gson.toJson(value));
}
/**
* Retrieves a value associated with the specified key from the configuration,
* deserializing it into the specified class type using Gson.
* If the key does not exist in the settings, the method returns {@code null}.
*
* @param <T> the type of the object to be deserialized
* @param key the key corresponding to the value in the configuration
* @param clazz the {@code Class} object representing the expected type of the value
* @return the deserialized object of type {@code T}, or {@code null}
* if the key does not exist
*/
public <T> T get(String key, Class<T> clazz) {
String json = settings.get(key);
return json != null ? gson.fromJson(json, clazz) : null;
}
/**
* Retrieves a value associated with the given key from the configuration as an object of the specified type.
* If the key does not exist in the configuration, the provided default value is returned.
*
* @param <T> the type of the object to be returned
* @param key the key whose associated value is to be retrieved
* @param clazz the class of the object to deserialize the value into
* @param defaultValue the default value to return if the key does not exist or the value is null
* @return the value associated with the specified key, deserialized into the specified type,
* or the default value if the key does not exist or the value is null
*/
public <T> T get(String key, Class<T> clazz, T defaultValue) {
String json = settings.get(key);
return json != null ? gson.fromJson(json, clazz) : defaultValue;
}
/**
* Loads the configuration data from the JSON configuration file located at the path
* determined by the {@code getConfigFilePath()} method.
* If the configuration file exists, its content is read and deserialized into a map
* of key-value pairs using Gson. The method clears the current settings and populates
* them with the loaded values.
*
* @throws IOException if an I/O error occurs while reading the configuration file
*/
public void load() throws IOException {
Path configPath = getConfigFilePath();
if (Files.exists(configPath)) {
try (var reader = Files.newBufferedReader(configPath)) {
Map<String, String> loaded = gson.fromJson(reader, new TypeToken<Map<String, String>>() {
}.getType());
settings.clear();
settings.putAll(loaded);
}
}
}
/**
* Constructs the path to the configuration file for the application.
* The configuration file is located in the user's home directory, within a hidden
* folder named after the application, and is named "config.json".
*
* @return the {@code Path} to the application's configuration file
*/
private Path getConfigFilePath() {
return Path.of(homeDir, "." + appName, "config.json");
}
/**
* Saves the current configuration to a JSON file.
* <p>
* This method serializes the current settings into a JSON format and writes
* them to a file located at the configuration path. If the parent directory
* of the configuration file does not exist, it is created automatically.
* <p>
* The configuration file path is determined based on the application name
* and home directory. Any existing content in the file will be overwritten
* during this operation.
*
* @throws IOException if an I/O error occurs while creating the directory,
* opening the file, or writing to the file
*/
public void save() throws IOException {
Path configPath = getConfigFilePath();
Files.createDirectories(configPath.getParent());
try (var writer = Files.newBufferedWriter(configPath)) {
gson.toJson(settings, writer);
}
}
/**
* Builder class for creating instances of {@code JsonConfiguration}.
* The {@code JsonConfigurationBuilder} allows the configuration of application
* name, home directory, and custom Gson adapters before building a {@code JsonConfiguration} object.
*/
public static class JsonConfigurationBuilder {
/**
* A map that holds custom Gson adapters to register with a GsonBuilder.
* The keys represent the classes for which the adapters are applicable,
* and the values are the adapter instances associated with those classes.
* <p>
* This variable is used to store user-defined type adapters, allowing
* for customized serialization and deserialization behavior for specific
* classes when constructing a {@code JsonConfiguration}.
* <p>
* It is populated using the {@code addGsonAdapter} method in the
* {@code JsonConfigurationBuilder} class and is later passed to a
* {@code GsonBuilder} for registration.
*/
private final Map<Class<?>, Object> gsonAdapters = new HashMap<>();
/**
* The name of the application being configured.
* This variable holds a string representation of the application's name and is used
* to identify the application within the context of the {@code JsonConfiguration}.
* It is set during the construction of a {@code JsonConfigurationBuilder} instance.
*/
private String appName;
/**
* Represents the home directory path of the current user.
* By default, this variable is initialized to the value of the "user.home" system property,
* which typically points to the user's home directory on the filesystem.
* <p>
* This field can be customized to point to a different directory via the builder class
* methods when building an instance of {@code JsonConfiguration}.
* <p>
* Example system values for "user.home" may include paths such as "~/Users/username" for macOS,
* "C:/Users/username" for Windows, or "/home/username" for Unix/Linux systems.
*/
private String homeDir = System.getProperty("user.home");
/**
* Sets the application name to be used in the configuration.
*
* @param appName the name of the application to be set
* @return the current instance of {@code JsonConfigurationBuilder} for chaining
*/
public JsonConfigurationBuilder appName(String appName) {
this.appName = appName;
return this;
}
/**
* Sets the home directory for the configuration being built.
* This method specifies the directory path where the application's configuration
* files or settings should be stored.
*
* @param homeDir the path to the desired home directory
* @return the current instance of {@code JsonConfigurationBuilder} to enable method chaining
*/
public JsonConfigurationBuilder homeDir(String homeDir) {
this.homeDir = homeDir;
return this;
}
/**
* Adds a custom Gson adapter to the builder for the specified type.
* This method allows registration of a type-to-adapter mapping that will be applied
* when building the Gson instance within the {@code JsonConfiguration}.
*
* @param type the {@code Class} of the type for which the adapter is being registered
* @param adapter the adapter object to be used for the specified type
* @return the current instance of {@code JsonConfigurationBuilder}, allowing method chaining
*/
public JsonConfigurationBuilder addGsonAdapter(Class<?> type, Object adapter) {
gsonAdapters.put(type, adapter);
return this;
}
/**
* Builds and returns an instance of {@code JsonConfiguration} with the specified
* settings, including the application name, home directory, and any registered
* custom Gson adapters.
*
* @return a {@code JsonConfiguration} instance configured with the builder's parameters.
*/
public JsonConfiguration build() {
GsonBuilder gsonBuilder = new GsonBuilder();
for (var entry : gsonAdapters.entrySet()) {
gsonBuilder.registerTypeAdapter(entry.getKey(), entry.getValue());
}
Gson gson = gsonBuilder.create();
return new JsonConfiguration(
appName,
homeDir,
new HashMap<>(),
gson
);
}
}
}

View File

@ -0,0 +1,128 @@
package de.neitzel.gson.config;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class JsonConfigurationTest {
/**
* Test class for JsonConfiguration containing unit tests for the `get` method.
* The `get` method fetches the value associated with a given key and converts
* it from JSON representation to the specified class type.
*/
@Test
void testGetExistingKeyWithValidValue() {
// Arrange
Gson gson = new Gson();
HashMap<String, String> settings = new HashMap<>();
settings.put("testKey", gson.toJson(42));
JsonConfiguration jsonConfiguration = JsonConfiguration.builder()
.settings(settings)
.gson(gson)
.build();
// Act
Integer value = jsonConfiguration.get("testKey", Integer.class);
// Assert
assertEquals(42, value);
}
@Test
void testGetNonExistingKey() {
// Arrange
Gson gson = new Gson();
HashMap<String, String> settings = new HashMap<>();
JsonConfiguration jsonConfiguration = JsonConfiguration.builder()
.settings(settings)
.gson(gson)
.build();
// Act
String value = jsonConfiguration.get("nonExistentKey", String.class);
// Assert
assertNull(value);
}
@Test
void testGetWithDefaultValueWhenKeyExists() {
// Arrange
Gson gson = new Gson();
HashMap<String, String> settings = new HashMap<>();
settings.put("testKey", gson.toJson("Hello World"));
JsonConfiguration jsonConfiguration = JsonConfiguration.builder()
.settings(settings)
.gson(gson)
.build();
// Act
String value = jsonConfiguration.get("testKey", String.class, "Default Value");
// Assert
assertEquals("Hello World", value);
}
@Test
void testGetWithDefaultValueWhenKeyDoesNotExist() {
// Arrange
Gson gson = new Gson();
HashMap<String, String> settings = new HashMap<>();
JsonConfiguration jsonConfiguration = JsonConfiguration.builder()
.settings(settings)
.gson(gson)
.build();
// Act
String value = jsonConfiguration.get("nonExistentKey", String.class, "Default Value");
// Assert
assertEquals("Default Value", value);
}
@Test
void testGetWithComplexObject() {
// Arrange
Gson gson = new Gson();
HashMap<String, String> settings = new HashMap<>();
TestObject testObject = new TestObject("John Doe", 30);
settings.put("complexKey", gson.toJson(testObject));
JsonConfiguration jsonConfiguration = JsonConfiguration.builder()
.settings(settings)
.gson(gson)
.build();
// Act
TestObject result = jsonConfiguration.get("complexKey", TestObject.class);
// Assert
assertEquals("John Doe", result.name());
assertEquals(30, result.age());
}
// Helper class for complex object tests
static class TestObject {
private final String name;
private final int age;
TestObject(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
}

51
injection/pom.xml Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.lib</groupId>
<artifactId>neitzellib</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>injection</artifactId>
<properties>
<!-- Application Properties -->
<link.name>${project.artifactId}</link.name>
<launcher>${project.artifactId}</launcher>
<appName>${project.artifactId}</appName>
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
</properties>
<dependencies>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>${reflections.version}</version>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>${jakarta-inject-api}</version>
</dependency>
</dependencies>
<build>
<finalName>${jar.filename}</finalName>
<plugins>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,6 +1,6 @@
package de.neitzel.core.inject; package de.neitzel.injection;
import de.neitzel.core.inject.annotation.Config; import de.neitzel.injection.annotation.Config;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.Map; import java.util.Map;
@ -10,7 +10,7 @@ import java.util.stream.Stream;
/** /**
* Represents the context of the application and serves as the foundation of a dependency * Represents the context of the application and serves as the foundation of a dependency
* injection framework. * injection framework.
* * <p>
* The {@code ApplicationContext} is responsible for scanning, instantiating, and managing * The {@code ApplicationContext} is responsible for scanning, instantiating, and managing
* the lifecycle of components. Components are identified through the specified base package * the lifecycle of components. Components are identified through the specified base package
* (or derived from a configuration class) and instantiated based on their dependency * (or derived from a configuration class) and instantiated based on their dependency
@ -52,7 +52,7 @@ public class ApplicationContext {
/** /**
* Retrieves an instance of the specified component type from the application context. * Retrieves an instance of the specified component type from the application context.
* * <p>
* This method uses dependency injection to construct the component if it is not already * This method uses dependency injection to construct the component if it is not already
* a singleton instance. The component's type and scope are determined by the framework, * a singleton instance. The component's type and scope are determined by the framework,
* and the appropriate initialization and lifecycle management are performed. * and the appropriate initialization and lifecycle management are performed.
@ -103,7 +103,7 @@ public class ApplicationContext {
/** /**
* Determines if a given constructor can be instantiated using the provided parameter map. * Determines if a given constructor can be instantiated using the provided parameter map.
* * <p>
* The method evaluates whether every parameter type required by the constructor * The method evaluates whether every parameter type required by the constructor
* is available in the given parameter map. If all parameter types are present, * is available in the given parameter map. If all parameter types are present,
* the constructor is considered instantiable. * the constructor is considered instantiable.
@ -118,7 +118,7 @@ public class ApplicationContext {
/** /**
* Registers a singleton instance of a specific type into the application context. * Registers a singleton instance of a specific type into the application context.
* * <p>
* This method allows manual addition of singleton components to the context by * This method allows manual addition of singleton components to the context by
* providing a concrete instance of the required type. If the type is already mapped * providing a concrete instance of the required type. If the type is already mapped
* to a different instance or type within the context, an {@link IllegalStateException} * to a different instance or type within the context, an {@link IllegalStateException}

View File

@ -1,10 +1,10 @@
package de.neitzel.core.inject; package de.neitzel.injection;
import lombok.Getter; import lombok.Getter;
/** /**
* Represents a component in a dependency injection framework. * Represents a component in a dependency injection framework.
* * <p>
* A component is a unit of functionality that is managed by the framework, allowing * A component is a unit of functionality that is managed by the framework, allowing
* for controlled instantiation and lifecycle management. The component is associated * for controlled instantiation and lifecycle management. The component is associated
* with a specific type and a scope. The scope determines whether the component is * with a specific type and a scope. The scope determines whether the component is
@ -15,7 +15,7 @@ public class ComponentData {
/** /**
* Represents the type of the component being managed by the dependency injection framework. * Represents the type of the component being managed by the dependency injection framework.
* * <p>
* This variable holds the class object corresponding to the specific type of the component. * This variable holds the class object corresponding to the specific type of the component.
* It is used to identify and instantiate the component during the dependency injection process. * It is used to identify and instantiate the component during the dependency injection process.
* The type is immutable and is specified when the component is created. * The type is immutable and is specified when the component is created.
@ -24,10 +24,10 @@ public class ComponentData {
/** /**
* Defines the lifecycle and instantiation rules for the associated component. * Defines the lifecycle and instantiation rules for the associated component.
* * <p>
* The {@code Scope} determines whether the component is created as a singleton * The {@code Scope} determines whether the component is created as a singleton
* (a single shared instance) or as a prototype (a new instance for each request). * (a single shared instance) or as a prototype (a new instance for each request).
* * <p>
* This variable is immutable and represents the specific {@code Scope} assigned * This variable is immutable and represents the specific {@code Scope} assigned
* to the component, influencing its behavior within the dependency injection framework. * to the component, influencing its behavior within the dependency injection framework.
*/ */
@ -35,7 +35,7 @@ public class ComponentData {
/** /**
* Stores the instantiated object associated with the component. * Stores the instantiated object associated with the component.
* * <p>
* This field holds the actual instance of the component's type within the dependency * This field holds the actual instance of the component's type within the dependency
* injection framework. For components with a {@code SINGLETON} scope, this field is * injection framework. For components with a {@code SINGLETON} scope, this field is
* set only once and shared across the entire application. For components with a * set only once and shared across the entire application. For components with a
@ -57,7 +57,7 @@ public class ComponentData {
/** /**
* Constructs a new ComponentData instance with the specified type and an initial instance. * Constructs a new ComponentData instance with the specified type and an initial instance.
* * <p>
* This can be used to add Singletons manually without scanning for them. * This can be used to add Singletons manually without scanning for them.
* *
* @param type the class type of the component * @param type the class type of the component

View File

@ -1,11 +1,17 @@
package de.neitzel.core.inject; package de.neitzel.injection;
import de.neitzel.core.inject.annotation.Component; import de.neitzel.injection.annotation.Component;
import de.neitzel.core.inject.annotation.Inject; import de.neitzel.injection.annotation.Inject;
import org.reflections.Reflections; import org.reflections.Reflections;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -21,13 +27,13 @@ public class ComponentScanner {
* FXML-compatible components detected during the scanning process. * FXML-compatible components detected during the scanning process.
* These components are used as part of the dependency injection mechanism * These components are used as part of the dependency injection mechanism
* within the {@code InjectableComponentScanner}. * within the {@code InjectableComponentScanner}.
* * <p>
* The {@code fxmlComponents} set is populated during the component scanning * The {@code fxmlComponents} set is populated during the component scanning
* phase by identifying all classes within a specified package hierarchy * phase by identifying all classes within a specified package hierarchy
* annotated with the {@code @Component} annotation. These classes serve * annotated with the {@code @Component} annotation. These classes serve
* as the primary source for further analysis, resolution, and instantiation * as the primary source for further analysis, resolution, and instantiation
* in the scanning workflow. * in the scanning workflow.
* * <p>
* This field is immutable, ensuring thread safety and consistent state * This field is immutable, ensuring thread safety and consistent state
* throughout the lifetime of the {@code InjectableComponentScanner}. * throughout the lifetime of the {@code InjectableComponentScanner}.
*/ */
@ -35,12 +41,12 @@ public class ComponentScanner {
/** /**
* A set of component types that are not uniquely associated with a single implementation. * A set of component types that are not uniquely associated with a single implementation.
* * <p>
* This collection is populated during the analysis of discovered components to identify * This collection is populated during the analysis of discovered components to identify
* types that have multiple implementations, preventing them from being uniquely resolved. * types that have multiple implementations, preventing them from being uniquely resolved.
* For example, if multiple components implement the same interface, that interface will be * For example, if multiple components implement the same interface, that interface will be
* added to this set. * added to this set.
* * <p>
* It is primarily used during the component registration process to avoid ambiguities * It is primarily used during the component registration process to avoid ambiguities
* in the dependency injection system, ensuring that only resolvable, uniquely identifiable * in the dependency injection system, ensuring that only resolvable, uniquely identifiable
* components can be instantiated and injected. * components can be instantiated and injected.
@ -52,13 +58,13 @@ public class ComponentScanner {
* The key represents a super type (interface or abstract class), and the value * The key represents a super type (interface or abstract class), and the value
* is the specific implementation or subclass that is uniquely identified among * is the specific implementation or subclass that is uniquely identified among
* the scanned components. * the scanned components.
* * <p>
* This map is populated during the component analysis process. For each super type * This map is populated during the component analysis process. For each super type
* in the scanned components, if exactly one implementation is found, it is * in the scanned components, if exactly one implementation is found, it is
* considered unique and added to this mapping. In case multiple implementations * considered unique and added to this mapping. In case multiple implementations
* exist for a given super type, it is excluded from this map and classified as * exist for a given super type, it is excluded from this map and classified as
* a non-unique type. * a non-unique type.
* * <p>
* This mapping is utilized for resolving dependencies and determining which components * This mapping is utilized for resolving dependencies and determining which components
* can be instantiated based on unique type information. * can be instantiated based on unique type information.
*/ */
@ -70,7 +76,7 @@ public class ComponentScanner {
* that can be instantiated. The entries are resolved through the scanning and analysis * that can be instantiated. The entries are resolved through the scanning and analysis
* of available component types, ensuring that only components with resolvable * of available component types, ensuring that only components with resolvable
* dependencies are included. * dependencies are included.
* * <p>
* This field is part of the dependency injection process, where components annotated * This field is part of the dependency injection process, where components annotated
* with {@code @Component} or similar annotations are scanned, analyzed, and registered. * with {@code @Component} or similar annotations are scanned, analyzed, and registered.
* The resolution process checks if a component can be instantiated based on its * The resolution process checks if a component can be instantiated based on its
@ -81,11 +87,11 @@ public class ComponentScanner {
/** /**
* A list of error messages encountered during the scanning and analysis * A list of error messages encountered during the scanning and analysis
* of components in the dependency injection process. * of components in the dependency injection process.
* * <p>
* This list is populated when components cannot be resolved due to issues * This list is populated when components cannot be resolved due to issues
* such as multiple implementations of a superclass or interface, * such as multiple implementations of a superclass or interface,
* unmet construction requirements, or unresolved dependencies. * unmet construction requirements, or unresolved dependencies.
* * <p>
* Errors added to this list provide detailed descriptions of the specific * Errors added to this list provide detailed descriptions of the specific
* issues that prevent certain components from being instantiated correctly. * issues that prevent certain components from being instantiated correctly.
*/ */
@ -117,19 +123,19 @@ public class ComponentScanner {
/** /**
* Analyzes component types within the injected components and classifies them based on their * Analyzes component types within the injected components and classifies them based on their
* inheritance hierarchy and relationships. * inheritance hierarchy and relationships.
* * <p>
* This method performs the following operations: * This method performs the following operations:
* 1. Maps each component to all of its superclasses and interfaces. * 1. Maps each component to all of its superclasses and interfaces.
* 2. Identifies which superclasses or interfaces have multiple implementing components. * 2. Identifies which superclasses or interfaces have multiple implementing components.
* 3. Populates: * 3. Populates:
* - A list of superclasses or interfaces that are not uniquely linked to a single component. * - A list of superclasses or interfaces that are not uniquely linked to a single component.
* - A map where unique superclasses or interfaces are associated with a specific implementing component. * - A map where unique superclasses or interfaces are associated with a specific implementing component.
* * <p>
* The mappings are built using the following data structures: * The mappings are built using the following data structures:
* - A map from superclasses/interfaces to the list of components that implement or extend them. * - A map from superclasses/interfaces to the list of components that implement or extend them.
* - A list of non-unique types where multiple components exist for the same superclass or interface. * - A list of non-unique types where multiple components exist for the same superclass or interface.
* - A map of unique superclasses/interfaces to their corresponding component. * - A map of unique superclasses/interfaces to their corresponding component.
* * <p>
* This method is a key part of the component scanning and resolution process, facilitating * This method is a key part of the component scanning and resolution process, facilitating
* the identification of potential instantiation ambiguities or conflicts. * the identification of potential instantiation ambiguities or conflicts.
*/ */
@ -160,7 +166,7 @@ public class ComponentScanner {
* Resolves and identifies instantiable components from a set of scanned components. * Resolves and identifies instantiable components from a set of scanned components.
* This process determines which components can be instantiated based on their dependencies * This process determines which components can be instantiated based on their dependencies
* and class relationships, while tracking unresolved types and potential conflicts. * and class relationships, while tracking unresolved types and potential conflicts.
* * <p>
* The resolution process involves: * The resolution process involves:
* 1. Iteratively determining which components can be instantiated using the known types * 1. Iteratively determining which components can be instantiated using the known types
* map. A component is resolvable if its dependencies can be satisfied by the current * map. A component is resolvable if its dependencies can be satisfied by the current
@ -169,13 +175,13 @@ public class ComponentScanner {
* types map for future iterations. * types map for future iterations.
* 3. Removing successfully resolved components from the unresolved set. * 3. Removing successfully resolved components from the unresolved set.
* 4. Repeating the process until no further components can be resolved in a given iteration. * 4. Repeating the process until no further components can be resolved in a given iteration.
* * <p>
* At the end of the resolution process: * At the end of the resolution process:
* - Resolvable components are added to the `instantiableComponents` map, which maps types * - Resolvable components are added to the `instantiableComponents` map, which maps types
* to their corresponding instantiable implementations. * to their corresponding instantiable implementations.
* - Unresolved components are identified, and error details are collected to highlight * - Unresolved components are identified, and error details are collected to highlight
* dependencies or conflicts preventing their instantiation. * dependencies or conflicts preventing their instantiation.
* * <p>
* If errors are encountered due to unresolved components, they are logged for further analysis. * If errors are encountered due to unresolved components, they are logged for further analysis.
*/ */
private void resolveInstantiableComponents() { private void resolveInstantiableComponents() {
@ -208,21 +214,22 @@ public class ComponentScanner {
} }
/** /**
* Registers a component and its superclasses or interfaces in the provided map of known types. * Retrieves all superclasses and interfaces of the specified class, excluding the {@code Object} class.
* This method ensures that the component and its inheritance hierarchy are associated with the
* component's data unless the supertype is marked as non-unique.
* *
* @param component the {@code ComponentData} instance representing the component to be registered * @param clazz the class for which to retrieve all superclasses and interfaces
* @param knownTypes the map where component types and their data are stored * @return a set of all superclasses and interfaces implemented by the specified class
*/ */
private void registerComponentWithSuperTypes(ComponentData component, Map<Class<?>, ComponentData> knownTypes) { private Set<Class<?>> getAllSuperTypes(Class<?> clazz) {
knownTypes.put(component.getType(), component); Set<Class<?>> result = new HashSet<>();
Class<?> superClass = clazz.getSuperclass();
for (Class<?> superType : getAllSuperTypes(component.getType())) { while (superClass != null && superClass != Object.class) {
if (!notUniqueTypes.contains(superType)) { result.add(superClass);
knownTypes.put(superType, component); superClass = superClass.getSuperclass();
}
} }
result.addAll(Arrays.asList(clazz.getInterfaces()));
return result;
} }
/** /**
@ -260,6 +267,24 @@ public class ComponentScanner {
return true; return true;
} }
/**
* Registers a component and its superclasses or interfaces in the provided map of known types.
* This method ensures that the component and its inheritance hierarchy are associated with the
* component's data unless the supertype is marked as non-unique.
*
* @param component the {@code ComponentData} instance representing the component to be registered
* @param knownTypes the map where component types and their data are stored
*/
private void registerComponentWithSuperTypes(ComponentData component, Map<Class<?>, ComponentData> knownTypes) {
knownTypes.put(component.getType(), component);
for (Class<?> superType : getAllSuperTypes(component.getType())) {
if (!notUniqueTypes.contains(superType)) {
knownTypes.put(superType, component);
}
}
}
/** /**
* Collects the instantiation errors for a set of unresolved classes and appends * Collects the instantiation errors for a set of unresolved classes and appends
* detailed error messages to the internal error list. This method analyzes unresolved * detailed error messages to the internal error list. This method analyzes unresolved
@ -314,25 +339,6 @@ public class ComponentScanner {
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
} }
/**
* Retrieves all superclasses and interfaces of the specified class, excluding the {@code Object} class.
*
* @param clazz the class for which to retrieve all superclasses and interfaces
* @return a set of all superclasses and interfaces implemented by the specified class
*/
private Set<Class<?>> getAllSuperTypes(Class<?> clazz) {
Set<Class<?>> result = new HashSet<>();
Class<?> superClass = clazz.getSuperclass();
while (superClass != null && superClass != Object.class) {
result.add(superClass);
superClass = superClass.getSuperclass();
}
result.addAll(Arrays.asList(clazz.getInterfaces()));
return result;
}
/** /**
* Retrieves a map of classes representing component types to their corresponding instantiable implementations. * Retrieves a map of classes representing component types to their corresponding instantiable implementations.
* *

View File

@ -1,4 +1,4 @@
package de.neitzel.core.inject; package de.neitzel.injection;
/** /**
* Represents the scope of a component in a dependency injection framework. * Represents the scope of a component in a dependency injection framework.

View File

@ -1,6 +1,6 @@
package de.neitzel.core.inject.annotation; package de.neitzel.injection.annotation;
import de.neitzel.core.inject.Scope; import de.neitzel.injection.Scope;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -11,14 +11,14 @@ import java.lang.annotation.Target;
* Indicates that an annotated class is a "component" within a dependency injection framework. * Indicates that an annotated class is a "component" within a dependency injection framework.
* Classes annotated with {@code @Component} are recognized during the component scanning process * Classes annotated with {@code @Component} are recognized during the component scanning process
* as candidates for instantiation and management by the dependency injection framework. * as candidates for instantiation and management by the dependency injection framework.
* * <p>
* This annotation is typically used on classes that represent application-specific components, * This annotation is typically used on classes that represent application-specific components,
* such as service implementations, controllers, or other objects intended to be instantiated * such as service implementations, controllers, or other objects intended to be instantiated
* and injected into other components during runtime. * and injected into other components during runtime.
* * <p>
* The annotation must be placed at the type level, and it is retained at runtime * The annotation must be placed at the type level, and it is retained at runtime
* to allow for reflection-based scanning of classes within specified packages. * to allow for reflection-based scanning of classes within specified packages.
* * <p>
* Usage of this annotation assumes that there exists a component scanning mechanism * Usage of this annotation assumes that there exists a component scanning mechanism
* that processes annotated classes and identifies their roles, dependencies, and hierarchy * that processes annotated classes and identifies their roles, dependencies, and hierarchy
* within the application's structure. * within the application's structure.

View File

@ -1,4 +1,4 @@
package de.neitzel.core.inject.annotation; package de.neitzel.injection.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -10,14 +10,14 @@ import java.lang.annotation.Target;
* injection framework. Classes annotated with {@code @Config} are used as markers * injection framework. Classes annotated with {@code @Config} are used as markers
* for defining settings and application-specific configurations required by the * for defining settings and application-specific configurations required by the
* dependency injection mechanism. * dependency injection mechanism.
* * <p>
* Typically, configuration classes provide metadata required for setting up the * Typically, configuration classes provide metadata required for setting up the
* framework, such as specifying the base package to scan for components. * framework, such as specifying the base package to scan for components.
* * <p>
* This annotation must be applied at the type level and is retained at runtime to * This annotation must be applied at the type level and is retained at runtime to
* facilitate reflection-based processing. It is intended to serve as a declarative * facilitate reflection-based processing. It is intended to serve as a declarative
* representation of configuration options for the dependency injection container. * representation of configuration options for the dependency injection container.
* * <p>
* Attributes: * Attributes:
* - {@code basePackage}: Specifies the package name where the framework should scan * - {@code basePackage}: Specifies the package name where the framework should scan
* for classes annotated with dependency injection annotations such as {@code @Component}. * for classes annotated with dependency injection annotations such as {@code @Component}.

View File

@ -1,4 +1,4 @@
package de.neitzel.core.inject.annotation; package de.neitzel.injection.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -10,10 +10,10 @@ import java.lang.annotation.Target;
* a dependency injection framework. Fields annotated with {@code @Inject} * a dependency injection framework. Fields annotated with {@code @Inject}
* are automatically populated with the required component instance during runtime, * are automatically populated with the required component instance during runtime,
* typically by the dependency injection container. * typically by the dependency injection container.
* * <p>
* This annotation must be applied at the field level and is retained at runtime * This annotation must be applied at the field level and is retained at runtime
* to enable reflection-based identification and assignment of dependencies. * to enable reflection-based identification and assignment of dependencies.
* * <p>
* The framework's dependency resolution mechanism identifies the appropriate * The framework's dependency resolution mechanism identifies the appropriate
* instance to inject based on the field's type or custom configuration, * instance to inject based on the field's type or custom configuration,
* ensuring loose coupling and easier testability. * ensuring loose coupling and easier testability.

View File

@ -1,13 +1,17 @@
package de.neitzel.core.inject; package de.neitzel.injection;
import de.neitzel.core.inject.testcomponents.test1ok.SuperClass; import de.neitzel.injection.testcomponents.test1ok.SuperClass;
import de.neitzel.core.inject.testcomponents.test1ok.TestComponent1_1; import de.neitzel.injection.testcomponents.test1ok.TestComponent1_1;
import de.neitzel.core.inject.testcomponents.test1ok.TestInterface1_1; import de.neitzel.injection.testcomponents.test1ok.TestInterface1_1;
import de.neitzel.core.inject.testcomponents.test1ok.TestInterface1_2; import de.neitzel.injection.testcomponents.test1ok.TestInterface1_2;
import de.neitzel.core.inject.testcomponents.test1ok.sub.TestComponent1_2; import de.neitzel.injection.testcomponents.test1ok.sub.TestComponent1_2;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class InjectableComponentScannerTest { class InjectableComponentScannerTest {
@ -16,7 +20,7 @@ class InjectableComponentScannerTest {
*/ */
@Test @Test
void testLoadComponents() { void testLoadComponents() {
ComponentScanner scanner = new ComponentScanner("de.neitzel.core.inject.testcomponents.test1ok"); ComponentScanner scanner = new ComponentScanner("de.neitzel.injection.testcomponents.test1ok");
var instantiableComponents = scanner.getInstantiableComponents(); var instantiableComponents = scanner.getInstantiableComponents();
var nonUniqueTypes = scanner.getNotUniqueTypes(); var nonUniqueTypes = scanner.getNotUniqueTypes();
@ -37,7 +41,7 @@ class InjectableComponentScannerTest {
*/ */
@Test @Test
void testComponentsFailWithUnknownParameters() { void testComponentsFailWithUnknownParameters() {
ComponentScanner scanner = new ComponentScanner("de.neitzel.core.inject.testcomponents.test2fail"); ComponentScanner scanner = new ComponentScanner("de.neitzel.injection.testcomponents.test2fail");
var instantiableComponents = scanner.getInstantiableComponents(); var instantiableComponents = scanner.getInstantiableComponents();
var nonUniqueTypes = scanner.getNotUniqueTypes(); var nonUniqueTypes = scanner.getNotUniqueTypes();

View File

@ -0,0 +1,4 @@
package de.neitzel.injection.testcomponents.test1ok;
public class SuperClass {
}

View File

@ -1,6 +1,6 @@
package de.neitzel.core.inject.testcomponents.test1ok; package de.neitzel.injection.testcomponents.test1ok;
import de.neitzel.core.inject.annotation.Component; import de.neitzel.injection.annotation.Component;
@Component @Component
public class TestComponent1_1 extends SuperClass implements TestInterface1_2 { public class TestComponent1_1 extends SuperClass implements TestInterface1_2 {

View File

@ -0,0 +1,4 @@
package de.neitzel.injection.testcomponents.test1ok;
public interface TestInterface1_1 {
}

View File

@ -0,0 +1,4 @@
package de.neitzel.injection.testcomponents.test1ok;
public interface TestInterface1_2 {
}

View File

@ -0,0 +1,12 @@
package de.neitzel.injection.testcomponents.test1ok.sub;
import de.neitzel.injection.annotation.Component;
import de.neitzel.injection.testcomponents.test1ok.SuperClass;
import de.neitzel.injection.testcomponents.test1ok.TestInterface1_1;
import de.neitzel.injection.testcomponents.test1ok.TestInterface1_2;
@Component
public class TestComponent1_2 extends SuperClass implements TestInterface1_1, TestInterface1_2 {
public TestComponent1_2() {
}
}

View File

@ -1,6 +1,6 @@
package de.neitzel.core.inject.testcomponents.test2fail; package de.neitzel.injection.testcomponents.test2fail;
import de.neitzel.core.inject.annotation.Component; import de.neitzel.injection.annotation.Component;
/** /**
* TestComponent1 that should fail. * TestComponent1 that should fail.

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -1,5 +1,6 @@
package de.neitzel.log4j; package de.neitzel.log4j;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator; import org.apache.log4j.PropertyConfigurator;
import java.io.File; import java.io.File;
@ -9,6 +10,7 @@ import java.net.URISyntaxException;
* Utility class for managing Log4j configurations. Provides methods to set up * Utility class for managing Log4j configurations. Provides methods to set up
* Log4j configurations from default files, resources, or command line arguments. * Log4j configurations from default files, resources, or command line arguments.
*/ */
@SuppressWarnings("unused")
public class Log4jUtils { public class Log4jUtils {
/** /**
@ -24,13 +26,13 @@ public class Log4jUtils {
*/ */
public static final String DEFAULT_LOG4J_RESOURCE = "/log4j.default.properties"; public static final String DEFAULT_LOG4J_RESOURCE = "/log4j.default.properties";
private static final Logger LOGGER = Logger.getLogger(Log4jUtils.class);
/** /**
* Checks if the system property "log4j.configuration" is set. * Default constructor only
*
* @return true if the "log4j.configuration" property is defined, false otherwise.
*/ */
public static boolean isLog4jConfigFileSet() { public Log4jUtils() {
return System.getProperty("log4j.configuration") != null; // default constructor only
} }
/** /**
@ -38,7 +40,7 @@ public class Log4jUtils {
* This method leverages a default configuration file path and a default resource path * This method leverages a default configuration file path and a default resource path
* to set up Log4j logging if a configuration file is not already specified via * to set up Log4j logging if a configuration file is not already specified via
* a system property. If a valid configuration file or resource is found, it will be applied. * a system property. If a valid configuration file or resource is found, it will be applied.
* * <p>
* Delegates to the overloaded {@code setLog4jConfiguration(String log4jConfigFile, String defaultResource)} * Delegates to the overloaded {@code setLog4jConfiguration(String log4jConfigFile, String defaultResource)}
* method using predefined defaults. * method using predefined defaults.
*/ */
@ -46,22 +48,6 @@ public class Log4jUtils {
setLog4jConfiguration(DEFAULT_LOG4J_LOGFILE, DEFAULT_LOG4J_RESOURCE); setLog4jConfiguration(DEFAULT_LOG4J_LOGFILE, DEFAULT_LOG4J_RESOURCE);
} }
/**
* Constructs the absolute path to the specified Log4j configuration file located
* in the same directory as the JAR file of the application.
*
* @param log4jConfigFile The name of the Log4j configuration file.
* @return The absolute path to the specified Log4j configuration file if the
* path is successfully constructed; otherwise, returns null in case of an error.
*/
public static String getLog4jLogfileAtJar(final String log4jConfigFile) {
try {
return new File(Log4jUtils.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent() + "/" + log4jConfigFile;
} catch (URISyntaxException ex) {
return null;
}
}
/** /**
* Sets the Log4j configuration using the specified configuration file or a default resource * Sets the Log4j configuration using the specified configuration file or a default resource
* if the configuration file is not found. If a Log4j configuration is already set, the method * if the configuration file is not found. If a Log4j configuration is already set, the method
@ -79,10 +65,35 @@ public class Log4jUtils {
if (new File(log4jConfigFile).exists()) { if (new File(log4jConfigFile).exists()) {
PropertyConfigurator.configure(log4jConfigFile); PropertyConfigurator.configure(log4jConfigFile);
} else if (fileAtJar != null && new File(fileAtJar).exists()) { } else if (fileAtJar != null && new File(fileAtJar).exists()) {
System.out.println("Nutze Log4J Konfiguration bei jar File: " + fileAtJar); LOGGER.info("Using Log4J configuration from jar file: " + fileAtJar);
PropertyConfigurator.configure(fileAtJar); PropertyConfigurator.configure(fileAtJar);
}else { } else {
PropertyConfigurator.configure(Log4jUtils.class.getResource(defaultResource)); PropertyConfigurator.configure(Log4jUtils.class.getResource(defaultResource));
} }
} }
/**
* Checks if the system property "log4j.configuration" is set.
*
* @return true if the "log4j.configuration" property is defined, false otherwise.
*/
public static boolean isLog4jConfigFileSet() {
return System.getProperty("log4j.configuration") != null;
}
/**
* Constructs the absolute path to the specified Log4j configuration file located
* in the same directory as the JAR file of the application.
*
* @param log4jConfigFile The name of the Log4j configuration file.
* @return The absolute path to the specified Log4j configuration file if the
* path is successfully constructed; otherwise, returns null in case of an error.
*/
public static String getLog4jLogfileAtJar(final String log4jConfigFile) {
try {
return new File(Log4jUtils.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent() + "/" + log4jConfigFile;
} catch (URISyntaxException ex) {
return null;
}
}
} }

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -7,6 +7,7 @@ import java.util.Objects;
* This class provides methods to set or get the account details and overrides methods for string * This class provides methods to set or get the account details and overrides methods for string
* representation, equality checks, and hashing. * representation, equality checks, and hashing.
*/ */
@SuppressWarnings("unused")
public class ImapAccount { public class ImapAccount {
/** /**
@ -15,15 +16,6 @@ public class ImapAccount {
* of the mail server that handles IMAP communication. * of the mail server that handles IMAP communication.
*/ */
protected String server; protected String server;
/**
* Retrieves the server address associated with this IMAP*/
public String getServer() { return server; }
/**
* Sets the server address for the IMAP account.
*
* @param server the server address to be configured for the IMAP account
*/
public void setServer(String server) { this.server = server; }
/** /**
* Represents the username or identifier used to authenticate the IMAP account. * Represents the username or identifier used to authenticate the IMAP account.
@ -31,66 +23,94 @@ public class ImapAccount {
* to the IMAP server. * to the IMAP server.
*/ */
protected String user; protected String user;
/**
* Retrieves the username associated with this IMAP account.
*
* @return the username of the IMAP account as a String.
*/
public String getUser() { return user; }
/**
* Sets the username associated with the IMAP account.
*
* @param user The username to be set for this IMAP account.
*/
public void setUser(String user) { this.user = user; }
/** /**
* The password associated with the IMAP account. * The password associated with the IMAP account.
* This is used to authenticate the user when connecting to the IMAP server. * This is used to authenticate the user when connecting to the IMAP server.
*/ */
protected String password; protected String password;
/**
* Retrieves the password associated with this IMAP account.
*
* @return the password of the IMAP account.
*/
public String getPassword() { return password; }
/**
* Sets the password for the IMAP account.
*
* @param password The password to be set for the account.
*/
public void setPassword(String password) { this.password = password; }
/** /**
* Specifies the protocol used for the IMAP account, such as "IMAP" or "IMAPS". * Specifies the protocol used for the IMAP account, such as "IMAP" or "IMAPS".
* This field determines the communication protocol for interacting with the server. * This field determines the communication protocol for interacting with the server.
*/ */
protected String protocol; protected String protocol;
/**
* Default constructor only
*/
public ImapAccount() {
// default constructor only
}
/**
* Retrieves the server address associated with this IMAP
*/
public String getServer() {
return server;
}
/**
* Sets the server address for the IMAP account.
*
* @param server the server address to be configured for the IMAP account
*/
public void setServer(String server) {
this.server = server;
}
/**
* Retrieves the username associated with this IMAP account.
*
* @return the username of the IMAP account as a String.
*/
public String getUser() {
return user;
}
/**
* Sets the username associated with the IMAP account.
*
* @param user The username to be set for this IMAP account.
*/
public void setUser(String user) {
this.user = user;
}
/**
* Retrieves the password associated with this IMAP account.
*
* @return the password of the IMAP account.
*/
public String getPassword() {
return password;
}
/**
* Sets the password for the IMAP account.
*
* @param password The password to be set for the account.
*/
public void setPassword(String password) {
this.password = password;
}
/** /**
* Retrieves the protocol used by this IMAP account. * Retrieves the protocol used by this IMAP account.
* *
* @return the protocol as a String, which defines the communication method for the IMAP account. * @return the protocol as a String, which defines the communication method for the IMAP account.
*/ */
public String getProtocol() { return protocol; } public String getProtocol() {
/** return protocol;
* Sets the protocol used by the IMAP account. }
* @param protocol the protocol to be used, e.g., "IMAP" or "IMAPS".
*/
public void setProtocol(String protocol) { this.protocol = protocol; }
/** /**
* Provides a string representation of the ImapAccount object including its server, user, * Sets the protocol used by the IMAP account.
* a masked password, and protocol information. The password is represented as '########' if it is not empty.
* *
* @return A formatted string that describes the ImapAccount object with its server, user, masked password, and protocol. * @param protocol the protocol to be used, e.g., "IMAP" or "IMAPS".
*/ */
@Override public String toString() { public void setProtocol(String protocol) {
return String.format("ImapAccount(%s, %s, %s, %s", this.protocol = protocol;
server,
user,
password.length()==0 ? "''" : "########" ,
protocol);
} }
/** /**
@ -100,7 +120,8 @@ public class ImapAccount {
* *
* @return An integer hash code representing this ImapAccount instance. * @return An integer hash code representing this ImapAccount instance.
*/ */
@Override public int hashCode() { @Override
public int hashCode() {
return Objects.hash(server, user, password, protocol); return Objects.hash(server, user, password, protocol);
} }
@ -111,7 +132,8 @@ public class ImapAccount {
* @param obj the object to compare with this instance * @param obj the object to compare with this instance
* @return true if the specified object is equal to this ImapAccount instance; false otherwise * @return true if the specified object is equal to this ImapAccount instance; false otherwise
*/ */
@Override public boolean equals(Object obj) { @Override
public boolean equals(Object obj) {
// Check type of argument, includes check of null. // Check type of argument, includes check of null.
if (!(obj instanceof ImapAccount)) return false; if (!(obj instanceof ImapAccount)) return false;
@ -120,9 +142,24 @@ public class ImapAccount {
// Check the comparison of all field // Check the comparison of all field
ImapAccount other = (ImapAccount) obj; ImapAccount other = (ImapAccount) obj;
return (server == other.server || (server != null && server.equals(other.server))) && return Objects.equals(server, other.server) &&
(user == other.user || (user != null && user.equals(other.user))) && Objects.equals(user, other.user) &&
(password == other.password || (password != null && password.equals(other.password))) && Objects.equals(password, other.password) &&
(protocol == other.protocol || (protocol != null && protocol.equals(other.protocol))); Objects.equals(protocol, other.protocol);
}
/**
* Provides a string representation of the ImapAccount object including its server, user,
* a masked password, and protocol information. The password is represented as '########' if it is not empty.
*
* @return A formatted string that describes the ImapAccount object with its server, user, masked password, and protocol.
*/
@Override
public String toString() {
return String.format("ImapAccount(%s, %s, %s, %s",
server,
user,
(password == null || password.isEmpty()) ? "''" : "########",
protocol);
} }
} }

View File

@ -2,8 +2,18 @@ package de.neitzel.net.imap;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.mail.*; import javax.mail.Flags;
import java.util.*; import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Store;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/** /**
* The ImapAccountMonitor class is responsible for monitoring an IMAP email account * The ImapAccountMonitor class is responsible for monitoring an IMAP email account
@ -44,6 +54,13 @@ public class ImapAccountMonitor {
*/ */
protected Map<String, Folder> folders = new HashMap<String, Folder>(); protected Map<String, Folder> folders = new HashMap<String, Folder>();
/**
* A collection of listeners that respond to IMAP-related events.
* Each listener in this collection can perform specific actions when triggered
* by events such as receiving new email notifications.
*/
private Collection<ImapListener> imapListeners = new ArrayList<ImapListener>();
/** /**
* Monitors an IMAP account by establishing a connection to the specified mail server * Monitors an IMAP account by establishing a connection to the specified mail server
* using the provided account credentials and protocol. * using the provided account credentials and protocol.
@ -77,19 +94,19 @@ public class ImapAccountMonitor {
* Closes the resources associated with the current instance, including * Closes the resources associated with the current instance, including
* IMAP folders and the store. The method ensures that all folders are * IMAP folders and the store. The method ensures that all folders are
* closed properly, followed by the closure of the store. * closed properly, followed by the closure of the store.
* * <p>
* Any exceptions encountered during the closure of folders or the store * Any exceptions encountered during the closure of folders or the store
* are caught and logged without interrupting the overall closing process. * are caught and logged without interrupting the overall closing process.
* This is to ensure that all resources are attempted to be closed even if * This is to ensure that all resources are attempted to be closed even if
* an error occurs with one of them. * an error occurs with one of them.
* * <p>
* The method logs the start and end of the closing process for traceability. * The method logs the start and end of the closing process for traceability.
*/ */
public void close() { public void close() {
log.trace("close() called."); log.trace("close() called.");
// Close the folders // Close the folders
for (Folder folder: folders.values()) { for (Folder folder : folders.values()) {
try { try {
folder.close(false); folder.close(false);
} catch (MessagingException ex) { } catch (MessagingException ex) {
@ -152,28 +169,28 @@ public class ImapAccountMonitor {
* Checks all monitored folders for new email messages, processes unseen and undeleted messages, * Checks all monitored folders for new email messages, processes unseen and undeleted messages,
* and raises a NewMailEvent for each new email discovered. The method ensures processed messages * and raises a NewMailEvent for each new email discovered. The method ensures processed messages
* are marked as seen. * are marked as seen.
* * <p>
* The method iterates through all folders currently being monitored, fetching their messages and * The method iterates through all folders currently being monitored, fetching their messages and
* evaluating the state of each. If a message is neither marked as seen nor deleted, it is marked * evaluating the state of each. If a message is neither marked as seen nor deleted, it is marked
* as seen and a NewMailEvent is raised for it. * as seen and a NewMailEvent is raised for it.
* * <p>
* Proper error handling is implemented to log any MessagingException that occurs while processing * Proper error handling is implemented to log any MessagingException that occurs while processing
* the messages of a folder without interrupting the processing of other folders. * the messages of a folder without interrupting the processing of other folders.
* * <p>
* Note: This method relies on external logging (via `log`) and appropriate event handling * Note: This method relies on external logging (via `log`) and appropriate event handling
* via `raiseNewEmailEvent`. The `folders` collection holds the monitored folders required. * via `raiseNewEmailEvent`. The `folders` collection holds the monitored folders required.
* * <p>
* The folder processing stops only in case of irrecoverable exceptions outside this method's scope. * The folder processing stops only in case of irrecoverable exceptions outside this method's scope.
*/ */
public void check() { public void check() {
log.trace("check() called."); log.trace("check() called.");
// Loop through all folders that are monitored // Loop through all folders that are monitored
for (Folder folder: folders.values()) { for (Folder folder : folders.values()) {
try { try {
// Loop through all messages. // Loop through all messages.
Message[] messages = folder.getMessages(); Message[] messages = folder.getMessages();
for (Message message: messages) { for (Message message : messages) {
// Check if message wasn't seen and wasn't deleted // Check if message wasn't seen and wasn't deleted
if (!message.isSet(Flags.Flag.SEEN) && !message.isSet(Flags.Flag.DELETED)) { if (!message.isSet(Flags.Flag.SEEN) && !message.isSet(Flags.Flag.DELETED)) {
// Mark message as seen. // Mark message as seen.
@ -193,11 +210,35 @@ public class ImapAccountMonitor {
} }
/** /**
* A collection of listeners that respond to IMAP-related events. * Raises a new email event and notifies all registered listeners.
* Each listener in this collection can perform specific actions when triggered * The method loops through the listeners and invokes their {@code NewEmailReceived}
* by events such as receiving new email notifications. * method with a newly created {@link NewEmailEvent}.
* If a listener marks the event as handled, the remaining listeners will
* not be notified.
*
* @param message The email message that triggered the event. This message
* is included in the {@link NewEmailEvent} passed to the listeners.
*/ */
private Collection<ImapListener> imapListeners = new ArrayList<ImapListener>(); protected void raiseNewEmailEvent(Message message) {
log.trace("raiseNewEmailEvent() called!");
// Create the event.
NewEmailEvent event = new NewEmailEvent(this, message);
// Call all listeners.
for (ImapListener listener : imapListeners) {
log.trace("Calling listener in {} ....", listener.getClass().getName());
listener.newEmailReceived(event);
// Check if the event was handled so no further ImaListeners will be called.
if (event.isHandled()) {
log.trace("raiseNewEmailEvent(): Breaking out of listener loop because event was handled.");
break;
}
}
log.trace("raiseNewEmailEvent() call ended!");
}
/** /**
* Adds an IMAP listener to receive notifications about IMAP-related events such as new email arrivals. * Adds an IMAP listener to receive notifications about IMAP-related events such as new email arrivals.
@ -222,35 +263,4 @@ public class ImapAccountMonitor {
imapListeners.remove(listener); imapListeners.remove(listener);
} }
} }
/**
* Raises a new email event and notifies all registered listeners.
* The method loops through the listeners and invokes their {@code NewEmailReceived}
* method with a newly created {@link NewEmailEvent}.
* If a listener marks the event as handled, the remaining listeners will
* not be notified.
*
* @param message The email message that triggered the event. This message
* is included in the {@link NewEmailEvent} passed to the listeners.
*/
protected void raiseNewEmailEvent(Message message) {
log.trace("raiseNewEmailEvent() called!");
// Create the event.
NewEmailEvent event = new NewEmailEvent(this, message);
// Call all listeners.
for (ImapListener listener: imapListeners) {
log.trace("Calling listener in {} ....", listener.getClass().getName());
listener.newEmailReceived(event);
// Check if the event was handled so no further ImaListeners will be called.
if (event.isHandled()) {
log.trace("raiseNewEmailEvent(): Breaking out of listener loop because event was handled.");
break;
}
}
log.trace("raiseNewEmailEvent() call ended!");
}
} }

View File

@ -8,15 +8,49 @@ import javax.mail.internet.MimeMultipart;
import java.io.IOException; import java.io.IOException;
/** /**
* Utility functions to use with javax.mail.Message instances. * Utility methods for extracting textual content from {@link javax.mail.Message} instances.
*
* <p>This class provides helpers to obtain a best-effort plain-text representation of an email's
* content. It supports the common MIME types:
* <ul>
* <li>{@code text/plain} returned as-is</li>
* <li>{@code text/html} the HTML body is returned as a string (not converted to plain text)</li>
* <li>{@code multipart/*} parts are inspected recursively; for {@code multipart/alternative}
* the last (most faithful) alternative is preferred; for other multipart types the textual
* content of all parts is concatenated in order.</li>
* </ul>
*
* <p>All methods return an empty string when no textual content can be found. The methods may throw
* {@link IOException} or {@link MessagingException} when the underlying message parts cannot be read.
*/ */
@SuppressWarnings("unused")
public class MessageUtils { public class MessageUtils {
/** /**
* Gets the text of the email message. * Private constructor for utility class.
* @param message Message to get the text from. * Prevents instantiation of this utility class.
* @return Text of the email message. */
* @throws IOException private MessageUtils() {
* @throws MessagingException // Utility class - private constructor
}
/**
* Extracts a best-effort textual representation from the provided {@link Message}.
*
* <p>Behavior:
* <ul>
* <li>If the message mime-type is {@code text/plain} the plain text content is returned.</li>
* <li>If the message mime-type is {@code text/html} the HTML content is returned as a string.
* (This method does not perform HTML-to-plain-text conversion.)</li>
* <li>If the message mime-type is {@code multipart/*} the method inspects all parts recursively.
* For {@code multipart/alternative} the most faithful alternative (the last part) is chosen.
* For other multipart types the textual contents of all parts are concatenated in order.</li>
* </ul>
*
* @param message the email message from which to extract text; must not be {@code null}
* @return a <strong>non-null</strong> string containing the extracted text (possibly empty)
* @throws IOException if an I/O error occurs while reading message content
* @throws MessagingException if the message content cannot be processed due to MIME/format errors
*/ */
public static String getTextFromMessage(Message message) throws IOException, MessagingException { public static String getTextFromMessage(Message message) throws IOException, MessagingException {
String result = ""; String result = "";
@ -30,11 +64,23 @@ public class MessageUtils {
} }
/** /**
* Gets the text of a MimeMultipart. * Extracts the textual content from a {@link MimeMultipart} instance.
* @param mimeMultipart MimeMultipart to get the text from. *
* @return Text of the MimeMultipart instance. * <p>Algorithm:
* @throws IOException * <ul>
* @throws MessagingException * <li>If the multipart has zero parts a {@link MessagingException} is thrown.</li>
* <li>If the multipart's content type matches {@code multipart/alternative} the last part
* (the most faithful alternative) is returned.</li>
* <li>Otherwise the method concatenates the result of {@link #getTextFromBodyPart(BodyPart)}
* for each part in document order and returns the combined text.</li>
* </ul>
*
* <p>The method handles nested multiparts by recursive calls.
*
* @param mimeMultipart the multipart object to inspect
* @return a non-null string containing the concatenated textual content of the multipart
* @throws IOException if an I/O error occurs while reading parts
* @throws MessagingException if the multipart is malformed or cannot be processed
*/ */
private static String getTextFromMimeMultipart( private static String getTextFromMimeMultipart(
MimeMultipart mimeMultipart) throws IOException, MessagingException { MimeMultipart mimeMultipart) throws IOException, MessagingException {
@ -47,20 +93,29 @@ public class MessageUtils {
// alternatives appear in an order of increasing // alternatives appear in an order of increasing
// faithfulness to the original content. Customize as req'd. // faithfulness to the original content. Customize as req'd.
return getTextFromBodyPart(mimeMultipart.getBodyPart(count - 1)); return getTextFromBodyPart(mimeMultipart.getBodyPart(count - 1));
String result = ""; StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
BodyPart bodyPart = mimeMultipart.getBodyPart(i); BodyPart bodyPart = mimeMultipart.getBodyPart(i);
result += getTextFromBodyPart(bodyPart); result.append(getTextFromBodyPart(bodyPart));
} }
return result; return result.toString();
} }
/** /**
* Gets the text of a BodyPart * Extracts textual content from a single {@link BodyPart}.
* @param bodyPart BodyPart to get text from. *
* @return Text of body part or empty string if not a known type. * <p>Behavior:
* @throws IOException * <ul>
* @throws MessagingException * <li>{@code text/plain}: returns the plain text content.</li>
* <li>{@code text/html}: returns the HTML content as a string (no conversion performed).</li>
* <li>If the body part itself contains a {@link MimeMultipart} this method recurses into it.</li>
* <li>For unknown or non-textual content types an empty string is returned.</li>
* </ul>
*
* @param bodyPart the body part to extract text from
* @return the textual content of the body part, or an empty string if none is available
* @throws IOException if an I/O error occurs while reading the body part content
* @throws MessagingException if the body part cannot be processed
*/ */
private static String getTextFromBodyPart( private static String getTextFromBodyPart(
BodyPart bodyPart) throws IOException, MessagingException { BodyPart bodyPart) throws IOException, MessagingException {
@ -73,8 +128,8 @@ public class MessageUtils {
//String html = (String) bodyPart.getContent(); //String html = (String) bodyPart.getContent();
// Not parsing the html right now! // Not parsing the html right now!
// result = org.jsoup.Jsoup.parse(html).text(); // result = org.jsoup.Jsoup.parse(html).text();
} else if (bodyPart.getContent() instanceof MimeMultipart){ } else if (bodyPart.getContent() instanceof MimeMultipart) {
result = getTextFromMimeMultipart((MimeMultipart)bodyPart.getContent()); result = getTextFromMimeMultipart((MimeMultipart) bodyPart.getContent());
} }
return result; return result;
} }

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ruleset name="Custom Java Ruleset" <ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
name="Custom Java Ruleset"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd"> xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
<description> <description>
@ -11,27 +11,27 @@
<rule ref="category/java/bestpractices.xml"> <rule ref="category/java/bestpractices.xml">
<!-- System.println is ok in simple Console App --> <!-- System.println is ok in simple Console App -->
<exclude name="SystemPrintln" /> <exclude name="SystemPrintln"/>
</rule> </rule>
<rule ref="category/java/codestyle.xml" /> <rule ref="category/java/codestyle.xml"/>
<rule ref="category/java/design.xml" /> <rule ref="category/java/design.xml"/>
<rule ref="category/java/design.xml/LoosePackageCoupling"> <rule ref="category/java/design.xml/LoosePackageCoupling">
<properties> <properties>
<property name="packages" value="de.kneitzel" /> <property name="packages" value="de.kneitzel"/>
<!-- <property name="classes" value="" /> --> <!-- <property name="classes" value="" /> -->
</properties> </properties>
</rule> </rule>
<rule ref="category/java/documentation.xml" /> <rule ref="category/java/documentation.xml"/>
<rule ref="category/java/errorprone.xml" /> <rule ref="category/java/errorprone.xml"/>
<rule ref="category/java/multithreading.xml" /> <rule ref="category/java/multithreading.xml"/>
<rule ref="category/java/performance.xml" /> <rule ref="category/java/performance.xml"/>
<rule ref="category/java/security.xml" /> <rule ref="category/java/security.xml"/>
</ruleset> </ruleset>

43
pom.xml
View File

@ -13,6 +13,7 @@
<modules> <modules>
<module>core</module> <module>core</module>
<module>encryption</module> <module>encryption</module>
<module>injection</module>
<module>fx</module> <module>fx</module>
<module>gson</module> <module>gson</module>
<module>net</module> <module>net</module>
@ -22,38 +23,39 @@
</modules> </modules>
<properties> <properties>
<java.version>21</java.version> <java.version>25</java.version>
<required.maven.version>3.8.6</required.maven.version> <required.maven.version>3.8.6</required.maven.version>
<!-- Dependency versions --> <!-- Dependency versions -->
<javafx.version>21.0.3</javafx.version> <jakarta-inject-api>2.0.1</jakarta-inject-api>
<javafx.version>25.0.1</javafx.version>
<jetbrains.annotations.version>26.0.2</jetbrains.annotations.version> <jetbrains.annotations.version>26.0.2</jetbrains.annotations.version>
<junit.version>5.12.1</junit.version> <junit.version>6.0.1</junit.version>
<lombok.version>1.18.38</lombok.version> <lombok.version>1.18.42</lombok.version>
<mockito.version>5.16.1</mockito.version> <mockito.version>5.20.0</mockito.version>
<reflections.version>0.10.2</reflections.version> <reflections.version>0.10.2</reflections.version>
<slf4j.version>2.0.17</slf4j.version> <slf4j.version>2.0.17</slf4j.version>
<!-- Plugin dependencies --> <!-- Plugin dependencies -->
<codehaus.version.plugin>2.16.2</codehaus.version.plugin> <codehaus.version.plugin>2.20.1</codehaus.version.plugin>
<javafx.maven.plugin>0.0.8</javafx.maven.plugin> <javafx.maven.plugin>0.0.8</javafx.maven.plugin>
<jpackage.maven.plugin>0.1.5</jpackage.maven.plugin> <jpackage.maven.plugin>0.1.5</jpackage.maven.plugin>
<maven.clean.plugin>3.3.2</maven.clean.plugin> <maven.clean.plugin>3.5.0</maven.clean.plugin>
<maven.compiler.plugin>3.13.0</maven.compiler.plugin> <maven.compiler.plugin>3.14.1</maven.compiler.plugin>
<maven.dependency.plugin>3.6.1</maven.dependency.plugin> <maven.dependency.plugin>3.6.1</maven.dependency.plugin>
<maven.deploy.plugin>3.1.2</maven.deploy.plugin> <maven.deploy.plugin>3.1.4</maven.deploy.plugin>
<maven.enforcer.plugin>3.4.1</maven.enforcer.plugin> <maven.enforcer.plugin>3.4.1</maven.enforcer.plugin>
<maven.install.plugin>3.1.2</maven.install.plugin> <maven.install.plugin>3.1.2</maven.install.plugin>
<maven.jar.plugin>3.4.1</maven.jar.plugin> <maven.jar.plugin>3.5.0</maven.jar.plugin>
<maven.javadoc.plugin>3.6.3</maven.javadoc.plugin> <maven.javadoc.plugin>3.12.0</maven.javadoc.plugin>
<maven.project.info.reports.plugin>3.5.0</maven.project.info.reports.plugin> <maven.project.info.reports.plugin>3.9.0</maven.project.info.reports.plugin>
<maven.resources.plugin>3.3.1</maven.resources.plugin> <maven.resources.plugin>3.4.0</maven.resources.plugin>
<maven.shade.plugin>3.5.3</maven.shade.plugin> <maven.shade.plugin>3.5.3</maven.shade.plugin>
<maven.site.plugin>4.0.0-M14</maven.site.plugin> <maven.site.plugin>4.0.0-M16</maven.site.plugin>
<maven.surfire.plugin>3.2.5</maven.surfire.plugin> <maven.surfire.plugin>3.5.4</maven.surfire.plugin>
<moditect.maven.plugin>1.0.0.RC2</moditect.maven.plugin> <moditect.maven.plugin>1.0.0.RC2</moditect.maven.plugin>
<maven.pmd.plugin>3.22.0</maven.pmd.plugin> <maven.pmd.plugin>3.28.0</maven.pmd.plugin>
<spotbugs.maven.plugin>4.8.5.0</spotbugs.maven.plugin> <spotbugs.maven.plugin>4.9.8.2</spotbugs.maven.plugin>
<!-- other properties --> <!-- other properties -->
<pmd.target>pmd</pmd.target> <pmd.target>pmd</pmd.target>
@ -274,6 +276,9 @@
<goals> <goals>
<goal>jar</goal> <goal>jar</goal>
</goals> </goals>
<configuration>
<sourcepath>${project.build.sourceDirectory}</sourcepath>
</configuration>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
@ -347,15 +352,19 @@
<report>javadoc</report> <report>javadoc</report>
</reports> </reports>
</reportSet> </reportSet>
<!--
<reportSet> <reportSet>
<id>tests</id> <id>tests</id>
<configuration> <configuration>
<show>private</show> <show>private</show>
<failOnError>false</failOnError>
<quiet>true</quiet>
</configuration> </configuration>
<reports> <reports>
<report>test-javadoc</report> <report>test-javadoc</report>
</reports> </reports>
</reportSet> </reportSet>
-->
</reportSets> </reportSets>
</plugin> </plugin>
<plugin> <plugin>