diff --git a/src/org/kar/archidata/converter/jackson/DateDeserializer.java b/src/org/kar/archidata/converter/jackson/DateDeserializer.java new file mode 100644 index 0000000..04c0167 --- /dev/null +++ b/src/org/kar/archidata/converter/jackson/DateDeserializer.java @@ -0,0 +1,19 @@ +package org.kar.archidata.converter.jackson; + +import java.io.IOException; +import java.util.Date; + +import org.kar.archidata.tools.DateTools; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +public class DateDeserializer extends JsonDeserializer { + @Override + public Date deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException { + final String value = p.getText(); + final Date ret = DateTools.parseDate(value); + return ret; + } +} \ No newline at end of file diff --git a/src/org/kar/archidata/converter/jackson/DateSerializer.java b/src/org/kar/archidata/converter/jackson/DateSerializer.java new file mode 100644 index 0000000..40b5005 --- /dev/null +++ b/src/org/kar/archidata/converter/jackson/DateSerializer.java @@ -0,0 +1,18 @@ +package org.kar.archidata.converter.jackson; + +import java.io.IOException; +import java.util.Date; + +import org.kar.archidata.tools.DateTools; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class DateSerializer extends JsonSerializer { + @Override + public void serialize(final Date value, final JsonGenerator gen, final SerializerProvider serializers) + throws IOException { + gen.writeString(DateTools.serializeMilliWithUTCTimeZone(value)); + } +} \ No newline at end of file diff --git a/src/org/kar/archidata/converter/jackson/JacksonModules.java b/src/org/kar/archidata/converter/jackson/JacksonModules.java index 434c53a..abda4fc 100644 --- a/src/org/kar/archidata/converter/jackson/JacksonModules.java +++ b/src/org/kar/archidata/converter/jackson/JacksonModules.java @@ -1,5 +1,8 @@ package org.kar.archidata.converter.jackson; +import java.time.OffsetDateTime; +import java.util.Date; + import org.bson.types.ObjectId; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -9,6 +12,10 @@ public class JacksonModules { final SimpleModule module = new SimpleModule(); module.addSerializer(ObjectId.class, new ObjectIdSerializer()); module.addDeserializer(ObjectId.class, new ObjectIdDeserializer()); + module.addSerializer(Date.class, new DateSerializer()); + module.addDeserializer(Date.class, new DateDeserializer()); + module.addSerializer(OffsetDateTime.class, new OffsetDateTimeSerializer()); + module.addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer()); return module; } } diff --git a/src/org/kar/archidata/converter/jackson/OffsetDateTimeDeserializer.java b/src/org/kar/archidata/converter/jackson/OffsetDateTimeDeserializer.java new file mode 100644 index 0000000..bc9e86c --- /dev/null +++ b/src/org/kar/archidata/converter/jackson/OffsetDateTimeDeserializer.java @@ -0,0 +1,19 @@ +package org.kar.archidata.converter.jackson; + +import java.io.IOException; +import java.time.OffsetDateTime; + +import org.kar.archidata.tools.DateTools; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +public class OffsetDateTimeDeserializer extends JsonDeserializer { + @Override + public OffsetDateTime deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException { + final String value = p.getText(); + final OffsetDateTime ret = DateTools.parseOffsetDateTime(value); + return ret; + } +} \ No newline at end of file diff --git a/src/org/kar/archidata/converter/jackson/OffsetDateTimeSerializer.java b/src/org/kar/archidata/converter/jackson/OffsetDateTimeSerializer.java new file mode 100644 index 0000000..b2252f6 --- /dev/null +++ b/src/org/kar/archidata/converter/jackson/OffsetDateTimeSerializer.java @@ -0,0 +1,18 @@ +package org.kar.archidata.converter.jackson; + +import java.io.IOException; +import java.time.OffsetDateTime; + +import org.kar.archidata.tools.DateTools; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class OffsetDateTimeSerializer extends JsonSerializer { + @Override + public void serialize(final OffsetDateTime value, final JsonGenerator gen, final SerializerProvider serializers) + throws IOException { + gen.writeString(DateTools.serializeMilliWithUTCTimeZone(value)); + } +} \ No newline at end of file diff --git a/src/org/kar/archidata/tools/ContextGenericTools.java b/src/org/kar/archidata/tools/ContextGenericTools.java index 20b3398..81b21d7 100644 --- a/src/org/kar/archidata/tools/ContextGenericTools.java +++ b/src/org/kar/archidata/tools/ContextGenericTools.java @@ -12,11 +12,11 @@ public class ContextGenericTools { public static ObjectMapper createObjectMapper() { final ObjectMapper objectMapper = new ObjectMapper(); - // Configure Jackson for dates and times - objectMapper.registerModule(new JavaTimeModule()); // Module for Java 8+ Date and Time API objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // configure the local serialization modules objectMapper.registerModule(JacksonModules.getAllModules()); + // Add java time module at the end to prevent use it in first but in backup + objectMapper.registerModule(new JavaTimeModule()); // Module for Java 8+ Date and Time API return objectMapper; } diff --git a/src/org/kar/archidata/tools/DateTools.java b/src/org/kar/archidata/tools/DateTools.java index e5603ef..4d3b3f1 100644 --- a/src/org/kar/archidata/tools/DateTools.java +++ b/src/org/kar/archidata/tools/DateTools.java @@ -1,53 +1,193 @@ package org.kar.archidata.tools; +import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; import java.util.Date; import java.util.List; -public class DateTools { - static private List knownPatterns = new ArrayList<>(); - { - // SYSTEM mode - DateTools.knownPatterns.add(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")); - DateTools.knownPatterns.add(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm.ss.SSS'Z'")); - DateTools.knownPatterns.add(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")); - DateTools.knownPatterns.add(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")); - // Human mode - DateTools.knownPatterns.add(new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss")); - DateTools.knownPatterns.add(new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss.SSS")); - // date mode - DateTools.knownPatterns.add(new SimpleDateFormat("yyyy-MM-dd")); - // time mode - DateTools.knownPatterns.add(new SimpleDateFormat("HH:mm:ss")); - DateTools.knownPatterns.add(new SimpleDateFormat("HH:mm:ss.SSS")); - } +import org.slf4j.Logger; +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); } - public static Date parseDate(final String inputDate) throws ParseException { - for (final SimpleDateFormat pattern : DateTools.knownPatterns) { - try { - return pattern.parse(inputDate); - } catch (final ParseException e) { - continue; - } - } - throw new ParseException("Can not parse the date-time format: '" + inputDate + "'", 0); - - } - + /** + * 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); - final String outputDateFormatted = df.format(date); - return outputDateFormatted; + 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 formatDate(date, "yyyy-MM-dd'T'HH:mm.ss.SSS'Z'"); + 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( + DateTimeFormatter.ofPattern("[yyyy[-MM[-dd]]]['T'][' '][HH[:mm[:ss['.'nnnnnnnnn]]]]XXXXX"), + DateTimeFormatter.ofPattern("[yyyy[/MM[/dd]]]['T'][' '][HH[:mm[:ss['.'nnnnnnnnn]]]]XXXXX"), + DateTimeFormatter.ISO_OFFSET_DATE_TIME, // e.g., 2025-04-04T15:30:00+02:00 + DateTimeFormatter.ISO_ZONED_DATE_TIME, // e.g., 2025-04-04T15:30:00+02:00[Europe/Paris] + DateTimeFormatter.ISO_INSTANT // e.g., 2025-04-04T13:30:00Z + ); + + /** + * Attempts to parse a date string into an OffsetDateTime using a flexible list of patterns. + * Supports ISO 8601 formats, optional zone, and fallback to LocalDate or LocalTime if needed. + * + * @param dateString the date string to parse + * @return OffsetDateTime representation of the parsed input + * @throws IOException if no supported format matches the input + */ + public static OffsetDateTime parseOffsetDateTime(final String dateString) throws IOException { + 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; + } + 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"); + } + + /** + * Parses a flexible date string and returns a java.util.Date, + * using system default timezone for conversion. + * + * @param dateString the input string to parse + * @return java.util.Date object + * @throws ParseException if parsing fails entirely + */ + public static Date parseDate(final String dateString) throws IOException { + final OffsetDateTime dateTime = parseOffsetDateTime(dateString); + dateTime.atZoneSameInstant(ZoneId.systemDefault()); + return Date.from(dateTime.toInstant()); + } + + /** + * Formatter for date-time with milliseconds and original timezone offset (e.g., 2025-04-06T15:00:00.123+02:00) + */ + public static final DateTimeFormatter PATTERN_MS_TIME_WITH_ZONE = DateTimeFormatter + .ofPattern("yyyy-MM-dd'T'HH:mm:ss'.'SSSXXX"); + + /** + * Serializes an OffsetDateTime to a string including milliseconds and the original timezone offset. + * Example output: 2025-04-06T15:00:00.123+02:00 + */ + public static String serializeMilliWithOriginalTimeZone(final OffsetDateTime offsetDateTime) { + return offsetDateTime.format(PATTERN_MS_TIME_WITH_ZONE); + } + + /** + * Converts a java.util.Date to OffsetDateTime using the system's default timezone, + * then serializes it to a string with milliseconds and original timezone offset. + */ + public static String serializeMilliWithOriginalTimeZone(final Date date) { + return serializeMilliWithOriginalTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime()); + } + + /** + * Formatter for date-time with milliseconds in UTC offset (e.g., 2025-04-06T13:00:00.123Z) + */ + public static final DateTimeFormatter PATTERN_MS_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'.'SSSX"); + + /** + * Serializes an OffsetDateTime to a string with milliseconds in UTC. + * The offset is explicitly changed to UTC before formatting. + */ + public static String serializeMilliWithUTCTimeZone(final OffsetDateTime offsetDateTime) { + return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).format(PATTERN_MS_TIME); + } + + /** + * Converts a java.util.Date to OffsetDateTime in the system's default timezone, + * then serializes it with milliseconds in UTC. + */ + public static String serializeMilliWithUTCTimeZone(final Date date) { + return serializeMilliWithUTCTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime()); + } + + /** + * Formatter for date-time with nanoseconds in UTC offset (e.g., 2025-04-06T13:00:00.123456789Z) + */ + public static final DateTimeFormatter PATTERN_NS_TIME = DateTimeFormatter + .ofPattern("yyyy-MM-dd'T'HH:mm:ss'.'nnnnnnnnnX"); + + /** + * Serializes an OffsetDateTime to a string with nanosecond precision in UTC. + */ + public static String serializeNanoWithUTCTimeZone(final OffsetDateTime offsetDateTime) { + return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).format(PATTERN_NS_TIME); + } + + /** + * Converts a java.util.Date to OffsetDateTime in the system's default timezone, + * then serializes it with nanoseconds in UTC. + */ + public static String serializeNanoWithUTCTimeZone(final Date date) { + return serializeNanoWithUTCTimeZone(date.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime()); } }