From 6f7e951eed1b9e6136e31c17a669423d06856e02 Mon Sep 17 00:00:00 2001
From: Tatu Saloranta <tatu.saloranta@iki.fi>
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);
     }