[FEAT] Jackson update date serializer to be more permissive

This commit is contained in:
Edouard DUPIN 2025-04-06 23:23:31 +02:00
parent 1d375f8580
commit f044473a67
7 changed files with 256 additions and 35 deletions

View File

@ -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<Date> {
@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;
}
}

View File

@ -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<Date> {
@Override
public void serialize(final Date value, final JsonGenerator gen, final SerializerProvider serializers)
throws IOException {
gen.writeString(DateTools.serializeMilliWithUTCTimeZone(value));
}
}

View File

@ -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;
}
}

View File

@ -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<OffsetDateTime> {
@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;
}
}

View File

@ -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<OffsetDateTime> {
@Override
public void serialize(final OffsetDateTime value, final JsonGenerator gen, final SerializerProvider serializers)
throws IOException {
gen.writeString(DateTools.serializeMilliWithUTCTimeZone(value));
}
}

View File

@ -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;
}

View File

@ -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<SimpleDateFormat> 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<DateTimeFormatter> 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());
}
}