From 6f7e951eed1b9e6136e31c17a669423d06856e02 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 10 Jun 2020 22:40:30 -0700 Subject: [PATCH] Minor improvements to coercion checking for java.util.Date/Calendar --- .../jackson/databind/cfg/CoercionConfigs.java | 1 + .../databind/deser/std/DateDeserializers.java | 38 +++++++++++++++++-- .../databind/deser/std/StdDeserializer.java | 16 +++++++- .../convert/CoerceMiscScalarsTest.java | 31 ++++++++++++++- 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfigs.java b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfigs.java index b0cfb46627..094999b184 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfigs.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfigs.java @@ -218,6 +218,7 @@ public CoercionAction findCoercion(DeserializationConfig config, // Since coercion of scalar must be enabled (see check above), allow empty-string // coercions by default even without this setting if (classicScalar + || (targetType == LogicalType.DateTime) // Default for setting is false || config.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) { return CoercionAction.AsNull; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/DateDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/DateDeserializers.java index d287f03647..a3d3c7577c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/DateDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/DateDeserializers.java @@ -13,6 +13,8 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; @@ -191,7 +193,15 @@ protected java.util.Date _parseDate(JsonParser p, DeserializationContext ctxt) if (p.hasToken(JsonToken.VALUE_STRING)) { String str = p.getText().trim(); if (str.length() == 0) { - return (Date) getEmptyValue(ctxt); + final CoercionAction act = _checkFromStringCoercion(ctxt, str); + switch (act) { // note: Fail handled above + case AsEmpty: + return new java.util.Date(0L); + case AsNull: + case TryConvert: + default: + } + return null; } synchronized (_customFormat) { try { @@ -245,6 +255,13 @@ protected CalendarDeserializer withDateFormat(DateFormat df, String formatString return new CalendarDeserializer(this, df, formatString); } + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + GregorianCalendar cal = new GregorianCalendar(); + cal.setTimeInMillis(0L); + return cal; + } + @Override public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -290,6 +307,11 @@ public DateDeserializer(DateDeserializer base, DateFormat df, String formatStrin protected DateDeserializer withDateFormat(DateFormat df, String formatString) { return new DateDeserializer(this, df, formatString); } + + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + return new Date(0L); + } @Override public java.util.Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -313,7 +335,12 @@ public SqlDateDeserializer(SqlDateDeserializer src, DateFormat df, String format protected SqlDateDeserializer withDateFormat(DateFormat df, String formatString) { return new SqlDateDeserializer(this, df, formatString); } - + + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + return new java.sql.Date(0L); + } + @Override public java.sql.Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { Date d = _parseDate(p, ctxt); @@ -339,7 +366,12 @@ public TimestampDeserializer(TimestampDeserializer src, DateFormat df, String fo protected TimestampDeserializer withDateFormat(DateFormat df, String formatString) { return new TimestampDeserializer(this, df, formatString); } - + + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + return new Timestamp(0L); + } + @Override public java.sql.Timestamp deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java index c575dd65bb..71cb5dc02e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java @@ -748,8 +748,20 @@ protected java.util.Date _parseDate(String value, DeserializationContext ctxt) { try { // Take empty Strings to mean 'empty' Value, usually 'null': - if (_isEmptyOrTextualNull(value)) { - return (java.util.Date) getNullValue(ctxt); + if (value.length() == 0) { + final CoercionAction act = _checkFromStringCoercion(ctxt, value); + switch (act) { // note: Fail handled above + case AsEmpty: + return new java.util.Date(0L); + case AsNull: + case TryConvert: + default: + } + return null; + } + // 10-Jun-2020, tatu: Legacy handling from pre-2.12... should we still have it? + if (_hasTextualNull(value)) { + return null; } return ctxt.parseDate(value); } catch (IllegalArgumentException iae) { diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceMiscScalarsTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceMiscScalarsTest.java index 7256a3b2a0..405dd35e38 100644 --- a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceMiscScalarsTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceMiscScalarsTest.java @@ -6,7 +6,10 @@ import java.net.URI; import java.net.URL; import java.nio.charset.Charset; +import java.util.Calendar; import java.util.Currency; +import java.util.Date; +import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; import java.util.UUID; @@ -188,7 +191,7 @@ public void testUUIDCoercions() throws Exception _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, UUID.class); _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, UUID.class); - // but allow separate "empty" value is specifically requeted + // but allow separate "empty" value is specifically requested _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_EMPTY, UUID.class, new UUID(0L, 0L)); @@ -220,6 +223,29 @@ private void _checkEmptyStringBuilder(StringBuilder sb) { assertEquals(0, sb.length()); } + // Date, Calendar also included here for convenience + + public void testLegacyDateTimeCoercions() throws Exception + { + // Coerce to `null` both by default, "TryConvert" and explicit + _testScalarEmptyToNull(DEFAULT_MAPPER, Calendar.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, Date.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Calendar.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Date.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, Calendar.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, Date.class); + + // but allow separate "empty" value is specifically requested + Calendar emptyCal = new GregorianCalendar(); + emptyCal.setTimeInMillis(0L); +// _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_EMPTY, Calendar.class, emptyCal); + _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_EMPTY, Date.class, new Date(0L)); + + // allow forcing failure, too + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Calendar.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Date.class); + } + /* /******************************************************** /* Second-level test helper methods @@ -235,6 +261,9 @@ private void _testScalarEmptyToEmpty(ObjectMapper mapper, Class target, Object emptyValue) throws Exception { Object result = mapper.readerFor(target).readValue(JSON_EMPTY); + if (result == null) { + fail("Expected empty, non-null value for "+target.getName()+", got null"); + } assertEquals(emptyValue, result); }