Skip to content

Commit

Permalink
Merge branch '2.19'
Browse files Browse the repository at this point in the history
  • Loading branch information
cowtowncoder committed Feb 7, 2025
2 parents f478801 + c2f86ca commit c84b1a9
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ public enum JavaTimeFeature implements JacksonFeature
*/
NORMALIZE_DESERIALIZED_ZONE_ID(true),

/**
* Feature that determines whether the {@link java.util.TimeZone} of the
* {@link tools.jackson.databind.DeserializationContext} is used
* when leniently deserializing {@link java.time.LocalDate} or
* {@link java.time.LocalDateTime} from the UTC/ISO instant format.
* <p>
* Default setting is disabled, for backwards-compatibility with
* Jackson 2.18.
*
* @since 2.19
*/
USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING(false),

/**
* Feature that controls whether stringified numbers (Strings that without
* quotes would be legal JSON Numbers) may be interpreted as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ public void setupModule(SetupContext context) {

// // Other deserializers
.addDeserializer(Duration.class, DurationDeserializer.INSTANCE)
.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE)
.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE)
.addDeserializer(LocalDateTime.class,
LocalDateTimeDeserializer.INSTANCE.withFeatures(_features))
.addDeserializer(LocalDate.class,
LocalDateDeserializer.INSTANCE.withFeatures(_features))
.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE)
.addDeserializer(MonthDay.class, MonthDayDeserializer.INSTANCE)
.addDeserializer(OffsetTime.class, OffsetTimeDeserializer.INSTANCE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,76 @@
package tools.jackson.datatype.jsr310.deser;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.core.*;
import tools.jackson.core.util.JacksonFeatureSet;

import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.cfg.CoercionAction;
import tools.jackson.databind.cfg.CoercionInputShape;

import tools.jackson.datatype.jsr310.JavaTimeFeature;

/**
* Deserializer for Java 8 temporal {@link LocalDate}s.
*
* @author Nick Williams
*/
public class LocalDateDeserializer extends JSR310DateTimeDeserializerBase<LocalDate>
{
private final static boolean DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING
= JavaTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING.enabledByDefault();

private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;

public static final LocalDateDeserializer INSTANCE = new LocalDateDeserializer();

/**
* Flag set from
* {@link tools.jackson.datatype.jsr310.JavaTimeFeature#USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING}
* to determine whether the {@link java.util.TimeZone} of the
* {@link tools.jackson.databind.DeserializationContext} is used
* when leniently deserializing from the UTC/ISO instant format.
*/
protected final boolean _useTimeZoneForLenientDateParsing;

protected LocalDateDeserializer() {
this(DEFAULT_FORMATTER);
}

public LocalDateDeserializer(DateTimeFormatter dtf) {
super(LocalDate.class, dtf);
_useTimeZoneForLenientDateParsing = DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING;
}

/**
* Since 2.10
*/
public LocalDateDeserializer(LocalDateDeserializer base, DateTimeFormatter dtf) {
super(base, dtf);
_useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
}

/**
* Since 2.10
*/
protected LocalDateDeserializer(LocalDateDeserializer base, Boolean leniency) {
super(base, leniency);
_useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
}

/**
* Since 2.11
*/
protected LocalDateDeserializer(LocalDateDeserializer base, JsonFormat.Shape shape) {
super(base, shape);
_useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
}

/**
* Since 2.19
*/
protected LocalDateDeserializer(LocalDateDeserializer base, JacksonFeatureSet<JavaTimeFeature> features) {
super(LocalDate.class, base._formatter);
_useTimeZoneForLenientDateParsing = features.isEnabled(JavaTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING);
}

@Override
Expand All @@ -83,6 +102,17 @@ protected LocalDateDeserializer withLeniency(Boolean leniency) {
@Override
protected LocalDateDeserializer withShape(JsonFormat.Shape shape) { return new LocalDateDeserializer(this, shape); }

/**
* Since 2.19
*/
public LocalDateDeserializer withFeatures(JacksonFeatureSet<JavaTimeFeature> features) {
if (_useTimeZoneForLenientDateParsing ==
features.isEnabled(JavaTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING)) {
return this;
}
return new LocalDateDeserializer(this, features);
}

@Override
public LocalDate deserialize(JsonParser parser, DeserializationContext context)
throws JacksonException
Expand Down Expand Up @@ -162,6 +192,9 @@ protected LocalDate _fromString(JsonParser p, DeserializationContext ctxt,
if (string.length() > 10 && string.charAt(10) == 'T') {
if (isLenient()) {
if (string.endsWith("Z")) {
if (_useTimeZoneForLenientDateParsing) {
return Instant.parse(string).atZone(ctxt.getTimeZone().toZoneId()).toLocalDate();
}
return LocalDate.parse(string.substring(0, string.length() - 1),
DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,17 @@
package tools.jackson.datatype.jsr310.deser;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.JsonTokenId;
import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JavaType;
import tools.jackson.core.*;
import tools.jackson.core.util.JacksonFeatureSet;
import tools.jackson.databind.*;
import tools.jackson.datatype.jsr310.JavaTimeFeature;

/**
* Deserializer for Java 8 temporal {@link LocalDateTime}s.
Expand All @@ -40,6 +37,9 @@
public class LocalDateTimeDeserializer
extends JSR310DateTimeDeserializerBase<LocalDateTime>
{
private final static boolean DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING
= JavaTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING.enabledByDefault();

private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer();
Expand All @@ -51,13 +51,25 @@ public class LocalDateTimeDeserializer
*/
protected final Boolean _readTimestampsAsNanosOverride;

/**
* Flag set from
* {@link tools.jackson.datatype.jsr310.JavaTimeFeature#USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING}
* to determine whether the {@link java.util.TimeZone} of the
* {@link tools.jackson.databind.DeserializationContext} is used
* when leniently deserializing from the UTC/ISO instant format.
*
* @since 2.19
*/
protected final boolean _useTimeZoneForLenientDateParsing;

protected LocalDateTimeDeserializer() { // was private before 2.12
this(DEFAULT_FORMATTER);
}

public LocalDateTimeDeserializer(DateTimeFormatter formatter) {
super(LocalDateTime.class, formatter);
_readTimestampsAsNanosOverride = null;
_useTimeZoneForLenientDateParsing = DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING;
}

/**
Expand All @@ -66,6 +78,7 @@ public LocalDateTimeDeserializer(DateTimeFormatter formatter) {
protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base, Boolean leniency) {
super(base, leniency);
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
_useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
}

/**
Expand All @@ -78,6 +91,16 @@ protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base,
Boolean readTimestampsAsNanosOverride) {
super(base, leniency, formatter, shape);
_readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
_useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
}

/**
* Since 2.19
*/
protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base, JacksonFeatureSet<JavaTimeFeature> features) {
super(LocalDateTime.class, base._formatter);
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
_useTimeZoneForLenientDateParsing = features.isEnabled(JavaTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING);
}

@Override
Expand Down Expand Up @@ -105,6 +128,17 @@ protected JSR310DateTimeDeserializerBase<?> _withFormatOverrides(Deserialization
return deser;
}

/**
* Since 2.19
*/
public LocalDateTimeDeserializer withFeatures(JacksonFeatureSet<JavaTimeFeature> features) {
if (_useTimeZoneForLenientDateParsing ==
features.isEnabled(JavaTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING)) {
return this;
}
return new LocalDateTimeDeserializer(this, features);
}

@Override
public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws JacksonException
{
Expand Down Expand Up @@ -195,11 +229,12 @@ protected LocalDateTime _fromString(JsonParser p, DeserializationContext ctxt,
if (_formatter == DEFAULT_FORMATTER) {
// ... only allow iff lenient mode enabled since
// JavaScript by default includes time and zone in JSON serialized Dates (UTC/ISO instant format).
// And if so, do NOT use zoned date parsing as that can easily produce
// incorrect answer.
if (string.length() > 10 && string.charAt(10) == 'T') {
if (string.endsWith("Z")) {
if (isLenient()) {
if (_useTimeZoneForLenientDateParsing) {
return Instant.parse(string).atZone(ctxt.getTimeZone().toZoneId()).toLocalDateTime();
}
return LocalDateTime.parse(string.substring(0, string.length()-1),
_formatter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ protected boolean useTimestamp(SerializationContext ctxt) {
return (_formatter == null) && useTimestampFromGlobalDefaults(ctxt);
}

// @since 2.19
protected boolean useTimestampFromGlobalDefaults(SerializationContext ctxt) {
return (ctxt != null)
&& ctxt.isEnabled(getTimestampsFeature());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import java.time.*;
import java.time.temporal.Temporal;
import java.util.Map;
import java.util.TimeZone;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import com.fasterxml.jackson.annotation.OptBoolean;

Expand All @@ -18,7 +21,9 @@

import tools.jackson.databind.*;
import tools.jackson.databind.exc.MismatchedInputException;

import tools.jackson.databind.json.JsonMapper;
import tools.jackson.datatype.jsr310.JavaTimeFeature;
import tools.jackson.datatype.jsr310.JavaTimeModule;
import tools.jackson.datatype.jsr310.MockObjectConfiguration;
import tools.jackson.datatype.jsr310.ModuleTestBase;

Expand All @@ -28,6 +33,11 @@ public class LocalDateDeserTest extends ModuleTestBase
{
private final ObjectMapper MAPPER = newMapper();
private final ObjectReader READER = MAPPER.readerFor(LocalDate.class);
private final ObjectReader READER_USING_TIME_ZONE = JsonMapper.builder()
.addModule(new JavaTimeModule().enable(JavaTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING))
.build()
.readerFor(LocalDate.class);

private final TypeReference<Map<String, LocalDate>> MAP_TYPE_REF = new TypeReference<Map<String, LocalDate>>() { };

final static class Wrapper {
Expand Down Expand Up @@ -120,12 +130,42 @@ public void testDeserializationAsString02()
}

@Test
public void testDeserializationAsString03()
public void testLenientDeserializationAsString01() throws Exception
{
Instant instant = Instant.now();
LocalDate value = READER.readValue(q(instant.toString()));
assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value);
}

@Test
public void testLenientDeserializationAsString02() throws Exception
{
ObjectReader reader = READER.with(TimeZone.getTimeZone(Z_BUDAPEST));
Instant instant = Instant.now();
LocalDate value = READER.readValue('"' + instant.toString() + '"');
assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(),
value);
LocalDate value = reader.readValue(q(instant.toString()));
assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value);
}

@Test
public void testLenientDeserializationAsString03() throws Exception
{
Instant instant = Instant.now();
LocalDate value = READER_USING_TIME_ZONE.readValue(q(instant.toString()));
assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value);
}

@ParameterizedTest
@CsvSource({
"Europe/Budapest, 2024-07-21T21:59:59Z, 2024-07-21",
"Europe/Budapest, 2024-07-21T22:00:00Z, 2024-07-22",
"America/Chicago, 2024-07-22T04:59:59Z, 2024-07-21",
"America/Chicago, 2024-07-22T05:00:00Z, 2024-07-22"
})
public void testLenientDeserializationAsString04(TimeZone zone, String string, LocalDate expected) throws Exception
{
ObjectReader reader = READER_USING_TIME_ZONE.with(zone);
LocalDate value = reader.readValue(q(string));
assertEquals(expected, value);
}

@Test
Expand Down
Loading

0 comments on commit c84b1a9

Please sign in to comment.