feature/github-copilot-consolidation #1

Merged
konrad merged 10 commits from feature/github-copilot-consolidation into main 2025-12-14 13:29:43 +01:00
64 changed files with 4856 additions and 4253 deletions
Showing only changes of commit 5503dd192e - Show all commits

View File

@ -2,24 +2,32 @@
## 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.
---
## 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
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
@ -47,7 +55,8 @@ TypeAdapter for gson.
### 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

View File

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

View File

@ -6,10 +6,10 @@ import java.util.Iterator;
* The ArgumentProvider class is a helper utility for iterating over an array
* of command-line arguments. It provides methods to check for available arguments
* and retrieve them in sequence.
*
* This class is designed to work seamlessly with a command-line parser
* and handle tokenized inputs efficiently. It implements the Iterator interface
* for ease of iteration.
* <p>
* This class is designed to work seamlessly with a command-line parser and handle
* tokenized inputs efficiently. It implements the {@link Iterator} interface for ease
* of iteration and provides convenience methods for peeking ahead.
*/
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}
* class to iterate through and retrieve arguments systematically.
*/
private String[] arguments;
private final String[] arguments;
/**
* Tracks the current position within an array of arguments.
*
* This variable is used to index into the arguments array, enabling
* sequential access to command-line tokens. It is incremented as arguments
* are processed or retrieved using methods such as {@code next()} or {@code peek()}
* in the {@link ArgumentProvider} class.
*
* Its value starts at 0 and increases until it reaches the length of
* the provided arguments array, at which point iteration ends.
* This variable is used to index into the arguments array, enabling sequential access
* to command-line tokens. It is incremented as arguments are processed or retrieved
* using methods such as {@code next()} or {@code peek()} in the {@link ArgumentProvider}
* class. 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;
@ -43,21 +40,12 @@ public class ArgumentProvider implements Iterator<String> {
*/
public ArgumentProvider(final String[] arguments) {
if (arguments == null) {
this.arguments = new String[] {};
this.arguments = new String[]{};
} else {
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.
*
@ -68,6 +56,26 @@ public class ArgumentProvider implements Iterator<String> {
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.
* If no more arguments are available, returns {@code null}.
@ -81,15 +89,4 @@ public class ArgumentProvider implements Iterator<String> {
current++;
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,
* such as constraints on the number of values it accepts, descriptions, and aliases.
* 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
* structured handling of arguments provided via the command line.
*/
@ -23,14 +23,141 @@ import java.util.function.Consumer;
@Setter
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.
*
* <p>
* This class encapsulates the metadata and configuration of a command-line parameter,
* 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
* recognizes and processes.
*
* <p>
* 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
* command provides usage information for users.
@ -41,16 +168,16 @@ public class Parameter {
/**
* Constructs a new Parameter with the given attributes.
*
* @param name The name of the parameter.
* @param minNumberValues The minimum number of values allowed for this parameter.
* @param maxNumberValues The maximum number of values allowed for this parameter.
* @param multipleEntries Indicates whether multiple entries are allowed for this parameter.
* @param shortDescription A brief description of the parameter.
* @param longDescription A detailed description of the parameter.
* @param callback A consumer function that processes a list of values associated with this parameter.
* @param isHelpCommand Indicates whether this parameter represents a help command.
* @param name The name of the parameter.
* @param minNumberValues The minimum number of values allowed for this parameter.
* @param maxNumberValues The maximum number of values allowed for this parameter.
* @param multipleEntries Indicates whether multiple entries are allowed for this parameter.
* @param shortDescription A brief description of the parameter.
* @param longDescription A detailed description of the parameter.
* @param callback A consumer function that processes a list of values associated with this parameter.
* @param isHelpCommand Indicates whether this parameter represents a help command.
* @param isDefaultParameter Indicates whether this parameter is the default parameter.
* @param aliasList A list of aliases for this parameter.
* @param aliasList A list of aliases for this parameter.
*/
public Parameter(String name, int minNumberValues, int maxNumberValues, boolean multipleEntries, String shortDescription, String longDescription, Consumer<List<String>> callback, boolean isHelpCommand, boolean isDefaultParameter, List<String> aliasList) {
this.name = name;
@ -64,131 +191,4 @@ public class Parameter {
this.isDefaultParameter = isDefaultParameter;
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 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.
@ -15,32 +19,32 @@ public class Parser {
/**
* A map to store parameters where the key is a string representing
* the parameter name and the value is the corresponding {@link Parameter} object.
*
* <p>
* This map serves as a registry for defining, organizing, and accessing
* command-line parameters. Each entry holds a parameter's metadata and
* its associated configuration to facilitate effective command-line parsing.
*
* <p>
* Key considerations:
* - The key represents the primary name of the parameter.
* - The value, encapsulated as a {@link Parameter} object, includes
* details such as descriptions, value constraints, aliases, and processing logic.
* details such as descriptions, value constraints, aliases, and processing logic.
*/
private Map<String, Parameter> parameters = new HashMap<>();
/**
* 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
* 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.
*
* <p>
* It is essential to note:
* - 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`
* should not disallow input).
* should not disallow input).
* - 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
* 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.
*
* <p>
* The `Parser` class is responsible for parsing and processing
* command-line arguments. It interprets input data based on defined
* parameters and provides structured access to arguments, enabling
* streamlined application configuration and execution.
*
* <p>
* This constructor initializes the parser without any predefined
* configuration or parameters. Users can define parameters,
* 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.
* The parameter must fulfill specific constraints if defined as the default
* parameter (e.g., accepting values).
*
* @throws IllegalArgumentException if the parameter is defined as a default parameter and
* does not accept values (both minNumberValues and maxNumberValues
* are set to 0).
@ -81,7 +84,7 @@ public class Parser {
parameters.put(parameter.getName().toLowerCase(), parameter);
// Add for all aliases
for (String alias: parameter.getAliasList()) {
for (String alias : parameter.getAliasList()) {
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
* 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 ("-").
* @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.
*/
protected boolean isParameter(final String param) {
if (parameters.containsKey(param)) return true;
if (param.startsWith("-")) return false;
return defaultParameter != null;
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());
}
}
}
/**
* 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 registered parameters, and invokes the corresponding processing callback if applicable.
* 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}
* within the range specified by the minimum and maximum values.
@ -163,8 +197,8 @@ public class Parser {
* than `min`, it will be adjusted to match `min`. Only up to `max`
* arguments will be added to the result.
* @return A list of argument strings, containing up to `max` elements and at least `min`
* elements if sufficient arguments are available. The list will not include
* arguments already recognized in the `parameters` map.
* elements if sufficient arguments are available. The list will not include
* arguments already recognized in the `parameters` map.
*/
private List<String> getOptions(ArgumentProvider provider, int min, int max) {
if (max < min) max = min;
@ -182,35 +216,4 @@ public class Parser {
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

@ -17,6 +17,19 @@ import java.io.InputStream;
@Slf4j
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.
*/
@ -24,59 +37,17 @@ public class ImageScaler {
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.
*
* <p>
* 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 true, the resulting image will match the exact dimensions, potentially with transparent borders to keep the original aspect ratio.
*
* @param originalImageBytes The byte array representing the original image.
* @param width The (maximum) width of the target scaled image.
* @param height The (maximum) height of the target scaled image.
* @param enforceSize A flag indicating whether to enforce the exact scaled dimensions. If false, the resulting image retains its aspect ratio.
* @param width The (maximum) width of the target scaled image.
* @param height The (maximum) height of the target scaled image.
* @param enforceSize A flag indicating whether to enforce the exact scaled dimensions. If false, the resulting image retains its aspect ratio.
* @return An InputStream representing the scaled image, or null if the operation fails or the input image is invalid.
*/
public static InputStream scaledImage(final byte[] originalImageBytes, final int width, final int height, final boolean enforceSize) {
@ -91,20 +62,49 @@ 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:
* - 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
* dimensions.
* will maintain its original aspect ratio. The resulting image may be smaller than the specified
* dimensions.
* - If enforceSize is true, the resulting image will match the exact specified dimensions, potentially
* adding transparent padding to fit the aspect ratio.
* adding transparent padding to fit the aspect ratio.
*
* @param originalImage The original image to be scaled. Must not be null.
* @param width The specified (maximum) width of the scaled image.
* @param height The specified (maximum) height of the scaled image.
* @param enforceSize A flag indicating whether the scaled image should strictly conform to the specified
* dimensions. If true, the image may include transparent areas to maintain the aspect ratio.
* @param width The specified (maximum) width of the scaled image.
* @param height The specified (maximum) height of the scaled image.
* @param enforceSize A flag indicating whether the scaled image should strictly conform to the specified
* dimensions. If true, the image may include transparent areas to maintain the aspect ratio.
* @return A byte array representing the scaled image, or null if the scaling operation fails or the input image is invalid.
*/
protected static byte[] createScaledImage(final BufferedImage originalImage, final int width, final int height, final boolean enforceSize) {

View File

@ -10,7 +10,7 @@ import java.util.stream.Stream;
/**
* Represents the context of the application and serves as the foundation of a dependency
* injection framework.
*
* <p>
* The {@code ApplicationContext} is responsible for scanning, instantiating, and managing
* the lifecycle of components. Components are identified through the specified base package
* (or derived from a configuration class) and instantiated based on their dependency
@ -52,17 +52,17 @@ public class ApplicationContext {
/**
* 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
* a singleton instance. The component's type and scope are determined by the framework,
* and the appropriate initialization and lifecycle management are performed.
*
* @param <T> the type of the component to retrieve
* @param <T> the type of the component to retrieve
* @param type the {@code Class} object representing the type of the component
* @return an instance of the requested component type
* @throws IllegalArgumentException if no component is found for the specified type
* @throws IllegalStateException if no suitable constructor is found for the component
* @throws RuntimeException if any error occurs during instantiation
* @throws IllegalStateException if no suitable constructor is found for the component
* @throws RuntimeException if any error occurs during instantiation
*/
@SuppressWarnings("unchecked")
public <T> T getComponent(Class<? extends T> type) {
@ -85,7 +85,7 @@ public class ApplicationContext {
Class<?>[] paramTypes = constructor.getParameterTypes();
Object[] parameters = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
parameters[i] = getComponent(paramTypes[i]);
parameters[i] = getComponent(paramTypes[i]);
}
instance = constructor.newInstance(parameters);
if (scope == Scope.SINGLETON) {
@ -103,12 +103,12 @@ public class ApplicationContext {
/**
* 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
* is available in the given parameter map. If all parameter types are present,
* the constructor is considered instantiable.
*
* @param constructor the constructor to check for instantiation feasibility
* @param constructor the constructor to check for instantiation feasibility
* @param parameterMap a map containing available parameter types and their associated component data
* @return true if all parameter types required by the constructor are contained in the parameter map, false otherwise
*/
@ -118,14 +118,14 @@ public class ApplicationContext {
/**
* 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
* 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}
* is thrown to prevent replacement of an existing singleton.
*
* @param <T> the type of the component to register
* @param type the class type of the component being added
* @param <T> the type of the component to register
* @param type the class type of the component being added
* @param instance the singleton instance of the component to register
* @throws IllegalStateException if the type is already registered with a different instance
*/

View File

@ -4,7 +4,7 @@ import lombok.Getter;
/**
* Represents a component in a dependency injection framework.
*
* <p>
* A component is a unit of functionality that is managed by the framework, allowing
* for controlled instantiation and lifecycle management. The component is associated
* 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.
*
* <p>
* 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.
* 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.
*
* <p>
* 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).
*
* <p>
* This variable is immutable and represents the specific {@code Scope} assigned
* 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.
*
* <p>
* 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
* set only once and shared across the entire application. For components with a
@ -47,7 +47,7 @@ public class ComponentData {
/**
* Constructs a new Component instance with the specified type and scope.
*
* @param type the class type of the component
* @param type the class type of the component
* @param scope the scope of the component, which determines its lifecycle
*/
public ComponentData(Class<?> type, Scope scope) {
@ -57,10 +57,10 @@ public class ComponentData {
/**
* 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.
*
* @param type the class type of the component
* @param type the class type of the component
* @param instance the initial instance of the component
*/
public ComponentData(Class<?> type, Object instance) {

View File

@ -5,7 +5,13 @@ import de.neitzel.core.inject.annotation.Inject;
import org.reflections.Reflections;
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;
/**
@ -21,13 +27,13 @@ public class ComponentScanner {
* FXML-compatible components detected during the scanning process.
* These components are used as part of the dependency injection mechanism
* within the {@code InjectableComponentScanner}.
*
* <p>
* The {@code fxmlComponents} set is populated during the component scanning
* phase by identifying all classes within a specified package hierarchy
* annotated with the {@code @Component} annotation. These classes serve
* as the primary source for further analysis, resolution, and instantiation
* in the scanning workflow.
*
* <p>
* This field is immutable, ensuring thread safety and consistent state
* 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.
*
* <p>
* This collection is populated during the analysis of discovered components to identify
* types that have multiple implementations, preventing them from being uniquely resolved.
* For example, if multiple components implement the same interface, that interface will be
* added to this set.
*
* <p>
* It is primarily used during the component registration process to avoid ambiguities
* in the dependency injection system, ensuring that only resolvable, uniquely identifiable
* 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
* is the specific implementation or subclass that is uniquely identified among
* the scanned components.
*
* <p>
* 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
* 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
* a non-unique type.
*
* <p>
* This mapping is utilized for resolving dependencies and determining which components
* 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
* of available component types, ensuring that only components with resolvable
* dependencies are included.
*
* <p>
* This field is part of the dependency injection process, where components annotated
* with {@code @Component} or similar annotations are scanned, analyzed, and registered.
* 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
* of components in the dependency injection process.
*
* <p>
* This list is populated when components cannot be resolved due to issues
* such as multiple implementations of a superclass or interface,
* unmet construction requirements, or unresolved dependencies.
*
* <p>
* Errors added to this list provide detailed descriptions of the specific
* 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
* inheritance hierarchy and relationships.
*
* <p>
* This method performs the following operations:
* 1. Maps each component to all of its superclasses and interfaces.
* 2. Identifies which superclasses or interfaces have multiple implementing components.
* 3. Populates:
* - 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 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.
* <p>
* 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 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.
*
* <p>
* This method is a key part of the component scanning and resolution process, facilitating
* the identification of potential instantiation ambiguities or conflicts.
*/
@ -160,22 +166,22 @@ public class ComponentScanner {
* Resolves and identifies instantiable components from a set of scanned components.
* This process determines which components can be instantiated based on their dependencies
* and class relationships, while tracking unresolved types and potential conflicts.
*
* <p>
* The resolution process involves:
* 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
* set of known types.
* map. A component is resolvable if its dependencies can be satisfied by the current
* set of known types.
* 2. Registering resolvable components and their superclasses/interfaces into the known
* types map for future iterations.
* types map for future iterations.
* 3. Removing successfully resolved components from the unresolved set.
* 4. Repeating the process until no further components can be resolved in a given iteration.
*
* <p>
* At the end of the resolution process:
* - 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
* 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.
*/
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.
* 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.
* Retrieves all superclasses and interfaces of the specified class, excluding the {@code Object} class.
*
* @param component the {@code ComponentData} instance representing the component to be registered
* @param knownTypes the map where component types and their data are stored
* @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 void registerComponentWithSuperTypes(ComponentData component, Map<Class<?>, ComponentData> knownTypes) {
knownTypes.put(component.getType(), component);
private Set<Class<?>> getAllSuperTypes(Class<?> clazz) {
Set<Class<?>> result = new HashSet<>();
Class<?> superClass = clazz.getSuperclass();
for (Class<?> superType : getAllSuperTypes(component.getType())) {
if (!notUniqueTypes.contains(superType)) {
knownTypes.put(superType, component);
}
while (superClass != null && superClass != Object.class) {
result.add(superClass);
superClass = superClass.getSuperclass();
}
result.addAll(Arrays.asList(clazz.getInterfaces()));
return result;
}
/**
@ -232,7 +239,7 @@ public class ComponentScanner {
* Additionally, all fields annotated with {@code @Inject} must have their types present in the
* known types set.
*
* @param component the class to check for instantiability
* @param component the class to check for instantiability
* @param knownTypes the set of currently known types that can be used to satisfy dependencies
* @return {@code true} if the component can be instantiated; {@code false} otherwise
*/
@ -260,6 +267,24 @@ public class ComponentScanner {
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
* detailed error messages to the internal error list. This method analyzes unresolved
@ -304,7 +329,7 @@ public class ComponentScanner {
*
* @param component the class for which conflicting types need to be identified.
* @return a comma-separated string of fully qualified names of conflicting parameter types,
* or an empty string if no conflicts are found.
* or an empty string if no conflicts are found.
*/
private String getConflictingTypes(Class<?> component) {
return Arrays.stream(component.getConstructors())
@ -314,30 +339,11 @@ public class ComponentScanner {
.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.
*
* @return A map where the key is a class type and the value is the corresponding class implementation
* that can be instantiated.
* that can be instantiated.
*/
public Map<Class<?>, ComponentData> getInstantiableComponents() {
return instantiableComponents;
@ -349,7 +355,7 @@ public class ComponentScanner {
* component scanning and analysis process.
*
* @return a set of classes representing the types that have multiple
* implementations and cannot be resolved uniquely
* implementations and cannot be resolved uniquely
*/
public Set<Class<?>> getNotUniqueTypes() {
return notUniqueTypes;
@ -361,7 +367,7 @@ public class ComponentScanner {
* implementation, ensuring no ambiguity in resolution.
*
* @return a map where the keys are types (e.g., interfaces or superclasses)
* and the values are their unique component implementations.
* and the values are their unique component implementations.
*/
public Map<Class<?>, Class<?>> getUniqueTypeToComponent() {
return uniqueTypeToComponent;

View File

@ -11,14 +11,14 @@ import java.lang.annotation.Target;
* 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
* as candidates for instantiation and management by the dependency injection framework.
*
* <p>
* This annotation is typically used on classes that represent application-specific components,
* such as service implementations, controllers, or other objects intended to be instantiated
* and injected into other components during runtime.
*
* <p>
* 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.
*
* <p>
* Usage of this annotation assumes that there exists a component scanning mechanism
* that processes annotated classes and identifies their roles, dependencies, and hierarchy
* within the application's structure.

View File

@ -10,17 +10,17 @@ import java.lang.annotation.Target;
* injection framework. Classes annotated with {@code @Config} are used as markers
* for defining settings and application-specific configurations required by the
* dependency injection mechanism.
*
* <p>
* Typically, configuration classes provide metadata required for setting up the
* 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
* facilitate reflection-based processing. It is intended to serve as a declarative
* representation of configuration options for the dependency injection container.
*
* <p>
* Attributes:
* - {@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}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ -31,7 +31,7 @@ public @interface Config {
* can be scanned and identified as candidates for dependency injection.
*
* @return the base package name as a string; returns an empty string by default
* if no specific package is defined.
* if no specific package is defined.
*/
String basePackage() default "";
}

View File

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

View File

@ -2,7 +2,11 @@ package de.neitzel.core.io;
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.IllegalCharsetNameException;

View File

@ -3,7 +3,15 @@ package de.neitzel.core.io;
import de.neitzel.core.util.ArrayUtils;
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.file.Files;
import java.nio.file.Path;

View File

@ -13,7 +13,7 @@ public class StringEncoder {
* static utility methods for array-related operations.
*
* @throws UnsupportedOperationException always, to indicate that this class
* should not be instantiated.
* should not be instantiated.
*/
private StringEncoder() {
throw new UnsupportedOperationException("Utility class");
@ -27,7 +27,7 @@ public class StringEncoder {
* @param data the string to decode; may contain encoded characters or regular text. If null, an empty string is returned.
* @return the decoded string with all encoded characters replaced by their original representations.
* @throws IllegalArgumentException if the input string has encoding markup
* that is improperly formatted or incomplete.
* that is improperly formatted or incomplete.
*/
public static String decodeData(final String data) {
if (data == null) return "";
@ -38,17 +38,17 @@ public class StringEncoder {
int indexAmp = remaining.indexOf("&");
if (indexAmp == -1) {
result.append(remaining);
remaining="";
remaining = "";
} else {
// First get the elements till the &
if (indexAmp > 0) {
result.append(remaining.substring(0, 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 + ")");
String special = remaining.substring(0, endSpecial+1);
remaining = remaining.substring(endSpecial+1);
String special = remaining.substring(0, endSpecial + 1);
remaining = remaining.substring(endSpecial + 1);
result.append(decodeCharacter(special));
}
}
@ -67,7 +67,7 @@ public class StringEncoder {
public static char decodeCharacter(final String 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 + ")");
return (char)Integer.parseInt(data.substring(2, data.length()-1));
return (char) Integer.parseInt(data.substring(2, data.length() - 1));
}
/**
@ -77,16 +77,16 @@ public class StringEncoder {
*
* @param data the input string to encode; if null, an empty string is returned
* @return an encoded string where non-ASCII and ampersand characters
* are replaced with numeric character references
* are replaced with numeric character references
*/
public static String encodeData(final String data) {
if (data == null) return "";
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);
if (character < 32 || character > 127 || character == 38) {
result.append("&#" + (int)character + ";");
result.append("&#" + (int) character + ";");
} else {
result.append(character);
}

View File

@ -16,16 +16,30 @@ public class ToneGenerator {
* static utility methods for array-related operations.
*
* @throws UnsupportedOperationException always, to indicate that this class
* should not be instantiated.
* should not be instantiated.
*/
private ToneGenerator() {
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.
*
* @param tone The name of the tone to play. Must correspond to a predefined tone in the tone table.
* @param tone The name of the tone to play. Must correspond to a predefined tone in the tone table.
* @param duration The duration of the tone in milliseconds.
* @throws LineUnavailableException If a line for audio playback cannot be opened.
*/
@ -64,18 +78,4 @@ public class ToneGenerator {
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
* retrieve the frequency of a tone based on its standard notation name,
* such as "C3", "A4", or "G#5".
*
* <p>
* This class can be particularly useful in applications related to sound synthesis,
* music theory, signal processing, and other audio-related domains that require precise
* frequency information for specific tones.
*/
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).
* 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,
* 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
* for predefined tone names.
*/
private static final HashMap<String, Double> toneMap = new HashMap<>();
static {
toneMap.put("C0", 16.35);
toneMap.put("C#0", 17.32);
@ -191,6 +180,18 @@ public class ToneTable {
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.
* 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.ResultSet;
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.stream.Stream;
import java.util.stream.StreamSupport;
@ -23,311 +30,312 @@ import java.util.stream.StreamSupport;
@Slf4j
public class Query<T> {
/**
* A factory function that extracts an Integer from the first column of a given ResultSet.
* <p>
* This function is designed to facilitate the conversion of a ResultSet row into an Integer
* by accessing the value in the first column of the result set. If an SQL exception is encountered
* during the retrieval of the integer value, it is wrapped and re-thrown as a runtime exception.
*/
public static final Function<ResultSet, Integer> INTEGER_FACTORY = rs -> {
try {
return rs.getInt(1);
} catch (SQLException e) {
throw new RuntimeException(e);
}
};
/**
* Represents a database connection instance.
* This connection is used to interact with a database
* through executing queries and retrieving results.
* It is immutable and initialized once.
*/
private final Connection connection;
/**
* A list of ParameterSetter objects used to configure parameters.
* This collection serves as a storage for managing parameter-setting logic dynamically.
* It is initialized as an empty ArrayList and is immutable since it is declared as final.
*/
private final List<ParameterSetter> parameterSetters = new ArrayList<>();
/**
* A functional interface representing a factory method used to produce instances of type T.
* This factory is typically implemented to map rows from a {@link ResultSet} to objects of type T.
* The input parameter is a {@link ResultSet}, which provides access to database query results.
* The resulting output is an instance of type T created based on the data in the provided ResultSet.
*/
private Function<ResultSet, T> factory;
/**
* Represents the text of a query.
* This variable stores the query string used for processing or execution purposes within the application.
*/
private String queryText;
/**
* Constructs a new Query object with the given database connection.
*
* @param connection The Connection object used to interact with the database.
*/
public Query(Connection connection) {
this.connection = connection;
}
/**
* Executes the given SQL query using the provided database connection.
*
* @param connection the database connection to be used for executing the query
* @param query the SQL query to be executed
* @throws SQLException if a database access error occurs or the query execution fails
*/
public static void execute(final Connection connection, final String query) throws SQLException {
new Query<Object>(connection)
.setQueryText(query)
.execute();
}
/**
* Sets the query text for the current query instance.
*
* @param queryText the text of the query to be set
* @return the current instance of the query
*/
public Query<T> setQueryText(final String queryText) {
this.queryText = queryText;
return this;
}
/**
* 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.
*
* @param resourcePath the path to the resource file containing the query text
* @return the current Query object with the loaded query text
*/
public Query<T> setQueryResource(final String resourcePath) {
log.info("loading resource: {}", resourcePath);
try (Scanner scanner = new Scanner(Objects.requireNonNull(getClass().getResourceAsStream(resourcePath)),
StandardCharsets.UTF_8)) {
this.queryText = scanner.useDelimiter("\\A").next();
}
log.info("query text: {}", this.queryText);
return this;
}
/**
* Replaces all occurrences of the specified placeholder in the query text with the provided value.
*
* @param placeholder the placeholder string to be replaced in the query text
* @param value the value to replace the placeholder with
* @return the updated Query instance with the modified query text
*/
public Query<T> replace(final String placeholder, final String value) {
this.queryText = this.queryText.replace(placeholder, value);
log.info("query text: {}", this.queryText);
return this;
}
/**
* Adds a parameter to the query at the specified index with a custom setter function.
*
* @param index The position of the parameter in the query starting from 1.
* @param value The value of the parameter to be set.
* @param setter A TriSqlFunction that defines how to set the parameter in the PreparedStatement.
* @param <X> The type of the parameter value.
* @return The current query instance for method chaining.
*/
public <X> Query<T> addParameter(final int index, final X value, final TriSqlFunction<PreparedStatement, Integer, X> setter) {
parameterSetters.add(ps -> setter.apply(ps, index, value));
return this;
}
/**
* Adds a parameter to the query using the specified ParameterSetter.
*
* @param setter the ParameterSetter used to configure the parameter
* @return the current Query instance for method chaining
*/
public Query<T> addParameter(ParameterSetter setter) {
parameterSetters.add(setter);
return this;
}
/**
* Sets the factory function used to convert a {@link ResultSet} into an instance of type {@code T}.
*
* @param factory the function responsible for mapping a {@code ResultSet} to an object of type {@code T}
* @return the current {@code Query} instance with the specified factory function set
*/
public Query<T> factory(final Function<ResultSet, T> factory) {
this.factory = factory;
return this;
}
/**
* 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.
*
* @return a {@link Stream} of type T populated with the query results processed using the specified factory
* @throws SQLException if an SQL error occurs while executing the prepared statement or creating the stream
*/
private Stream<T> stream() throws SQLException {
ResultSet rs = createPreparedStatement().executeQuery();
TrimmingResultSet trimmingResultSet = new TrimmingResultSet(rs);
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}
* factory. The stream ensures resources such as the {@code ResultSet} and its associated statement are
* closed when the stream is closed.
*
* @param rs the {@link ResultSet} to be transformed into a stream
* @param factory a {@link Function} that maps rows of the {@link ResultSet} to objects of type T
* @return a {@link Stream} of objects of type T created from the {@code ResultSet} rows
*/
private Stream<T> streamFromResultSet(final ResultSet rs, final Function<ResultSet, T> factory) {
Iterator<T> iterator = new Iterator<>() {
private boolean hasNextChecked = false;
private boolean hasNext;
/**
* Checks if the iteration has more elements. This method verifies if the
* underlying {@link ResultSet} contains another row that has not yet been processed.
*
* @return {@code true} if there are more elements in the {@link ResultSet},
* otherwise {@code false}.
*/
@Override
@SneakyThrows
public boolean hasNext() {
if (!hasNextChecked) {
hasNext = rs.next();
hasNextChecked = true;
/**
* A factory function that extracts an Integer from the first column of a given ResultSet.
* <p>
* This function is designed to facilitate the conversion of a ResultSet row into an Integer
* by accessing the value in the first column of the result set. If an SQL exception is encountered
* during the retrieval of the integer value, it is wrapped and re-thrown as a runtime exception.
*/
public static final Function<ResultSet, Integer> INTEGER_FACTORY = rs -> {
try {
return rs.getInt(1);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return hasNext;
}
/**
* Returns the next element in the iteration. This method applies the provided
* factory function to the current row of the {@link ResultSet} to create an object of type T.
* It throws {@link NoSuchElementException} if there are no more elements to iterate.
*
* @return the next element of type T as created by the factory function from the current row of the {@link ResultSet}
* @throws NoSuchElementException if the iteration has no more elements
*/
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
hasNextChecked = false;
return factory.apply(rs);
}
};
Spliterator<T> spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED);
return StreamSupport.stream(spliterator, false)
.onClose(() -> {
try {
rs.getStatement().close();
} catch (SQLException ignored) {
// Exception is ignored.
}
try {
rs.close();
} catch (SQLException ignored) {
// Exception is ignored.
}
});
}
/**
* 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 database connection instance.
* This connection is used to interact with a database
* through executing queries and retrieving results.
* It is immutable and initialized once.
*/
private final Connection connection;
/**
* A list of ParameterSetter objects used to configure parameters.
* This collection serves as a storage for managing parameter-setting logic dynamically.
* It is initialized as an empty ArrayList and is immutable since it is declared as final.
*/
private final List<ParameterSetter> parameterSetters = new ArrayList<>();
/**
* A functional interface representing a factory method used to produce instances of type T.
* This factory is typically implemented to map rows from a {@link ResultSet} to objects of type T.
* The input parameter is a {@link ResultSet}, which provides access to database query results.
* The resulting output is an instance of type T created based on the data in the provided ResultSet.
*/
private Function<ResultSet, T> factory;
/**
* Represents the text of a query.
* This variable stores the query string used for processing or execution purposes within the application.
*/
private String queryText;
/**
* Constructs a new Query object with the given database connection.
*
* @param connection The Connection object used to interact with the database.
*/
public Query(Connection connection) {
this.connection = connection;
}
}
/**
* 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
* logic requires processing with three parameters.
*
* @param <T> the type of the first input to the operation
* @param <U> the type of the second input to the operation
* @param <V> the type of the third input to the operation
*/
@FunctionalInterface
public interface TriSqlFunction<T, U, V> {
/**
* Executes an operation on three input arguments and allows for the operation to throw
* an {@link SQLException}. This method is intended to facilitate SQL-related processes
* where three parameters are involved.
* Executes the given SQL query using the provided database connection.
*
* @param t the first input argument
* @param u the second input argument
* @param v the third input argument
* @throws SQLException if an SQL error occurs during execution
* @param connection the database connection to be used for executing the query
* @param query the SQL query to be executed
* @throws SQLException if a database access error occurs or the query execution fails
*/
void apply(T t, U u, V v) throws SQLException;
}
/**
* Functional interface designed to handle operations on a PreparedStatement.
* Represents a single abstract method that performs specific work on the
* provided PreparedStatement instance.
*/
@FunctionalInterface
public interface ParameterSetter {
public static void execute(final Connection connection, final String query) throws SQLException {
new Query<Object>(connection)
.setQueryText(query)
.execute();
}
/**
* Applies an operation to the given PreparedStatement object.
* This method is intended to set parameters or perform other modifications on a PreparedStatement.
* 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.
*
* @param ps the PreparedStatement instance to be modified or configured
* @throws SQLException if a database access error occurs while applying the operation
* @throws SQLException if a database access error occurs or the SQL statement is invalid.
*/
void apply(PreparedStatement ps) throws SQLException;
}
public void execute() throws SQLException {
try (PreparedStatement ps = createPreparedStatement()) {
ps.execute();
}
}
/**
* Sets the query text for the current query instance.
*
* @param queryText the text of the query to be set
* @return the current instance of the query
*/
public Query<T> setQueryText(final String queryText) {
this.queryText = queryText;
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.
* The resource content is read and stored as the query text for the query object.
*
* @param resourcePath the path to the resource file containing the query text
* @return the current Query object with the loaded query text
*/
public Query<T> setQueryResource(final String resourcePath) {
log.info("loading resource: {}", resourcePath);
try (Scanner scanner = new Scanner(Objects.requireNonNull(getClass().getResourceAsStream(resourcePath)),
StandardCharsets.UTF_8)) {
this.queryText = scanner.useDelimiter("\\A").next();
}
log.info("query text: {}", this.queryText);
return this;
}
/**
* Replaces all occurrences of the specified placeholder in the query text with the provided value.
*
* @param placeholder the placeholder string to be replaced in the query text
* @param value the value to replace the placeholder with
* @return the updated Query instance with the modified query text
*/
public Query<T> replace(final String placeholder, final String value) {
this.queryText = this.queryText.replace(placeholder, value);
log.info("query text: {}", this.queryText);
return this;
}
/**
* Adds a parameter to the query at the specified index with a custom setter function.
*
* @param index The position of the parameter in the query starting from 1.
* @param value The value of the parameter to be set.
* @param setter A TriSqlFunction that defines how to set the parameter in the PreparedStatement.
* @param <X> The type of the parameter value.
* @return The current query instance for method chaining.
*/
public <X> Query<T> addParameter(final int index, final X value, final TriSqlFunction<PreparedStatement, Integer, X> setter) {
parameterSetters.add(ps -> setter.apply(ps, index, value));
return this;
}
/**
* Adds a parameter to the query using the specified ParameterSetter.
*
* @param setter the ParameterSetter used to configure the parameter
* @return the current Query instance for method chaining
*/
public Query<T> addParameter(ParameterSetter setter) {
parameterSetters.add(setter);
return this;
}
/**
* Sets the factory function used to convert a {@link ResultSet} into an instance of type {@code T}.
*
* @param factory the function responsible for mapping a {@code ResultSet} to an object of type {@code T}
* @return the current {@code Query} instance with the specified factory function set
*/
public Query<T> factory(final Function<ResultSet, T> factory) {
this.factory = factory;
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.
* The result set is wrapped in a {@link TrimmingResultSet} to handle data trimming.
*
* @return a {@link Stream} of type T populated with the query results processed using the specified factory
* @throws SQLException if an SQL error occurs while executing the prepared statement or creating the stream
*/
private Stream<T> stream() throws SQLException {
ResultSet rs = createPreparedStatement().executeQuery();
TrimmingResultSet trimmingResultSet = new TrimmingResultSet(rs);
return streamFromResultSet(trimmingResultSet, factory);
}
/**
* 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
* closed when the stream is closed.
*
* @param rs the {@link ResultSet} to be transformed into a stream
* @param factory a {@link Function} that maps rows of the {@link ResultSet} to objects of type T
* @return a {@link Stream} of objects of type T created from the {@code ResultSet} rows
*/
private Stream<T> streamFromResultSet(final ResultSet rs, final Function<ResultSet, T> factory) {
Iterator<T> iterator = new Iterator<>() {
private boolean hasNextChecked = false;
private boolean hasNext;
/**
* Checks if the iteration has more elements. This method verifies if the
* underlying {@link ResultSet} contains another row that has not yet been processed.
*
* @return {@code true} if there are more elements in the {@link ResultSet},
* otherwise {@code false}.
*/
@Override
@SneakyThrows
public boolean hasNext() {
if (!hasNextChecked) {
hasNext = rs.next();
hasNextChecked = true;
}
return hasNext;
}
/**
* Returns the next element in the iteration. This method applies the provided
* factory function to the current row of the {@link ResultSet} to create an object of type T.
* It throws {@link NoSuchElementException} if there are no more elements to iterate.
*
* @return the next element of type T as created by the factory function from the current row of the {@link ResultSet}
* @throws NoSuchElementException if the iteration has no more elements
*/
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
hasNextChecked = false;
return factory.apply(rs);
}
};
Spliterator<T> spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED);
return StreamSupport.stream(spliterator, false)
.onClose(() -> {
try {
rs.getStatement().close();
} catch (SQLException ignored) {
// Exception is ignored.
}
try {
rs.close();
} catch (SQLException ignored) {
// Exception is ignored.
}
});
}
/**
* 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
* logic requires processing with three parameters.
*
* @param <T> the type of the first input to the operation
* @param <U> the type of the second input to the operation
* @param <V> the type of the third input to the operation
*/
@FunctionalInterface
public interface TriSqlFunction<T, U, V> {
/**
* Executes an operation on three input arguments and allows for the operation to throw
* an {@link SQLException}. This method is intended to facilitate SQL-related processes
* where three parameters are involved.
*
* @param t the first input argument
* @param u the second input argument
* @param v the third input argument
* @throws SQLException if an SQL error occurs during execution
*/
void apply(T t, U u, V v) throws SQLException;
}
/**
* Functional interface designed to handle operations on a PreparedStatement.
* Represents a single abstract method that performs specific work on the
* provided PreparedStatement instance.
*/
@FunctionalInterface
public interface ParameterSetter {
/**
* Applies an operation to the given PreparedStatement object.
* This method is intended to set parameters or perform other modifications on a PreparedStatement.
*
* @param ps the PreparedStatement instance to be modified or configured
* @throws SQLException if a database access error occurs while applying the operation
*/
void apply(PreparedStatement ps) throws SQLException;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ public class ArrayUtils {
* static utility methods for array-related operations.
*
* @throws UnsupportedOperationException always, to indicate that this class
* should not be instantiated.
* should not be instantiated.
*/
private ArrayUtils() {
throw new UnsupportedOperationException("Utility class");
@ -23,11 +23,11 @@ public class ArrayUtils {
* Checks if the given character array contains the specified character.
*
* @param array the character array to be searched
* @param ch the character to search for in the array
* @param ch the character to search for in the array
* @return true if the character is found in the array, false otherwise
*/
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;
}
return false;
@ -37,11 +37,11 @@ public class ArrayUtils {
* Checks if the specified integer array contains the given integer value.
*
* @param array the array of integers to search
* @param ch the integer value to search for in the array
* @param ch the integer value to search for in the array
* @return true if the array contains the specified integer value, false otherwise
*/
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;
}
return false;
@ -51,11 +51,11 @@ public class ArrayUtils {
* Checks if the specified long array contains the given long value.
*
* @param array the array of long values to search
* @param ch the long value to search for in the array
* @param ch the long value to search for in the array
* @return true if the array contains the specified long value, false otherwise
*/
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;
}
return false;
@ -65,12 +65,12 @@ public class ArrayUtils {
* Checks if the specified array contains the given element.
*
* @param array the array to be searched
* @param ch the element to search for in the array
* @param <T> the type of the elements in the array
* @param ch the element to search for in the array
* @param <T> the type of the elements in the array
* @return true if the element is found in the array, false otherwise
*/
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;
}
return false;

View File

@ -14,7 +14,7 @@ public class EnumUtil {
* static utility methods for array-related operations.
*
* @throws UnsupportedOperationException always, to indicate that this class
* should not be instantiated.
* should not be instantiated.
*/
private EnumUtil() {
throw new UnsupportedOperationException("Utility class");
@ -25,7 +25,7 @@ public class EnumUtil {
* of the enum constants within the specified Enum class. The pattern also includes optional delimiters
* such as commas, spaces, and empty values, enabling the matching of lists or sequences of flag values.
*
* @param <T> The type of the Enum.
* @param <T> The type of the Enum.
* @param clazz The Enum class for which the regular expression is to be generated.
* @return A String containing the regular expression pattern for matching the Enum's flag values.
*/
@ -33,9 +33,9 @@ public class EnumUtil {
StringBuilder result = new StringBuilder();
result.append("(|,|\\s");
for (T flag: clazz.getEnumConstants()) {
for (T flag : clazz.getEnumConstants()) {
result.append("|");
for(char ch: flag.toString().toUpperCase().toCharArray()) {
for (char ch : flag.toString().toUpperCase().toCharArray()) {
if (Character.isAlphabetic(ch)) {
result.append("[");
result.append(ch);
@ -52,25 +52,25 @@ public class EnumUtil {
/**
* 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
* 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
* to the Enum constants.
*
* @param <T> The type of the Enum.
* @param <T> The type of the Enum.
* @param clazz The Enum class to which the flags should be parsed.
* @param flags A string containing the delimited list of flag names to parse.
* Individual names can be separated by commas or whitespace.
* @return A list of Enum constants parsed from the input string. If the input string
* is null or empty, an empty list is returned.
* is null or empty, an empty list is returned.
* @throws IllegalArgumentException if any of the flag names do not match the constants in the given Enum class.
* @throws NullPointerException if the clazz parameter is null.
* @throws NullPointerException if the clazz parameter is null.
*/
public static <T extends Enum<T>> List<T> parseFlags(final Class<T> clazz, final String flags) {
List<T> result = new ArrayList<>();
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()));
}
}

View File

@ -16,7 +16,7 @@ public class RegionalizationMessage {
* which is used to retrieve locale-specific objects and messages.
* It facilitates the process of internationalization by loading resources
* such as text and messages from specified bundles based on the provided locale.
*
* <p>
* This variable is initialized in constructors of the `RegionalizationMessage` class
* and is used internally by various methods to fetch localized messages.
*/
@ -50,21 +50,36 @@ public class RegionalizationMessage {
*
* @param key The key for which the localized message needs to be retrieved.
* @return The localized message as a String if the key exists in the resource bundle;
* otherwise, null.
* otherwise, null.
*/
public String getMessage(final String key) {
if (!res.containsKey(key)) return null;
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.
* If the key is not found, the specified default message is returned.
*
* @param key The key to look up in the resource bundle.
* @param key The key to look up in the resource bundle.
* @param defaultMessage The default message to return if the key is not found.
* @return The localized message corresponding to the key, or the default message
* if the key does not exist in the resource bundle.
* if the key does not exist in the resource bundle.
*/
public String getMessage(final String key, final String defaultMessage) {
if (res.containsKey(key))
@ -72,19 +87,4 @@ public class RegionalizationMessage {
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,122 +7,140 @@ 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.
*/
@SuppressWarnings("unused")
public class StopWatch {
/**
* Represents the starting time of the stopwatch. This variable is set when the stopwatch is started and is used to calculate the elapsed
* duration between the start and the stop or the current time.
*/
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
* duration between the start and stop times.
*/
private Instant stopTime;
/**
* Represents the starting time of the stopwatch. This variable is set when the stopwatch is started and is used to calculate the elapsed
* duration between the start and the stop or the current time.
*/
private Instant startTime;
/**
* 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
* elapsed time.
*/
public void start() {
this.startTime = Instant.now();
this.stopTime = null;
}
/**
* 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.
*/
private Instant stopTime;
/**
* Stops the stopwatch by recording the current time as the stopping time.
* <p>
* This method should only be called after the stopwatch has been started using the {@code start()} method. If the stopwatch has not been
* started, an {@code IllegalStateException} will be thrown. Once this method is called, the elapsed time can be calculated as the
* duration between the starting and stopping times.
*
* @throws IllegalStateException if the stopwatch has not been started prior to calling this method.
*/
public void stop() {
if (startTime == null) {
throw new IllegalStateException("StopWatch must be started before stopping.");
}
this.stopTime = Instant.now();
}
/**
* Calculates the amount of time elapsed between the stopwatch's starting time and either its stopping time or the current time if the
* stopwatch is still running.
*
* @return the duration representing the elapsed time between the start and end points of the stopwatch.
* @throws IllegalStateException if the stopwatch has not been started before calling this method.
*/
public Duration getUsedTime() {
if (startTime == null) {
throw new IllegalStateException("StopWatch has not been started.");
}
Instant end = (stopTime != null) ? stopTime : Instant.now();
return Duration.between(startTime, end);
}
/**
* Formats the duration of elapsed time measured by the stopwatch into a human-readable string. The formatted output may include days,
* hours, minutes, seconds, and milliseconds, depending on the parameters. The method retrieves the elapsed time via the
* {@code getUsedTime} method and formats it based on the specified flags.
*
* @param includeMillis whether to include milliseconds in the formatted output
* @param includeSeconds whether to include seconds in the formatted output
* @param includeMinutes whether to include minutes in the formatted output
* @param includeHours whether to include hours in the formatted output
* @param includeDays whether to include days in the formatted output
* @return a formatted string representing the elapsed time, including the components specified by the parameters
*/
public String getUsedTimeFormatted(boolean includeMillis, boolean includeSeconds, boolean includeMinutes, boolean includeHours,
boolean includeDays) {
Duration duration = getUsedTime();
long millis = duration.toMillis();
long days = millis / (24 * 60 * 60 * 1000);
millis %= (24 * 60 * 60 * 1000);
long hours = millis / (60 * 60 * 1000);
millis %= (60 * 60 * 1000);
long minutes = millis / (60 * 1000);
millis %= (60 * 1000);
long seconds = millis / 1000;
millis %= 1000;
StringBuilder sb = new StringBuilder();
if (includeDays && days > 0) {
sb.append(days).append("d ");
}
if (includeHours && (hours > 0 || sb.length() > 0)) {
sb.append(hours).append("h ");
}
if (includeMinutes && (minutes > 0 || sb.length() > 0)) {
sb.append(minutes).append("m ");
}
if (includeSeconds && (seconds > 0 || sb.length() > 0)) {
sb.append(seconds);
}
if (includeMillis) {
if (includeSeconds) {
sb.append(String.format(Locale.US, ".%03d", millis));
} else {
sb.append(millis).append("ms");
}
/**
* 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 ...
}
String result = sb.toString().trim();
return result.isEmpty() ? "0ms" : result;
}
/**
* 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
* elapsed time.
*/
public void start() {
this.startTime = Instant.now();
this.stopTime = null;
}
/**
* Provides a compact and human-readable formatted string representing the elapsed time measured by the stopwatch. The method formats the
* 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 a compact formatted string representing the elapsed time, including days, hours, minutes, seconds, and milliseconds as
* applicable
*/
public String getUsedTimeFormattedCompact() {
return getUsedTimeFormatted(true, true, true, true, true);
}
/**
* Stops the stopwatch by recording the current time as the stopping time.
* <p>
* This method should only be called after the stopwatch has been started using the {@code start()} method. If the stopwatch has not been
* started, an {@code IllegalStateException} will be thrown. Once this method is called, the elapsed time can be calculated as the
* duration between the starting and stopping times.
*
* @throws IllegalStateException if the stopwatch has not been started prior to calling this method.
*/
public void stop() {
if (startTime == null) {
throw new IllegalStateException("StopWatch must be started before stopping.");
}
this.stopTime = Instant.now();
}
/**
* Provides a compact and human-readable formatted string representing the elapsed time measured by the stopwatch. The method formats the
* 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 a compact formatted string representing the elapsed time, including days, hours, minutes, seconds, and milliseconds as
* applicable
*/
public String getUsedTimeFormattedCompact() {
return getUsedTimeFormatted(true, true, true, true, true);
}
/**
* Formats the duration of elapsed time measured by the stopwatch into a human-readable string. The formatted output may include days,
* hours, minutes, seconds, and milliseconds, depending on the parameters. The method retrieves the elapsed time via the
* {@code getUsedTime} method and formats it based on the specified flags.
*
* @param includeMillis whether to include milliseconds in the formatted output
* @param includeSeconds whether to include seconds in the formatted output
* @param includeMinutes whether to include minutes in the formatted output
* @param includeHours whether to include hours in the formatted output
* @param includeDays whether to include days in the formatted output
* @return a formatted string representing the elapsed time, including the components specified by the parameters
*/
public String getUsedTimeFormatted(boolean includeMillis, boolean includeSeconds, boolean includeMinutes, boolean includeHours,
boolean includeDays) {
Duration duration = getUsedTime();
long millis = duration.toMillis();
long days = millis / (24 * 60 * 60 * 1000);
millis %= (24 * 60 * 60 * 1000);
long hours = millis / (60 * 60 * 1000);
millis %= (60 * 60 * 1000);
long minutes = millis / (60 * 1000);
millis %= (60 * 1000);
long seconds = millis / 1000;
millis %= 1000;
StringBuilder sb = new StringBuilder();
if (includeDays && days > 0) {
sb.append(days).append("d ");
}
if (includeHours && (hours > 0 || !sb.isEmpty())) {
sb.append(hours).append("h ");
}
if (includeMinutes && (minutes > 0 || !sb.isEmpty())) {
sb.append(minutes).append("m ");
}
if (includeSeconds && (seconds > 0 || !sb.isEmpty())) {
sb.append(seconds);
}
if (includeMillis) {
if (includeSeconds) {
sb.append(String.format(Locale.US, ".%03d", millis));
} else {
sb.append(millis).append("ms");
}
}
String result = sb.toString().trim();
return result.isEmpty() ? "0ms" : result;
}
/**
* Calculates the amount of time elapsed between the stopwatch's starting time and either its stopping time or the current time if the
* stopwatch is still running.
*
* @return the duration representing the elapsed time between the start and end points of the stopwatch.
* @throws IllegalStateException if the stopwatch has not been started before calling this method.
*/
public Duration getUsedTime() {
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,33 +8,33 @@ import java.util.Map;
* Utility class providing common string manipulation methods.
*/
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.
* 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.
* should not be instantiated.
*/
private Strings() {
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.
* Placeholders in the format ${VARIABLE_NAME} are replaced with their
@ -43,8 +43,8 @@ public class Strings {
* @param text The input string containing environment variable placeholders.
* If null, the method returns null.
* @return A string with the placeholders replaced by their corresponding
* environment variable values. If no placeholders are found,
* the original string is returned.
* environment variable values. If no placeholders are found,
* the original string is returned.
*/
public static String expandEnvironmentVariables(String text) {
if (text == null) return null;
@ -63,7 +63,7 @@ public class Strings {
* @return true if the string is null or has a length of 0; false otherwise.
*/
public static boolean isNullOrEmpty(final String string) {
return (string == null || string.length()==0);
return (string == null || string.length() == 0);
}
/**
@ -73,12 +73,12 @@ public class Strings {
* @param value The input string that might contain surrounding double quotes.
* The input cannot be null, and leading/trailing whitespaces will be ignored.
* @return A string with leading and trailing double quotes removed if present.
* Returns the original string if no surrounding double quotes are detected.
* Returns the original string if no surrounding double quotes are detected.
*/
public static String removeQuotes(final String value) {
String trimmedValue = value.trim();
if (trimmedValue.length() > 1 && trimmedValue.startsWith("\"") && trimmedValue.endsWith("\""))
return trimmedValue.substring(1, trimmedValue.length()-1);
return trimmedValue.substring(1, trimmedValue.length() - 1);
return value;
}
@ -87,11 +87,11 @@ public class Strings {
* Replaces illegal characters in a given string with a specified replacement string.
* Optionally, consecutive illegal characters can be combined into a single replacement.
*
* @param value The input string in which illegal characters are to be replaced.
* @param value The input string in which illegal characters are to be replaced.
* @param illegalCharacters A string specifying the set of illegal characters to be matched.
* @param replacement The string to replace each illegal character or group of characters.
* @param combine A boolean flag indicating whether consecutive illegal characters should be replaced
* with a single replacement string. If true, groups of illegal characters are combined.
* @param replacement The string to replace each illegal character or group of characters.
* @param combine A boolean flag indicating whether consecutive illegal characters should be replaced
* with a single replacement string. If true, groups of illegal characters are combined.
* @return A new string with the illegal characters replaced by the provided replacement string.
*/
public static String replaceIllegalCharacters(final String value, final String illegalCharacters, final String replacement, final boolean combine) {
@ -104,12 +104,12 @@ public class Strings {
* with a specified replacement string. Optionally combines consecutive non-allowed
* characters into a single replacement.
*
* @param value The input string to be processed.
* @param value The input string to be processed.
* @param allowedCharacters A string containing the set of characters that are allowed.
* Any character not in this set will be replaced.
* @param replacement The string to replace non-allowed characters with.
* @param combine If true, consecutive non-allowed characters will be replaced with
* a single instance of the replacement string.
* Any character not in this set will be replaced.
* @param replacement The string to replace non-allowed characters with.
* @param combine If true, consecutive non-allowed characters will be replaced with
* a single instance of the replacement string.
* @return A new string with non-allowed characters replaced according to the specified rules.
*/
public static String replaceNonAllowedCharacters(final String value, final String allowedCharacters, final String replacement, final boolean combine) {
@ -131,11 +131,11 @@ public class Strings {
public static String increment(@NonNull final String element) {
if (element.isEmpty()) return "1";
String firstPart = element.substring(0, element.length()-1);
char lastChar = element.charAt(element.length()-1);
String firstPart = element.substring(0, element.length() - 1);
char lastChar = element.charAt(element.length() - 1);
if (lastChar == '9') return firstPart + 'A';
if (lastChar == 'Z') return increment(firstPart) + '0';
return firstPart + (char)(lastChar+1);
return firstPart + (char) (lastChar + 1);
}
/**
@ -144,10 +144,10 @@ public class Strings {
* the string is returned unchanged. If the input string's length exceeds the max
* length, it is truncated to that length.
*
* @param original The original input string to be processed. If null, the method returns null.
* @param original The original input string to be processed. If null, the method returns null.
* @param maxLength The maximum number of characters allowed in the resulting string.
* @return The original string if its length is less than or equal to maxLength,
* otherwise a truncated version of the string up to maxLength characters.
* otherwise a truncated version of the string up to maxLength characters.
*/
public static String limitCharNumber(final String original, final int maxLength) {
if (original == null || original.length() <= maxLength) return original;

View File

@ -8,7 +8,12 @@ import org.w3c.dom.Node;
import javax.xml.parsers.DocumentBuilder;
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.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
@ -25,214 +30,211 @@ import java.util.Date;
*/
@Slf4j
public class XmlUtils {
/**
* 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 XmlUtils() {
throw new UnsupportedOperationException("Utility class");
}
/**
* 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");
/**
* A {@link DateTimeFormatter} instance used for formatting or parsing dates in the "yyyy-MM-dd" pattern.
*
* 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.
*
* 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.
* 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 XmlUtils() {
throw new UnsupportedOperationException("Utility class");
}
/**
* 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 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 a new XML element with the specified name and value, appends the value
* as a text node to the element, and returns the resulting element.
*
* @param doc the XML document to which the element belongs. Must not be null.
* @param name the name of the element to create. Must not be null.
* @param value the text value to be set as the content of the created element.
* If null, an empty string will be used as the content.
* @return the newly created XML element containing the specified value as a text node.
*/
public static Element createElement(final Document doc, final String name, final String value) {
log.debug("Creating a new element " + name + " with value: " + value);
Element element = doc.createElement(name);
/**
* Creates a new XML element with the specified name and value, appends the value
* as a text node to the element, and returns the resulting element.
*
* @param doc the XML document to which the element belongs. Must not be null.
* @param name the name of the element to create. Must not be null.
* @param value the text value to be set as the content of the created element.
* If null, an empty string will be used as the content.
* @return the newly created XML element containing the specified value as a text node.
*/
public static Element createElement(final Document doc, final String name, final String value) {
log.debug("Creating a new element " + name + " with value: " + value);
Element element = doc.createElement(name);
Node content = doc.createTextNode(value != null ? value : "");
element.appendChild(content);
Node content = doc.createTextNode(value != null ? value : "");
element.appendChild(content);
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.
*
* @param doc The Document object to which the element will belong. Must not be null.
* @param name The name of the element to be created. Must not be null or empty.
* @param value The optional value to be set as the text content of the element. If null,
* the element will have an empty text content.
* @return The newly created Element object with the specified name and value.
*/
public static Element createElement(final Document doc, final String name, final Integer value) {
log.debug("Creating a new element " + name + " with value: " + value);
Element element = doc.createElement(name);
/**
* Creates an XML element with the specified name and optional value.
*
* @param doc The Document object to which the element will belong. Must not be null.
* @param name The name of the element to be created. Must not be null or empty.
* @param value The optional value to be set as the text content of the element. If null,
* the element will have an empty text content.
* @return The newly created Element object with the specified name and value.
*/
public static Element createElement(final Document doc, final String name, final Integer value) {
log.debug("Creating a new element " + name + " with value: " + value);
Element element = doc.createElement(name);
Node content = doc.createTextNode(value != null ? "" + value : "");
element.appendChild(content);
Node content = doc.createTextNode(value != null ? ""+value : "");
element.appendChild(content);
return element;
}
return element;
}
/**
* Creates a new element with the specified name, appends it to the provided parent element,
* and returns the newly created element.
*
* @param doc The Document to which the new element belongs. Must not be null.
* @param name The name of the new element to be created. Must not be null or empty.
* @param parent The parent element to which the new element will be appended. Must not be null.
* @return The newly created and appended Element object.
*/
public static Element createAndInsertElement(final Document doc, final String name, @NonNull final Element parent) {
log.debug("Creating a new element " + name + " and adding it to a parent.");
Element element = createElement(doc, name);
parent.appendChild(element);
return element;
}
/**
* Creates a new element with the specified name, appends it to the provided parent element,
* and returns the newly created element.
*
* @param doc The Document to which the new element belongs. Must not be null.
* @param name The name of the new element to be created. Must not be null or empty.
* @param parent The parent element to which the new element will be appended. Must not be null.
* @return The newly created and appended Element object.
*/
public static Element createAndInsertElement(final Document doc, final String name, @NonNull final Element parent) {
log.debug("Creating a new element " + name + " and adding it to a parent.");
Element element = createElement(doc, name);
parent.appendChild(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,
* and returns the created element.
*
* @param doc The XML document to which the new element belongs. Must not be null.
* @param name The name of the element to create. Must not be null.
* @param value The value to be set for the created element. Can be null if no value is required.
* @param parent The parent element to which the new element will be appended. Must not be null.
* @return The newly created and inserted element.
*/
public static Element createAndInsertElement(final Document doc, final String name, final String value, @NonNull final Element parent) {
log.debug("Creating a new element " + name + " with value: " + value + " and adding it to a parent.");
Element element = createElement(doc, name, value);
parent.appendChild(element);
return element;
}
/**
* Creates a new XML element with a specified name and value, adds it to the specified parent element,
* and returns the created element.
*
* @param doc The XML document to which the new element belongs. Must not be null.
* @param name The name of the element to create. Must not be null.
* @param value The value to be set for the created element. Can be null if no value is required.
* @param parent The parent element to which the new element will be appended. Must not be null.
* @return The newly created and inserted element.
*/
public static Element createAndInsertElement(final Document doc, final String name, final String value, @NonNull final Element parent) {
log.debug("Creating a new element " + name + " with value: " + value + " and adding it to a parent.");
Element element = createElement(doc, name, value);
parent.appendChild(element);
return element;
}
/**
* Formats a given XML string by applying proper indentation to enhance readability.
* The method uses a transformer to process the XML input, applying indentation
* with a four-space configuration. In case of an error, the original XML string
* is returned without any modifications.
*
* @param xmlStream The raw XML string to be formatted. Cannot be null.
* @return The formatted XML string with proper indentation. If an exception occurs
* during formatting, the original XML string is returned.
*/
public static String format(final String xmlStream) {
log.debug("formatXML");
/**
* Formats a given XML string by applying proper indentation to enhance readability.
* The method uses a transformer to process the XML input, applying indentation
* with a four-space configuration. In case of an error, the original XML string
* is returned without any modifications.
*
* @param xmlStream The raw XML string to be formatted. Cannot be null.
* @return The formatted XML string with proper indentation. If an exception occurs
* during formatting, the original XML string is returned.
*/
public static String format(final String xmlStream) {
log.debug("formatXML");
try {
Source xmlInput = new StreamSource(new StringReader(xmlStream));
StringWriter stringWriter = new StringWriter();
StreamResult xmlOutput = new StreamResult(stringWriter);
try {
Source xmlInput = new StreamSource(new StringReader(xmlStream));
StringWriter stringWriter = new StringWriter();
StreamResult xmlOutput = new StreamResult(stringWriter);
TransformerFactory transformerFactory = TransformerFactory.newInstance();
transformerFactory.setAttribute("indent-number", "4");
TransformerFactory transformerFactory = TransformerFactory.newInstance();
transformerFactory.setAttribute("indent-number", "4");
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(xmlInput, xmlOutput);
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(xmlInput, xmlOutput);
return xmlOutput.getWriter().toString();
} catch (TransformerException e) {
log.error("Error in XML: " + e.getMessage() + "\n" + xmlStream, e);
}
return xmlOutput.getWriter().toString();
}
catch(TransformerException e) {
log.error("Error in XML: " + e.getMessage() + "\n"+xmlStream, e);
}
return xmlStream;
}
return xmlStream;
}
/**
* Formats a given XML Document into a human-readable string representation.
* The method applies indentation and specific output properties to the XML content.
* If an exception occurs during the transformation process, it logs the error
* and returns null.
*
* @param doc The XML Document to be formatted. Must not be null.
* @return A formatted string representation of the XML document, or null if an error occurs during processing.
*/
public static String format(final Document doc) {
try {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
transformerFactory.setAttribute("indent-number", "4");
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
StringWriter stringWriter = new StringWriter();
/**
* Formats a given XML Document into a human-readable string representation.
* The method applies indentation and specific output properties to the XML content.
* If an exception occurs during the transformation process, it logs the error
* and returns null.
*
* @param doc The XML Document to be formatted. Must not be null.
* @return A formatted string representation of the XML document, or null if an error occurs during processing.
*/
public static String format(final Document doc) {
try {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
transformerFactory.setAttribute("indent-number", "4");
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
StringWriter stringWriter = new StringWriter();
Source source = new DOMSource(doc);
Result result = new StreamResult(stringWriter);
transformer.transform(source, result);
Source source = new DOMSource(doc);
Result result = new StreamResult(stringWriter);
transformer.transform(source, result);
String xmlString = stringWriter.toString();
log.info("MO Request: " + xmlString);
return xmlString;
} catch (TransformerException e) {
log.error("Error in XML Transformation: " + e.getMessage(), e);
}
String xmlString = stringWriter.toString();
log.info("MO Request: " + xmlString);
return xmlString;
}
catch(TransformerException e) {
log.error("Error in XML Transformation: " + e.getMessage(), e);
}
return null;
}
return null;
}
/**
* Validates whether a given XML string is well-formed.
* This method attempts to parse the input XML string and checks
* if it can successfully create a valid DOM object from it.
*
* @param xml The XML string to be validated. Must not be null.
* If null or invalid, the method will return false.
* @return true if the XML string is well-formed and can be successfully parsed;
* false otherwise, such as in the case of invalid content or parsing errors.
*/
public static boolean checkXml(final String xml) {
try {
InputStream stream = new ByteArrayInputStream(xml.getBytes());
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = dbf.newDocumentBuilder();
Document document = docBuilder.parse(stream);
return document != null;
} catch (Exception ex) {
log.warn("Exception when validating xml.", ex);
return false;
}
}
/**
* Validates whether a given XML string is well-formed.
* This method attempts to parse the input XML string and checks
* if it can successfully create a valid DOM object from it.
*
* @param xml The XML string to be validated. Must not be null.
* If null or invalid, the method will return false.
* @return true if the XML string is well-formed and can be successfully parsed;
* false otherwise, such as in the case of invalid content or parsing errors.
*/
public static boolean checkXml(final String xml) {
try {
InputStream stream = new ByteArrayInputStream(xml.getBytes());
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = dbf.newDocumentBuilder();
Document document = docBuilder.parse(stream);
return document != null;
} catch (Exception ex) {
log.warn("Exception when validating xml.", ex);
return false;
}
}
}

View File

@ -7,7 +7,11 @@ import de.neitzel.core.inject.testcomponents.test1ok.TestInterface1_2;
import de.neitzel.core.inject.testcomponents.test1ok.sub.TestComponent1_2;
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 {

View File

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

View File

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

View File

@ -7,49 +7,53 @@ import com.idealista.fpe.config.LengthRange;
import javax.crypto.KeyGenerator;
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.
*/
@SuppressWarnings("unused")
public class FF1Encryption {
/**
* Key to use for all encryption / unencryption
*/
private byte[] key;
private static final SecureRandom RANDOM = new SecureRandom();
/**
* Should to small strings be ignored?
*
* If this is set to true, small strings (less than 2 characters) will not be encrypted!
* Key to use for all encryption / unencryption.
*/
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
* @param key AES key to use.
* @param tweak tweak to use for encryption / decryption
*
* @param key AES key to use.
* @param tweak tweak to use for encryption / decryption
* @param ignoreToShortStrings Ignore strings that are to short.
*/
public FF1Encryption(byte[] key, byte[] tweak, boolean ignoreToShortStrings) {
@ -58,12 +62,13 @@ public class FF1Encryption {
/**
* Creates a new instance of FF1Encryption
* @param key AES key to use.
* @param tweak tweak to use for encryption / decryption
*
* @param key AES key to use.
* @param tweak tweak to use for encryption / decryption
* @param ignoreToShortStrings Ignore strings that are to short.
* @param domain Domain to use for encryption
* @param minLength Minimum length of string.
* @param maxLength Maximum length of string.
* @param domain Domain to use for encryption
* @param minLength Minimum length of string.
* @param maxLength Maximum length of string.
*/
public FF1Encryption(byte[] key, byte[] tweak, boolean ignoreToShortStrings, Domain domain, int minLength, int maxLength) {
this.key = key;
@ -72,15 +77,45 @@ public class FF1Encryption {
this.domain = domain;
this.minLength = minLength;
encryption = FormatPreservingEncryptionBuilder
.ff1Implementation().withDomain(domain)
this.encryption = FormatPreservingEncryptionBuilder
.ff1Implementation()
.withDomain(domain)
.withDefaultPseudoRandomFunction(key)
.withLengthRange(new LengthRange(minLength, maxLength))
.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.
*
* @param plainText Unencrypted text.
* @return Encrypted text.
*/
@ -97,6 +132,7 @@ public class FF1Encryption {
/**
* Decrypt a given text.
*
* @param cipherText Encrypted text.
* @return Decrypted text.
*/
@ -110,31 +146,4 @@ public class FF1Encryption {
// Return decrypted text.
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.
*/
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
'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',
@ -21,16 +23,38 @@ public class MainAlphabet extends BaseAlphabet {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
// 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.
* @return
*
* @return an array containing all supported characters in the alphabet
*/
@Override
public char[] availableCharacters() {
@ -39,7 +63,8 @@ public class MainAlphabet extends BaseAlphabet {
/**
* Gets the radix of the alphabet.
* @return
*
* @return the number of characters in the alphabet (radix)
*/
@Override
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.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 {
/**
* The alphabet used by the domain for encoding/decoding characters.
*/
private Alphabet _alphabet;
/**
* Helper instance performing actual transformations between characters and indices.
*/
private GenericTransformations transformations;
/**
* Constructs a new {@code MainDomain} instance and initializes its alphabet and transformation helper.
*/
public MainDomain() {
_alphabet = new MainAlphabet();
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
public Alphabet 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
public int[] transform(String 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
public String transform(int[] data) {
return transformations.transform(data);

View File

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

View File

@ -3,9 +3,27 @@ package de.neitzel.fx.component.example;
import lombok.Getter;
import lombok.Setter;
/**
* Simple address model used in the FX example component.
*/
@Getter
@Setter
public class Address {
/**
* Street name and number.
*/
private String street;
/**
* City name.
*/
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.stage.Stage;
/**
* Example JavaFX application demonstrating the {@link de.neitzel.fx.component.ComponentLoader} usage.
*/
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
public void start(Stage primaryStage) throws Exception {
// Beispielmodel initialisieren
@ -29,8 +53,4 @@ public class ExampleApp extends Application {
primaryStage.setScene(scene);
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!)
*/
public class Main {
/**
* Default constructor only
*/
public Main() {
// default constructor only
}
/**
* Additional main methode to start Application.
*
* @param args Commandline Arguments.
*/
public static void main(String[] args) {

View File

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

View File

@ -6,23 +6,44 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
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 {
@Override
public void start(Stage primaryStage) {
try {
primaryStage.setTitle("Hello World!");
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("MainWindow.fxml"));
Parent root = fxmlLoader.load();
primaryStage.setScene(new Scene(root));
primaryStage.show();
} catch (Exception ex) {
System.out.println("Exception: " + ex.getMessage());
ex.printStackTrace();
}
}
private static final Logger LOGGER = Logger.getLogger(JavaFXApp.class.getName());
public static void main(String[] args) {
launch(args);
}
/**
* 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
public void start(Stage primaryStage) {
try {
primaryStage.setTitle("Hello World!");
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("MainWindow.fxml"));
Parent root = fxmlLoader.load();
primaryStage.setScene(new Scene(root));
primaryStage.show();
} catch (Exception ex) {
LOGGER.log(Level.SEVERE, "Exception while starting JavaFXApp", ex);
}
}
}

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!)
*/
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.
*
* @param args Commandline Arguments.
*/
public static void main(String[] args) {

View File

@ -8,13 +8,49 @@ import javafx.scene.control.TextField;
import java.net.URL;
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 {
/**
* Counter for button clicks shown in the UI.
*/
private int counter = 0;
/**
* Text field UI control showing the click counter.
*/
@FXML
private TextField textField;
/**
* Default constructor only
*/
public MainWindow() {
// default constructor only
}
@Override
public void initialize(URL location, ResourceBundle resources) {
displayCounter();

View File

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

View File

@ -3,9 +3,11 @@
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?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">
<children>
<Button fx:id="button" layoutX="44.0" layoutY="70.0" mnemonicParsing="false" onAction="#onButtonClick" text="Click Me" />
<TextField fx:id="textField" layoutX="14.0" layoutY="24.0" />
</children>
<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>
<Button fx:id="button" layoutX="44.0" layoutY="70.0" mnemonicParsing="false" onAction="#onButtonClick"
text="Click Me"/>
<TextField fx:id="textField" layoutX="14.0" layoutY="24.0"/>
</children>
</AnchorPane>

View File

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

View File

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

View File

@ -1,17 +1,48 @@
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 java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 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
*/
@SuppressWarnings("unused")
public class AutoViewModel<T> {
private static final Logger LOGGER = Logger.getLogger(AutoViewModel.class.getName());
/**
* The wrapped model instance.
*/
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<>();
/**
* 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) {
this.model = model;
initProperties();
@ -27,13 +58,17 @@ public class AutoViewModel<T> {
properties.put(fieldName, prop);
// 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);
if (setter != null) {
try {
setter.invoke(model, newVal);
} catch (Exception e) {
e.printStackTrace();
LOGGER.log(Level.WARNING, "Failed to invoke setter for field " + fieldName, e);
}
}
});
@ -41,16 +76,6 @@ public class AutoViewModel<T> {
}
}
public Property<?> getProperty(String name) {
return properties.get(name);
}
public T getModel() {
return model;
}
// ========== Hilfsmethoden ==========
private boolean isGetter(Method method) {
return Modifier.isPublic(method.getModifiers())
&& method.getParameterCount() == 0
@ -68,20 +93,27 @@ public class AutoViewModel<T> {
return name;
}
private String decapitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toLowerCase() + str.substring(1);
}
// ========== Hilfsmethoden ==========
private Object invokeGetter(Method method) {
try {
return method.invoke(model);
} catch (Exception e) {
e.printStackTrace();
LOGGER.log(Level.WARNING, "Failed to invoke getter: " + method.getName(), e);
return null;
}
}
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);
}
private Method findSetterFor(Class<?> clazz, String fieldName, Class<?> valueType) {
String setterName = "set" + capitalize(fieldName);
for (Method m : clazz.getMethods()) {
@ -94,18 +126,21 @@ public class AutoViewModel<T> {
return null;
}
private String decapitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toLowerCase() + str.substring(1);
}
private String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
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);
public Property<?> getProperty(String name) {
return properties.get(name);
}
public T getModel() {
return model;
}
}

View File

@ -4,11 +4,9 @@ import de.neitzel.fx.component.controls.Binding;
import de.neitzel.fx.component.controls.FxmlComponent;
import de.neitzel.fx.component.model.BindingData;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.Pane;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@ -17,9 +15,9 @@ import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* ComponentLoader is responsible for loading JavaFX FXML components and binding
@ -27,45 +25,18 @@ import java.util.Map;
*/
@Slf4j
public class ComponentLoader {
private Map<String, Map<String, String>> nfxBindingMap = new HashMap<>();
@Getter
private Object controller;
public Parent load(URL fxmlPath) {
return load(null, fxmlPath);
}
public Parent load(String fxmlPath) {
return load(null, fxmlPath);
}
public Parent load(Object model, URL fxmlPath) {
try {
AutoViewModel<?> viewModel = new AutoViewModel<>(model);
FXMLLoader loader = new FXMLLoader(fxmlPath);
loader.setControllerFactory(type -> new ComponentController(viewModel));
Parent root = loader.load();
controller = loader.getController();
return root;
} catch (IOException e) {
throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
}
}
/**
* Loads an FXML file and binds its elements to a generated ViewModel
* based on the given POJO model.
*
* @param model the data model (POJO) to bind to the UI
* @param fxmlPath the path to the FXML file
* @return the root JavaFX node loaded from FXML
* Default constructor only
*/
public Parent load(Object model, String fxmlPath) {
return load(model, getClass().getResource(fxmlPath));
public ComponentLoader() {
// default constructor only
}
public static <T extends Parent> T load(URL fxmlUrl, Object controller, String nothing) throws IOException {
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();
@ -85,21 +56,23 @@ public class ComponentLoader {
return root;
}
@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: %s ⇄ %s%n", source.getClass(), target.getClass(), e);
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
}
}
@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: %s ⇄ %s%n", source.getClass(), target.getClass(), e);
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
}
}
@ -109,9 +82,7 @@ public class ComponentLoader {
Object source = resolveExpression(binding.getSource(), namespace);
Object target = resolveExpression(binding.getTarget(), namespace);
if (source instanceof Property && target instanceof Property) {
Property<?> sourceProp = (Property<?>) source;
Property<?> targetProp = (Property<?>) target;
if (source instanceof Property<?> sourceProp && target instanceof Property<?> targetProp) {
Class<?> sourceType = getPropertyType(sourceProp);
Class<?> targetType = getPropertyType(targetProp);
@ -124,7 +95,7 @@ public class ComponentLoader {
if (bindableForward && bindableBackward) {
bindBidirectionalSafe(sourceProp, targetProp);
} else {
log.error("⚠️ Kann bidirektionales Binding nicht durchführen: Typen inkompatibel (%s ⇄ %s)%n", sourceType, targetType);
log.error("⚠️ Kann bidirektionales Binding nicht durchführen: Typen inkompatibel ({} ⇄ {})", sourceType, targetType);
}
break;
case "unidirectional":
@ -132,13 +103,13 @@ public class ComponentLoader {
if (bindableForward) {
bindSafe(sourceProp, targetProp);
} else {
log.error("⚠️ Kann unidirektionales Binding nicht durchführen: %s → %s nicht zuweisbar%n", sourceType, targetType);
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);
log.error("Fehler beim Binding: {} → {}", binding.getSource(), binding.getTarget(), e);
}
}
}
@ -174,6 +145,42 @@ public class ComponentLoader {
return nodes;
}
public Parent load(URL fxmlPath) {
return load(null, fxmlPath);
}
public Parent load(Object model, URL fxmlPath) {
try {
AutoViewModel<?> viewModel = new AutoViewModel<>(model);
FXMLLoader loader = new FXMLLoader(fxmlPath);
loader.setControllerFactory(type -> {
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);
}
}
public Parent load(String fxmlPath) {
return load(null, fxmlPath);
}
/**
* Loads an FXML file and binds its elements to a generated ViewModel
* based on the given POJO model.
*
* @param model the data model (POJO) to bind to the UI
* @param fxmlPath the path to the FXML file
* @return the root JavaFX node loaded from FXML
*/
public Parent load(Object model, String fxmlPath) {
return load(model, getClass().getResource(fxmlPath));
}
/**
* Binds a JavaFX UI control to a ViewModel property according to the specified direction.
*
@ -181,6 +188,7 @@ public class ComponentLoader {
* @param vmProp the ViewModel property to bind to
* @param direction the direction of the binding (e.g., "bidirectional", "read")
*/
@SuppressWarnings("unused")
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 ("bidirectional".equalsIgnoreCase(direction)) {

View File

@ -11,19 +11,53 @@ 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 {
/**
* The FXML resource path to be loaded by this component. A value of {@code null} or blank disables loading.
*/
private final StringProperty fxml = new SimpleStringProperty();
/**
* Human-readable binding direction hint used by outer frameworks; defaults to "unidirectional".
*/
private final StringProperty direction = new SimpleStringProperty("unidirectional");
/**
* Optional data object that is injected into the controller of the loaded FXML when available.
*/
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();
@ -37,6 +71,10 @@ public class FxmlComponent extends StackPane {
}
}
/**
* 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));
@ -46,22 +84,50 @@ public class FxmlComponent extends StackPane {
}
}
/**
* 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)`
@ -79,6 +145,12 @@ public class FxmlComponent extends StackPane {
// 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");
@ -86,24 +158,48 @@ public class FxmlComponent extends StackPane {
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

@ -7,21 +7,22 @@ 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.
*/
@ -34,12 +35,20 @@ public class BindingData {
* 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
@ -47,37 +56,52 @@ public class BindingData {
*
* @return the current direction of the binding
*/
public String getDirection() { return direction.get(); }
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); }
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(); }
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); }
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(); }
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); }
public void setTarget(String target) {
this.target.set(target);
}
}

View File

@ -19,7 +19,9 @@ public class FXMLComponentInstances {
/** Map holding instances of all @FXMLComponent classes, indexed by class and its unique superclasses/interfaces. */
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;
/**

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
* controller constructors dynamically during instantiation.
*
* This class simplifies the process of injecting dependencies into JavaFX controllers by analyzing
* the constructors of the given controller classes at runtime. It selects the constructor that best
* matches the available dependencies in the parameter map and creates an instance of the controller.
* <p>This class analyzes available constructors of a requested controller class at runtime and
* selects the first constructor whose parameter types are all present in the internal parameter map.
* 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
* controllers with dependencies to be injected seamlessly during the FXML loading process.
* <p>It implements the {@link javafx.util.Callback} interface to integrate with the JavaFX
* {@link javafx.fxml.FXMLLoader} controller factory mechanism.
*/
public class InjectingControllerFactory implements Callback<Class<?>, Object> {
/**
* A map that stores class-to-object mappings used for dependency injection
* in controller instantiation. This map is utilized to resolve and supply
* the required dependencies for constructors during the creation of controller
* 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.
* in controller instantiation. Each key represents a parameter type and the
* associated value is the instance to be injected for that type.
*/
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
* to the parameter map used for dependency injection.
@ -81,11 +83,11 @@ public class InjectingControllerFactory implements Callback<Class<?>, Object> {
* This method checks if the parameter map contains entries for all parameter types required
* by the specified constructor.
*
* @param constructor The constructor to be evaluated for instantiability.
* @param constructor The constructor to be evaluated for instantiability.
* @param parameterMap A map where keys are parameter types and values are the corresponding
* instances available for injection.
* @return {@code true} if the constructor can be instantiated with the given parameter map;
* {@code false} otherwise.
* {@code false} otherwise.
*/
private boolean canBeInstantiated(Constructor<?> constructor, Map<Class<?>, Object> parameterMap) {
return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey);

View File

@ -11,12 +11,12 @@ import java.net.URL;
* The InjectingFXMLLoader class provides a custom implementation of JavaFX's FXMLLoader
* that supports dependency injection. It facilitates the loading of FXML files while
* dynamically injecting dependencies into controllers during the loading process.
*
* <p>
* This class utilizes the InjectingControllerFactory to enable seamless integration
* of dependency injection with JavaFX controllers. Dependencies can be added to the
* controller factory, and the loader will use this factory to instantiate controllers
* with the appropriate dependencies.
*
* <p>
* Features of this loader include:
* - Support for dependency injection into JavaFX controllers by using a custom factory.
* - 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
* 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
* required objects during controller instantiation. It plays a critical role in enabling seamless
* integration of dependency injection with JavaFX's {@link FXMLLoader}.
*
* <p>
* 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.
*
* <p>
* Key responsibilities:
* - 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
* of dependencies.
* of dependencies.
* - 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
* mappings at runtime using relevant methods.
*/
@ -51,7 +51,7 @@ public class InjectingFXMLLoader {
* Default constructor for the InjectingFXMLLoader class.
* This initializes the loader with a new instance of the {@link InjectingControllerFactory},
* which is used to provide dependency injection capabilities for JavaFX controllers.
*
* <p>
* The {@link InjectingControllerFactory} allows for the registration and dynamic injection
* of dependencies into controllers when they are instantiated during the FXML loading process.
*/
@ -100,7 +100,7 @@ public class InjectingFXMLLoader {
* available for dependency injection.
*/
private void addInjectingData(FXMLComponentInstances instances) {
for (var clazz: instances.getInstanceMap().keySet()) {
for (var clazz : instances.getInstanceMap().keySet()) {
addInjectingData(clazz, instances.getInstance(clazz));
}
}

View File

@ -54,7 +54,7 @@ public class BindingAwareFXMLLoader<T> {
* Recursively traverses the scene graph and binds controls to properties
* in the ViewModel based on custom metadata in the node properties map.
*
* @param node the current node to inspect
* @param node the current node to inspect
* @param viewModel the ViewModel holding the properties
*/
private void bindNodesRecursively(Node node, GenericViewModel<T> viewModel) {
@ -92,9 +92,9 @@ public class BindingAwareFXMLLoader<T> {
* Binds two JavaFX properties according to the specified binding direction.
*
* @param controlProperty the property of the control (e.g. TextField.textProperty)
* @param modelProperty the property from the ViewModel
* @param direction the direction of the binding
* @param <V> the value type of the property
* @param modelProperty the property from the ViewModel
* @param direction the direction of the binding
* @param <V> the value type of the property
*/
private <V> void bind(Property<V> controlProperty, Property<V> modelProperty, BindDirection direction) {
switch (direction) {

View File

@ -12,18 +12,21 @@ import java.util.ResourceBundle;
* @param <T> the type of the model
*/
public class GenericViewController<T> implements Initializable {
/**
* The GenericViewModel instance wrapping the underlying model.
*/
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) {
this.viewModel = new GenericViewModel<>(model);
public GenericViewController() {
// default constructor only ...
}
/**
@ -35,10 +38,19 @@ public class GenericViewController<T> implements Initializable {
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.
*
* @param location the location used to resolve relative paths for the root object, or null if unknown
* @param location the location used to resolve relative paths for the root object, or null if unknown
* @param resources the resources used to localize the root object, or null if not localized
*/
@Override

View File

@ -1,6 +1,12 @@
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.IntrospectionException;
@ -80,7 +86,7 @@ public class GenericViewModel<T> {
/**
* Creates an appropriate JavaFX Property instance for the given type and initial value.
*
* @param type the type of the property (e.g., String.class, int.class)
* @param type the type of the property (e.g., String.class, int.class)
* @param initialValue the initial value of the property
* @return a new JavaFX Property instance
*/
@ -98,8 +104,8 @@ public class GenericViewModel<T> {
* Retrieves the JavaFX property associated with the given name and type.
*
* @param propertyClass the expected property type (e.g., StringProperty.class)
* @param name the name of the model field
* @param <P> the type of the Property
* @param name the name of the model field
* @param <P> the type of the Property
* @return the corresponding JavaFX property
* @throws IllegalArgumentException if the property doesn't exist or the type mismatches
*/

View File

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

View File

@ -14,7 +14,7 @@ import java.util.Set;
/**
* 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
* fields from the serialized output. The set of excluded field names is provided during the adapter's initialization.
*
@ -22,115 +22,116 @@ import java.util.Set;
*/
public class ExcludeFieldsAdapter<T> extends TypeAdapter<T> {
/**
* The type of objects that this adapter handles for serialization and deserialization.
*
* This is a generic class type parameter representing the class of objects that the
* {@link ExcludeFieldsAdapter} instance is designed to process. It ensures type
* safety and guarantees the compatibility of the adapter with a specific object type.
*/
private final Class<T> type;
/**
* A set containing the names of fields to be excluded during JSON serialization.
*
* 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
* of the adapter.
*/
private final Set<String> excludedFields;
/**
* 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
* {@link ExcludeFieldsAdapter} instance is designed to process. It ensures type
* safety and guarantees the compatibility of the adapter with a specific object type.
*/
private final Class<T> type;
/**
* An instance of the Gson library used for JSON serialization and deserialization.
*
* 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,
* it can be lazily initialized using a GsonBuilder.
*/
private Gson gson;
/**
* 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
* serialized JSON representation of an object. The fields to be excluded are specified during the initialization
* of the adapter.
*/
private final Set<String> excludedFields;
/**
* A {@link GsonBuilder} instance used for configuring and creating {@link Gson} objects within the adapter.
*
* 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.
*
* 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.
*/
private GsonBuilder gsonBuilder;
/**
* 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
* custom serialization and deserialization logic. If not explicitly provided during initialization,
* it can be lazily initialized using a GsonBuilder.
*/
private Gson gson;
/**
* Creates a new instance of {@code ExcludeFieldsAdapter} for managing the serialization and deserialization
* of objects of the specified {@code type}, while excluding certain fields during serialization.
*
* @param type the class type of the objects to be serialized and deserialized
* @param excludedFields the set of field names to be excluded during serialization
* @param gson the Gson instance to be used for serialization and deserialization
*/
public ExcludeFieldsAdapter(Class<T> type, Set<String> excludedFields, Gson gson) {
this.type = type;
this.excludedFields = new HashSet<>(excludedFields);
this.gsonBuilder = null;
this.gson = gson;
}
/**
* 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,
* 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.
* It ensures that the adapter can defer the creation of a Gson object until it is explicitly required.
*/
private GsonBuilder gsonBuilder;
/**
* Constructs an instance of {@code ExcludeFieldsAdapter} that selectively excludes specified fields from
* the JSON output based on the configuration provided during initialization.
*
* @param type the class of the type to be serialized and deserialized
* @param excludedFields a set of field names to be excluded from the serialized JSON output
* @param gsonBuilder an instance of {@code GsonBuilder} used to create a custom {@code Gson} instance if needed
*/
public ExcludeFieldsAdapter(Class<T> type, Set<String> excludedFields, GsonBuilder gsonBuilder) {
this.type = type;
this.excludedFields = new HashSet<>(excludedFields);
this.gsonBuilder = gsonBuilder;
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.
* The object is first converted into a {@link JsonObject}, then the fields listed in the excluded fields set are removed from the JSON
* representation before writing it to the provided {@link JsonWriter}.
*
* @param out the {@link JsonWriter} to write the serialized JSON data to
* @param value the object of type {@code T} to serialize into JSON
* @throws IOException if an I/O error occurs during writing
*/
@Override
public void write(JsonWriter out, T value) throws IOException {
JsonObject obj = getGson().toJsonTree(value).getAsJsonObject();
for (String field : excludedFields) {
obj.remove(field);
/**
* Creates a new instance of {@code ExcludeFieldsAdapter} for managing the serialization and deserialization
* of objects of the specified {@code type}, while excluding certain fields during serialization.
*
* @param type the class type of the objects to be serialized and deserialized
* @param excludedFields the set of field names to be excluded during serialization
* @param gson the Gson instance to be used for serialization and deserialization
*/
public ExcludeFieldsAdapter(Class<T> type, Set<String> excludedFields, Gson gson) {
this.type = type;
this.excludedFields = new HashSet<>(excludedFields);
this.gsonBuilder = null;
this.gson = gson;
}
Streams.write(obj, out);
}
/**
* Constructs an instance of {@code ExcludeFieldsAdapter} that selectively excludes specified fields from
* the JSON output based on the configuration provided during initialization.
*
* @param type the class of the type to be serialized and deserialized
* @param excludedFields a set of field names to be excluded from the serialized JSON output
* @param gsonBuilder an instance of {@code GsonBuilder} used to create a custom {@code Gson} instance if needed
*/
public ExcludeFieldsAdapter(Class<T> type, Set<String> excludedFields, GsonBuilder gsonBuilder) {
this.type = type;
this.excludedFields = new HashSet<>(excludedFields);
this.gsonBuilder = gsonBuilder;
this.gson = null;
}
/**
* Reads a JSON input stream and deserializes it into an object of the specified type.
*
* @param in the {@link JsonReader} to read the JSON input from
* @return an instance of type {@code T} deserialized from the JSON input
* @throws IOException if an error occurs during reading or deserialization
*/
@Override
public T read(JsonReader in) throws IOException {
return getGson().fromJson(in, type);
}
/**
* 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
* representation before writing it to the provided {@link JsonWriter}.
*
* @param out the {@link JsonWriter} to write the serialized JSON data to
* @param value the object of type {@code T} to serialize into JSON
* @throws IOException if an I/O error occurs during writing
*/
@Override
public void write(JsonWriter out, T value) throws IOException {
JsonObject obj = getGson().toJsonTree(value).getAsJsonObject();
for (String field : excludedFields) {
obj.remove(field);
}
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.
*
* @param in the {@link JsonReader} to read the JSON input from
* @return an instance of type {@code T} deserialized from the JSON input
* @throws IOException if an error occurs during reading or deserialization
*/
@Override
public T read(JsonReader in) throws IOException {
return getGson().fromJson(in, type);
}
}

View File

@ -1,6 +1,12 @@
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.time.Instant;
@ -18,35 +24,35 @@ import java.time.format.DateTimeParseException;
*/
public class InstantTypeAdapter implements JsonSerializer<Instant>, JsonDeserializer<Instant> {
/**
* Serializes an {@link Instant} object into its ISO-8601 string representation as a {@link JsonElement}.
*
* @param src the {@link Instant} object to be serialized
* @param typeOfSrc the specific genericized type of {@code src} (typically ignored in this implementation)
* @param context the context of the serialization process to provide custom serialization logic
* @return a {@link JsonPrimitive} containing the ISO-8601 formatted string representation of the {@link Instant}
*/
@Override
public JsonElement serialize(Instant src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString()); // ISO-8601: "2025-04-02T10:15:30Z"
}
/**
* Deserializes a JSON element into an {@link Instant} object.
*
* @param json the JSON data being deserialized
* @param typeOfT the specific genericized type of the object to deserialize
* @param context the deserialization context
* @return the deserialized {@link Instant} object
* @throws JsonParseException if the JSON element does not represent a valid ISO-8601 instant format
*/
@Override
public Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
try {
return Instant.parse(json.getAsString());
} catch (DateTimeParseException e) {
throw new JsonParseException("Invalid Instant format", e);
/**
* Serializes an {@link Instant} object into its ISO-8601 string representation as a {@link JsonElement}.
*
* @param src the {@link Instant} object to be serialized
* @param typeOfSrc the specific genericized type of {@code src} (typically ignored in this implementation)
* @param context the context of the serialization process to provide custom serialization logic
* @return a {@link JsonPrimitive} containing the ISO-8601 formatted string representation of the {@link Instant}
*/
@Override
public JsonElement serialize(Instant src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString()); // ISO-8601: "2025-04-02T10:15:30Z"
}
/**
* Deserializes a JSON element into an {@link Instant} object.
*
* @param json the JSON data being deserialized
* @param typeOfT the specific genericized type of the object to deserialize
* @param context the deserialization context
* @return the deserialized {@link Instant} object
* @throws JsonParseException if the JSON element does not represent a valid ISO-8601 instant format
*/
@Override
public Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
try {
return Instant.parse(json.getAsString());
} catch (DateTimeParseException e) {
throw new JsonParseException("Invalid Instant format", e);
}
}
}
}

View File

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

View File

@ -1,5 +1,6 @@
package de.neitzel.log4j;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import java.io.File;
@ -9,6 +10,7 @@ import java.net.URISyntaxException;
* Utility class for managing Log4j configurations. Provides methods to set up
* Log4j configurations from default files, resources, or command line arguments.
*/
@SuppressWarnings("unused")
public class Log4jUtils {
/**
@ -24,13 +26,13 @@ public class Log4jUtils {
*/
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.
*
* @return true if the "log4j.configuration" property is defined, false otherwise.
* Default constructor only
*/
public static boolean isLog4jConfigFileSet() {
return System.getProperty("log4j.configuration") != null;
public Log4jUtils() {
// default constructor only
}
/**
@ -38,7 +40,7 @@ public class Log4jUtils {
* 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
* 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)}
* method using predefined defaults.
*/
@ -46,22 +48,6 @@ public class Log4jUtils {
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
* 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()) {
PropertyConfigurator.configure(log4jConfigFile);
} 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);
}else {
} else {
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"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<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>

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
* representation, equality checks, and hashing.
*/
@SuppressWarnings("unused")
public class ImapAccount {
/**
@ -15,15 +16,6 @@ public class ImapAccount {
* of the mail server that handles IMAP communication.
*/
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.
@ -31,66 +23,94 @@ public class ImapAccount {
* to the IMAP server.
*/
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.
* This is used to authenticate the user when connecting to the IMAP server.
*/
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".
* This field determines the communication protocol for interacting with the server.
*/
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.
*
* @return the protocol as a String, which defines the communication method for the IMAP account.
*/
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; }
public String getProtocol() {
return 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.
* Sets the protocol used by the IMAP account.
*
* @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() {
return String.format("ImapAccount(%s, %s, %s, %s",
server,
user,
password.length()==0 ? "''" : "########" ,
protocol);
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
@ -100,7 +120,8 @@ public class ImapAccount {
*
* @return An integer hash code representing this ImapAccount instance.
*/
@Override public int hashCode() {
@Override
public int hashCode() {
return Objects.hash(server, user, password, protocol);
}
@ -111,7 +132,8 @@ public class ImapAccount {
* @param obj the object to compare with this instance
* @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.
if (!(obj instanceof ImapAccount)) return false;
@ -120,9 +142,24 @@ public class ImapAccount {
// Check the comparison of all field
ImapAccount other = (ImapAccount) obj;
return (server == other.server || (server != null && server.equals(other.server))) &&
(user == other.user || (user != null && user.equals(other.user))) &&
(password == other.password || (password != null && password.equals(other.password))) &&
(protocol == other.protocol || (protocol != null && protocol.equals(other.protocol)));
return Objects.equals(server, other.server) &&
Objects.equals(user, other.user) &&
Objects.equals(password, other.password) &&
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 javax.mail.*;
import java.util.*;
import javax.mail.Flags;
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
@ -44,13 +54,20 @@ public class ImapAccountMonitor {
*/
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
* using the provided account credentials and protocol.
*
* @param account the IMAP account containing details such as server, user, password, and protocol
* @throws NoSuchProviderException if the specified mail store protocol is not available
* @throws MessagingException if the connection to the mail server fails
* @throws MessagingException if the connection to the mail server fails
*/
public ImapAccountMonitor(ImapAccount account) throws NoSuchProviderException, MessagingException {
log.trace("Constructor ({})", account.toString());
@ -77,19 +94,19 @@ public class ImapAccountMonitor {
* Closes the resources associated with the current instance, including
* IMAP folders and the store. The method ensures that all folders are
* closed properly, followed by the closure of the store.
*
* <p>
* Any exceptions encountered during the closure of folders or the store
* are caught and logged without interrupting the overall closing process.
* This is to ensure that all resources are attempted to be closed even if
* an error occurs with one of them.
*
* <p>
* The method logs the start and end of the closing process for traceability.
*/
public void close() {
log.trace("close() called.");
// Close the folders
for (Folder folder: folders.values()) {
for (Folder folder : folders.values()) {
try {
folder.close(false);
} catch (MessagingException ex) {
@ -152,28 +169,28 @@ public class ImapAccountMonitor {
* 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
* are marked as seen.
*
* <p>
* 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
* as seen and a NewMailEvent is raised for it.
*
* <p>
* 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.
*
* <p>
* Note: This method relies on external logging (via `log`) and appropriate event handling
* 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.
*/
public void check() {
log.trace("check() called.");
// Loop through all folders that are monitored
for (Folder folder: folders.values()) {
for (Folder folder : folders.values()) {
try {
// Loop through all messages.
Message[] messages = folder.getMessages();
for (Message message: messages) {
for (Message message : messages) {
// Check if message wasn't seen and wasn't deleted
if (!message.isSet(Flags.Flag.SEEN) && !message.isSet(Flags.Flag.DELETED)) {
// Mark message as seen.
@ -193,11 +210,35 @@ public class ImapAccountMonitor {
}
/**
* 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.
* 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.
*/
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.
@ -222,35 +263,4 @@ public class ImapAccountMonitor {
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

@ -37,8 +37,8 @@ public class NewEmailEvent extends EventObject {
* This event encapsulates information about a newly received email,
* including its associated message and the source of the event.
*
* @param source the object on which the event initially occurred; typically represents
* the source of the email event, such as an email client or server.
* @param source the object on which the event initially occurred; typically represents
* the source of the email event, such as an email client or server.
* @param message the Message object representing the email associated with this event.
*/
public NewEmailEvent(Object source, Message message) {

View File

@ -8,15 +8,49 @@ import javax.mail.internet.MimeMultipart;
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 {
/**
* Gets the text of the email message.
* @param message Message to get the text from.
* @return Text of the email message.
* @throws IOException
* @throws MessagingException
* Private constructor for utility class.
* Prevents instantiation of this utility class.
*/
private MessageUtils() {
// 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 {
String result = "";
@ -30,11 +64,23 @@ public class MessageUtils {
}
/**
* Gets the text of a MimeMultipart.
* @param mimeMultipart MimeMultipart to get the text from.
* @return Text of the MimeMultipart instance.
* @throws IOException
* @throws MessagingException
* Extracts the textual content from a {@link MimeMultipart} instance.
*
* <p>Algorithm:
* <ul>
* <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(
MimeMultipart mimeMultipart) throws IOException, MessagingException {
@ -47,20 +93,29 @@ public class MessageUtils {
// alternatives appear in an order of increasing
// faithfulness to the original content. Customize as req'd.
return getTextFromBodyPart(mimeMultipart.getBodyPart(count - 1));
String result = "";
StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) {
BodyPart bodyPart = mimeMultipart.getBodyPart(i);
result += getTextFromBodyPart(bodyPart);
result.append(getTextFromBodyPart(bodyPart));
}
return result;
return result.toString();
}
/**
* Gets the text of a BodyPart
* @param bodyPart BodyPart to get text from.
* @return Text of body part or empty string if not a known type.
* @throws IOException
* @throws MessagingException
* Extracts textual content from a single {@link BodyPart}.
*
* <p>Behavior:
* <ul>
* <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(
BodyPart bodyPart) throws IOException, MessagingException {
@ -73,8 +128,8 @@ public class MessageUtils {
//String html = (String) bodyPart.getContent();
// Not parsing the html right now!
// result = org.jsoup.Jsoup.parse(html).text();
} else if (bodyPart.getContent() instanceof MimeMultipart){
result = getTextFromMimeMultipart((MimeMultipart)bodyPart.getContent());
} else if (bodyPart.getContent() instanceof MimeMultipart) {
result = getTextFromMimeMultipart((MimeMultipart) bodyPart.getContent());
}
return result;
}

View File

@ -1,8 +1,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: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">
<description>
@ -11,27 +11,27 @@
<rule ref="category/java/bestpractices.xml">
<!-- System.println is ok in simple Console App -->
<exclude name="SystemPrintln" />
<exclude name="SystemPrintln"/>
</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">
<properties>
<property name="packages" value="de.kneitzel" />
<property name="packages" value="de.kneitzel"/>
<!-- <property name="classes" value="" /> -->
</properties>
</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>