Merge pull request #5 from kneitzel/feature/first-neitzellib-content

Feature/first neitzellib content
This commit is contained in:
Konrad Neitzel 2025-04-11 21:38:35 +02:00 committed by GitHub
commit 5798955c65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 975 additions and 98 deletions

View File

@ -2,11 +2,9 @@
## A Java Utilities Library
This repository is **not** a production-ready library, but rather a loose collection of small helpers, utilities, and ideas that might come in handy for Java developers.
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.
The code in this project is intended as a **starting point** or **inspiration** for your own solutions. It is often easier to copy and adapt individual classes as needed instead of using the whole library as a dependency.
Feel free to explore, copy, modify, and improve whatever you find useful.
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.
@ -17,6 +15,12 @@ This is free and unencumbered software released into the public domain. Please s
---
## 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.
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
### core

View File

@ -1,7 +1,9 @@
package de.neitzel.core.commandline;
import lombok.*;
import lombok.extern.log4j.Log4j;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.Singular;
import lombok.extern.slf4j.Slf4j;
import java.util.List;

View File

@ -48,7 +48,7 @@ public class Configuration {
*
* @param properties the Properties object containing configuration key-value pairs
*/
public Configuration(Properties properties) {
public Configuration(final Properties properties) {
this.properties = properties;
}

View File

@ -1,16 +1,20 @@
package de.neitzel.core.image;
import lombok.extern.slf4j.Slf4j;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Utility class for scaling images while maintaining the aspect ratio.
* Provides methods to create scaled images either as byte arrays or InputStreams.
*/
@Slf4j
public class ImageScaler {
/**
@ -148,7 +152,8 @@ public class ImageScaler {
ImageIO.write(scaledImage, TARGET_FORMAT, stream);
return stream.toByteArray();
}
} catch (Exception ex) {
} catch (IOException ex) {
log.error("IOException while scaling image.", ex);
return null;
}
}

View File

@ -0,0 +1,141 @@
package de.neitzel.core.inject;
import de.neitzel.core.inject.annotation.Config;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Represents the context of the application and serves as the foundation of a dependency
* injection framework.
*
* 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
* requirements and scope. The framework supports constructor-based dependency injection
* for resolving and creating components.
*/
public class ApplicationContext {
private final Map<Class<?>, ComponentData> components;
/**
* Erstellt einen ApplicationContext auf Basis eines expliziten Package-Namens.
*
* @param basePackage das Basis-Package, das nach Komponenten durchsucht werden soll
*/
public ApplicationContext(String basePackage) {
ComponentScanner scanner = new ComponentScanner(basePackage);
this.components = scanner.getInstantiableComponents();
}
/**
* Erstellt einen ApplicationContext auf Basis einer Startklasse.
* Das zu scannende Package wird aus der @Config Annotation gelesen (basePackage).
* Wenn keine Annotation vorhanden ist oder kein basePackage gesetzt wurde, wird
* das Package der übergebenen Klasse verwendet.
*
* @param configClass Klasse mit oder ohne @Config Annotation
*/
public ApplicationContext(Class<?> configClass) {
String basePackage;
Config config = configClass.getAnnotation(Config.class);
if (config != null && !config.basePackage().isEmpty()) {
basePackage = config.basePackage();
} else {
basePackage = configClass.getPackageName();
}
ComponentScanner scanner = new ComponentScanner(basePackage);
this.components = scanner.getInstantiableComponents();
}
/**
* Retrieves an instance of the specified component type from the application context.
*
* 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 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
*/
@SuppressWarnings("unchecked")
public <T> T getComponent(Class<? extends T> type) {
ComponentData data = components.get(type);
if (data == null) {
throw new IllegalArgumentException("No component found for type: " + type.getName());
}
Scope scope = data.getScope();
Object instance = data.getInstance();
if (scope == Scope.SINGLETON && instance != null) {
return (T) instance;
}
try {
Constructor<?>[] constructors = data.getType().getConstructors();
for (Constructor<?> constructor : constructors) {
if (canBeInstantiated(constructor, components)) {
Class<?>[] paramTypes = constructor.getParameterTypes();
Object[] parameters = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
parameters[i] = getComponent(paramTypes[i]);
}
instance = constructor.newInstance(parameters);
if (scope == Scope.SINGLETON) {
data.setInstance(instance);
}
return (T) instance;
}
}
throw new IllegalStateException("Kein passender Konstruktor gefunden für " + type.getName());
} catch (Exception e) {
throw new RuntimeException("Fehler beim Erstellen der Instanz für " + type.getName(), e);
}
}
/**
* Determines if a given constructor can be instantiated using the provided parameter map.
*
* 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 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
*/
private boolean canBeInstantiated(Constructor<?> constructor, Map<Class<?>, ComponentData> parameterMap) {
return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey);
}
/**
* Registers a singleton instance of a specific type into the application context.
*
* 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 instance the singleton instance of the component to register
* @throws IllegalStateException if the type is already registered with a different instance
*/
public <T> void addSingleton(Class<? extends T> type, T instance) {
if (components.get(type) == null ||
!Objects.equals(components.get(type).getType().getName(), type.getName())) {
components.put(type, new ComponentData(type, instance));
} else {
throw new IllegalStateException("Type cannot be replaced: " + type.getName());
}
}
}

View File

@ -0,0 +1,89 @@
package de.neitzel.core.inject;
import lombok.Getter;
/**
* Represents a component in a dependency injection framework.
*
* 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
* instantiated as a singleton or as a prototype.
*/
@Getter
public class ComponentData {
/**
* Represents the type of the component being managed by the dependency injection framework.
*
* 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.
*/
private final Class<?> type;
/**
* Defines the lifecycle and instantiation rules for the associated component.
*
* 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).
*
* This variable is immutable and represents the specific {@code Scope} assigned
* to the component, influencing its behavior within the dependency injection framework.
*/
private final Scope scope;
/**
* Stores the instantiated object associated with the component.
*
* 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
* {@code PROTOTYPE} scope, this field may be null since a new instance is created
* upon each request.
*/
private Object instance;
/**
* Constructs a new Component instance with the specified type and scope.
*
* @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) {
this.type = type;
this.scope = scope;
}
/**
* Constructs a new ComponentData instance with the specified type and an initial instance.
*
* This can be used to add Singletons manually without scanning for them.
*
* @param type the class type of the component
* @param instance the initial instance of the component
*/
public ComponentData(Class<?> type, Object instance) {
this.type = type;
this.scope = Scope.SINGLETON;
this.instance = instance;
}
/**
* Sets the instance for this component if it is configured with a {@code SINGLETON} scope.
* This method ensures that the instance is only set once for a {@code SINGLETON} component.
* If an instance has already been set, it throws an {@link IllegalStateException}.
*
* @param instance the object instance to associate with this component
* @throws IllegalStateException if an instance has already been set for this {@code SINGLETON} component
*/
public void setInstance(Object instance) {
if (scope != Scope.SINGLETON) {
return;
}
if (this.instance != null) {
throw new IllegalStateException("Instance already set for singleton: " + type.getName());
}
this.instance = instance;
}
}

View File

@ -1,6 +1,7 @@
package de.neitzel.core.inject;
import de.neitzel.core.inject.annotation.Component;
import de.neitzel.core.inject.annotation.Inject;
import org.reflections.Reflections;
import java.lang.reflect.Constructor;
@ -13,7 +14,7 @@ import java.util.stream.Collectors;
* The resulting analysis identifies unique and shared interfaces/superclasses as well as
* potentially instantiable components, collecting relevant errors if instantiation is not feasible.
*/
public class InjectableComponentScanner {
public class ComponentScanner {
/**
* A set that stores classes annotated with {@code @Component}, representing
@ -30,7 +31,7 @@ public class InjectableComponentScanner {
* This field is immutable, ensuring thread safety and consistent state
* throughout the lifetime of the {@code InjectableComponentScanner}.
*/
private final Set<Class<?>> fxmlComponents = new HashSet<>();
private final Set<Class<?>> components = new HashSet<>();
/**
* A set of component types that are not uniquely associated with a single implementation.
@ -75,7 +76,7 @@ public class InjectableComponentScanner {
* The resolution process checks if a component can be instantiated based on its
* constructor dependencies being resolvable using known types or other registered components.
*/
private final Map<Class<?>, Class<?>> instantiableComponents = new HashMap<>();
private final Map<Class<?>, ComponentData> instantiableComponents = new HashMap<>();
/**
* A list of error messages encountered during the scanning and analysis
@ -96,7 +97,7 @@ public class InjectableComponentScanner {
*
* @param basePackage the base package to scan for injectable components
*/
public InjectableComponentScanner(String basePackage) {
public ComponentScanner(String basePackage) {
scanForComponents(basePackage);
analyzeComponentTypes();
resolveInstantiableComponents();
@ -110,7 +111,7 @@ public class InjectableComponentScanner {
*/
private void scanForComponents(String basePackage) {
Reflections reflections = new Reflections(basePackage);
fxmlComponents.addAll(reflections.getTypesAnnotatedWith(Component.class));
components.addAll(reflections.getTypesAnnotatedWith(Component.class));
}
/**
@ -135,7 +136,7 @@ public class InjectableComponentScanner {
private void analyzeComponentTypes() {
Map<Class<?>, List<Class<?>>> superTypesMap = new HashMap<>();
for (Class<?> component : fxmlComponents) {
for (Class<?> component : components) {
Set<Class<?>> allSuperTypes = getAllSuperTypes(component);
for (Class<?> superType : allSuperTypes) {
@ -156,44 +157,48 @@ public class InjectableComponentScanner {
}
/**
* Resolves components from the set of scanned classes that can be instantiated based on their constructors
* and existing known types. The method iteratively processes classes to identify those whose dependencies
* can be satisfied, marking them as resolved and registering them alongside their supertypes.
* 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.
*
* If progress is made during a pass (i.e., a component is resolved), the process continues until no more
* components can be resolved. Any components that remain unresolved are recorded for further inspection.
* 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.
* 2. Registering resolvable components and their superclasses/interfaces into the known
* 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.
*
* The resolved components are stored in a map where each type and its supertypes are associated
* with the component class itself. This map allows for subsequent lookups when verifying dependency satisfiability.
* At the end of the resolution process:
* - Resolvable components are added to the `instantiableComponents` map, which maps types
* to their corresponding instantiable implementations.
* - Unresolved components are identified, and error details are collected to highlight
* dependencies or conflicts preventing their instantiation.
*
* If unresolved components remain after the resolution process, detailed instantiation errors are collected
* for debugging or logging purposes.
*
* This method depends on the following utility methods:
* - {@link #canInstantiate(Class, Set)}: Determines if a component can be instantiated based on existing known types.
* - {@link #registerComponentWithSuperTypes(Class, Map)}: Registers a component and its supertypes in the known types map.
* - {@link #collectInstantiationErrors(Set)}: Collects detailed error messages for unresolved components.
* If errors are encountered due to unresolved components, they are logged for further analysis.
*/
private void resolveInstantiableComponents() {
Set<Class<?>> resolved = new HashSet<>();
Set<Class<?>> unresolved = new HashSet<>(fxmlComponents);
Map<Class<?>, Class<?>> knownTypes = new HashMap<>();
Set<Class<?>> unresolved = new HashSet<>(components);
Map<Class<?>, ComponentData> knownTypes = new HashMap<>();
Set<Class<?>> resolvableNow;
boolean progress;
do {
progress = false;
Iterator<Class<?>> iterator = unresolved.iterator();
resolvableNow = unresolved.stream()
.filter(c -> canInstantiate(c, knownTypes.keySet()))
.collect(Collectors.toSet());
while (iterator.hasNext()) {
Class<?> component = iterator.next();
if (canInstantiate(component, knownTypes.keySet())) {
resolved.add(component);
registerComponentWithSuperTypes(component, knownTypes);
iterator.remove();
progress = true;
for (Class<?> clazz : resolvableNow) {
Component annotation = clazz.getAnnotation(Component.class);
ComponentData componentInfo = new ComponentData(clazz, annotation.scope());
resolved.add(clazz);
registerComponentWithSuperTypes(componentInfo, knownTypes);
}
}
} while (progress);
unresolved.removeAll(resolvableNow);
} while (!resolvableNow.isEmpty());
instantiableComponents.putAll(knownTypes);
@ -203,16 +208,17 @@ public class InjectableComponentScanner {
}
/**
* Registers the given component class in the provided map. The component is registered along with all of its
* accessible superclasses and interfaces, unless those types are identified as non-unique.
* 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 component class to register
* @param knownTypes the map where the component and its types will be registered
* @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(Class<?> component, Map<Class<?>, Class<?>> knownTypes) {
knownTypes.put(component, component);
private void registerComponentWithSuperTypes(ComponentData component, Map<Class<?>, ComponentData> knownTypes) {
knownTypes.put(component.getType(), component);
for (Class<?> superType : getAllSuperTypes(component)) {
for (Class<?> superType : getAllSuperTypes(component.getType())) {
if (!notUniqueTypes.contains(superType)) {
knownTypes.put(superType, component);
}
@ -220,24 +226,40 @@ public class InjectableComponentScanner {
}
/**
* Determines whether a given class can be instantiated based on its constructors and
* the provided known types. A class is considered instantiable if it has a parameterless
* constructor or if all the parameter types of its constructors are present in the known types.
* Determines whether the specified component class can be instantiated based on the provided set
* of known types. A component is considered instantiable if it has at least one constructor where
* all parameter types are contained in the known types set or if it has a no-argument constructor.
* Additionally, all fields annotated with {@code @Inject} must have their types present in the
* known types set.
*
* @param component the class to check for instantiation eligibility
* @param knownTypes the set of types known to be instantiable and available for constructor injection
* @return true if the class can be instantiated; false otherwise
* @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
*/
private boolean canInstantiate(Class<?> component, Set<Class<?>> knownTypes) {
boolean hasValidConstructor = false;
for (Constructor<?> constructor : component.getConstructors()) {
Class<?>[] paramTypes = constructor.getParameterTypes();
if (paramTypes.length == 0 || Arrays.stream(paramTypes).allMatch(knownTypes::contains)) {
return true;
hasValidConstructor = true;
break;
}
}
if (!hasValidConstructor) {
return false;
}
for (var field : component.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class) && !knownTypes.contains(field.getType())) {
return false;
}
}
return true;
}
/**
* Collects the instantiation errors for a set of unresolved classes and appends
* detailed error messages to the internal error list. This method analyzes unresolved
@ -256,7 +278,7 @@ public class InjectableComponentScanner {
Class<?>[] paramTypes = constructor.getParameterTypes();
List<Class<?>> problematicTypes = Arrays.stream(paramTypes)
.filter(t -> !instantiableComponents.containsKey(t) && !notUniqueTypes.contains(t))
.collect(Collectors.toList());
.toList();
if (problematicTypes.isEmpty()) {
possibleWithUniqueTypes = true;
@ -317,7 +339,7 @@ public class InjectableComponentScanner {
* @return A map where the key is a class type and the value is the corresponding class implementation
* that can be instantiated.
*/
public Map<Class<?>, Class<?>> getInstantiableComponents() {
public Map<Class<?>, ComponentData> getInstantiableComponents() {
return instantiableComponents;
}

View File

@ -0,0 +1,41 @@
package de.neitzel.core.inject;
/**
* Represents the scope of a component in a dependency injection framework.
* <p>
* The scope determines the lifecycle and visibility of a component instance
* within the framework. The available scope types are:
* <p>
* - {@code SINGLETON}: A single instance of the component is created and
* shared across the entire application.
* - {@code PROTOTYPE}: A new instance of the component is created
* each time it is requested or injected.
* <p>
* This enumeration is typically used in conjunction with the {@code @Component}
* annotation to specify the instantiation behavior of a component.
*/
public enum Scope {
/**
* Specifies that the component should be instantiated as a singleton within the
* dependency injection framework. A single instance of the component is created
* and shared across the entire application lifecycle, ensuring efficient reuse
* of resources and consistent behavior for the component throughout the application.
* <p>
* This scope is typically applied to components where maintaining a single shared
* instance is necessary or beneficial, such as managing shared state or providing
* utility services.
*/
SINGLETON,
/**
* Specifies that the component should be instantiated as a prototype within the
* dependency injection framework. A new instance of the component is created
* each time it is requested or injected, ensuring that no two requests or injections
* share the same instance.
* <p>
* This scope is typically applied to components where maintaining unique instances
* per request is necessary, such as request-specific data processing or handling
* of transient state. It allows for complete isolation between multiple users
* or operations requiring individual resources.
*/
PROTOTYPE
}

View File

@ -1,5 +1,7 @@
package de.neitzel.core.inject.annotation;
import de.neitzel.core.inject.Scope;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -24,4 +26,12 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
/**
* Defines the scope of a component within the dependency injection framework.
* The scope determines whether the component is instantiated as a singleton or
* as a prototype.
*
* @return the scope of the component, defaulting to {@code Scope.SINGLETON}.
*/
Scope scope() default Scope.SINGLETON;
}

View File

@ -0,0 +1,37 @@
package de.neitzel.core.inject.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that an annotated class is a configuration class within a dependency
* injection framework. Classes annotated with {@code @Config} are used as markers
* for defining settings and application-specific configurations required by the
* dependency injection mechanism.
*
* Typically, configuration classes provide metadata required for setting up the
* framework, such as specifying the base package to scan for components.
*
* 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.
*
* Attributes:
* - {@code basePackage}: Specifies the package name where the framework should scan
* for classes annotated with dependency injection annotations such as {@code @Component}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Config {
/**
* Specifies the base package for component scanning within the dependency
* injection framework. Classes within the defined package and its sub-packages
* 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.
*/
String basePackage() default "";
}

View File

@ -0,0 +1,24 @@
package de.neitzel.core.inject.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that a field is a candidate for dependency injection within
* 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.
*
* This annotation must be applied at the field level and is retained at runtime
* to enable reflection-based identification and assignment of dependencies.
*
* 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.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}

View File

@ -1,6 +1,5 @@
package de.neitzel.core.sql;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

View File

@ -1,7 +1,5 @@
package de.neitzel.core.sql;
import lombok.RequiredArgsConstructor;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;

View File

@ -1,6 +1,5 @@
package de.neitzel.core.util;
import lombok.extern.log4j.Log4j;
import lombok.extern.slf4j.Slf4j;
import java.io.*;

View File

@ -0,0 +1,128 @@
package de.neitzel.core.util;
import java.time.Duration;
import java.time.Instant;
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.
*/
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;
/**
* 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;
}
/**
* 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");
}
}
String result = sb.toString().trim();
return result.isEmpty() ? "0ms" : result;
}
/**
* 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);
}
}

View File

@ -1,7 +1,6 @@
package de.neitzel.core.util;
import lombok.NonNull;
import lombok.extern.log4j.Log4j;
import lombok.extern.slf4j.Slf4j;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

View File

@ -16,7 +16,7 @@ class InjectableComponentScannerTest {
*/
@Test
void testLoadComponents() {
InjectableComponentScanner scanner = new InjectableComponentScanner("de.neitzel.core.inject.testcomponents.test1ok");
ComponentScanner scanner = new ComponentScanner("de.neitzel.core.inject.testcomponents.test1ok");
var instantiableComponents = scanner.getInstantiableComponents();
var nonUniqueTypes = scanner.getNotUniqueTypes();
@ -37,7 +37,7 @@ class InjectableComponentScannerTest {
*/
@Test
void testComponentsFailWithUnknownParameters() {
InjectableComponentScanner scanner = new InjectableComponentScanner("de.neitzel.core.inject.testcomponents.test2fail");
ComponentScanner scanner = new ComponentScanner("de.neitzel.core.inject.testcomponents.test2fail");
var instantiableComponents = scanner.getInstantiableComponents();
var nonUniqueTypes = scanner.getNotUniqueTypes();

View File

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

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
xmlns:nfx="http://example.com/nfx"

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?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" />

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?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"
xmlns:nfx="http://example.com/nfx"

View File

@ -1,8 +1,11 @@
package de.neitzel.fx.component;
import javafx.beans.property.*;
import java.lang.reflect.*;
import java.util.*;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
public class AutoViewModel<T> {

View File

@ -1,9 +1,13 @@
package de.neitzel.fx.injectfx;
import de.neitzel.core.inject.InjectableComponentScanner;
import de.neitzel.core.inject.ComponentData;
import de.neitzel.core.inject.ComponentScanner;
import lombok.Getter;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Manages the creation and storage of singleton instances for all @FXMLComponent classes.
@ -11,18 +15,19 @@ import java.util.*;
*/
public class FXMLComponentInstances {
@Getter
/** 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. */
private final InjectableComponentScanner injectableScanner;
private final ComponentScanner injectableScanner;
/**
* Constructs an FXMLComponentInstances manager and initializes all component instances.
*
* @param injectableScanner The InjectableComponents instance containing resolved component types.
*/
public FXMLComponentInstances(InjectableComponentScanner injectableScanner) {
public FXMLComponentInstances(ComponentScanner injectableScanner) {
this.injectableScanner = injectableScanner;
createAllInstances();
}
@ -47,7 +52,7 @@ public class FXMLComponentInstances {
return instanceMap.get(componentClass);
}
Class<?> concreteClass = injectableScanner.getInstantiableComponents().get(componentClass);
Class<?> concreteClass = injectableScanner.getInstantiableComponents().get(componentClass).getType();
if (concreteClass == null) {
throw new IllegalStateException("No concrete implementation found for: " + componentClass.getName());
}
@ -122,19 +127,10 @@ public class FXMLComponentInstances {
private void registerInstance(Class<?> concreteClass, Object instance) {
instanceMap.put(concreteClass, instance);
for (Map.Entry<Class<?>, Class<?>> entry : injectableScanner.getInstantiableComponents().entrySet()) {
if (entry.getValue().equals(concreteClass)) {
for (Map.Entry<Class<?>, ComponentData> entry : injectableScanner.getInstantiableComponents().entrySet()) {
if (entry.getValue().getType().equals(concreteClass)) {
instanceMap.put(entry.getKey(), instance);
}
}
}
/**
* Retrieves the instance map containing all component instances.
*
* @return A map of class types to their instances.
*/
public Map<Class<?>, Object> getInstanceMap() {
return instanceMap;
}
}

View File

@ -8,13 +8,52 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
/**
* The InjectingControllerFactory is responsible for creating controller instances for JavaFX that
* 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.
*
* 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.
*/
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.
*/
private final Map<Class<?>, Object> parameterMap = new HashMap<>();
/**
* Adds a mapping between a class and its corresponding object instance
* to the parameter map used for dependency injection.
*
* @param clazz The class type to be associated with the provided object instance.
* @param object The object instance to be injected for the specified class type.
*/
public void addInjectingData(Class<?> clazz, Object object) {
parameterMap.put(clazz, object);
}
/**
* Creates an instance of a controller class using a constructor that matches the dependencies
* defined in the parameter map. The method dynamically analyzes the constructors of the given
* class and attempts to instantiate the class by injecting required dependencies.
*
* @param controllerClass the class of the controller to be instantiated
* @return an instance of the specified controller class
* @throws RuntimeException if an error occurs while creating the controller instance or if no suitable constructor is found
*/
@Override
public Object call(Class<?> controllerClass) {
try {
@ -37,6 +76,17 @@ public class InjectingControllerFactory implements Callback<Class<?>, Object> {
}
}
/**
* Determines if a given constructor can be instantiated using the provided parameter map.
* 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 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.
*/
private boolean canBeInstantiated(Constructor<?> constructor, Map<Class<?>, Object> parameterMap) {
return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey);
}

View File

@ -1,42 +1,131 @@
package de.neitzel.fx.injectfx;
import de.neitzel.core.inject.InjectableComponentScanner;
import de.neitzel.core.inject.ComponentScanner;
import javafx.fxml.FXMLLoader;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
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.
*
* 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.
*
* Features of this loader include:
* - Support for dependency injection into JavaFX controllers by using a custom factory.
* - Adding custom dependency mappings at runtime.
* - Scanning and initializing injectable components from a specified package.
*/
@Slf4j
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.
*
* 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}.
*
* 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.
*
* 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.
* - Ensures that controllers are created with required dependencies, preventing manual resolution of injections.
*
* This variable is initialized in the {@link InjectingFXMLLoader} and can be extended with additional
* mappings at runtime using relevant methods.
*/
private final InjectingControllerFactory controllerFactory;
/**
* 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.
*
* The {@link InjectingControllerFactory} allows for the registration and dynamic injection
* of dependencies into controllers when they are instantiated during the FXML loading process.
*/
public InjectingFXMLLoader() {
controllerFactory = new InjectingControllerFactory();
}
/**
* Constructs a new instance of the InjectingFXMLLoader class with a specified
* InjectingControllerFactory for dependency injection. This enables the FXMLLoader
* to use the provided controller factory to instantiate controllers with their
* required dependencies during the FXML loading process.
*
* @param controllerFactory The controller factory responsible for creating controller
* instances with dependency injection support.
*/
public InjectingFXMLLoader(InjectingControllerFactory controllerFactory) {
this.controllerFactory = controllerFactory;
}
/**
* Constructs an instance of `InjectingFXMLLoader` and initializes the component scanner
* for dependency injection based on components found within a specified package.
* The scanned components are analyzed and their instances are prepared for injection
* using the internal `InjectingControllerFactory`.
*
* @param packageName The package name to be scanned for injectable components.
* Classes within the specified package will be identified
* as potential dependencies and made available for injection
* into JavaFX controllers during FXML loading.
*/
public InjectingFXMLLoader(String packageName) {
controllerFactory = new InjectingControllerFactory();
InjectableComponentScanner scanner = new InjectableComponentScanner(packageName);
ComponentScanner scanner = new ComponentScanner(packageName);
FXMLComponentInstances instances = new FXMLComponentInstances(scanner);
addInjectingData(instances);
}
/**
* Adds all injectable data from the given {@code FXMLComponentInstances} to the controller factory.
* Iterates through the classes in the instance map and delegates adding each class-instance pair
* to the {@link #addInjectingData(Class, Object)} method.
*
* @param instances An {@code FXMLComponentInstances} object containing the mapping of classes
* to their respective singleton instances. This data represents the components
* available for dependency injection.
*/
private void addInjectingData(FXMLComponentInstances instances) {
for (var clazz: instances.getInstanceMap().keySet()) {
addInjectingData(clazz, instances.getInstance(clazz));
}
}
/**
* Adds a specific class-object mapping to the controller factory for dependency injection.
* This method allows the association of a given class type with its corresponding instance,
* enabling dependency injection into controllers during the FXML loading process.
*
* @param clazz The class type to be associated with the provided object instance.
* @param object The object instance to be injected for the specified class type.
*/
public void addInjectingData(Class<?> clazz, Object object) {
controllerFactory.addInjectingData(clazz, object);
}
/**
* Loads an FXML resource from the provided URL and injects dependencies into its controller
* using the configured {@code InjectingControllerFactory}.
*
* @param url the URL of the FXML file to be loaded
* @param <T> the type of the root element defined in the FXML
* @return the root element of the FXML file
* @throws IOException if an error occurs during the loading of the FXML file
*/
public <T> T load(URL url) throws IOException {
FXMLLoader loader = new FXMLLoader(url);
loader.setControllerFactory(controllerFactory);

View File

@ -1,6 +1,7 @@
package de.neitzel.fx.mvvm;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;

View File

@ -6,8 +6,11 @@ import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.*;
import java.util.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A generic ViewModel for JavaFX using reflection to automatically create properties for model attributes.

49
gson/pom.xml Normal file
View File

@ -0,0 +1,49 @@
<?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"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.lib</groupId>
<artifactId>neitzellib</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>gson</artifactId>
<properties>
<!-- Application Properties -->
<link.name>${project.artifactId}</link.name>
<launcher>${project.artifactId}</launcher>
<appName>${project.artifactId}</appName>
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
<!-- Dependency Versions -->
<gson.version>2.11.0</gson.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
</dependencies>
<build>
<finalName>${jar.filename}</finalName>
<plugins>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,136 @@
package de.neitzel.gson.adapter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.Streams;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
/**
* A custom {@link TypeAdapter} implementation for selectively excluding specified fields during JSON serialization.
*
* 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.
*
* @param <T> the type of objects that this adapter can serialize and deserialize
*/
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;
/**
* 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 {@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;
/**
* 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;
}
/**
* 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);
}
Streams.write(obj, out);
}
/**
* 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

@ -0,0 +1,52 @@
package de.neitzel.gson.adapter;
import com.google.gson.*;
import java.lang.reflect.Type;
import java.time.Instant;
import java.time.format.DateTimeParseException;
/**
* A custom Gson type adapter for serializing and deserializing {@link Instant} objects. This adapter handles conversion between
* {@code Instant} and its ISO-8601 formatted string representation.
* <p>
* Implementation details: - Serialization: Converts {@link Instant} to its ISO-8601 string representation. - Deserialization: Parses a
* valid ISO-8601 string to an {@link Instant} instance.
* <p>
* The adapter ensures compliance with the ISO-8601 standard for interoperability. A {@link JsonParseException} is thrown if the provided
* JSON element during deserialization does not conform to the supported {@code Instant} format.
*/
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);
}
}
}

View File

@ -14,6 +14,7 @@
<module>core</module>
<module>encryption</module>
<module>fx</module>
<module>gson</module>
<module>net</module>
<module>log4j</module>
<module>fx-example</module>