Refactor and expand core functionalities with new modules

Added robust utility modules: Tone generation, image scaling, and sound tone mapping. Developed an encryption domain with new abstractions. Updated and annotated the SQL query class for clarity and functionality. Removed unused `@Config` annotation.
This commit is contained in:
Konrad Neitzel 2025-04-02 22:09:02 +02:00
parent 8694899e57
commit 77ea70f4e6
21 changed files with 2754 additions and 904 deletions

View File

@ -0,0 +1,155 @@
package de.neitzel.core.image;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.
*/
public class ImageScaler {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private 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.
*
* 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.
* @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) {
// Get the scaled image.
byte[] bytes = createScaledImage(originalImageBytes, width, height, enforceSize);
// Validation of result.
if (bytes == null || bytes.length == 0) return null;
// Return new InputStream.
return new ByteArrayInputStream(bytes);
}
/**
* Creates a scaled version of the given {@link BufferedImage} and returns it as a byte array.
*
* 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.
* - If enforceSize is true, the resulting image will match the exact specified dimensions, potentially
* 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.
* @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) {
// Validation
if (originalImage == null) return null;
try {
// Get the scale factor.
double scaleWidth = (double) width / (double) originalImage.getWidth();
double scaleHeight = (double) height / (double) originalImage.getHeight();
double scaleFactor;
if (scaleWidth > scaleHeight) {
scaleFactor = scaleHeight;
} else {
scaleFactor = scaleWidth;
}
// Calculate target size of scaled image.
int newHeight = (int) (scaleFactor * originalImage.getHeight());
int newWidth = (int) (scaleFactor * originalImage.getWidth());
// Cooordinates of new picture and size of new picture.
int x, y, usedHeight, usedWidth;
if (enforceSize) {
usedHeight = height;
usedWidth = width;
x = (width - newWidth) / 2;
y = (height - newHeight) / 2;
} else {
x = 0;
y = 0;
usedHeight = newHeight;
usedWidth = newWidth;
}
// Scale the image
BufferedImage scaledImage = new BufferedImage(usedWidth, usedHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = scaledImage.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics.drawImage(originalImage, x, y, newWidth, newHeight, null);
// Get the bytes of the image
try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
ImageIO.write(scaledImage, TARGET_FORMAT, stream);
return stream.toByteArray();
}
} catch (Exception ex) {
return null;
}
}
}

View File

@ -8,30 +8,93 @@ import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* InjectableComponents scans a package for classes annotated with {@link Component}. * InjectableComponentScanner is responsible for scanning packages to detect classes annotated
* It determines which components can be instantiated and manages type mappings for dependency injection. * with @FXMLComponent and analyzing their compatibility for instantiation and dependency injection.
* 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 InjectableComponentScanner {
/** Set of all detected @FXMLComponent classes within the given package. */ /**
* A set that stores classes annotated with {@code @Component}, representing
* FXML-compatible components detected during the scanning process.
* These components are used as part of the dependency injection mechanism
* within the {@code InjectableComponentScanner}.
*
* 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.
*
* 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<?>> fxmlComponents = new HashSet<>();
/** Set of all superclasses and interfaces that are implemented by multiple @FXMLComponent classes. */ /**
* A set of component types that are not uniquely associated with a single implementation.
*
* 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.
*
* 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.
*/
private final Set<Class<?>> notUniqueTypes = new HashSet<>(); private final Set<Class<?>> notUniqueTypes = new HashSet<>();
/** Map of unique superclasses/interfaces to a single corresponding @FXMLComponent class. */ /**
* A mapping of unique super types to their corresponding component classes.
* 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.
*
* 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.
*
* This mapping is utilized for resolving dependencies and determining which components
* can be instantiated based on unique type information.
*/
private final Map<Class<?>, Class<?>> uniqueTypeToComponent = new HashMap<>(); private final Map<Class<?>, Class<?>> uniqueTypeToComponent = new HashMap<>();
/** Map of instantiable @FXMLComponent classes and their corresponding interfaces/superclasses (if unique). */ /**
* A mapping of components that can be instantiated with their corresponding types.
* This map links a component's class (key) to the specific implementation class (value)
* 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.
*
* 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
* constructor dependencies being resolvable using known types or other registered components.
*/
private final Map<Class<?>, Class<?>> instantiableComponents = new HashMap<>(); private final Map<Class<?>, Class<?>> instantiableComponents = new HashMap<>();
/** List of error messages generated when resolving component instantiability. */ /**
* A list of error messages encountered during the scanning and analysis
* of components in the dependency injection process.
*
* 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.
*
* Errors added to this list provide detailed descriptions of the specific
* issues that prevent certain components from being instantiated correctly.
*/
private final List<String> errors = new ArrayList<>(); private final List<String> errors = new ArrayList<>();
/** /**
* Constructs an InjectableComponents instance and scans the given package for @FXMLComponent classes. * Initializes a new instance of the {@code InjectableComponentScanner} class, which scans for injectable
* components within the specified base package and resolves the set of recognizable and instantiable components.
* *
* @param basePackage The base package to scan for @FXMLComponent classes. * @param basePackage the base package to scan for injectable components
*/ */
public InjectableComponentScanner(String basePackage) { public InjectableComponentScanner(String basePackage) {
scanForComponents(basePackage); scanForComponents(basePackage);
@ -40,9 +103,10 @@ public class InjectableComponentScanner {
} }
/** /**
* Scans the specified package for classes annotated with @FXMLComponent. * Scans the specified base package for classes annotated with {@link Component}.
* Identified component classes are added to a collection for further processing.
* *
* @param basePackage The package to scan. * @param basePackage the package to scan for component annotations
*/ */
private void scanForComponents(String basePackage) { private void scanForComponents(String basePackage) {
Reflections reflections = new Reflections(basePackage); Reflections reflections = new Reflections(basePackage);
@ -50,7 +114,23 @@ public class InjectableComponentScanner {
} }
/** /**
* Analyzes the collected @FXMLComponent classes to determine unique and non-unique superclasses/interfaces. * Analyzes component types within the injected components and classifies them based on their
* inheritance hierarchy and relationships.
*
* 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.
*
* 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.
*
* This method is a key part of the component scanning and resolution process, facilitating
* the identification of potential instantiation ambiguities or conflicts.
*/ */
private void analyzeComponentTypes() { private void analyzeComponentTypes() {
Map<Class<?>, List<Class<?>>> superTypesMap = new HashMap<>(); Map<Class<?>, List<Class<?>>> superTypesMap = new HashMap<>();
@ -76,8 +156,23 @@ public class InjectableComponentScanner {
} }
/** /**
* Determines which @FXMLComponent classes can be instantiated based on known dependencies. * Resolves components from the set of scanned classes that can be instantiated based on their constructors
* It registers valid components and collects errors for components that cannot be instantiated. * 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.
*
* 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 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.
*
* 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.
*/ */
private void resolveInstantiableComponents() { private void resolveInstantiableComponents() {
Set<Class<?>> resolved = new HashSet<>(); Set<Class<?>> resolved = new HashSet<>();
@ -108,10 +203,11 @@ public class InjectableComponentScanner {
} }
/** /**
* Registers a component along with its unique superclasses and interfaces in the known types map. * 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.
* *
* @param component The component class. * @param component the component class to register
* @param knownTypes The map of known instantiable types. * @param knownTypes the map where the component and its types will be registered
*/ */
private void registerComponentWithSuperTypes(Class<?> component, Map<Class<?>, Class<?>> knownTypes) { private void registerComponentWithSuperTypes(Class<?> component, Map<Class<?>, Class<?>> knownTypes) {
knownTypes.put(component, component); knownTypes.put(component, component);
@ -124,11 +220,13 @@ public class InjectableComponentScanner {
} }
/** /**
* Checks whether a given @FXMLComponent class can be instantiated based on known dependencies. * 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.
* *
* @param component The component class to check. * @param component the class to check for instantiation eligibility
* @param knownTypes Set of known instantiable types. * @param knownTypes the set of types known to be instantiable and available for constructor injection
* @return {@code true} if the component can be instantiated, otherwise {@code false}. * @return true if the class can be instantiated; false otherwise
*/ */
private boolean canInstantiate(Class<?> component, Set<Class<?>> knownTypes) { private boolean canInstantiate(Class<?> component, Set<Class<?>> knownTypes) {
for (Constructor<?> constructor : component.getConstructors()) { for (Constructor<?> constructor : component.getConstructors()) {
@ -141,9 +239,12 @@ public class InjectableComponentScanner {
} }
/** /**
* Collects error messages for components that cannot be instantiated and adds them to the error list. * Collects the instantiation errors for a set of unresolved classes and appends
* detailed error messages to the internal error list. This method analyzes unresolved
* components based on their constructors, determining if all required types are
* instantiable or if there are conflicting types that could prevent instantiation.
* *
* @param unresolved Set of components that could not be instantiated. * @param unresolved the set of classes for which instantiation errors are collected
*/ */
private void collectInstantiationErrors(Set<Class<?>> unresolved) { private void collectInstantiationErrors(Set<Class<?>> unresolved) {
for (Class<?> component : unresolved) { for (Class<?> component : unresolved) {
@ -175,10 +276,13 @@ public class InjectableComponentScanner {
} }
/** /**
* Returns a comma-separated list of conflicting types that prevent instantiation. * Identifies and retrieves a comma-separated string of conflicting types for a given component.
* A conflicting type is a parameter type in the constructor of the given component
* that is already marked as not unique within the application context.
* *
* @param component The component class. * @param component the class for which conflicting types need to be identified.
* @return A string listing the conflicting types. * @return a comma-separated string of fully qualified names of conflicting parameter types,
* or an empty string if no conflicts are found.
*/ */
private String getConflictingTypes(Class<?> component) { private String getConflictingTypes(Class<?> component) {
return Arrays.stream(component.getConstructors()) return Arrays.stream(component.getConstructors())
@ -189,10 +293,10 @@ public class InjectableComponentScanner {
} }
/** /**
* Retrieves all superclasses and interfaces of a given class. * Retrieves all superclasses and interfaces of the specified class, excluding the {@code Object} class.
* *
* @param clazz The class to analyze. * @param clazz the class for which to retrieve all superclasses and interfaces
* @return A set of all superclasses and interfaces of the given class. * @return a set of all superclasses and interfaces implemented by the specified class
*/ */
private Set<Class<?>> getAllSuperTypes(Class<?> clazz) { private Set<Class<?>> getAllSuperTypes(Class<?> clazz) {
Set<Class<?>> result = new HashSet<>(); Set<Class<?>> result = new HashSet<>();
@ -208,36 +312,46 @@ public class InjectableComponentScanner {
} }
/** /**
* Returns a map of instantiable @FXMLComponent classes and their associated interfaces/superclasses. * Retrieves a map of classes representing component types to their corresponding instantiable implementations.
* *
* @return A map of instantiable components. * @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<?>, Class<?>> getInstantiableComponents() {
return instantiableComponents; return instantiableComponents;
} }
/** /**
* Returns the set of non-unique types (superclasses/interfaces with multiple implementations). * Retrieves a set of types that have multiple implementations, making them
* non-unique. These types could not be uniquely resolved during the
* component scanning and analysis process.
* *
* @return A set of non-unique types. * @return a set of classes representing the types that have multiple
* implementations and cannot be resolved uniquely
*/ */
public Set<Class<?>> getNotUniqueTypes() { public Set<Class<?>> getNotUniqueTypes() {
return notUniqueTypes; return notUniqueTypes;
} }
/** /**
* Returns the map of unique types to corresponding @FXMLComponent implementations. * Returns a mapping of types to their unique component implementations.
* This map includes only those types that have a single corresponding component
* implementation, ensuring no ambiguity in resolution.
* *
* @return A map of unique types. * @return a map where the keys are types (e.g., interfaces or superclasses)
* and the values are their unique component implementations.
*/ */
public Map<Class<?>, Class<?>> getUniqueTypeToComponent() { public Map<Class<?>, Class<?>> getUniqueTypeToComponent() {
return uniqueTypeToComponent; return uniqueTypeToComponent;
} }
/** /**
* Returns a list of errors encountered during instantiation resolution. * Retrieves a list of error messages recorded during the scanning and analysis process.
* The errors typically indicate issues such as components that cannot be instantiated due to
* missing dependencies, unresolvable component types, or multiple implementations of a type
* that are not uniquely resolved.
* *
* @return A list of error messages. * @return a list of error messages explaining the issues encountered.
*/ */
public List<String> getErrors() { public List<String> getErrors() {
return errors; return errors;

View File

@ -5,6 +5,22 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; 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.
*
* 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.
*
* 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.
*
* 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.
*/
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
public @interface Component { public @interface Component {

View File

@ -1,12 +0,0 @@
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;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Config {
String value() default "";
}

View File

@ -0,0 +1,69 @@
package de.neitzel.core.sound;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
/**
* The ToneGenerator class provides methods to generate and play tones.
* It allows playing tones based on frequency or predefined tone names.
*/
public class ToneGenerator {
/**
* 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 duration The duration of the tone in milliseconds.
* @throws LineUnavailableException If a line for audio playback cannot be opened.
*/
public static void playTone(String tone, int duration) throws LineUnavailableException {
playTone(ToneTable.getFrequency(tone), duration);
}
/**
* Plays a tone at the specified frequency and duration.
*
* @param frequency the frequency of the tone in Hertz (Hz)
* @param duration the duration of the tone in milliseconds
* @throws LineUnavailableException if the audio line cannot be opened
*/
public static void playTone(double frequency, int duration) throws LineUnavailableException {
// Get the audio format
AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100, 16, 2, 4, 44100, false);
// Open the audio line
try (SourceDataLine line = AudioSystem.getSourceDataLine(format)) {
line.open(format);
line.start();
// Generate the tone data
byte[] toneBuffer = new byte[2 * duration * 44100 / 1000];
for (int i = 0; i < toneBuffer.length; i += 2) {
double angle = i / (44100.0 / frequency) * 2.0 * Math.PI;
short sample = (short) (Math.sin(angle) * 32767);
toneBuffer[i] = (byte) (sample & 0xFF);
toneBuffer[i + 1] = (byte) ((sample >> 8) & 0xFF);
}
// Play the tone
line.write(toneBuffer, 0, toneBuffer.length);
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

@ -0,0 +1,198 @@
package de.neitzel.core.sound;
import java.util.HashMap;
/**
* The ToneTable class provides a static lookup tool for associating musical tone names
* 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".
*
* 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 {
/**
* 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.
*
* 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);
toneMap.put("Db0", 17.32);
toneMap.put("D0", 18.35);
toneMap.put("D#0", 19.45);
toneMap.put("Eb0", 19.45);
toneMap.put("E0", 20.60);
toneMap.put("F0", 21.83);
toneMap.put("F#0", 23.12);
toneMap.put("Gb0", 23.12);
toneMap.put("G0", 24.50);
toneMap.put("G#0", 25.96);
toneMap.put("Ab0", 25.96);
toneMap.put("A0", 27.50);
toneMap.put("A#0", 29.14);
toneMap.put("Bb0", 29.14);
toneMap.put("B0", 30.87);
toneMap.put("C1", 32.70);
toneMap.put("C#1", 34.65);
toneMap.put("Db1", 34.65);
toneMap.put("D1", 36.71);
toneMap.put("D#1", 38.89);
toneMap.put("Eb1", 38.89);
toneMap.put("E1", 41.20);
toneMap.put("F1", 43.65);
toneMap.put("F#1", 46.25);
toneMap.put("Gb1", 46.25);
toneMap.put("G1", 49.00);
toneMap.put("G#1", 51.91);
toneMap.put("Ab1", 51.91);
toneMap.put("A1", 55.00);
toneMap.put("A#1", 58.27);
toneMap.put("Bb1", 58.27);
toneMap.put("B1", 61.74);
toneMap.put("C2", 65.41);
toneMap.put("C#2", 69.30);
toneMap.put("Db2", 69.30);
toneMap.put("D2", 73.42);
toneMap.put("D#2", 77.78);
toneMap.put("Eb2", 77.78);
toneMap.put("E2", 82.41);
toneMap.put("F2", 87.31);
toneMap.put("F#2", 92.50);
toneMap.put("Gb2", 92.50);
toneMap.put("G2", 98.00);
toneMap.put("G#2", 103.83);
toneMap.put("Ab2", 103.83);
toneMap.put("A2", 110.00);
toneMap.put("A#2", 116.54);
toneMap.put("Bb2", 116.54);
toneMap.put("B2", 123.47);
toneMap.put("C3", 130.81);
toneMap.put("C#3", 138.59);
toneMap.put("Db3", 138.59);
toneMap.put("D3", 146.83);
toneMap.put("D#3", 155.56);
toneMap.put("Eb3", 155.56);
toneMap.put("E3", 164.81);
toneMap.put("F3", 174.61);
toneMap.put("F#3", 185.00);
toneMap.put("Gb3", 185.00);
toneMap.put("G3", 196.00);
toneMap.put("G#3", 207.65);
toneMap.put("Ab3", 207.65);
toneMap.put("A3", 220.00);
toneMap.put("A#3", 233.08);
toneMap.put("Bb3", 233.08);
toneMap.put("B3", 246.94);
toneMap.put("C4", 261.63);
toneMap.put("C#4", 277.18);
toneMap.put("Db4", 277.18);
toneMap.put("D4", 293.66);
toneMap.put("D#4", 311.13);
toneMap.put("Eb4", 311.13);
toneMap.put("E4", 329.63);
toneMap.put("F4", 349.23);
toneMap.put("F#4", 369.99);
toneMap.put("Gb4", 369.99);
toneMap.put("G4", 392.00);
toneMap.put("G#4", 415.30);
toneMap.put("Ab4", 415.30);
toneMap.put("A4", 440.00);
toneMap.put("A#4", 466.16);
toneMap.put("Bb4", 466.16);
toneMap.put("B4", 493.88);
toneMap.put("C5", 523.25);
toneMap.put("C#5", 554.37);
toneMap.put("Db5", 554.37);
toneMap.put("D5", 587.33);
toneMap.put("D#5", 622.25);
toneMap.put("Eb5", 622.25);
toneMap.put("E5", 659.25);
toneMap.put("F5", 698.46);
toneMap.put("F#5", 739.99);
toneMap.put("Gb5", 739.99);
toneMap.put("G5", 783.99);
toneMap.put("G#5", 830.61);
toneMap.put("Ab5", 830.61);
toneMap.put("A5", 880.00);
toneMap.put("A#5", 932.33);
toneMap.put("Bb5", 932.33);
toneMap.put("B5", 987.77);
toneMap.put("C6", 1046.50);
toneMap.put("C#6", 1108.73);
toneMap.put("Db6", 1108.73);
toneMap.put("D6", 1174.66);
toneMap.put("D#6", 1244.51);
toneMap.put("Eb6", 1244.51);
toneMap.put("E6", 1318.51);
toneMap.put("F6", 1396.91);
toneMap.put("F#6", 1479.98);
toneMap.put("Gb6", 1479.98);
toneMap.put("G6", 1567.98);
toneMap.put("G#6", 1661.22);
toneMap.put("Ab6", 1661.22);
toneMap.put("A6", 1760.00);
toneMap.put("A#6", 1864.66);
toneMap.put("Bb6", 1864.66);
toneMap.put("B6", 1975.53);
toneMap.put("C7", 2093.00);
toneMap.put("C#7", 2217.46);
toneMap.put("Db7", 2217.46);
toneMap.put("D7", 2349.32);
toneMap.put("D#7", 2489.02);
toneMap.put("Eb7", 2489.02);
toneMap.put("E7", 2637.02);
toneMap.put("F7", 2793.83);
toneMap.put("F#7", 2959.96);
toneMap.put("Gb7", 2959.96);
toneMap.put("G7", 3135.96);
toneMap.put("G#7", 3322.44);
toneMap.put("Ab7", 3322.44);
toneMap.put("A7", 3520.00);
toneMap.put("A#7", 3729.31);
toneMap.put("Bb7", 3729.31);
toneMap.put("B7", 3951.07);
toneMap.put("C8", 4186.01);
toneMap.put("C#8", 4434.92);
toneMap.put("Db8", 4434.92);
toneMap.put("D8", 4698.63);
toneMap.put("D#8", 4978.03);
toneMap.put("Eb8", 4978.03);
toneMap.put("E8", 5274.04);
toneMap.put("F8", 5587.65);
toneMap.put("F#8", 5919.91);
toneMap.put("Gb8", 5919.91);
toneMap.put("G8", 6271.93);
toneMap.put("G#8", 6644.88);
toneMap.put("Ab8", 6644.88);
toneMap.put("A8", 7040.00);
toneMap.put("A#8", 7458.62);
toneMap.put("Bb8", 7458.62);
toneMap.put("B8", 7902.13);
}
/**
* 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.
* Otherwise, an exception is thrown.
*
* @param toneName the name of the tone whose frequency is to be retrieved
* @return the frequency associated with the specified tone name
* @throws IllegalArgumentException if the tone name is not found in the tone map
*/
public static double getFrequency(String toneName) {
if (toneMap.containsKey(toneName)) {
return toneMap.get(toneName);
} else {
throw new IllegalArgumentException("Unknown tone name: " + toneName);
}
}
}

View File

@ -15,14 +15,22 @@ import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
/** /**
* Helper class to build and execute parameterized SQL queries with factory-based result mapping. * A generic class for managing SQL queries and their execution using JDBC.
* The class supports parameterized queries, allows loading queries from resources,
* and facilitates streaming of result sets.
*
* @param <T> The type of the objects returned by the query.
*/ */
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class Query<T> { public class Query<T> {
/** /**
* An Integer Factory that can be used to read Integer Values of a ResultSet that only has Integers (e.g. a min oder max query) * 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 -> { public static final Function<ResultSet, Integer> INTEGER_FACTORY = rs -> {
try { try {
@ -32,11 +40,42 @@ public class Query<T> {
} }
}; };
/**
* 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; 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<>(); 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; 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; private String queryText;
/**
* 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 { public static void execute(final Connection connection, final String query) throws SQLException {
new Query<Object>(connection) new Query<Object>(connection)
.setQueryText(query) .setQueryText(query)
@ -44,7 +83,10 @@ public class Query<T> {
} }
/** /**
* sets the query. * 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) { public Query<T> setQueryText(final String queryText) {
this.queryText = queryText; this.queryText = queryText;
@ -52,7 +94,11 @@ public class Query<T> {
} }
/** /**
* Loads the query content from a resource URL. * 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) { public Query<T> setQueryResource(final String resourcePath) {
log.info("loading resource: {}", resourcePath); log.info("loading resource: {}", resourcePath);
@ -65,7 +111,11 @@ public class Query<T> {
} }
/** /**
* Replaces placeholders in the query. * 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) { public Query<T> replace(final String placeholder, final String value) {
this.queryText = this.queryText.replace(placeholder, value); this.queryText = this.queryText.replace(placeholder, value);
@ -74,13 +124,13 @@ public class Query<T> {
} }
/** /**
* Adds another parameter setter * Adds a parameter to the query at the specified index with a custom setter function.
* *
* @param index Index of parameter in prepared statement * @param index The position of the parameter in the query starting from 1.
* @param value Value to set. * @param value The value of the parameter to be set.
* @param setter Setter to use on prepared statement. * @param setter A TriSqlFunction that defines how to set the parameter in the PreparedStatement.
* @param <X> Type of the Parameter. * @param <X> The type of the parameter value.
* @return The Query. * @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) { 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)); parameterSetters.add(ps -> setter.apply(ps, index, value));
@ -88,10 +138,10 @@ public class Query<T> {
} }
/** /**
* Adds a ParameterSetter/ * Adds a parameter to the query using the specified ParameterSetter.
* *
* @param setter Setter for a parameter. * @param setter the ParameterSetter used to configure the parameter
* @return the Qyery. * @return the current Query instance for method chaining
*/ */
public Query<T> addParameter(ParameterSetter setter) { public Query<T> addParameter(ParameterSetter setter) {
parameterSetters.add(setter); parameterSetters.add(setter);
@ -99,7 +149,10 @@ public class Query<T> {
} }
/** /**
* Adds a result factory by name. * 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) { public Query<T> factory(final Function<ResultSet, T> factory) {
this.factory = factory; this.factory = factory;
@ -107,7 +160,11 @@ public class Query<T> {
} }
/** /**
* Executes the query and returns a stream of results. This Stream must be closed so that the underlying ResultSet is closed. * 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 { private Stream<T> stream() throws SQLException {
ResultSet rs = createPreparedStatement().executeQuery(); ResultSet rs = createPreparedStatement().executeQuery();
@ -116,10 +173,11 @@ public class Query<T> {
} }
/** /**
* Creates a PreparedStatement and applies all Parameter. * Creates and returns a PreparedStatement object configured with the query text
* and parameters defined for the current instance.
* *
* @return the created PreparedStatement * @return a PreparedStatement object initialized with the query and parameter values
* @throws SQLException thrown if the PreparedStatement could not be created. * @throws SQLException if a database access error occurs or the PreparedStatement cannot be created
*/ */
private PreparedStatement createPreparedStatement() throws SQLException { private PreparedStatement createPreparedStatement() throws SQLException {
PreparedStatement ps = connection.prepareStatement(queryText); PreparedStatement ps = connection.prepareStatement(queryText);
@ -130,9 +188,12 @@ public class Query<T> {
} }
/** /**
* Executes a query wich has no result. * 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 Thrown when query cannot be executed. * @throws SQLException if a database access error occurs or the SQL statement is invalid.
*/ */
public void execute() throws SQLException { public void execute() throws SQLException {
try (PreparedStatement ps = createPreparedStatement()) { try (PreparedStatement ps = createPreparedStatement()) {
@ -141,17 +202,26 @@ public class Query<T> {
} }
/** /**
* Streams the results of a ResultSet using a factory that creates instances for each row of the ResultSet * 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 ResultSet to stream from * @param rs the {@link ResultSet} to be transformed into a stream
* @param factory Factory to create instances of T. * @param factory a {@link Function} that maps rows of the {@link ResultSet} to objects of type T
* @return Stream of T instances. * @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) { private Stream<T> streamFromResultSet(final ResultSet rs, final Function<ResultSet, T> factory) {
Iterator<T> iterator = new Iterator<>() { Iterator<T> iterator = new Iterator<>() {
private boolean hasNextChecked = false; private boolean hasNextChecked = false;
private boolean hasNext; private boolean hasNext;
/**
* 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 @Override
@SneakyThrows @SneakyThrows
public boolean hasNext() { public boolean hasNext() {
@ -162,6 +232,14 @@ public class Query<T> {
return hasNext; 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 @Override
public T next() { public T next() {
if (!hasNext()) { if (!hasNext()) {
@ -187,6 +265,16 @@ public class Query<T> {
}); });
} }
/**
* Provides a managed stream to the given handler and ensures proper resource closing after use.
* A {@code Stream} is opened and passed to the specified handler as an argument.
* The stream will be automatically closed when the handler finishes execution or throws an exception.
*
* @param handler a function which receives a {@code Stream<T>} and processes it, returning a result of type {@code R}.
* @param <R> the type of the result produced by the handler.
* @return the result returned by the handler after processing the stream.
* @throws SQLException if an SQL error occurs during the creation or handling of the stream.
*/
public <R> R withStream(Function<Stream<T>, R> handler) throws SQLException { public <R> R withStream(Function<Stream<T>, R> handler) throws SQLException {
try (Stream<T> stream = stream()) { try (Stream<T> stream = stream()) {
return handler.apply(stream); return handler.apply(stream);
@ -194,37 +282,44 @@ public class Query<T> {
} }
/** /**
* Function with three parameters and no result which can throw an SQLException * 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> type of first parameter * @param <T> the type of the first input to the operation
* @param <U> type of second parameter * @param <U> the type of the second input to the operation
* @param <V> type of third parameter * @param <V> the type of the third input to the operation
*/ */
@FunctionalInterface @FunctionalInterface
public interface TriSqlFunction<T, U, V> { public interface TriSqlFunction<T, U, V> {
/** /**
* Calls the thre parameter function. * 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 first parameter. * @param t the first input argument
* @param u second parameter. * @param u the second input argument
* @param v third parameter. * @param v the third input argument
* @throws SQLException Function could throw an SQLException. * @throws SQLException if an SQL error occurs during execution
*/ */
void apply(T t, U u, V v) throws SQLException; void apply(T t, U u, V v) throws SQLException;
} }
/** /**
* ParameterSetter is a function Interface that could be used to alter a PreparedStatement. * Functional interface designed to handle operations on a PreparedStatement.
* Represents a single abstract method that performs specific work on the
* provided PreparedStatement instance.
*/ */
@FunctionalInterface @FunctionalInterface
public interface ParameterSetter { public interface ParameterSetter {
/** /**
* Does the required work on PreparedStatement. * Applies an operation to the given PreparedStatement object.
* This method is intended to set parameters or perform other modifications on a PreparedStatement.
* *
* @param ps PreparedStatement to work on. * @param ps the PreparedStatement instance to be modified or configured
* @throws SQLException Could be thrown when working on PreparedStatement. * @throws SQLException if a database access error occurs while applying the operation
*/ */
void apply(PreparedStatement ps) throws SQLException; void apply(PreparedStatement ps) throws SQLException;
} }

File diff suppressed because it is too large Load Diff

46
encryption/pom.xml Normal file
View File

@ -0,0 +1,46 @@
<?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>encryption</artifactId>
<properties>
<!-- Application Properties -->
<link.name>${project.artifactId}</link.name>
<launcher>${project.artifactId}</launcher>
<appName>${project.artifactId}</appName>
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
</properties>
<dependencies>
<dependency>
<groupId>com.idealista</groupId>
<artifactId>format-preserving-encryption</artifactId>
<version>1.0.0</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,41 @@
package de.neitzel.encryption;
import com.idealista.fpe.config.Alphabet;
public abstract class BaseAlphabet implements 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.
*/
public 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)){
result.append(ch);
} else {
result.append(replacement);
}
}
return result.toString();
}
/**
* 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) {
// Compare with all characters
for (char ch : availableCharacters()) {
if (ch == character) return true;
}
// Character not found.
return false;
}
}

View File

@ -0,0 +1,140 @@
package de.neitzel.encryption;
import com.idealista.fpe.FormatPreservingEncryption;
import com.idealista.fpe.builder.FormatPreservingEncryptionBuilder;
import com.idealista.fpe.config.Domain;
import com.idealista.fpe.config.LengthRange;
import javax.crypto.KeyGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
/**
* Helper class to deal with FF1Encryption using com.idealista:format-preserving-encryption:1.0.0.
*/
public class FF1Encryption {
/**
* Key to use for all encryption / unencryption
*/
private byte[] key;
/**
* Should to small strings be ignored?
*
* If this is set to true, small strings (less than 2 characters) will not be encrypted!
*/
private boolean ignoreToShortStrings;
/**
* tweak to use for encryption
*/
private byte[] tweak;
/**
* Domain used for encryption
*/
private Domain domain;
/**
* Format preserving encryption to use.
*/
private FormatPreservingEncryption encryption;
/**
* Minimum length of a string.
*/
private int minLength;
/**
* Creates a new instance of FF1Encryption
* @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) {
this(key, tweak, ignoreToShortStrings, new MainDomain(), 2, 1024);
}
/**
* Creates a new instance of FF1Encryption
* @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.
*/
public FF1Encryption(byte[] key, byte[] tweak, boolean ignoreToShortStrings, Domain domain, int minLength, int maxLength) {
this.key = key;
this.tweak = tweak;
this.ignoreToShortStrings = ignoreToShortStrings;
this.domain = domain;
this.minLength = minLength;
encryption = FormatPreservingEncryptionBuilder
.ff1Implementation().withDomain(domain)
.withDefaultPseudoRandomFunction(key)
.withLengthRange(new LengthRange(minLength, maxLength))
.build();
}
/**
* Encrypts a given text.
* @param plainText Unencrypted text.
* @return Encrypted text.
*/
public String encrypt(String plainText) {
// Handle null
if (plainText == null) return null;
// Handle short strings
if (plainText.length() < minLength && ignoreToShortStrings) return plainText;
// Return encrypted text.
return encryption.encrypt(plainText, tweak);
}
/**
* Decrypt a given text.
* @param cipherText Encrypted text.
* @return Decrypted text.
*/
public String decrypt(String cipherText) {
// Handle null
if (cipherText == null) return null;
// Handle short strings
if (cipherText.length() < minLength && ignoreToShortStrings) return cipherText;
// 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

@ -0,0 +1,48 @@
package de.neitzel.encryption;
/**
* Main characters of the Alphabet.
*/
public class MainAlphabet extends BaseAlphabet {
/**
* All characters we want to use.
*/
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',
// uppercase 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',
// numbers
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
// special characters
'-', '_', '?', ' ', '#', '+', '/', '*', '!', '\"', '§', '$', '%', '&', '(', ')', '=', '@',
'€', ',', ';', '.', ':', '<', '>', '|', '\'', '\\', '{', '}', '[', ']',
// german special characters
'ä', 'Ä', 'ö', 'Ö', 'ü', 'Ü', 'ß'
};
/**
* Gets the available characters.
* @return
*/
@Override
public char[] availableCharacters() {
return ALL_CHARS;
}
/**
* Gets the radix of the alphabet.
* @return
*/
@Override
public Integer radix() {
return ALL_CHARS.length;
}
}

View File

@ -0,0 +1,31 @@
package de.neitzel.encryption;
import com.idealista.fpe.config.Alphabet;
import com.idealista.fpe.config.Domain;
import com.idealista.fpe.config.GenericTransformations;
public class MainDomain implements Domain {
private Alphabet _alphabet;
private GenericTransformations transformations;
public MainDomain() {
_alphabet = new MainAlphabet();
transformations = new GenericTransformations(_alphabet.availableCharacters());
}
@Override
public Alphabet alphabet() {
return _alphabet;
}
@Override
public int[] transform(String data) {
return transformations.transform(data);
}
@Override
public String transform(int[] data) {
return transformations.transform(data);
}
}

View File

@ -21,15 +21,33 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- JavaFX dependencies -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
</dependency>
<!-- NeitzelLib dependencies -->
<dependency> <dependency>
<groupId>de.neitzel.lib</groupId> <groupId>de.neitzel.lib</groupId>
<artifactId>core</artifactId> <artifactId>core</artifactId>
<version>1.0-SNAPSHOT</version> <version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>${reflections.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>

49
net/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>net</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 -->
<javax.mail.version>1.6.2</javax.mail.version>
</properties>
<dependencies>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>${javax.mail.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,128 @@
package de.neitzel.net.imap;
import java.util.Objects;
/**
* Represents an IMAP account with server configuration, user credentials, and protocol details.
* This class provides methods to set or get the account details and overrides methods for string
* representation, equality checks, and hashing.
*/
public class ImapAccount {
/**
* Specifies the server address for the IMAP account.
* This typically represents the hostname or IP address
* 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.
* This value is associated with the user's credentials required to log in
* 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;
/**
* 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; }
/**
* 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.length()==0 ? "''" : "########" ,
protocol);
}
/**
* Computes the hash code for this instance of the ImapAccount class.
* The hash code is based on the values of the server, user, password, and protocol fields.
* This ensures that instances with the same field values produce the same hash code.
*
* @return An integer hash code representing this ImapAccount instance.
*/
@Override public int hashCode() {
return Objects.hash(server, user, password, protocol);
}
/**
* Compares this ImapAccount instance with the specified object for equality.
* The comparison is based on the equality of all relevant fields: server, user, password, and protocol.
*
* @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) {
// Check type of argument, includes check of null.
if (!(obj instanceof ImapAccount)) return false;
// Check Reference equals
if (this == obj) return true;
// 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)));
}
}

View File

@ -0,0 +1,256 @@
package de.neitzel.net.imap;
import lombok.extern.slf4j.Slf4j;
import javax.mail.*;
import java.util.*;
/**
* The ImapAccountMonitor class is responsible for monitoring an IMAP email account
* by maintaining a connection to the server, tracking specific folders, and
* listening for new emails. This class allows managing the set of monitored folders,
* processing unseen and undeleted messages, and notifying registered listeners when
* new messages are received.
*/
@Slf4j
public class ImapAccountMonitor {
/**
* Represents an instance of an IMAP account, encapsulating details about
* server configuration, user credentials, and protocol settings.
* This variable is used to connect and authenticate the application's interaction
* with an IMAP email server.
*/
protected ImapAccount account;
/**
* Represents the current session for interaction with an IMAP server.
* The session encapsulates the configuration and context required
* to establish a connection and perform operations on the IMAP server.
* It is initialized and utilized internally within the application.
*/
protected Session session;
/**
* Represents the mail store associated with the IMAP account.
* The store is used to interact with the IMAP server for email retrieval and management.
*/
protected Store store;
/**
* Represents a collection of folders associated with an IMAP account.
* The map's keys are folder names (String), and the values are Folder objects.
* This variable is used to store and manage the hierarchy or collection
* of email folders for an IMAP account.
*/
protected Map<String, Folder> folders = new HashMap<String, Folder>();
/**
* 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
*/
public ImapAccountMonitor(ImapAccount account) throws NoSuchProviderException, MessagingException {
log.trace("Constructor ({})", account.toString());
this.account = account;
Properties props = System.getProperties();
props.setProperty("mail.store.protocol", account.getProtocol());
session = Session.getDefaultInstance(props);
try {
store = session.getStore("imaps");
store.connect(account.server, account.user, account.password);
} catch (NoSuchProviderException ex) {
log.error("Unable to get imaps Store.", ex);
throw ex;
} catch (MessagingException ex) {
log.error("Unable to connect.", ex);
throw ex;
}
log.trace("Constructor done.");
}
/**
* 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.
*
* 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.
*
* 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()) {
try {
folder.close(false);
} catch (MessagingException ex) {
// Only log the exception.
log.warn("Exception when closing folder.", ex);
}
}
// Close the store
try {
store.close();
} catch (MessagingException ex) {
// Only log the error.
log.warn("Exception when closing the store.", ex);
}
log.trace("close() call ended.");
}
/**
* Adds a new folder to the IMAP account and opens it in read-write mode.
* The folder is added to an internal collection for future access.
*
* @param name the name of the folder to be added and opened
* @throws MessagingException if any error occurs while accessing or opening the folder
*/
public void addFolder(String name) throws MessagingException {
log.trace("addFolder(%s) called.", name);
Folder folder = store.getFolder(name);
folder.open(Folder.READ_WRITE);
folders.put(name, folder);
log.trace("addFolder({}) call ended.", name);
}
/**
* Removes the folder with the specified name from the account's folder list.
* If the folder is not found, the method does nothing.
* After removing the folder, it attempts to close any associated resources.
*
* @param name the name of the folder to be removed
*/
public void removeFolder(String name) {
log.trace("removeFolder({}) called.", name);
// Validate folder is known.
if (!folders.containsKey(name)) return;
Folder folder = folders.get(name);
folders.remove(name);
try {
folder.close(false);
} catch (MessagingException ex) {
// TODO: Handle exception ...
}
log.trace("removeFolder({}) call ended.", name);
}
/**
* 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.
*
* 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.
*
* 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.
*
* Note: This method relies on external logging (via `log`) and appropriate event handling
* via `raiseNewEmailEvent`. The `folders` collection holds the monitored folders required.
*
* 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()) {
try {
// Loop through all messages.
Message[] messages = folder.getMessages();
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.
message.setFlag(Flags.Flag.SEEN, true);
// Raise NewMailEvent
raiseNewEmailEvent(message);
}
}
} catch (MessagingException ex) {
log.error("Exception when reading messages of folder {}.", folder.getName(), ex);
// So far no handling of this error except logging it.
}
}
log.trace("check() call ended.");
}
/**
* 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>();
/**
* Adds an IMAP listener to receive notifications about IMAP-related events such as new email arrivals.
*
* @param listener the ImapListener instance to be added to the list of listeners
*/
public void addImapListener(ImapListener listener) {
log.trace("addImapListener() called!");
imapListeners.add(listener);
}
/**
* Removes the specified IMAP listener from the list of active listeners.
* If the listener is present, it will be removed; otherwise, no action is taken.
*
* @param listener The IMAP listener to be removed.
*/
public void removeImapListener(ImapListener listener) {
log.trace("removeImapListener() called!");
if (imapListeners.contains(listener)) {
log.info("Removing the IMAP listener.");
imapListeners.remove(listener);
}
}
/**
* Raises a new email event and notifies all registered listeners.
* The method loops through the listeners and invokes their {@code NewEmailReceived}
* method with a newly created {@link NewEmailEvent}.
* If a listener marks the event as handled, the remaining listeners will
* not be notified.
*
* @param message The email message that triggered the event. This message
* is included in the {@link NewEmailEvent} passed to the listeners.
*/
protected void raiseNewEmailEvent(Message message) {
log.trace("raiseNewEmailEvent() called!");
// Create the event.
NewEmailEvent event = new NewEmailEvent(this, message);
// Call all listeners.
for (ImapListener listener: imapListeners) {
log.trace("Calling listener in {} ....", listener.getClass().getName());
listener.newEmailReceived(event);
// Check if the event was handled so no further ImaListeners will be called.
if (event.isHandled()) {
log.trace("raiseNewEmailEvent(): Breaking out of listener loop because event was handled.");
break;
}
}
log.trace("raiseNewEmailEvent() call ended!");
}
}

View File

@ -0,0 +1,17 @@
package de.neitzel.net.imap;
/**
* Listener interface for receiving notifications about new emails.
* Classes interested in processing new emails should implement this interface and
* register themselves to receive events via supported mechanisms.
*/
public interface ImapListener {
/**
* This method gets invoked when a new email is received. Implementers of this method
* handle the processing of new email events triggered by the associated source.
*
* @param event the NewEmailEvent object that contains information about the received email,
* including the email message and its source.
*/
void newEmailReceived(NewEmailEvent event);
}

View File

@ -0,0 +1,48 @@
package de.neitzel.net.imap;
import lombok.Getter;
import lombok.Setter;
import javax.mail.Message;
import java.util.EventObject;
/**
* Event class representing the occurrence of a new email.
* It is typically used to encapsulate information about the received email
* and provide a means for event listeners to handle the event.
*/
public class NewEmailEvent extends EventObject {
/**
* Represents the email message associated with the NewEmailEvent.
* This field encapsulates the content and metadata of the email
* received, allowing event listeners to process the message details.
*/
@Getter
@Setter
protected Message message;
/**
* Indicates whether the event has already been handled or processed.
* When true, it signifies that the event has been handled and no further
* processing is required. It is used to prevent multiple handling
* of the same event.
*/
@Getter
@Setter
protected boolean handled = false;
/**
* Constructs a new NewEmailEvent instance.
* 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 message the Message object representing the email associated with this event.
*/
public NewEmailEvent(Object source, Message message) {
super(source);
this.message = message;
}
}

View File

@ -0,0 +1,82 @@
package de.neitzel.net.mail;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.ContentType;
import javax.mail.internet.MimeMultipart;
import java.io.IOException;
/**
* Utility functions to use with javax.mail.Message instances.
*/
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
*/
public static String getTextFromMessage(Message message) throws IOException, MessagingException {
String result = "";
if (message.isMimeType("text/plain")) {
result = message.getContent().toString();
} else if (message.isMimeType("multipart/*")) {
MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
result = getTextFromMimeMultipart(mimeMultipart);
}
return result;
}
/**
* Gets the text of a MimeMultipart.
* @param mimeMultipart MimeMultipart to get the text from.
* @return Text of the MimeMultipart instance.
* @throws IOException
* @throws MessagingException
*/
private static String getTextFromMimeMultipart(
MimeMultipart mimeMultipart) throws IOException, MessagingException {
int count = mimeMultipart.getCount();
if (count == 0)
throw new MessagingException("Multipart with no body parts not supported.");
boolean multipartAlt = new ContentType(mimeMultipart.getContentType()).match("multipart/alternative");
if (multipartAlt)
// alternatives appear in an order of increasing
// faithfulness to the original content. Customize as req'd.
return getTextFromBodyPart(mimeMultipart.getBodyPart(count - 1));
String result = "";
for (int i = 0; i < count; i++) {
BodyPart bodyPart = mimeMultipart.getBodyPart(i);
result += getTextFromBodyPart(bodyPart);
}
return result;
}
/**
* 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
*/
private static String getTextFromBodyPart(
BodyPart bodyPart) throws IOException, MessagingException {
String result = "";
if (bodyPart.isMimeType("text/plain")) {
result = (String) bodyPart.getContent();
} else if (bodyPart.isMimeType("text/html")) {
result = (String) bodyPart.getContent();
//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());
}
return result;
}
}

75
pom.xml
View File

@ -12,7 +12,9 @@
<modules> <modules>
<module>core</module> <module>core</module>
<module>encryption</module>
<module>fx</module> <module>fx</module>
<module>net</module>
<module>fx-example</module> <module>fx-example</module>
</modules> </modules>
@ -27,7 +29,7 @@
<lombok.version>1.18.32</lombok.version> <lombok.version>1.18.32</lombok.version>
<mockito.version>5.12.0</mockito.version> <mockito.version>5.12.0</mockito.version>
<reflections.version>0.10.2</reflections.version> <reflections.version>0.10.2</reflections.version>
<slf4j.version></slf4j.version> <slf4j.version>2.0.17</slf4j.version>
<!-- Plugin dependencies --> <!-- Plugin dependencies -->
<codehaus.version.plugin>2.16.2</codehaus.version.plugin> <codehaus.version.plugin>2.16.2</codehaus.version.plugin>
@ -60,34 +62,44 @@
</properties> </properties>
<dependencies> <dependencyManagement>
<!-- JavaFX dependencies --> <dependencies>
<dependency> <!-- JavaFX dependencies -->
<groupId>org.openjfx</groupId> <dependency>
<artifactId>javafx-base</artifactId> <groupId>org.openjfx</groupId>
<version>${javafx.version}</version> <artifactId>javafx-base</artifactId>
</dependency> <version>${javafx.version}</version>
<dependency> </dependency>
<groupId>org.openjfx</groupId> <dependency>
<artifactId>javafx-controls</artifactId> <groupId>org.openjfx</groupId>
<version>${javafx.version}</version> <artifactId>javafx-controls</artifactId>
</dependency> <version>${javafx.version}</version>
<dependency> </dependency>
<groupId>org.openjfx</groupId> <dependency>
<artifactId>javafx-graphics</artifactId> <groupId>org.openjfx</groupId>
<version>${javafx.version}</version> <artifactId>javafx-graphics</artifactId>
</dependency> <version>${javafx.version}</version>
<dependency> </dependency>
<groupId>org.openjfx</groupId> <dependency>
<artifactId>javafx-fxml</artifactId> <groupId>org.openjfx</groupId>
<version>${javafx.version}</version> <artifactId>javafx-fxml</artifactId>
</dependency> <version>${javafx.version}</version>
<dependency> </dependency>
<groupId>org.openjfx</groupId> <dependency>
<artifactId>javafx-web</artifactId> <groupId>org.openjfx</groupId>
<version>${javafx.version}</version> <artifactId>javafx-web</artifactId>
</dependency> <version>${javafx.version}</version>
</dependency>
<!-- Logging implementation -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Lombok --> <!-- Lombok -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
@ -96,6 +108,13 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- Logging API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- JUnit 5 --> <!-- JUnit 5 -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>