supplier) {
+ return (T) instances.computeIfAbsent(clazz, k -> supplier.get());
+ }
+}
diff --git a/core/src/main/java/com/euonia/core/SnowflakeId.java b/core/src/main/java/com/euonia/core/SnowflakeId.java
new file mode 100644
index 0000000..04a4fcb
--- /dev/null
+++ b/core/src/main/java/com/euonia/core/SnowflakeId.java
@@ -0,0 +1,90 @@
+package com.euonia.core;
+
+/**
+ * SnowflakeId is a distributed unique ID generator inspired by Twitter's Snowflake algorithm.
+ * It generates 64-bit unique IDs based on the current timestamp, a worker ID, a datacenter ID, and a sequence number.
+ * The generated IDs are sortable by time and can be generated in a distributed environment without coordination between nodes.
+ * The structure of the generated ID is as follows:
+ * - 41 bits for the timestamp (in milliseconds since a custom epoch)
+ * - 5 bits for the datacenter ID
+ * - 5 bits for the worker ID
+ * - 12 bits for the sequence number
+ * This implementation allows for up to 1024 unique IDs per millisecond per worker and supports up to 32 datacenters and 32 workers per datacenter.
+ * The custom epoch is set to January 1, 2021, which allows for a long lifespan of the ID generation without running out of bits for the timestamp.
+ */
+@SuppressWarnings("unused")
+public final class SnowflakeId {
+ private static final long EPOCH = 1609459200000L; // 2021-01-01 00:00:00 UTC
+ private static final long WORKER_ID_BITS = 5L;
+ private static final long DATACENTER_ID_BITS = 5L;
+ private static final long SEQUENCE_BITS = 12L;
+
+ private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
+ private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
+ private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
+
+ private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
+ private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
+ private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
+
+ private final long workerId;
+ private final long datacenterId;
+ private long sequence = 0L;
+ private long lastTimestamp = -1L;
+
+ private SnowflakeId(long workerId, long datacenterId) {
+ if (workerId > MAX_WORKER_ID || workerId < 0) {
+ throw new IllegalArgumentException(String.format("Worker ID must be between 0 and %d", MAX_WORKER_ID));
+ }
+ if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
+ throw new IllegalArgumentException(String.format("Datacenter ID must be between 0 and %d", MAX_DATACENTER_ID));
+ }
+ this.workerId = workerId;
+ this.datacenterId = datacenterId;
+ }
+
+ public static synchronized SnowflakeId getInstance(long workerId, long datacenterId) {
+ return new SnowflakeId(workerId, datacenterId);
+ }
+
+ public static synchronized SnowflakeId getInstance() {
+ return new SnowflakeId(0, 0);
+ }
+
+ public synchronized long nextId() {
+ long timestamp = timeGen();
+
+ if (timestamp < lastTimestamp) {
+ throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
+ }
+
+ if (lastTimestamp == timestamp) {
+ sequence = (sequence + 1) & MAX_SEQUENCE;
+ if (sequence == 0) {
+ timestamp = tilNextMillis(lastTimestamp);
+ }
+ } else {
+ sequence = 0L;
+ }
+
+ lastTimestamp = timestamp;
+
+ return ((timestamp - EPOCH) << TIMESTAMP_LEFT_SHIFT)
+ | (datacenterId << DATACENTER_ID_SHIFT)
+ | (workerId << WORKER_ID_SHIFT)
+ | sequence;
+ }
+
+ private long tilNextMillis(long lastTimestamp) {
+ long timestamp = timeGen();
+ while (timestamp <= lastTimestamp) {
+ timestamp = timeGen();
+ }
+ return timestamp;
+ }
+
+ private long timeGen() {
+ return System.currentTimeMillis();
+ }
+}
+
diff --git a/core/src/main/java/com/euonia/core/ULID.java b/core/src/main/java/com/euonia/core/ULID.java
new file mode 100644
index 0000000..7a79ea1
--- /dev/null
+++ b/core/src/main/java/com/euonia/core/ULID.java
@@ -0,0 +1,96 @@
+package com.euonia.core;
+
+import java.security.SecureRandom;
+import java.time.Instant;
+
+/**
+ * ULID (Universally Unique Lexicographically Sortable Identifier) generator.
+ *
+ * ULID is a 128-bit universally unique identifier that is lexicographically
+ * sortable and URL-safe.
+ *
+ */
+@SuppressWarnings("unused")
+public final class ULID {
+
+ /**
+ * ULID uses a specific 32-character encoding known as Crockford's Base32,
+ * which includes digits and upper-case letters but excludes letters like "I",
+ * "L", "O"
+ * to avoid confusion with digits.
+ */
+ private static final String CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
+
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private ULID() {
+ // utility class, prevent instantiation
+ }
+
+ /**
+ * Generates a new ULID string.
+ *
+ * @return a 32-character ULID string
+ */
+ public static String generate() {
+ byte[] timestamp = getTimestamp();
+ byte[] randomBytes = getRandomBytes();
+ return encode(timestamp, randomBytes);
+ }
+
+ /**
+ * Gets the current UTC timestamp as a 48-bit (6 bytes) big-endian value.
+ *
+ * @return 6 bytes representing the timestamp in milliseconds since Unix epoch
+ */
+ private static byte[] getTimestamp() {
+ long timestamp = Instant.now().toEpochMilli();
+ // Convert to 8 bytes in big-endian order
+ byte[] bytes = new byte[8];
+ bytes[0] = (byte) (timestamp >> 56);
+ bytes[1] = (byte) (timestamp >> 48);
+ bytes[2] = (byte) (timestamp >> 40);
+ bytes[3] = (byte) (timestamp >> 32);
+ bytes[4] = (byte) (timestamp >> 24);
+ bytes[5] = (byte) (timestamp >> 16);
+ bytes[6] = (byte) (timestamp >> 8);
+ bytes[7] = (byte) timestamp;
+ // Extract the last 6 bytes (lower 48 bits)
+ byte[] result = new byte[6];
+ System.arraycopy(bytes, 2, result, 0, 6);
+ return result;
+ }
+
+ /**
+ * Generates 10 cryptographically secure random bytes (80 bits).
+ *
+ * @return 10 random bytes
+ */
+ private static byte[] getRandomBytes() {
+ byte[] randomBytes = new byte[10];
+ SECURE_RANDOM.nextBytes(randomBytes);
+ return randomBytes;
+ }
+
+ /**
+ * Encodes the timestamp and random bytes into a Crockford's Base32 encoded ULID
+ * string.
+ *
+ * @param timestamp 6 bytes of timestamp data
+ * @param randomBytes 10 bytes of random data
+ * @return a Base32-encoded ULID string
+ */
+ private static String encode(byte[] timestamp, byte[] randomBytes) {
+ byte[] ulidBytes = new byte[16];
+ System.arraycopy(timestamp, 0, ulidBytes, 0, 6);
+ System.arraycopy(randomBytes, 0, ulidBytes, 6, 10);
+
+ StringBuilder ulid = new StringBuilder(32);
+ for (byte b : ulidBytes) {
+ int value = b & 0xFF;
+ ulid.append(CROCKFORD_BASE32.charAt((value >> 3) & 0x1F));
+ ulid.append(CROCKFORD_BASE32.charAt(value & 0x1F));
+ }
+ return ulid.toString();
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/BadGatewayException.java b/core/src/main/java/com/euonia/http/BadGatewayException.java
new file mode 100644
index 0000000..a0f3de2
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/BadGatewayException.java
@@ -0,0 +1,29 @@
+package com.euonia.http;
+
+/**
+ * The BadGatewayException is thrown when an HTTP request results in a 502 Bad Gateway status code.
+ * This exception indicates that the server, while acting as a gateway or proxy, received an invalid response from the upstream server it accessed in attempting to fulfill the request.
+ * It is typically used in scenarios where the server is acting as a gateway or proxy and encounters an error while trying to communicate with the upstream server, such as when the upstream server is down, overloaded, or returns an invalid response.
+ * This exception provides constructors to create an exception with a specific message and an optional cause for the exception, allowing for more detailed error information to be included when the exception is thrown.
+ */
+public class BadGatewayException extends HttpStatusException {
+
+ /**
+ * Creates a new BadGatewayException with the specified message. This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public BadGatewayException(String message) {
+ super(502, message);
+ }
+
+ /**
+ * Creates a new BadGatewayException with the specified message and cause. This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public BadGatewayException(String message, Throwable cause) {
+ super(502, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/BadRequestException.java b/core/src/main/java/com/euonia/http/BadRequestException.java
new file mode 100644
index 0000000..bfb608c
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/BadRequestException.java
@@ -0,0 +1,26 @@
+package com.euonia.http;
+
+/**
+ * The BadRequestException class represents an HTTP 400 Bad Request error. It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class BadRequestException extends HttpStatusException {
+
+ /**
+ * Creates a new BadRequestException with the specified message. This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public BadRequestException(String message) {
+ super(400, message);
+ }
+
+ /**
+ * Creates a new BadRequestException with the specified message and cause. This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public BadRequestException(String message, Throwable cause) {
+ super(400, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/ConflictException.java b/core/src/main/java/com/euonia/http/ConflictException.java
new file mode 100644
index 0000000..810e8f1
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/ConflictException.java
@@ -0,0 +1,27 @@
+package com.euonia.http;
+
+/**
+ * The ConflictException class represents an HTTP 409 Conflict error. It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class ConflictException extends HttpStatusException {
+
+ /**
+ * Creates a new ConflictException with the specified message. This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public ConflictException(String message) {
+ super(409, message);
+ }
+
+ /**
+ * Creates a new ConflictException with the specified message and cause. This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public ConflictException(String message, Throwable cause) {
+ super(409, message, cause);
+ }
+
+}
diff --git a/core/src/main/java/com/euonia/http/ForbiddenException.java b/core/src/main/java/com/euonia/http/ForbiddenException.java
new file mode 100644
index 0000000..4641af8
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/ForbiddenException.java
@@ -0,0 +1,30 @@
+package com.euonia.http;
+
+/**
+ * The ForbiddenException class represents an HTTP 403 Forbidden error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class ForbiddenException extends HttpStatusException {
+
+ /**
+ * Creates a new ForbiddenException with the specified message.
+ * This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public ForbiddenException(String message) {
+ super(403, message);
+ }
+
+ /**
+ * Creates a new ForbiddenException with the specified message and cause.
+ * This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public ForbiddenException(String message, Throwable cause) {
+ super(403, message, cause);
+ }
+
+}
diff --git a/core/src/main/java/com/euonia/http/GatewayTimeoutException.java b/core/src/main/java/com/euonia/http/GatewayTimeoutException.java
new file mode 100644
index 0000000..5d382ab
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/GatewayTimeoutException.java
@@ -0,0 +1,30 @@
+package com.euonia.http;
+
+/**
+ * The GatewayTimeoutException class represents an HTTP 504 Gateway Timeout error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class GatewayTimeoutException extends HttpStatusException {
+
+ /**
+ * Creates a new GatewayTimeoutException with the specified message.
+ * This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public GatewayTimeoutException(String message) {
+ super(504, message);
+ }
+
+ /**
+ * Creates a new GatewayTimeoutException with the specified message and cause.
+ * This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public GatewayTimeoutException(String message, Throwable cause) {
+ super(504, message, cause);
+ }
+
+}
diff --git a/core/src/main/java/com/euonia/http/HttpStatusException.java b/core/src/main/java/com/euonia/http/HttpStatusException.java
new file mode 100644
index 0000000..a91b2fe
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/HttpStatusException.java
@@ -0,0 +1,26 @@
+package com.euonia.http;
+
+/**
+ * The HttpStatusException is thrown when an HTTP request results in an error status code.
+ * It contains the HTTP status code and a message describing the error.
+ * This exception can be used to represent various HTTP errors, such as 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error, etc.
+ * It provides constructors to create an exception with a specific status code and message, as well as an optional cause for the exception.
+ */
+public class HttpStatusException extends RuntimeException {
+ private final int statusCode;
+
+ public HttpStatusException(int statusCode, String message) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+
+ public HttpStatusException(int statusCode, String message, Throwable cause) {
+ super(message, cause);
+ this.statusCode = statusCode;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+}
diff --git a/core/src/main/java/com/euonia/http/InternalServerErrorException.java b/core/src/main/java/com/euonia/http/InternalServerErrorException.java
new file mode 100644
index 0000000..55ae5c8
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/InternalServerErrorException.java
@@ -0,0 +1,26 @@
+package com.euonia.http;
+
+/**
+ * The InternalServerErrorException class represents an HTTP 500 Internal Server Error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class InternalServerErrorException extends HttpStatusException {
+ /**
+ * Creates a new InternalServerErrorException with the specified message. This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public InternalServerErrorException(String message) {
+ super(500, message);
+ }
+
+ /**
+ * Creates a new InternalServerErrorException with the specified message and cause. This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public InternalServerErrorException(String message, Throwable cause) {
+ super(500, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/MethodNotAllowedException.java b/core/src/main/java/com/euonia/http/MethodNotAllowedException.java
new file mode 100644
index 0000000..c9fe84e
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/MethodNotAllowedException.java
@@ -0,0 +1,26 @@
+package com.euonia.http;
+
+/**
+ * The MethodNotAllowedException class represents an HTTP 405 Method Not Allowed error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class MethodNotAllowedException extends HttpStatusException {
+ /**
+ * Creates a new MethodNotAllowedException with the specified message. This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public MethodNotAllowedException(String message) {
+ super(405, message);
+ }
+
+ /**
+ * Creates a new MethodNotAllowedException with the specified message and cause. This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public MethodNotAllowedException(String message, Throwable cause) {
+ super(405, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/RequestTimeoutException.java b/core/src/main/java/com/euonia/http/RequestTimeoutException.java
new file mode 100644
index 0000000..d67643c
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/RequestTimeoutException.java
@@ -0,0 +1,27 @@
+package com.euonia.http;
+/**
+ * The RequestTimeoutException class represents an HTTP 408 Request Timeout error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class RequestTimeoutException extends HttpStatusException {
+ /**
+ * Creates a new RequestTimeoutException with the specified message.
+ * This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public RequestTimeoutException(String message) {
+ super(408, message);
+ }
+
+ /**
+ * Creates a new RequestTimeoutException with the specified message and cause.
+ * This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public RequestTimeoutException(String message, Throwable cause) {
+ super(408, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/ResourceNotFoundException.java b/core/src/main/java/com/euonia/http/ResourceNotFoundException.java
new file mode 100644
index 0000000..dad5a5b
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/ResourceNotFoundException.java
@@ -0,0 +1,28 @@
+package com.euonia.http;
+
+/**
+ * The ResourceNotFoundException class represents an HTTP 404 Not Found error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class ResourceNotFoundException extends HttpStatusException {
+ /**
+ * Creates a new ResourceNotFoundException with the specified message.
+ * This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public ResourceNotFoundException(String message) {
+ super(404, message);
+ }
+
+ /**
+ * Creates a new ResourceNotFoundException with the specified message and cause.
+ * This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public ResourceNotFoundException(String message, Throwable cause) {
+ super(404, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/ResponseHttpStatusCode.java b/core/src/main/java/com/euonia/http/ResponseHttpStatusCode.java
new file mode 100644
index 0000000..35c6160
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/ResponseHttpStatusCode.java
@@ -0,0 +1,18 @@
+package com.euonia.http;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ResponseHttpStatusCode {
+ /**
+ * The HTTP status code associated with the response.
+ * This value is used to indicate the status of the HTTP response, such as 400 for bad request, 404 for not found, etc.
+ *
+ * @return The HTTP status code for the response.
+ */
+ int value();
+}
diff --git a/core/src/main/java/com/euonia/http/ServiceUnavailableException.java b/core/src/main/java/com/euonia/http/ServiceUnavailableException.java
new file mode 100644
index 0000000..24e11ad
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/ServiceUnavailableException.java
@@ -0,0 +1,28 @@
+package com.euonia.http;
+
+/**
+ * The ServiceUnavailableException class represents an HTTP 503 Service Unavailable error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class ServiceUnavailableException extends HttpStatusException {
+ /**
+ * Creates a new ServiceUnavailableException with the specified message.
+ * This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public ServiceUnavailableException(String message) {
+ super(503, message);
+ }
+
+ /**
+ * Creates a new ServiceUnavailableException with the specified message and cause.
+ * This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public ServiceUnavailableException(String message, Throwable cause) {
+ super(503, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/TooManyRequestsException.java b/core/src/main/java/com/euonia/http/TooManyRequestsException.java
new file mode 100644
index 0000000..e6d9e37
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/TooManyRequestsException.java
@@ -0,0 +1,28 @@
+package com.euonia.http;
+
+/**
+ * The TooManyRequestsException class represents an HTTP 429 Too Many Requests error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class TooManyRequestsException extends HttpStatusException {
+ /**
+ * Creates a new TooManyRequestsException with the specified message.
+ * This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public TooManyRequestsException(String message) {
+ super(429, message);
+ }
+
+ /**
+ * Creates a new TooManyRequestsException with the specified message and cause.
+ * This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public TooManyRequestsException(String message, Throwable cause) {
+ super(429, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/http/UpgradeRequiredException.java b/core/src/main/java/com/euonia/http/UpgradeRequiredException.java
new file mode 100644
index 0000000..6b418dd
--- /dev/null
+++ b/core/src/main/java/com/euonia/http/UpgradeRequiredException.java
@@ -0,0 +1,28 @@
+package com.euonia.http;
+
+/**
+ * The UpgradeRequiredException class represents an HTTP 426 Upgrade Required error.
+ * It extends the HttpStatusException class and provides constructors for creating instances of this exception with a specific message and an optional cause.
+ */
+public class UpgradeRequiredException extends HttpStatusException {
+ /**
+ * Creates a new UpgradeRequiredException with the specified message.
+ * This constructor allows for creating an exception with a specific message, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ */
+ public UpgradeRequiredException(String message) {
+ super(426, message);
+ }
+
+ /**
+ * Creates a new UpgradeRequiredException with the specified message and cause.
+ * This constructor allows for creating an exception with a specific message and cause, providing more detailed error information when the exception is thrown.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception.
+ */
+ public UpgradeRequiredException(String message, Throwable cause) {
+ super(426, message, cause);
+ }
+}
diff --git a/core/src/main/java/com/euonia/reflection/DisplayName.java b/core/src/main/java/com/euonia/reflection/DisplayName.java
new file mode 100644
index 0000000..6864f82
--- /dev/null
+++ b/core/src/main/java/com/euonia/reflection/DisplayName.java
@@ -0,0 +1,16 @@
+package com.euonia.reflection;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to specify a display name for a field, which can be used in UI or error messages instead of the actual field name.
+ * This is particularly useful for providing more user-friendly names for fields that may have technical or non-descriptive names in the code.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD})
+public @interface DisplayName {
+ String value();
+}
diff --git a/core/src/main/java/com/euonia/reflection/GenericType.java b/core/src/main/java/com/euonia/reflection/GenericType.java
new file mode 100644
index 0000000..018a548
--- /dev/null
+++ b/core/src/main/java/com/euonia/reflection/GenericType.java
@@ -0,0 +1,62 @@
+package com.euonia.reflection;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+public abstract class GenericType {
+ private final Type type;
+
+ private GenericType(Type type) {
+ this.type = type;
+ }
+
+ protected GenericType() {
+ Class> parameterizedTypeReferenceSubclass = findGenericTypeReferenceSubclass(getClass());
+ Type type = parameterizedTypeReferenceSubclass.getGenericSuperclass();
+ if (!(type instanceof ParameterizedType parameterizedType)) {
+ throw new IllegalArgumentException("Type must be a parameterized type");
+ }
+ //Assert.isInstanceOf(ParameterizedType.class, type, "Type must be a parameterized type");
+ Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+ if (actualTypeArguments.length != 1) {
+ throw new IllegalArgumentException("Wrong number of actual type arguments");
+ }
+ //Assert.isTrue(actualTypeArguments.length == 1, "Number of type arguments must be 1");
+ this.type = actualTypeArguments[0];
+ }
+
+ public Type getType() {
+ return this.type;
+ }
+
+ public static GenericType forType(Type type) {
+ return new GenericType<>(type) {
+ };
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return (this == other || (other instanceof GenericType> that && this.type.equals(that.type)));
+ }
+
+ @Override
+ public int hashCode() {
+ return this.type.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "GenericType<" + this.type + ">";
+ }
+
+ private static Class> findGenericTypeReferenceSubclass(Class> child) {
+ Class> parent = child.getSuperclass();
+ if (Object.class == parent) {
+ throw new IllegalStateException("Expected GenericType superclass");
+ } else if (GenericType.class == parent) {
+ return child;
+ } else {
+ return findGenericTypeReferenceSubclass(parent);
+ }
+ }
+}
diff --git a/core/src/main/java/com/euonia/reflection/TypeHelper.java b/core/src/main/java/com/euonia/reflection/TypeHelper.java
new file mode 100644
index 0000000..866f8f1
--- /dev/null
+++ b/core/src/main/java/com/euonia/reflection/TypeHelper.java
@@ -0,0 +1,463 @@
+package com.euonia.reflection;
+
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.time.*;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.Temporal;
+import java.util.*;
+
+@SuppressWarnings("unused")
+public final class TypeHelper {
+ private TypeHelper() {
+ }
+
+ public static Object coerceValue(Class> desiredType, Class> valueType, Object value) {
+ if (desiredType == null)
+ throw new IllegalArgumentException("desiredType is null");
+
+ if (value == null) {
+ if (desiredType.isPrimitive()) {
+ return defaultPrimitiveValue(desiredType);
+ }
+ return null;
+ }
+
+ if (valueType == null)
+ valueType = value.getClass();
+
+ if (desiredType.isAssignableFrom(valueType)) {
+ return value;
+ }
+
+ Class> boxedDesired = boxIfPrimitive(desiredType);
+ Class> boxedValue = boxIfPrimitive(valueType);
+
+ // Enums
+ if (boxedDesired.isEnum()) {
+ return convertToEnum(boxedDesired, value);
+ }
+
+ // Date/time targets
+ if (isDateTimeTarget(boxedDesired)) {
+ return convertToDateTime(boxedDesired, value);
+ }
+
+ // Collections/Map/JSON
+ if (Collection.class.isAssignableFrom(boxedDesired) || boxedDesired.isArray()
+ || Map.class.isAssignableFrom(boxedDesired)) {
+ return convertToCollectionOrMap(boxedDesired, value);
+ }
+
+ // String target
+ if (boxedDesired == String.class) {
+ return value.toString();
+ }
+
+ // Boolean target
+ if (boxedDesired == Boolean.class) {
+ return convertToBoolean(value);
+ }
+
+ // Numeric
+ if (Number.class.isAssignableFrom(boxedDesired) || isPrimitiveNumber(desiredType)) {
+ return convertToNumber(boxedDesired, value);
+ }
+
+ if (boxedDesired == UUID.class) {
+ return convertToUUID(value);
+ }
+
+ if (boxedDesired == Character.class) {
+ return convertToCharacter(value);
+ }
+
+ if (boxedDesired.isAssignableFrom(boxedValue)) {
+ return value;
+ }
+
+ // As a last attempt, try Jackson convert if present
+ Object conv = tryJacksonConvert(value, desiredType);
+ if (conv != null)
+ return conv;
+
+ throw new IllegalArgumentException(String.format("Cannot convert value of type %s to %s (value=%s)",
+ value.getClass().getName(), desiredType.getName(), value));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T coerceValue(Class desiredType, Object value) {
+ return (T) coerceValue(desiredType, (value == null ? null : value.getClass()), value);
+ }
+
+ private static Class> boxIfPrimitive(Class> c) {
+ if (!c.isPrimitive())
+ return c;
+ if (c == int.class)
+ return Integer.class;
+ if (c == long.class)
+ return Long.class;
+ if (c == short.class)
+ return Short.class;
+ if (c == byte.class)
+ return Byte.class;
+ if (c == float.class)
+ return Float.class;
+ if (c == double.class)
+ return Double.class;
+ if (c == boolean.class)
+ return Boolean.class;
+ if (c == char.class)
+ return Character.class;
+ return c;
+ }
+
+ private static boolean isPrimitiveNumber(Class> c) {
+ return c == int.class || c == long.class || c == short.class || c == byte.class
+ || c == float.class || c == double.class;
+ }
+
+ private static Object defaultPrimitiveValue(Class> primitiveType) {
+ if (primitiveType == boolean.class)
+ return false;
+ if (primitiveType == char.class)
+ return '\0';
+ if (primitiveType == byte.class)
+ return (byte) 0;
+ if (primitiveType == short.class)
+ return (short) 0;
+ if (primitiveType == int.class)
+ return 0;
+ if (primitiveType == long.class)
+ return 0L;
+ if (primitiveType == float.class)
+ return 0.0f;
+ if (primitiveType == double.class)
+ return 0.0d;
+ return null;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked", "IfCanBeSwitch"})
+ private static Object convertToEnum(Class> enumType, Object value) {
+
+ if (value == null) {
+ return null;
+ }
+
+ if (value instanceof Enum) {
+ return value;
+ }
+
+ if (value instanceof String target) {
+ String string = target.trim();
+ if (string.isEmpty()) {
+ return null;
+ }
+ try {
+ return Enum.valueOf((Class extends Enum>) enumType, string);
+ } catch (IllegalArgumentException ex) {
+ // Try ordinal
+ try {
+ int ord = Integer.parseInt(string);
+ Enum[] constants = (Enum[]) enumType.getEnumConstants();
+ if (ord >= 0 && ord < constants.length)
+ return constants[ord];
+ } catch (NumberFormatException ignored) {
+ }
+ throw ex;
+ }
+ }
+
+ if (value instanceof Number number) {
+ int ord = number.intValue();
+ Enum[] constants = (Enum[]) enumType.getEnumConstants();
+ if (ord >= 0 && ord < constants.length) {
+ return constants[ord];
+ }
+ throw new IllegalArgumentException("Enum ordinal out of range: " + ord);
+ }
+
+ String vs = value.toString();
+ if (vs != null && !vs.isEmpty()) {
+ return convertToEnum(enumType, vs);
+ }
+ throw new IllegalArgumentException("Cannot convert to enum: " + value);
+ }
+
+ private static Object convertToBoolean(Object value) {
+ if (value instanceof Boolean)
+ return value;
+ if (value instanceof Number) {
+ return ((Number) value).intValue() != 0;
+ }
+ String s = value.toString().trim().toLowerCase(Locale.ROOT);
+ return switch (s) {
+ case "", "false", "0", "no", "n" -> false;
+ case "true", "1", "yes", "y" -> true;
+ default -> throw new IllegalArgumentException("Cannot convert to boolean: " + value);
+ };
+ }
+
+ private static Object convertToNumber(Class> targetNumberClass, Object value) {
+ if (value instanceof Number) {
+ return castNumber((Number) value, targetNumberClass);
+ }
+ String s = value.toString().trim();
+ if (s.isEmpty()) {
+ return castNumber(BigDecimal.ZERO, targetNumberClass);
+ }
+ try {
+ BigDecimal bd = new BigDecimal(s);
+ return castNumber(bd, targetNumberClass);
+ } catch (Exception ex) {
+ throw new IllegalArgumentException("Cannot convert to number: " + value, ex);
+ }
+ }
+
+ private static Object castNumber(Number number, Class> targetNumberClass) {
+ if (targetNumberClass == Byte.class)
+ return number.byteValue();
+ if (targetNumberClass == Short.class)
+ return number.shortValue();
+ if (targetNumberClass == Integer.class)
+ return number.intValue();
+ if (targetNumberClass == Long.class)
+ return number.longValue();
+ if (targetNumberClass == Float.class)
+ return number.floatValue();
+ if (targetNumberClass == Double.class)
+ return number.doubleValue();
+ if (targetNumberClass == BigDecimal.class) {
+ if (number instanceof BigDecimal)
+ return number;
+ return BigDecimal.valueOf(number.doubleValue());
+ }
+ if (targetNumberClass.isInstance(number))
+ return number;
+ throw new IllegalArgumentException("Unsupported target numeric type: " + targetNumberClass.getName());
+ }
+
+ private static Object convertToUUID(Object value) {
+ if (value instanceof UUID) {
+ return value;
+ }
+ String s = value.toString().trim();
+ if (s.isEmpty())
+ return null;
+ return UUID.fromString(s);
+ }
+
+ private static Object convertToCharacter(Object value) {
+
+ if (value == null) {
+ return '\0';
+ }
+ if (value instanceof Character) {
+ return value;
+ }
+ String s = value.toString();
+ if (s.isEmpty())
+ return '\0';
+ return s.charAt(0);
+ }
+
+ // Extended conversions: date/time, collections, maps, JSON (when Jackson is
+ // available)
+ private static boolean isDateTimeTarget(Class> boxedDesired) {
+ return boxedDesired == Date.class || boxedDesired == Instant.class || boxedDesired == LocalDateTime.class
+ || boxedDesired == LocalDate.class || boxedDesired == LocalTime.class
+ || boxedDesired == OffsetDateTime.class
+ || boxedDesired == ZonedDateTime.class;
+ }
+
+ private static Object convertToDateTime(Class> target, Object value) {
+ if (value == null)
+ return null;
+
+ if (target.isInstance(value))
+ return value;
+
+ if (target == Date.class) {
+ if (value instanceof Date)
+ return value;
+ if (value instanceof Number)
+ return new Date(((Number) value).longValue());
+ String s = value.toString().trim();
+ if (s.isEmpty())
+ return null;
+ try {
+ Instant inst = Instant.parse(s);
+ return Date.from(inst);
+ } catch (DateTimeParseException ignored) {
+ }
+ try {
+ long l = Long.parseLong(s);
+ return new Date(l);
+ } catch (NumberFormatException ignored) {
+ }
+ throw new IllegalArgumentException("Cannot convert to Date: " + value);
+ }
+
+ if (Temporal.class.isAssignableFrom(target) || target == LocalDate.class || target == LocalDateTime.class
+ || target == LocalTime.class || target == Instant.class || target == OffsetDateTime.class
+ || target == ZonedDateTime.class) {
+ if (value instanceof Number) {
+ long epoch = ((Number) value).longValue();
+ Instant inst = Instant.ofEpochMilli(epoch);
+ return convertInstantToTarget(target, inst);
+ }
+
+ String s = value.toString().trim();
+ if (s.isEmpty())
+ return null;
+ List> parsers = Arrays.asList(
+ Instant::parse,
+ OffsetDateTime::parse,
+ ZonedDateTime::parse,
+ LocalDateTime::parse,
+ LocalDate::parse,
+ LocalTime::parse);
+ for (var p : parsers) {
+ try {
+ Object parsed = p.apply(s);
+
+ return switch (target.getSimpleName()) {
+ case "Instant" -> convertInstantToTarget(target, (Instant) parsed);
+ case "OffsetDateTime" -> convertInstantToTarget(target, ((OffsetDateTime) parsed).toInstant());
+ case "ZonedDateTime" -> convertInstantToTarget(target, ((ZonedDateTime) parsed).toInstant());
+ case "LocalDateTime" -> convertInstantToTarget(target,
+ ((LocalDateTime) parsed).atZone(ZoneId.systemDefault()).toInstant());
+ case "LocalDate" -> convertInstantToTarget(target,
+ ((LocalDate) parsed).atStartOfDay(ZoneId.systemDefault()).toInstant());
+ case "LocalTime" -> convertInstantToTarget(target, LocalDateTime
+ .of(LocalDate.now(), (LocalTime) parsed).atZone(ZoneId.systemDefault()).toInstant());
+ default -> null;
+ };
+ } catch (DateTimeParseException ignored) {
+ }
+ }
+
+ try {
+ long l = Long.parseLong(s);
+ Instant inst = Instant.ofEpochMilli(l);
+ return convertInstantToTarget(target, inst);
+ } catch (NumberFormatException ignored) {
+ }
+
+ throw new IllegalArgumentException("Cannot parse date/time value: " + value);
+ }
+
+ throw new IllegalArgumentException("Unsupported date/time target: " + target.getName());
+ }
+
+ private static Object convertInstantToTarget(Class> target, Instant inst) {
+ if (target == Instant.class)
+ return inst;
+ if (target == LocalDateTime.class)
+ return LocalDateTime.ofInstant(inst, ZoneId.systemDefault());
+ if (target == LocalDate.class)
+ return LocalDateTime.ofInstant(inst, ZoneId.systemDefault()).toLocalDate();
+ if (target == LocalTime.class)
+ return LocalDateTime.ofInstant(inst, ZoneId.systemDefault()).toLocalTime();
+ if (target == OffsetDateTime.class)
+ return OffsetDateTime.ofInstant(inst, ZoneId.systemDefault());
+ if (target == ZonedDateTime.class)
+ return ZonedDateTime.ofInstant(inst, ZoneId.systemDefault());
+ if (target == Date.class)
+ return Date.from(inst);
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Object convertToCollectionOrMap(Class> desiredType, Object value) {
+ if (value == null)
+ return null;
+ if (desiredType.isInstance(value))
+ return value;
+
+ if (value instanceof Map) {
+ Object conv = tryJacksonConvert(value, desiredType);
+ if (conv != null)
+ return conv;
+ if (Map.class.isAssignableFrom(desiredType))
+ return value;
+ }
+
+ if (value instanceof Iterable || value.getClass().isArray()) {
+ List