[FIX] test on DateTime is ended and solution in 90% operational

This commit is contained in:
Edouard DUPIN 2025-04-07 23:08:23 +02:00
parent ecc8829e8c
commit b12108ec00
14 changed files with 326 additions and 303 deletions

View File

@ -11,15 +11,16 @@ public class GenericCatcher {
public static void addAll(final ResourceConfig rc) { public static void addAll(final ResourceConfig rc) {
// Generic Json parsing error // Generic Json parsing error
rc.register(JacksonExceptionCatcher.class); rc.register(JacksonExceptionCatcher.class);
// Catch jakarta generic errors
rc.register(WebApplicationExceptionCatcher.class);
// Archidata exceptions // Archidata exceptions
rc.register(InputExceptionCatcher.class); rc.register(InputExceptionCatcher.class);
rc.register(SystemExceptionCatcher.class); rc.register(SystemExceptionCatcher.class);
rc.register(FailExceptionCatcher.class); rc.register(FailExceptionCatcher.class);
// generic Exception catcher // generic Exception catcher
rc.register(ExceptionCatcher.class);
rc.register(ConstraintViolationExceptionCatcher.class); rc.register(ConstraintViolationExceptionCatcher.class);
rc.register(QueryParamExceptionCatcher.class);
rc.register(ExceptionCatcher.class);
// Catch jakarta generic errors
rc.register(WebApplicationExceptionCatcher.class);
} }
} }

View File

@ -0,0 +1,34 @@
package org.kar.archidata.catcher;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.glassfish.jersey.server.ParamException.QueryParamException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
public class QueryParamExceptionCatcher implements ExceptionMapper<QueryParamException> {
private static final Logger LOGGER = LoggerFactory.getLogger(QueryParamExceptionCatcher.class);
@Override
public Response toResponse(final QueryParamException exception) {
LOGGER.trace("Catch IllegalArgumentException: {}", exception.getLocalizedMessage());
final RestErrorResponse ret = build(exception);
LOGGER.error("Error OID={}", ret.oid);
return Response.status(Response.Status.BAD_REQUEST).entity(ret).type(MediaType.APPLICATION_JSON).build();
}
private RestErrorResponse build(final QueryParamException exception) {
final List<RestInputError> inputError = new ArrayList<>();
inputError.add(new RestInputError("query", exception.getParameterName(), exception.getCause().getMessage()));
final String errorType = "Error on query input='" + exception.getParameterName() + "'";
return new RestErrorResponse(Response.Status.BAD_REQUEST, Instant.now().toString(), errorType,
"Input parsing fail", inputError);
}
}

View File

@ -27,6 +27,17 @@ public class RestInputError {
public RestInputError() {} public RestInputError() {}
public RestInputError(final String argument, final String path, final String message) {
this.path = argument;
this.path = path;
this.message = message;
}
public RestInputError(final String path, final String message) {
this.path = path;
this.message = message;
}
public RestInputError(final Path path, final String message) { public RestInputError(final Path path, final String message) {
final Matcher matcher = PATTERN.matcher(path.toString()); final Matcher matcher = PATTERN.matcher(path.toString());
if (matcher.find()) { if (matcher.find()) {
@ -39,11 +50,6 @@ public class RestInputError {
this.message = message; this.message = message;
} }
public RestInputError(final String path, final String message) {
this.path = path;
this.message = message;
}
String getFullPath() { String getFullPath() {
if (this.path == null) { if (this.path == null) {
return this.argument; return this.argument;

View File

@ -1,51 +0,0 @@
package org.kar.archidata.converter.Jakarta;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.OffsetDateTime;
import java.util.Date;
import org.kar.archidata.tools.DateTools;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.ws.rs.ext.Provider;
@Provider
public class DateParamConverter implements ParamConverterProvider {
@SuppressWarnings("unchecked")
@Override
public <T> ParamConverter<T> getConverter(
final Class<T> rawType,
final Type genericType,
final Annotation[] annotations) {
if (rawType != OffsetDateTime.class) {
return null;
}
return (ParamConverter<T>) new ParamConverter<Date>() {
@Override
public Date fromString(final String value) {
if (value == null || value.isEmpty()) {
return null;
}
System.out.println(value);
try {
return DateTools.parseDate(value);
} catch (final IOException e) {
throw new IllegalArgumentException("Invalid date format. Please use ISO8601", e);
}
}
@Override
public String toString(final Date value) {
if (value == null) {
return null;
}
return DateTools.serializeMilliWithUTCTimeZone(value);
}
};
}
}

View File

@ -0,0 +1,53 @@
package org.kar.archidata.converter.Jakarta;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Date;
import org.kar.archidata.tools.DateTools;
import jakarta.annotation.Priority;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.ws.rs.ext.Provider;
@Provider
@Priority(1)
public class DateParamConverterProvider implements ParamConverterProvider {
@SuppressWarnings("unchecked")
@Override
public <T> ParamConverter<T> getConverter(
final Class<T> rawType,
final Type genericType,
final Annotation[] annotations) {
if (rawType != Date.class) {
return null;
}
return (ParamConverter<T>) new DateParamConverter();
}
public class DateParamConverter implements ParamConverter<Date> {
@Override
public Date fromString(final String value) {
if (value == null || value.isEmpty()) {
return null;
}
try {
return DateTools.parseDate(value);
} catch (final IOException e) {
throw new IllegalArgumentException("Invalid date format. Please use ISO8601", e);
}
}
@Override
public String toString(final Date value) {
if (value == null) {
return null;
}
return DateTools.serializeMilliWithUTCTimeZone(value);
}
}
}

View File

@ -1,50 +0,0 @@
package org.kar.archidata.converter.Jakarta;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.OffsetDateTime;
import org.kar.archidata.tools.DateTools;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.ws.rs.ext.Provider;
@Provider
public class OffsetDateTimeParamConverter implements ParamConverterProvider {
@SuppressWarnings("unchecked")
@Override
public <T> ParamConverter<T> getConverter(
final Class<T> rawType,
final Type genericType,
final Annotation[] annotations) {
if (rawType != OffsetDateTime.class) {
return null;
}
return (ParamConverter<T>) new ParamConverter<OffsetDateTime>() {
@Override
public OffsetDateTime fromString(final String value) {
if (value == null || value.isEmpty()) {
return null;
}
System.out.println(value);
try {
return DateTools.parseOffsetDateTime(value);
} catch (final IOException e) {
throw new IllegalArgumentException("Invalid date format. Please use ISO8601", e);
}
}
@Override
public String toString(final OffsetDateTime value) {
if (value == null) {
return null;
}
return DateTools.serializeMilliWithUTCTimeZone(value);
}
};
}
}

View File

@ -0,0 +1,52 @@
package org.kar.archidata.converter.Jakarta;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.OffsetDateTime;
import org.kar.archidata.tools.DateTools;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.ws.rs.ext.Provider;
@Provider
public class OffsetDateTimeParamConverterProvider implements ParamConverterProvider {
@SuppressWarnings("unchecked")
@Override
public <T> ParamConverter<T> getConverter(
final Class<T> rawType,
final Type genericType,
final Annotation[] annotations) {
if (rawType != OffsetDateTime.class) {
return null;
}
return (ParamConverter<T>) new OffsetDateTimeParamConverter();
}
public class OffsetDateTimeParamConverter implements ParamConverter<OffsetDateTime> {
@Override
public OffsetDateTime fromString(final String value) {
if (value == null || value.isEmpty()) {
return null;
}
try {
return DateTools.parseOffsetDateTime(value);
} catch (final IOException e) {
throw new IllegalArgumentException("Invalid date format. Please use ISO8601", e);
}
}
@Override
public String toString(final OffsetDateTime value) {
if (value == null) {
return null;
}
return DateTools.serializeMilliWithUTCTimeZone(value);
}
};
}

View File

@ -1,6 +1,8 @@
package org.kar.archidata.filter; package org.kar.archidata.filter;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -18,7 +20,12 @@ public class OptionFilter implements ContainerRequestFilter {
@Override @Override
public void filter(final ContainerRequestContext requestContext) throws IOException { public void filter(final ContainerRequestContext requestContext) throws IOException {
LOGGER.warn("Receive message from: [{}] {}", requestContext.getMethod(), requestContext.getUriInfo().getPath()); LOGGER.warn("Receive message from: [{}] '{}'", requestContext.getMethod(),
requestContext.getUriInfo().getPath());
final Map<String, List<String>> queryParams = requestContext.getUriInfo().getQueryParameters();
for (final Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
LOGGER.warn("queryParam: '{}' => '{}'", entry.getKey(), entry.getValue());
}
if (requestContext.getMethod().contentEquals("OPTIONS")) { if (requestContext.getMethod().contentEquals("OPTIONS")) {
requestContext.abortWith(Response.status(Response.Status.NO_CONTENT).build()); requestContext.abortWith(Response.status(Response.Status.NO_CONTENT).build());
} }

View File

@ -2,9 +2,6 @@ package org.kar.archidata.tools;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
@ -20,44 +17,6 @@ import org.slf4j.LoggerFactory;
public class DateTools { public class DateTools {
private final static Logger LOGGER = LoggerFactory.getLogger(DateTools.class); private final static Logger LOGGER = LoggerFactory.getLogger(DateTools.class);
/**
* Parses a date string using a given pattern into a java.util.Date.
*
* @param inputDate the string to parse
* @param pattern the pattern to use (e.g., "yyyy-MM-dd")
* @return parsed Date object
* @throws ParseException if parsing fails
*/
@Deprecated
public static Date parseDate(final String inputDate, final String pattern) throws ParseException {
final SimpleDateFormat format = new SimpleDateFormat(pattern);
return format.parse(inputDate);
}
/**
* Formats a java.util.Date using the specified format pattern.
*
* @param date the Date to format
* @param requiredDateFormat the format string (e.g., "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
* @return formatted string
*/
@Deprecated
public static String formatDate(final Date date, final String requiredDateFormat) {
final SimpleDateFormat df = new SimpleDateFormat(requiredDateFormat);
return df.format(date);
}
/**
* Formats a java.util.Date using a default ISO 8601-like format.
*
* @param date the Date to format
* @return formatted string in pattern "yyyy-MM-dd'T'HH:mm.ss.SSS'Z'"
*/
@Deprecated
public static String formatDate(final Date date) {
return serializeMilliWithUTCTimeZone(date);
}
// List of supported parsers for flexible date string parsing. // List of supported parsers for flexible date string parsing.
// Includes patterns with optional parts, slashes, and ISO standard formats. // Includes patterns with optional parts, slashes, and ISO standard formats.
static final List<DateTimeFormatter> FORMATTERS = Arrays.asList( static final List<DateTimeFormatter> FORMATTERS = Arrays.asList(
@ -77,10 +36,19 @@ public class DateTools {
* @throws IOException if no supported format matches the input * @throws IOException if no supported format matches the input
*/ */
public static OffsetDateTime parseOffsetDateTime(final String dateString) throws IOException { public static OffsetDateTime parseOffsetDateTime(final String dateString) throws IOException {
return parseOffsetDateTime(dateString, false);
}
public static OffsetDateTime parseOffsetDateTime(final String dateString, final boolean missingAsUTC)
throws IOException {
if (dateString == null) {
return null;
}
for (final DateTimeFormatter formatter : FORMATTERS) { for (final DateTimeFormatter formatter : FORMATTERS) {
try { try {
return OffsetDateTime.parse(dateString, formatter); return OffsetDateTime.parse(dateString, formatter);
} catch (final DateTimeParseException ex) { } catch (final DateTimeParseException ex) {
if (missingAsUTC) {
// If the date string is missing a zone, try appending "Z" // If the date string is missing a zone, try appending "Z"
try { try {
if (dateString.endsWith("Z") || dateString.endsWith("z")) { if (dateString.endsWith("Z") || dateString.endsWith("z")) {
@ -92,24 +60,7 @@ public class DateTools {
} }
} }
} }
// Fallback: Try parsing as LocalDate (assume start of day UTC)
try {
final LocalDate dateTmp = LocalDate.parse(dateString);
return dateTmp.atStartOfDay(ZoneOffset.UTC).toInstant().atZone(ZoneOffset.UTC).toOffsetDateTime();
} catch (final DateTimeParseException e) {
// Ignore and try next fallback
} }
// Fallback: Try parsing as LocalTime (assume date is 0000-01-01 UTC)
try {
final LocalTime timeTmp = LocalTime.parse(dateString);
return OffsetDateTime.of(0, 1, 1, // year, month, day
timeTmp.getHour(), timeTmp.getMinute(), timeTmp.getSecond(), timeTmp.getNano(), ZoneOffset.UTC);
} catch (final DateTimeParseException e) {
// All parsing attempts failed
}
throw new IOException("Unrecognized DATE format: '" + dateString + "' supported format ISO8601"); throw new IOException("Unrecognized DATE format: '" + dateString + "' supported format ISO8601");
} }
@ -122,7 +73,7 @@ public class DateTools {
* @throws ParseException if parsing fails entirely * @throws ParseException if parsing fails entirely
*/ */
public static Date parseDate(final String dateString) throws IOException { public static Date parseDate(final String dateString) throws IOException {
final OffsetDateTime dateTime = parseOffsetDateTime(dateString); final OffsetDateTime dateTime = parseOffsetDateTime(dateString, true);
dateTime.atZoneSameInstant(ZoneId.systemDefault()); dateTime.atZoneSameInstant(ZoneId.systemDefault());
return Date.from(dateTime.toInstant()); return Date.from(dateTime.toInstant());
} }
@ -138,6 +89,9 @@ public class DateTools {
* Example output: 2025-04-06T15:00:00.123+02:00 * Example output: 2025-04-06T15:00:00.123+02:00
*/ */
public static String serializeMilliWithOriginalTimeZone(final OffsetDateTime offsetDateTime) { public static String serializeMilliWithOriginalTimeZone(final OffsetDateTime offsetDateTime) {
if (offsetDateTime == null) {
return null;
}
return offsetDateTime.format(PATTERN_MS_TIME_WITH_ZONE); return offsetDateTime.format(PATTERN_MS_TIME_WITH_ZONE);
} }
@ -146,6 +100,9 @@ public class DateTools {
* then serializes it to a string with milliseconds and original timezone offset. * then serializes it to a string with milliseconds and original timezone offset.
*/ */
public static String serializeMilliWithOriginalTimeZone(final Date date) { public static String serializeMilliWithOriginalTimeZone(final Date date) {
if (date == null) {
return null;
}
return serializeMilliWithOriginalTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime()); return serializeMilliWithOriginalTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime());
} }
@ -159,6 +116,9 @@ public class DateTools {
* The offset is explicitly changed to UTC before formatting. * The offset is explicitly changed to UTC before formatting.
*/ */
public static String serializeMilliWithUTCTimeZone(final OffsetDateTime offsetDateTime) { public static String serializeMilliWithUTCTimeZone(final OffsetDateTime offsetDateTime) {
if (offsetDateTime == null) {
return null;
}
return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).format(PATTERN_MS_TIME); return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).format(PATTERN_MS_TIME);
} }
@ -167,6 +127,9 @@ public class DateTools {
* then serializes it with milliseconds in UTC. * then serializes it with milliseconds in UTC.
*/ */
public static String serializeMilliWithUTCTimeZone(final Date date) { public static String serializeMilliWithUTCTimeZone(final Date date) {
if (date == null) {
return null;
}
return serializeMilliWithUTCTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime()); return serializeMilliWithUTCTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime());
} }
@ -180,6 +143,9 @@ public class DateTools {
* Serializes an OffsetDateTime to a string with nanosecond precision in UTC. * Serializes an OffsetDateTime to a string with nanosecond precision in UTC.
*/ */
public static String serializeNanoWithUTCTimeZone(final OffsetDateTime offsetDateTime) { public static String serializeNanoWithUTCTimeZone(final OffsetDateTime offsetDateTime) {
if (offsetDateTime == null) {
return null;
}
return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).format(PATTERN_NS_TIME); return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).format(PATTERN_NS_TIME);
} }
@ -188,6 +154,9 @@ public class DateTools {
* then serializes it with nanoseconds in UTC. * then serializes it with nanoseconds in UTC.
*/ */
public static String serializeNanoWithUTCTimeZone(final Date date) { public static String serializeNanoWithUTCTimeZone(final Date date) {
if (date == null) {
return null;
}
return serializeNanoWithUTCTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime()); return serializeNanoWithUTCTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime());
} }
} }

View File

@ -41,6 +41,10 @@ public class RESTApi {
this.token = token; this.token = token;
} }
public RESTApiRequest request() {
return request("");
}
public RESTApiRequest request(final String urlOffset) { public RESTApiRequest request(final String urlOffset) {
return new RESTApiRequest(this.baseUrl + urlOffset, this.token); return new RESTApiRequest(this.baseUrl + urlOffset, this.token);
} }

View File

@ -80,6 +80,20 @@ public class RESTApiRequest {
return this; return this;
} }
/**
* Sets the request body as a raw String.
*
* @param body The raw string body (consider as "text/plain").
* @param contentType The content type of the request body.
* @return The updated RESTApiRequest instance.
*/
public <TYPE_BODY> RESTApiRequest bodyString(final String body) {
this.serializedBodyByte = null;
this.serializedBodyString = body;
this.contentType = "text/plain";
return this;
}
/** /**
* Serializes a Map to a JSON string and sets it as the body. * Serializes a Map to a JSON string and sets it as the body.
* *
@ -121,6 +135,19 @@ public class RESTApiRequest {
return this; return this;
} }
/**
* Sed data as a json body.
*
* @param body a serialized Json object.
* @return The updated RESTApiRequest instance.
* @throws JsonProcessingException If serialization fails.
*/
public <TYPE_BODY> RESTApiRequest bodyAsJson(final String body) {
this.serializedBodyString = body;
this.contentType = "application/json";
return this;
}
/** /**
* Builds a multipart/form-data request body from a map of fields. * Builds a multipart/form-data request body from a map of fields.
* Handles both File and standard form fields. * Handles both File and standard form fields.
@ -184,22 +211,6 @@ public class RESTApiRequest {
return this; return this;
} }
/**
* Sends a GET request and parses the response as a List of objects.
*
* @param clazz The class of the expected response elements.
* @return A list of parsed objects.
* @throws RESTErrorResponseException If an error response is received.
* @throws IOException If the request fails.
* @throws InterruptedException If the request is interrupted.
*/
public <TYPE_RESPONSE> List<TYPE_RESPONSE> gets(final Class<TYPE_RESPONSE> clazz)
throws RESTErrorResponseException, IOException, InterruptedException {
verb("GET");
final HttpRequest request = request();
return callAndParseRequestList(clazz, request);
}
/** /**
* Sets the HTTP verb to GET. * Sets the HTTP verb to GET.
* *
@ -220,6 +231,16 @@ public class RESTApiRequest {
return this; return this;
} }
/**
* Sets the HTTP verb to POST.
*
* @return The updated RESTApiRequest instance.
*/
public RESTApiRequest post() {
verb("POST");
return this;
}
/** /**
* Sets the HTTP verb to PATCH. * Sets the HTTP verb to PATCH.
* *
@ -269,9 +290,9 @@ public class RESTApiRequest {
* @throws IOException If the request fails. * @throws IOException If the request fails.
* @throws InterruptedException If the request is interrupted. * @throws InterruptedException If the request is interrupted.
*/ */
public <TYPE_RESPONSE> List<TYPE_RESPONSE> requestList(final Class<TYPE_RESPONSE> clazz) public <TYPE_RESPONSE> List<TYPE_RESPONSE> fetchList(final Class<TYPE_RESPONSE> clazz)
throws RESTErrorResponseException, IOException, InterruptedException { throws RESTErrorResponseException, IOException, InterruptedException {
final HttpRequest request = request(); final HttpRequest request = fetch();
return callAndParseRequestList(clazz, request); return callAndParseRequestList(clazz, request);
} }
@ -284,12 +305,23 @@ public class RESTApiRequest {
* @throws IOException If the request fails. * @throws IOException If the request fails.
* @throws InterruptedException If the request is interrupted. * @throws InterruptedException If the request is interrupted.
*/ */
public <TYPE_RESPONSE> TYPE_RESPONSE request(final Class<TYPE_RESPONSE> clazz) public <TYPE_RESPONSE> TYPE_RESPONSE fetch(final Class<TYPE_RESPONSE> clazz)
throws RESTErrorResponseException, IOException, InterruptedException { throws RESTErrorResponseException, IOException, InterruptedException {
final HttpRequest request = request(); final HttpRequest request = fetch();
return callAndParseRequest(clazz, request); return callAndParseRequest(clazz, request);
} }
/**
* Builds a query parameter string from a map of key-value pairs.
*
* <p>This method encodes each key and value using UTF-8 encoding to ensure that
* the resulting query string is safe for use in a URL. The encoded key-value pairs
* are then joined together with '&' separators.</p>
*
* @param params A map containing query parameter names and their corresponding values.
* Both keys and values will be URL-encoded.
* @return A URL-encoded query string (e.g., "name=John+Doe&age=30")
*/
public static String buildQueryParams(final Map<String, String> params) { public static String buildQueryParams(final Map<String, String> params) {
return params.entrySet().stream().map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" return params.entrySet().stream().map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "="
+ URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).collect(Collectors.joining("&")); + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).collect(Collectors.joining("&"));
@ -303,13 +335,13 @@ public class RESTApiRequest {
* @throws IOException If body serialization fails. * @throws IOException If body serialization fails.
* @throws InterruptedException If the request is interrupted. * @throws InterruptedException If the request is interrupted.
*/ */
public HttpRequest request() throws RESTErrorResponseException, IOException, InterruptedException { public HttpRequest fetch() throws RESTErrorResponseException, IOException, InterruptedException {
Builder requestBuilding = null; Builder requestBuilding = null;
final String queryParams = buildQueryParams(this.queryParam); final String queryParams = buildQueryParams(this.queryParam);
if (queryParams != null && !queryParams.isEmpty()) { if (queryParams == null || queryParams.isEmpty()) {
requestBuilding = createRequestBuilder(this.url); requestBuilding = createRequestBuilder("");
} else { } else {
requestBuilding = createRequestBuilder(this.url + "?" + queryParams); requestBuilding = createRequestBuilder("?" + queryParams);
} }
if (this.contentType != null) { if (this.contentType != null) {
requestBuilding.header("Content-Type", this.contentType); requestBuilding.header("Content-Type", this.contentType);
@ -336,7 +368,7 @@ public class RESTApiRequest {
* @throws IOException If the request fails. * @throws IOException If the request fails.
* @throws InterruptedException If the request is interrupted. * @throws InterruptedException If the request is interrupted.
*/ */
public HttpResponse<byte[]> getRaw(final String urlOffset) throws IOException, InterruptedException { protected HttpResponse<byte[]> getRaw(final String urlOffset) throws IOException, InterruptedException {
final Builder requestBuilding = createRequestBuilder(urlOffset); final Builder requestBuilding = createRequestBuilder(urlOffset);
final HttpRequest request = requestBuilding.method("GET", BodyPublishers.ofString("")).build(); final HttpRequest request = requestBuilding.method("GET", BodyPublishers.ofString("")).build();
final HttpClient client = HttpClient.newHttpClient(); final HttpClient client = HttpClient.newHttpClient();
@ -350,7 +382,7 @@ public class RESTApiRequest {
* @param urlOffset The URL path relative to the base URL. * @param urlOffset The URL path relative to the base URL.
* @return Initialized HttpRequest.Builder. * @return Initialized HttpRequest.Builder.
*/ */
public Builder createRequestBuilder(final String urlOffset) { private Builder createRequestBuilder(final String urlOffset) {
Builder requestBuilding = HttpRequest.newBuilder().version(Version.HTTP_1_1) Builder requestBuilding = HttpRequest.newBuilder().version(Version.HTTP_1_1)
.uri(URI.create(this.url + urlOffset)); .uri(URI.create(this.url + urlOffset));
if (this.token != null) { if (this.token != null) {
@ -370,7 +402,7 @@ public class RESTApiRequest {
* @throws InterruptedException If the request is interrupted. * @throws InterruptedException If the request is interrupted.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <TYPE_RESPONSE> TYPE_RESPONSE callAndParseRequest( private <TYPE_RESPONSE> TYPE_RESPONSE callAndParseRequest(
final Class<TYPE_RESPONSE> clazzReturn, final Class<TYPE_RESPONSE> clazzReturn,
final HttpRequest request) throws RESTErrorResponseException, IOException, InterruptedException { final HttpRequest request) throws RESTErrorResponseException, IOException, InterruptedException {
final HttpClient client = HttpClient.newHttpClient(); final HttpClient client = HttpClient.newHttpClient();
@ -423,7 +455,7 @@ public class RESTApiRequest {
* @throws InterruptedException If the request is interrupted. * @throws InterruptedException If the request is interrupted.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <TYPE_RESPONSE> List<TYPE_RESPONSE> callAndParseRequestList( private <TYPE_RESPONSE> List<TYPE_RESPONSE> callAndParseRequestList(
final Class<TYPE_RESPONSE> clazzReturn, final Class<TYPE_RESPONSE> clazzReturn,
final HttpRequest request) throws IOException, InterruptedException, RESTErrorResponseException { final HttpRequest request) throws IOException, InterruptedException, RESTErrorResponseException {
final HttpClient client = HttpClient.newHttpClient(); final HttpClient client = HttpClient.newHttpClient();

View File

@ -12,6 +12,7 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.kar.archidata.exception.RESTErrorResponseException;
import org.kar.archidata.tools.ConfigBaseVariable; import org.kar.archidata.tools.ConfigBaseVariable;
import org.kar.archidata.tools.RESTApi; import org.kar.archidata.tools.RESTApi;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -62,7 +63,8 @@ public class TestTime {
data.localDate = LocalDate.now(); data.localDate = LocalDate.now();
data.localDateTime = LocalDateTime.now(); data.localDateTime = LocalDateTime.now();
final DataForJSR310 inserted = api.post(DataForJSR310.class, TestTime.ENDPOINT_NAME, data); final DataForJSR310 inserted = api.request(TestTime.ENDPOINT_NAME).post().bodyJson(data)
.fetch(DataForJSR310.class);
Assertions.assertNotNull(inserted); Assertions.assertNotNull(inserted);
Assertions.assertNotNull(inserted.localTime); Assertions.assertNotNull(inserted.localTime);
Assertions.assertNotNull(inserted.localDate); Assertions.assertNotNull(inserted.localDate);
@ -80,91 +82,88 @@ public class TestTime {
"date": "2025-04-04T15:15:07.123" "date": "2025-04-04T15:15:07.123"
} }
"""; """;
String received = api.postJson(String.class, TestTime.ENDPOINT_NAME + "/serialize", data); String received = api.request(TestTime.ENDPOINT_NAME + "/serialize").post().bodyAsJson(data)
LOGGER.info("received: '{}'", received); .fetch(String.class);
Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received); Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received);
data = """ data = """
{ {
"date": "2025-04-04T15:15:07.123Z" "date": "2025-04-04T15:15:07.123Z"
} }
"""; """;
received = api.postJson(String.class, TestTime.ENDPOINT_NAME + "/serialize", data); received = api.request(TestTime.ENDPOINT_NAME + "/serialize").post().bodyAsJson(data).fetch(String.class);
LOGGER.info("received: '{}'", received);
Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received); Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received);
data = """ data = """
{ {
"date": "2025-04-04T15:15:07.123+05:00" "date": "2025-04-04T15:15:07.123+05:00"
} }
"""; """;
received = api.postJson(String.class, TestTime.ENDPOINT_NAME + "/serialize", data); received = api.request(TestTime.ENDPOINT_NAME + "/serialize").post().bodyAsJson(data).fetch(String.class);
LOGGER.info("received: '{}'", received);
Assertions.assertEquals("Fri Apr 04 10:15:07 UTC 2025", received); Assertions.assertEquals("Fri Apr 04 10:15:07 UTC 2025", received);
Assertions.assertNotNull(received);
} }
@Order(3) @Order(3)
@Test @Test
public void unserializeValue() throws Exception { public void unserializeValue() throws Exception {
String data = "2025-04-04T15:15:07.123Z"; String data = "2025-04-04T15:15:07.123Z";
DataForJSR310String received = api.postJson(DataForJSR310String.class, TestTime.ENDPOINT_NAME + "/unserialize", DataForJSR310String received = api.request(TestTime.ENDPOINT_NAME + "/unserialize").post().bodyString(data)
data); .fetch(DataForJSR310String.class);
LOGGER.info("send : '{}'", data); Assertions.assertEquals("2025-04-04T15:15:07.123Z", received.date);
LOGGER.info("received: '{}'", received.date);
LOGGER.info("----------------------------------------------------");
data = "2025-04-04T15:15:07.123"; data = "2025-04-04T15:15:07.123";
received = api.postJson(DataForJSR310String.class, TestTime.ENDPOINT_NAME + "/unserialize", data); received = api.request(TestTime.ENDPOINT_NAME + "/unserialize").post().bodyString(data)
LOGGER.info("send : '{}'", data); .fetch(DataForJSR310String.class);
LOGGER.info("received: '{}'", received.date); Assertions.assertEquals("2025-04-04T15:15:07.123Z", received.date);
LOGGER.info("----------------------------------------------------");
data = "2025-04-04T15:15:07.123+05:00"; data = "2025-04-04T15:15:07.123+05:00";
received = api.postJson(DataForJSR310String.class, TestTime.ENDPOINT_NAME + "/unserialize", data); received = api.request(TestTime.ENDPOINT_NAME + "/unserialize").post().bodyString(data)
LOGGER.info("send : '{}'", data); .fetch(DataForJSR310String.class);
LOGGER.info("received: '{}'", received.date); Assertions.assertEquals("2025-04-04T10:15:07.123Z", received.date);
LOGGER.info("----------------------------------------------------");
//Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received);
} }
@Order(50) @Order(50)
@Test @Test
public void jakartaInputDate() throws Exception { public void jakartaInputDate() throws Exception {
String data = "2025-04-04T15:15:07.123Z"; String data = "2025-04-04T15:15:07.123Z";
String received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputDate"); String received = api.request(TestTime.ENDPOINT_NAME + "/inputDate").get().queryParam("date", data)
//String received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputDate?date=" + data); .fetch(String.class);
LOGGER.info("send : '{}'", data); Assertions.assertEquals("2025-04-04T15:15:07.123000000Z", received);
LOGGER.info("received: '{}'", received);
LOGGER.info("----------------------------------------------------");
data = "2025-04-04T15:15:07.123"; data = "2025-04-04T15:15:07.123";
received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputDate?date=" + data); received = api.request(TestTime.ENDPOINT_NAME + "/inputDate").get().queryParam("date", data)
LOGGER.info("send : '{}'", data); .fetch(String.class);
LOGGER.info("received: '{}'", received); Assertions.assertEquals("2025-04-04T15:15:07.123000000Z", received);
LOGGER.info("----------------------------------------------------");
data = "2025-04-04T15:15:07.123+05:00"; data = "2025-04-04T15:15:07.123+05:00";
received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputDate?date=" + data); received = api.request(TestTime.ENDPOINT_NAME + "/inputDate").get().queryParam("date", data)
LOGGER.info("send : '{}'", data); .fetch(String.class);
LOGGER.info("received: '{}'", received); Assertions.assertEquals("2025-04-04T10:15:07.123000000Z", received);
LOGGER.info("----------------------------------------------------");
//Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received);
} }
@Order(51) @Order(51)
@Test @Test
public void jakartaInputOffsetDateTime() throws Exception { public void jakartaInputOffsetDateTime() throws Exception {
String data = "2025-04-04T15:15:07.123Z"; String data = "2025-04-04T15:15:07.123Z";
String received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputOffsetDateTime?date=" + data); String received = api.request(TestTime.ENDPOINT_NAME + "/inputOffsetDateTime").get().queryParam("date", data)
LOGGER.info("send : '{}'", data); .fetch(String.class);
LOGGER.info("received: '{}'", received); Assertions.assertEquals("2025-04-04T15:15:07.123000000Z", received);
LOGGER.info("----------------------------------------------------");
data = "2025-04-04T15:15:07.123"; // check with offset:
received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputOffsetDateTime?date=" + data);
LOGGER.info("send : '{}'", data);
LOGGER.info("received: '{}'", received);
LOGGER.info("----------------------------------------------------");
data = "2025-04-04T15:15:07.123+05:00"; data = "2025-04-04T15:15:07.123+05:00";
received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputOffsetDateTime?date=" + data); received = api.request(TestTime.ENDPOINT_NAME + "/inputOffsetDateTime").get().queryParam("date", data)
LOGGER.info("send : '{}'", data); .fetch(String.class);
LOGGER.info("received: '{}'", received); Assertions.assertEquals("2025-04-04T10:15:07.123000000Z", received);
LOGGER.info("----------------------------------------------------");
//Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received); // Check parsing fail
final String dataFail = "2025-04-04T15:15:07.123";
final RESTErrorResponseException ex = Assertions.assertThrows(RESTErrorResponseException.class,
() -> api.request(TestTime.ENDPOINT_NAME + "/inputOffsetDateTime").get().queryParam("date", dataFail)
.fetch(String.class));
Assertions.assertEquals("Error on query input='date'", ex.name);
Assertions.assertEquals("Input parsing fail", ex.message);
Assertions.assertEquals(400, ex.status);
Assertions.assertNotNull(ex.inputError);
Assertions.assertEquals(1, ex.inputError.size());
Assertions.assertEquals("date", ex.inputError.get(0).path);
Assertions.assertEquals("Invalid date format. Please use ISO8601", ex.inputError.get(0).message);
} }
} }

View File

@ -1,23 +1,17 @@
package test.kar.archidata.apiExtern; package test.kar.archidata.apiExtern;
import java.io.IOException;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; import java.util.TimeZone;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.kar.archidata.tools.DateTools; import org.kar.archidata.tools.DateTools;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import test.kar.archidata.StepwiseExtension;
@ExtendWith(StepwiseExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestTimeParsing { public class TestTimeParsing {
private final static Logger LOGGER = LoggerFactory.getLogger(TestTime.class); private final static Logger LOGGER = LoggerFactory.getLogger(TestTime.class);
@ -124,23 +118,9 @@ public class TestTimeParsing {
parsed = DateTools.parseDate(data); parsed = DateTools.parseDate(data);
Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed)); Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed));
Assertions.assertEquals("1999-01-30T09:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed)); Assertions.assertEquals("1999-01-30T09:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
data = "1999-01-30";
parsed = DateTools.parseDate(data);
Assertions.assertEquals("1999-01-30T09:00:00.000+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed));
Assertions.assertEquals("1999-01-30T00:00:00.000Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
data = "18:16:17.123";
parsed = DateTools.parseDate(data);
Assertions.assertEquals("0001-01-02T03:35:16.123+09:18", DateTools.serializeMilliWithOriginalTimeZone(parsed));
Assertions.assertEquals("0001-01-01T18:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
// data = "1999-01-30T18:16:17.123 UTC+09:00"; Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("1999-01-30"));
// parsed = DateTools.parseDate2(data); Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("18:16:17.123"));
// LOGGER.info(">> send : '{}'", data);
// LOGGER.info(">> format OTZ: '{}'", DateTools.serializeMilliWithOriginalTimeZone(parsed));
// LOGGER.info(">> format UTC: '{}'", DateTools.serializeMilliWithUTCTimeZone(parsed));
// LOGGER.info("----------------------------------------------------");
// Assertions.assertEquals("", DateTools.serializeMilliWithOriginalTimeZone(parsed));
// Assertions.assertEquals("", DateTools.serializeMilliWithUTCTimeZone(parsed));
} }
@Test @Test
@ -160,18 +140,6 @@ public class TestTimeParsing {
parsed = DateTools.parseOffsetDateTime(data); parsed = DateTools.parseOffsetDateTime(data);
Assertions.assertEquals("1999-01-30T18:16:17.123+05:00", DateTools.serializeMilliWithOriginalTimeZone(parsed)); Assertions.assertEquals("1999-01-30T18:16:17.123+05:00", DateTools.serializeMilliWithOriginalTimeZone(parsed));
Assertions.assertEquals("1999-01-30T13:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed)); Assertions.assertEquals("1999-01-30T13:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
data = "1999-01-30T18:16:17.123456789";
parsed = DateTools.parseOffsetDateTime(data);
Assertions.assertEquals("1999-01-30T18:16:17.123Z", DateTools.serializeMilliWithOriginalTimeZone(parsed));
Assertions.assertEquals("1999-01-30T18:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
data = "1999-01-30 18:16:17";
parsed = DateTools.parseOffsetDateTime(data);
Assertions.assertEquals("1999-01-30T18:16:17.000Z", DateTools.serializeMilliWithOriginalTimeZone(parsed));
Assertions.assertEquals("1999-01-30T18:16:17.000Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
data = "1999-01-30T18:16:17";
parsed = DateTools.parseOffsetDateTime(data);
Assertions.assertEquals("1999-01-30T18:16:17.000Z", DateTools.serializeMilliWithOriginalTimeZone(parsed));
Assertions.assertEquals("1999-01-30T18:16:17.000Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
data = "1999-01-30T18:16:17.123+09:00"; data = "1999-01-30T18:16:17.123+09:00";
parsed = DateTools.parseOffsetDateTime(data); parsed = DateTools.parseOffsetDateTime(data);
Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed)); Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed));
@ -180,14 +148,13 @@ public class TestTimeParsing {
parsed = DateTools.parseOffsetDateTime(data); parsed = DateTools.parseOffsetDateTime(data);
Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed)); Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed));
Assertions.assertEquals("1999-01-30T09:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed)); Assertions.assertEquals("1999-01-30T09:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
data = "1999-01-30";
parsed = DateTools.parseOffsetDateTime(data); Assertions.assertThrows(IOException.class,
Assertions.assertEquals("1999-01-30T00:00:00.000Z", DateTools.serializeMilliWithOriginalTimeZone(parsed)); () -> DateTools.parseOffsetDateTime("1999-01-30T18:16:17.123456789"));
Assertions.assertEquals("1999-01-30T00:00:00.000Z", DateTools.serializeMilliWithUTCTimeZone(parsed)); Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("1999-01-30 18:16:17"));
data = "18:16:17.123"; Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("1999-01-30T18:16:17"));
parsed = DateTools.parseOffsetDateTime(data); Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("1999-01-30"));
Assertions.assertEquals("0001-01-01T18:16:17.123Z", DateTools.serializeMilliWithOriginalTimeZone(parsed)); Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("18:16:17.123"));
Assertions.assertEquals("0001-01-01T18:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed));
// data = "1999-01-30T18:16:17.123 UTC+09:00"; // data = "1999-01-30T18:16:17.123 UTC+09:00";
// parsed = DateTools.parseDate2(data); // parsed = DateTools.parseDate2(data);

View File

@ -17,8 +17,8 @@ import org.kar.archidata.UpdateJwtPublicKey;
import org.kar.archidata.api.DataResource; import org.kar.archidata.api.DataResource;
import org.kar.archidata.api.ProxyResource; import org.kar.archidata.api.ProxyResource;
import org.kar.archidata.catcher.GenericCatcher; import org.kar.archidata.catcher.GenericCatcher;
import org.kar.archidata.converter.Jakarta.DateParamConverter; import org.kar.archidata.converter.Jakarta.DateParamConverterProvider;
import org.kar.archidata.converter.Jakarta.OffsetDateTimeParamConverter; import org.kar.archidata.converter.Jakarta.OffsetDateTimeParamConverterProvider;
import org.kar.archidata.db.DbConfig; import org.kar.archidata.db.DbConfig;
import org.kar.archidata.exception.DataAccessException; import org.kar.archidata.exception.DataAccessException;
import org.kar.archidata.filter.CORSFilter; import org.kar.archidata.filter.CORSFilter;
@ -106,8 +106,8 @@ public class WebLauncher {
final ResourceConfig rc = new ResourceConfig(); final ResourceConfig rc = new ResourceConfig();
// Add permissive date converter for jakarta // Add permissive date converter for jakarta
rc.register(DateParamConverter.class); rc.register(DateParamConverterProvider.class);
rc.register(OffsetDateTimeParamConverter.class); rc.register(OffsetDateTimeParamConverterProvider.class);
// add multipart models .. // add multipart models ..
rc.register(MultiPartFeature.class); rc.register(MultiPartFeature.class);