From b12108ec002e3fe85362f8026992e7ac5da39dbb Mon Sep 17 00:00:00 2001 From: Edouard DUPIN Date: Mon, 7 Apr 2025 23:08:23 +0200 Subject: [PATCH] [FIX] test on DateTime is ended and solution in 90% operational --- .../kar/archidata/catcher/GenericCatcher.java | 7 +- .../catcher/QueryParamExceptionCatcher.java | 34 ++++++ .../kar/archidata/catcher/RestInputError.java | 16 ++- .../converter/Jakarta/DateParamConverter.java | 51 --------- .../Jakarta/DateParamConverterProvider.java | 53 +++++++++ .../Jakarta/OffsetDateTimeParamConverter.java | 50 --------- .../OffsetDateTimeParamConverterProvider.java | 52 +++++++++ .../kar/archidata/filter/OptionFilter.java | 9 +- src/org/kar/archidata/tools/DateTools.java | 103 ++++++------------ src/org/kar/archidata/tools/RESTApi.java | 4 + .../kar/archidata/tools/RESTApiRequest.java | 88 ++++++++++----- .../kar/archidata/apiExtern/TestTime.java | 101 +++++++++-------- .../archidata/apiExtern/TestTimeParsing.java | 53 ++------- .../kar/archidata/apiExtern/WebLauncher.java | 8 +- 14 files changed, 326 insertions(+), 303 deletions(-) create mode 100644 src/org/kar/archidata/catcher/QueryParamExceptionCatcher.java delete mode 100644 src/org/kar/archidata/converter/Jakarta/DateParamConverter.java create mode 100644 src/org/kar/archidata/converter/Jakarta/DateParamConverterProvider.java delete mode 100644 src/org/kar/archidata/converter/Jakarta/OffsetDateTimeParamConverter.java create mode 100644 src/org/kar/archidata/converter/Jakarta/OffsetDateTimeParamConverterProvider.java diff --git a/src/org/kar/archidata/catcher/GenericCatcher.java b/src/org/kar/archidata/catcher/GenericCatcher.java index 9de31c1..af664b1 100644 --- a/src/org/kar/archidata/catcher/GenericCatcher.java +++ b/src/org/kar/archidata/catcher/GenericCatcher.java @@ -11,15 +11,16 @@ public class GenericCatcher { public static void addAll(final ResourceConfig rc) { // Generic Json parsing error rc.register(JacksonExceptionCatcher.class); - // Catch jakarta generic errors - rc.register(WebApplicationExceptionCatcher.class); // Archidata exceptions rc.register(InputExceptionCatcher.class); rc.register(SystemExceptionCatcher.class); rc.register(FailExceptionCatcher.class); // generic Exception catcher - rc.register(ExceptionCatcher.class); rc.register(ConstraintViolationExceptionCatcher.class); + rc.register(QueryParamExceptionCatcher.class); + rc.register(ExceptionCatcher.class); + // Catch jakarta generic errors + rc.register(WebApplicationExceptionCatcher.class); } } diff --git a/src/org/kar/archidata/catcher/QueryParamExceptionCatcher.java b/src/org/kar/archidata/catcher/QueryParamExceptionCatcher.java new file mode 100644 index 0000000..34a38f7 --- /dev/null +++ b/src/org/kar/archidata/catcher/QueryParamExceptionCatcher.java @@ -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 { + 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 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); + } + +} diff --git a/src/org/kar/archidata/catcher/RestInputError.java b/src/org/kar/archidata/catcher/RestInputError.java index 5820054..4921345 100644 --- a/src/org/kar/archidata/catcher/RestInputError.java +++ b/src/org/kar/archidata/catcher/RestInputError.java @@ -27,6 +27,17 @@ public class 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) { final Matcher matcher = PATTERN.matcher(path.toString()); if (matcher.find()) { @@ -39,11 +50,6 @@ public class RestInputError { this.message = message; } - public RestInputError(final String path, final String message) { - this.path = path; - this.message = message; - } - String getFullPath() { if (this.path == null) { return this.argument; diff --git a/src/org/kar/archidata/converter/Jakarta/DateParamConverter.java b/src/org/kar/archidata/converter/Jakarta/DateParamConverter.java deleted file mode 100644 index a742846..0000000 --- a/src/org/kar/archidata/converter/Jakarta/DateParamConverter.java +++ /dev/null @@ -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 ParamConverter getConverter( - final Class rawType, - final Type genericType, - final Annotation[] annotations) { - if (rawType != OffsetDateTime.class) { - return null; - } - return (ParamConverter) new ParamConverter() { - @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); - } - - }; - } -} diff --git a/src/org/kar/archidata/converter/Jakarta/DateParamConverterProvider.java b/src/org/kar/archidata/converter/Jakarta/DateParamConverterProvider.java new file mode 100644 index 0000000..c9493ae --- /dev/null +++ b/src/org/kar/archidata/converter/Jakarta/DateParamConverterProvider.java @@ -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 ParamConverter getConverter( + final Class rawType, + final Type genericType, + final Annotation[] annotations) { + if (rawType != Date.class) { + return null; + } + return (ParamConverter) new DateParamConverter(); + } + + public class DateParamConverter implements ParamConverter { + @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); + } + } + +} diff --git a/src/org/kar/archidata/converter/Jakarta/OffsetDateTimeParamConverter.java b/src/org/kar/archidata/converter/Jakarta/OffsetDateTimeParamConverter.java deleted file mode 100644 index 7dcf719..0000000 --- a/src/org/kar/archidata/converter/Jakarta/OffsetDateTimeParamConverter.java +++ /dev/null @@ -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 ParamConverter getConverter( - final Class rawType, - final Type genericType, - final Annotation[] annotations) { - if (rawType != OffsetDateTime.class) { - return null; - } - return (ParamConverter) new ParamConverter() { - @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); - } - - }; - } -} diff --git a/src/org/kar/archidata/converter/Jakarta/OffsetDateTimeParamConverterProvider.java b/src/org/kar/archidata/converter/Jakarta/OffsetDateTimeParamConverterProvider.java new file mode 100644 index 0000000..fe1a4cc --- /dev/null +++ b/src/org/kar/archidata/converter/Jakarta/OffsetDateTimeParamConverterProvider.java @@ -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 ParamConverter getConverter( + final Class rawType, + final Type genericType, + final Annotation[] annotations) { + if (rawType != OffsetDateTime.class) { + return null; + } + return (ParamConverter) new OffsetDateTimeParamConverter(); + } + + public class OffsetDateTimeParamConverter implements ParamConverter { + + @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); + } + + }; +} diff --git a/src/org/kar/archidata/filter/OptionFilter.java b/src/org/kar/archidata/filter/OptionFilter.java index 17ff071..fffc1a2 100644 --- a/src/org/kar/archidata/filter/OptionFilter.java +++ b/src/org/kar/archidata/filter/OptionFilter.java @@ -1,6 +1,8 @@ package org.kar.archidata.filter; import java.io.IOException; +import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,7 +20,12 @@ public class OptionFilter implements ContainerRequestFilter { @Override 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> queryParams = requestContext.getUriInfo().getQueryParameters(); + for (final Map.Entry> entry : queryParams.entrySet()) { + LOGGER.warn("queryParam: '{}' => '{}'", entry.getKey(), entry.getValue()); + } if (requestContext.getMethod().contentEquals("OPTIONS")) { requestContext.abortWith(Response.status(Response.Status.NO_CONTENT).build()); } diff --git a/src/org/kar/archidata/tools/DateTools.java b/src/org/kar/archidata/tools/DateTools.java index 4d3b3f1..68a096a 100644 --- a/src/org/kar/archidata/tools/DateTools.java +++ b/src/org/kar/archidata/tools/DateTools.java @@ -2,9 +2,6 @@ package org.kar.archidata.tools; import java.io.IOException; import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.time.LocalDate; -import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; @@ -20,44 +17,6 @@ import org.slf4j.LoggerFactory; public class DateTools { 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. // Includes patterns with optional parts, slashes, and ISO standard formats. static final List FORMATTERS = Arrays.asList( @@ -77,39 +36,31 @@ public class DateTools { * @throws IOException if no supported format matches the input */ 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) { try { return OffsetDateTime.parse(dateString, formatter); } catch (final DateTimeParseException ex) { - // If the date string is missing a zone, try appending "Z" - try { - if (dateString.endsWith("Z") || dateString.endsWith("z")) { - continue; + if (missingAsUTC) { + // If the date string is missing a zone, try appending "Z" + try { + if (dateString.endsWith("Z") || dateString.endsWith("z")) { + continue; + } + return OffsetDateTime.parse(dateString + "Z", formatter); + } catch (final DateTimeParseException ex2) { + // Still failed, try next pattern } - return OffsetDateTime.parse(dateString + "Z", formatter); - } catch (final DateTimeParseException ex2) { - // Still failed, try next pattern } } } - - // 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"); } @@ -122,7 +73,7 @@ public class DateTools { * @throws ParseException if parsing fails entirely */ public static Date parseDate(final String dateString) throws IOException { - final OffsetDateTime dateTime = parseOffsetDateTime(dateString); + final OffsetDateTime dateTime = parseOffsetDateTime(dateString, true); dateTime.atZoneSameInstant(ZoneId.systemDefault()); return Date.from(dateTime.toInstant()); } @@ -138,6 +89,9 @@ public class DateTools { * Example output: 2025-04-06T15:00:00.123+02:00 */ public static String serializeMilliWithOriginalTimeZone(final OffsetDateTime offsetDateTime) { + if (offsetDateTime == null) { + return null; + } 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. */ public static String serializeMilliWithOriginalTimeZone(final Date date) { + if (date == null) { + return null; + } return serializeMilliWithOriginalTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime()); } @@ -159,6 +116,9 @@ public class DateTools { * The offset is explicitly changed to UTC before formatting. */ public static String serializeMilliWithUTCTimeZone(final OffsetDateTime offsetDateTime) { + if (offsetDateTime == null) { + return null; + } return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).format(PATTERN_MS_TIME); } @@ -167,6 +127,9 @@ public class DateTools { * then serializes it with milliseconds in UTC. */ public static String serializeMilliWithUTCTimeZone(final Date date) { + if (date == null) { + return null; + } 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. */ public static String serializeNanoWithUTCTimeZone(final OffsetDateTime offsetDateTime) { + if (offsetDateTime == null) { + return null; + } return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).format(PATTERN_NS_TIME); } @@ -188,6 +154,9 @@ public class DateTools { * then serializes it with nanoseconds in UTC. */ public static String serializeNanoWithUTCTimeZone(final Date date) { + if (date == null) { + return null; + } return serializeNanoWithUTCTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime()); } } diff --git a/src/org/kar/archidata/tools/RESTApi.java b/src/org/kar/archidata/tools/RESTApi.java index fb2a683..1a913af 100644 --- a/src/org/kar/archidata/tools/RESTApi.java +++ b/src/org/kar/archidata/tools/RESTApi.java @@ -41,6 +41,10 @@ public class RESTApi { this.token = token; } + public RESTApiRequest request() { + return request(""); + } + public RESTApiRequest request(final String urlOffset) { return new RESTApiRequest(this.baseUrl + urlOffset, this.token); } diff --git a/src/org/kar/archidata/tools/RESTApiRequest.java b/src/org/kar/archidata/tools/RESTApiRequest.java index 972a992..64f9fec 100644 --- a/src/org/kar/archidata/tools/RESTApiRequest.java +++ b/src/org/kar/archidata/tools/RESTApiRequest.java @@ -80,6 +80,20 @@ public class RESTApiRequest { 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 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. * @@ -121,6 +135,19 @@ public class RESTApiRequest { 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 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. * Handles both File and standard form fields. @@ -184,22 +211,6 @@ public class RESTApiRequest { 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 List gets(final Class clazz) - throws RESTErrorResponseException, IOException, InterruptedException { - verb("GET"); - final HttpRequest request = request(); - return callAndParseRequestList(clazz, request); - } - /** * Sets the HTTP verb to GET. * @@ -220,6 +231,16 @@ public class RESTApiRequest { 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. * @@ -269,9 +290,9 @@ public class RESTApiRequest { * @throws IOException If the request fails. * @throws InterruptedException If the request is interrupted. */ - public List requestList(final Class clazz) + public List fetchList(final Class clazz) throws RESTErrorResponseException, IOException, InterruptedException { - final HttpRequest request = request(); + final HttpRequest request = fetch(); return callAndParseRequestList(clazz, request); } @@ -284,12 +305,23 @@ public class RESTApiRequest { * @throws IOException If the request fails. * @throws InterruptedException If the request is interrupted. */ - public TYPE_RESPONSE request(final Class clazz) + public TYPE_RESPONSE fetch(final Class clazz) throws RESTErrorResponseException, IOException, InterruptedException { - final HttpRequest request = request(); + final HttpRequest request = fetch(); return callAndParseRequest(clazz, request); } + /** + * Builds a query parameter string from a map of key-value pairs. + * + *

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.

+ * + * @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 params) { return params.entrySet().stream().map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).collect(Collectors.joining("&")); @@ -303,13 +335,13 @@ public class RESTApiRequest { * @throws IOException If body serialization fails. * @throws InterruptedException If the request is interrupted. */ - public HttpRequest request() throws RESTErrorResponseException, IOException, InterruptedException { + public HttpRequest fetch() throws RESTErrorResponseException, IOException, InterruptedException { Builder requestBuilding = null; final String queryParams = buildQueryParams(this.queryParam); - if (queryParams != null && !queryParams.isEmpty()) { - requestBuilding = createRequestBuilder(this.url); + if (queryParams == null || queryParams.isEmpty()) { + requestBuilding = createRequestBuilder(""); } else { - requestBuilding = createRequestBuilder(this.url + "?" + queryParams); + requestBuilding = createRequestBuilder("?" + queryParams); } if (this.contentType != null) { requestBuilding.header("Content-Type", this.contentType); @@ -336,7 +368,7 @@ public class RESTApiRequest { * @throws IOException If the request fails. * @throws InterruptedException If the request is interrupted. */ - public HttpResponse getRaw(final String urlOffset) throws IOException, InterruptedException { + protected HttpResponse getRaw(final String urlOffset) throws IOException, InterruptedException { final Builder requestBuilding = createRequestBuilder(urlOffset); final HttpRequest request = requestBuilding.method("GET", BodyPublishers.ofString("")).build(); final HttpClient client = HttpClient.newHttpClient(); @@ -350,7 +382,7 @@ public class RESTApiRequest { * @param urlOffset The URL path relative to the base URL. * @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) .uri(URI.create(this.url + urlOffset)); if (this.token != null) { @@ -370,7 +402,7 @@ public class RESTApiRequest { * @throws InterruptedException If the request is interrupted. */ @SuppressWarnings("unchecked") - public TYPE_RESPONSE callAndParseRequest( + private TYPE_RESPONSE callAndParseRequest( final Class clazzReturn, final HttpRequest request) throws RESTErrorResponseException, IOException, InterruptedException { final HttpClient client = HttpClient.newHttpClient(); @@ -423,7 +455,7 @@ public class RESTApiRequest { * @throws InterruptedException If the request is interrupted. */ @SuppressWarnings("unchecked") - public List callAndParseRequestList( + private List callAndParseRequestList( final Class clazzReturn, final HttpRequest request) throws IOException, InterruptedException, RESTErrorResponseException { final HttpClient client = HttpClient.newHttpClient(); diff --git a/test/src/test/kar/archidata/apiExtern/TestTime.java b/test/src/test/kar/archidata/apiExtern/TestTime.java index 53e1004..94c9b5b 100644 --- a/test/src/test/kar/archidata/apiExtern/TestTime.java +++ b/test/src/test/kar/archidata/apiExtern/TestTime.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.ExtendWith; +import org.kar.archidata.exception.RESTErrorResponseException; import org.kar.archidata.tools.ConfigBaseVariable; import org.kar.archidata.tools.RESTApi; import org.slf4j.Logger; @@ -62,7 +63,8 @@ public class TestTime { data.localDate = LocalDate.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.localTime); Assertions.assertNotNull(inserted.localDate); @@ -80,91 +82,88 @@ public class TestTime { "date": "2025-04-04T15:15:07.123" } """; - String received = api.postJson(String.class, TestTime.ENDPOINT_NAME + "/serialize", data); - LOGGER.info("received: '{}'", received); + String received = api.request(TestTime.ENDPOINT_NAME + "/serialize").post().bodyAsJson(data) + .fetch(String.class); Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received); data = """ { "date": "2025-04-04T15:15:07.123Z" } """; - received = api.postJson(String.class, TestTime.ENDPOINT_NAME + "/serialize", data); - LOGGER.info("received: '{}'", received); + received = api.request(TestTime.ENDPOINT_NAME + "/serialize").post().bodyAsJson(data).fetch(String.class); Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received); data = """ { "date": "2025-04-04T15:15:07.123+05:00" } """; - received = api.postJson(String.class, TestTime.ENDPOINT_NAME + "/serialize", data); - LOGGER.info("received: '{}'", received); + received = api.request(TestTime.ENDPOINT_NAME + "/serialize").post().bodyAsJson(data).fetch(String.class); Assertions.assertEquals("Fri Apr 04 10:15:07 UTC 2025", received); - - Assertions.assertNotNull(received); } @Order(3) @Test public void unserializeValue() throws Exception { String data = "2025-04-04T15:15:07.123Z"; - DataForJSR310String received = api.postJson(DataForJSR310String.class, TestTime.ENDPOINT_NAME + "/unserialize", - data); - LOGGER.info("send : '{}'", data); - LOGGER.info("received: '{}'", received.date); - LOGGER.info("----------------------------------------------------"); + DataForJSR310String received = api.request(TestTime.ENDPOINT_NAME + "/unserialize").post().bodyString(data) + .fetch(DataForJSR310String.class); + Assertions.assertEquals("2025-04-04T15:15:07.123Z", received.date); + data = "2025-04-04T15:15:07.123"; - received = api.postJson(DataForJSR310String.class, TestTime.ENDPOINT_NAME + "/unserialize", data); - LOGGER.info("send : '{}'", data); - LOGGER.info("received: '{}'", received.date); - LOGGER.info("----------------------------------------------------"); + received = api.request(TestTime.ENDPOINT_NAME + "/unserialize").post().bodyString(data) + .fetch(DataForJSR310String.class); + Assertions.assertEquals("2025-04-04T15:15:07.123Z", received.date); + data = "2025-04-04T15:15:07.123+05:00"; - received = api.postJson(DataForJSR310String.class, TestTime.ENDPOINT_NAME + "/unserialize", data); - LOGGER.info("send : '{}'", data); - LOGGER.info("received: '{}'", received.date); - LOGGER.info("----------------------------------------------------"); - //Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received); + received = api.request(TestTime.ENDPOINT_NAME + "/unserialize").post().bodyString(data) + .fetch(DataForJSR310String.class); + Assertions.assertEquals("2025-04-04T10:15:07.123Z", received.date); } @Order(50) @Test public void jakartaInputDate() throws Exception { String data = "2025-04-04T15:15:07.123Z"; - String received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputDate"); - //String received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputDate?date=" + data); - LOGGER.info("send : '{}'", data); - LOGGER.info("received: '{}'", received); - LOGGER.info("----------------------------------------------------"); + String received = api.request(TestTime.ENDPOINT_NAME + "/inputDate").get().queryParam("date", data) + .fetch(String.class); + Assertions.assertEquals("2025-04-04T15:15:07.123000000Z", received); + data = "2025-04-04T15:15:07.123"; - received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputDate?date=" + data); - LOGGER.info("send : '{}'", data); - LOGGER.info("received: '{}'", received); - LOGGER.info("----------------------------------------------------"); + received = api.request(TestTime.ENDPOINT_NAME + "/inputDate").get().queryParam("date", data) + .fetch(String.class); + Assertions.assertEquals("2025-04-04T15:15:07.123000000Z", received); + data = "2025-04-04T15:15:07.123+05:00"; - received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputDate?date=" + data); - LOGGER.info("send : '{}'", data); - LOGGER.info("received: '{}'", received); - LOGGER.info("----------------------------------------------------"); - //Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received); + received = api.request(TestTime.ENDPOINT_NAME + "/inputDate").get().queryParam("date", data) + .fetch(String.class); + Assertions.assertEquals("2025-04-04T10:15:07.123000000Z", received); } @Order(51) @Test public void jakartaInputOffsetDateTime() throws Exception { String data = "2025-04-04T15:15:07.123Z"; - String 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"; - received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputOffsetDateTime?date=" + data); - LOGGER.info("send : '{}'", data); - LOGGER.info("received: '{}'", received); - LOGGER.info("----------------------------------------------------"); + String received = api.request(TestTime.ENDPOINT_NAME + "/inputOffsetDateTime").get().queryParam("date", data) + .fetch(String.class); + Assertions.assertEquals("2025-04-04T15:15:07.123000000Z", received); + + // check with offset: data = "2025-04-04T15:15:07.123+05:00"; - received = api.get(String.class, TestTime.ENDPOINT_NAME + "/inputOffsetDateTime?date=" + data); - LOGGER.info("send : '{}'", data); - LOGGER.info("received: '{}'", received); - LOGGER.info("----------------------------------------------------"); - //Assertions.assertEquals("Fri Apr 04 15:15:07 UTC 2025", received); + received = api.request(TestTime.ENDPOINT_NAME + "/inputOffsetDateTime").get().queryParam("date", data) + .fetch(String.class); + Assertions.assertEquals("2025-04-04T10:15:07.123000000Z", 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); } } diff --git a/test/src/test/kar/archidata/apiExtern/TestTimeParsing.java b/test/src/test/kar/archidata/apiExtern/TestTimeParsing.java index ff82ff0..6f5a91d 100644 --- a/test/src/test/kar/archidata/apiExtern/TestTimeParsing.java +++ b/test/src/test/kar/archidata/apiExtern/TestTimeParsing.java @@ -1,23 +1,17 @@ package test.kar.archidata.apiExtern; +import java.io.IOException; import java.time.OffsetDateTime; import java.util.Date; import java.util.TimeZone; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.MethodOrderer; 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.slf4j.Logger; import org.slf4j.LoggerFactory; -import test.kar.archidata.StepwiseExtension; - -@ExtendWith(StepwiseExtension.class) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TestTimeParsing { private final static Logger LOGGER = LoggerFactory.getLogger(TestTime.class); @@ -124,23 +118,9 @@ public class TestTimeParsing { parsed = DateTools.parseDate(data); Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(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"; - // parsed = DateTools.parseDate2(data); - // 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)); + Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("1999-01-30")); + Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("18:16:17.123")); } @Test @@ -160,18 +140,6 @@ public class TestTimeParsing { parsed = DateTools.parseOffsetDateTime(data); Assertions.assertEquals("1999-01-30T18:16:17.123+05:00", DateTools.serializeMilliWithOriginalTimeZone(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"; parsed = DateTools.parseOffsetDateTime(data); Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed)); @@ -180,14 +148,13 @@ public class TestTimeParsing { parsed = DateTools.parseOffsetDateTime(data); Assertions.assertEquals("1999-01-30T18:16:17.123+09:00", DateTools.serializeMilliWithOriginalTimeZone(parsed)); Assertions.assertEquals("1999-01-30T09:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed)); - data = "1999-01-30"; - parsed = DateTools.parseOffsetDateTime(data); - Assertions.assertEquals("1999-01-30T00:00:00.000Z", DateTools.serializeMilliWithOriginalTimeZone(parsed)); - Assertions.assertEquals("1999-01-30T00:00:00.000Z", DateTools.serializeMilliWithUTCTimeZone(parsed)); - data = "18:16:17.123"; - parsed = DateTools.parseOffsetDateTime(data); - Assertions.assertEquals("0001-01-01T18:16:17.123Z", DateTools.serializeMilliWithOriginalTimeZone(parsed)); - Assertions.assertEquals("0001-01-01T18:16:17.123Z", DateTools.serializeMilliWithUTCTimeZone(parsed)); + + Assertions.assertThrows(IOException.class, + () -> DateTools.parseOffsetDateTime("1999-01-30T18:16:17.123456789")); + Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("1999-01-30 18:16:17")); + Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("1999-01-30T18:16:17")); + Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("1999-01-30")); + Assertions.assertThrows(IOException.class, () -> DateTools.parseOffsetDateTime("18:16:17.123")); // data = "1999-01-30T18:16:17.123 UTC+09:00"; // parsed = DateTools.parseDate2(data); diff --git a/test/src/test/kar/archidata/apiExtern/WebLauncher.java b/test/src/test/kar/archidata/apiExtern/WebLauncher.java index f752e29..fe3a0f1 100755 --- a/test/src/test/kar/archidata/apiExtern/WebLauncher.java +++ b/test/src/test/kar/archidata/apiExtern/WebLauncher.java @@ -17,8 +17,8 @@ import org.kar.archidata.UpdateJwtPublicKey; import org.kar.archidata.api.DataResource; import org.kar.archidata.api.ProxyResource; import org.kar.archidata.catcher.GenericCatcher; -import org.kar.archidata.converter.Jakarta.DateParamConverter; -import org.kar.archidata.converter.Jakarta.OffsetDateTimeParamConverter; +import org.kar.archidata.converter.Jakarta.DateParamConverterProvider; +import org.kar.archidata.converter.Jakarta.OffsetDateTimeParamConverterProvider; import org.kar.archidata.db.DbConfig; import org.kar.archidata.exception.DataAccessException; import org.kar.archidata.filter.CORSFilter; @@ -106,8 +106,8 @@ public class WebLauncher { final ResourceConfig rc = new ResourceConfig(); // Add permissive date converter for jakarta - rc.register(DateParamConverter.class); - rc.register(OffsetDateTimeParamConverter.class); + rc.register(DateParamConverterProvider.class); + rc.register(OffsetDateTimeParamConverterProvider.class); // add multipart models .. rc.register(MultiPartFeature.class);