You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/04/24 07:49:27 UTC

(causeway) 17/28: CAUSEWAY-3989: [v2] fixes localdate marshalling

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch CAUSEWAY-3989
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 98c06bce31a4d5ccb9fa0cbc3c3f16a20882a582
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Apr 22 11:41:03 2026 +0100

    CAUSEWAY-3989: [v2] fixes localdate marshalling
---
 .../applib/util/schema/CommandDtoUtils.java        | 72 +++++++++++++++++++++-
 .../CommandDtoUtils_toYaml_fromYaml_Test.java      | 11 ++--
 2 files changed, 77 insertions(+), 6 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java
index 8fec5928b9d..85db27142b8 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java
@@ -18,10 +18,15 @@
  */
 package org.apache.causeway.applib.util.schema;
 
+import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
+import javax.xml.datatype.DatatypeConstants;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.commons.internal.base._Lazy;
 import org.apache.causeway.commons.internal.base._NullSafe;
@@ -44,7 +49,15 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.fasterxml.jackson.databind.jsontype.NamedType;
 
 import lombok.experimental.UtilityClass;
@@ -178,12 +191,69 @@ private abstract class AbstractDtoMixIn {}
 
     // Mix-in to ignore unknown properties for ValueDto
     @JsonIgnoreProperties(ignoreUnknown = true)
-    private abstract class AbstractValueDtoMixIn {}
+    private abstract class AbstractValueDtoMixIn {
+        @JsonSerialize(using = LocalDateXmlGregorianCalendarSerializer.class)
+        abstract XMLGregorianCalendar getLocalDate();
+
+        @JsonDeserialize(using = LocalDateXmlGregorianCalendarDeserializer.class)
+        abstract void setLocalDate(XMLGregorianCalendar localDate);
+    }
 
     private void valueDtoSupport(final ObjectMapper mb) {
         mb.addMixIn(ValueDto.class, AbstractValueDtoMixIn.class);
     }
 
+    private static final DatatypeFactory DATATYPE_FACTORY = datatypeFactory();
+
+    private static DatatypeFactory datatypeFactory() {
+        try {
+            return DatatypeFactory.newInstance();
+        } catch (Exception ex) {
+            throw new RuntimeException("Failed to initialize DatatypeFactory", ex);
+        }
+    }
+
+    private static final class LocalDateXmlGregorianCalendarSerializer
+            extends JsonSerializer<XMLGregorianCalendar> {
+
+        @Override
+        public void serialize(
+                final XMLGregorianCalendar value,
+                final JsonGenerator gen,
+                final SerializerProvider serializers) throws IOException {
+            if (value == null) {
+                gen.writeNull();
+                return;
+            }
+            final XMLGregorianCalendar dateOnly = DATATYPE_FACTORY.newXMLGregorianCalendarDate(
+                    value.getYear(),
+                    value.getMonth(),
+                    value.getDay(),
+                    DatatypeConstants.FIELD_UNDEFINED);
+            gen.writeString(dateOnly.toXMLFormat());
+        }
+    }
+
+    private static final class LocalDateXmlGregorianCalendarDeserializer
+            extends JsonDeserializer<XMLGregorianCalendar> {
+
+        @Override
+        public XMLGregorianCalendar deserialize(
+                final JsonParser p,
+                final DeserializationContext ctxt) throws IOException {
+            final String text = p.getValueAsString();
+            if (_Strings.isNullOrEmpty(text)) {
+                return null;
+            }
+            final XMLGregorianCalendar parsed = DATATYPE_FACTORY.newXMLGregorianCalendar(text);
+            return DATATYPE_FACTORY.newXMLGregorianCalendarDate(
+                    parsed.getYear(),
+                    parsed.getMonth(),
+                    parsed.getDay(),
+                    DatatypeConstants.FIELD_UNDEFINED);
+        }
+    }
+
     private void memberDtoSupport(final ObjectMapper mb) {
         // add mix-in so MemberDto carries @JsonTypeInfo without modifying source
         mb.addMixIn(MemberDto.class, AbstractDtoMixIn.class);
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java
index 43ef012b482..f06a622e968 100644
--- a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java
+++ b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java
@@ -41,7 +41,7 @@
 class CommandDtoUtils_toYaml_fromYaml_Test {
 
     @Test
-    void localDate_roundtrip_is_broken_due_to_timezone_marshalling() throws Exception {
+    void localDate_roundtrips_as_date_only_without_timezone() throws Exception {
         TimeZone originalDefault = TimeZone.getDefault();
         TimeZone.setDefault(TimeZone.getTimeZone("Europe/Amsterdam"));
         try {
@@ -84,14 +84,15 @@ void localDate_roundtrip_is_broken_due_to_timezone_marshalling() throws Exceptio
                     .get(0)
                     .getLocalDate();
 
-            // Demonstrates the current bug: localDate gets marshalled as date-time with timezone.
+            // Verify fixed behavior: localDate is emitted and roundtripped as date-only.
             Assertions.assertThat(yaml)
-                    .containsPattern("localDate:\\s+\"\\d{4}-\\d{2}-\\d{2}T.*(Z|[+-]\\d{2}:\\d{2})\"");
+                    .contains("localDate: \"2026-07-01\"")
+                    .doesNotContain("localDate: \"2026-06-30T22:00:00.000+00:00\"")
+                    .doesNotContain("localDate: \"2026-07-01T");
             Assertions.assertThat(roundtrippedDate.toXMLFormat())
-                    .isNotEqualTo(originalDate.toXMLFormat());
+                    .isEqualTo(originalDate.toXMLFormat());
         } finally {
             TimeZone.setDefault(originalDefault);
         }
     }
 }
-