From 2520e49abfcd369dd867efe47068bfd90e27422c Mon Sep 17 00:00:00 2001 From: Manon van Tilburg Date: Tue, 30 Sep 2025 14:28:09 +0200 Subject: [PATCH] Ensure Gregorian calendar system is used for historic date/time conversions. Fix mapped Java class not always correctly used for date/time conversions. fixes #OLINGO-1646 --- .../core/edm/primitivetype/EdmDate.java | 48 +++++++++++-------- .../edm/primitivetype/EdmDateTimeOffset.java | 18 +++---- .../core/edm/primitivetype/EdmDateTest.java | 45 +++++++++++++++++ .../primitivetype/EdmDateTimeOffsetTest.java | 23 +++++++-- .../json/ODataJsonDeserializerEntityTest.java | 4 +- 5 files changed, 104 insertions(+), 34 deletions(-) diff --git a/lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDate.java b/lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDate.java index 2abd04a31c..2c3f0c70ff 100644 --- a/lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDate.java +++ b/lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDate.java @@ -59,24 +59,25 @@ protected T internalValueOfString(final String value, final Boolean isNullab } // appropriate types - if (returnType.isAssignableFrom(LocalDate.class)) { + if (LocalDate.class.isAssignableFrom(returnType)) { return (T) date; - } else if (returnType.isAssignableFrom(java.sql.Date.class)) { - return (T) java.sql.Date.valueOf(date); + } else if (java.sql.Date.class.isAssignableFrom(returnType)) { + /* + Using java.sql.Date.valueOf would result in the Julian instead of the (proleptic) Gregorian calendar + being used for historical dates (before the Julian-Gregorian cutover date) + */ + return (T) new java.sql.Date(toZonedDateTime(date).toInstant().toEpochMilli()); } // inappropriate types, which need to be supported for backward compatibility - ZonedDateTime zdt = LocalDateTime.of(date, LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()); - if (returnType.isAssignableFrom(Calendar.class)) { - return (T) GregorianCalendar.from(zdt); - } else if (returnType.isAssignableFrom(Long.class)) { - return (T) Long.valueOf(zdt.toInstant().toEpochMilli()); - } else if (returnType.isAssignableFrom(java.sql.Date.class)) { - throw new EdmPrimitiveTypeException("The value type " + returnType + " is not supported."); - } else if (returnType.isAssignableFrom(java.sql.Timestamp.class)) { - return (T) java.sql.Timestamp.from(zdt.toInstant()); - } else if (returnType.isAssignableFrom(java.util.Date.class)) { - return (T) java.util.Date.from(zdt.toInstant()); + if (Calendar.class.isAssignableFrom(returnType)) { + return (T) GregorianCalendar.from(toZonedDateTime(date)); + } else if (Long.class.isAssignableFrom(returnType)) { + return (T) Long.valueOf(toZonedDateTime(date).toInstant().toEpochMilli()); + } else if (java.sql.Timestamp.class.isAssignableFrom(returnType)) { + return (T) java.sql.Timestamp.from(toZonedDateTime(date).toInstant()); + } else if (java.util.Date.class.isAssignableFrom(returnType)) { + return (T) java.util.Date.from(toZonedDateTime(date).toInstant()); } else { throw new EdmPrimitiveTypeException("The value type " + returnType + " is not supported."); } @@ -89,7 +90,11 @@ protected String internalValueToString(final T value, final Boolean isNullab if (value instanceof LocalDate) { return value.toString(); } else if (value instanceof java.sql.Date) { - return value.toString(); + /* + Using java.sql.Date.toString would result in the Julian instead of the (proleptic) Gregorian calendar + being used for historical dates (before the Julian-Gregorian cutover date) + */ + return toLocalDateString(((java.sql.Date) value).getTime()); } // inappropriate types, which need to be supported for backward compatibility @@ -98,17 +103,20 @@ protected String internalValueToString(final T value, final Boolean isNullab return calendar.toZonedDateTime().toLocalDate().toString(); } - long millis; if (value instanceof Long) { - millis = (Long) value; + return toLocalDateString((Long) value); } else if (value instanceof java.util.Date) { - millis = ((java.util.Date) value).getTime(); + return toLocalDateString(((java.util.Date) value).getTime()); } else { throw new EdmPrimitiveTypeException("The value type " + value.getClass() + " is not supported."); } + } - ZonedDateTime zdt = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()); + private ZonedDateTime toZonedDateTime(LocalDate localDate) { + return LocalDateTime.of(localDate, LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()); + } - return zdt.toLocalDate().toString(); + private String toLocalDateString(long millis) { + return Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate().toString(); } } diff --git a/lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffset.java b/lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffset.java index 0280c5ff09..8f87223527 100644 --- a/lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffset.java +++ b/lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffset.java @@ -90,21 +90,21 @@ private static ZonedDateTime parseZonedDateTime(final String value) { @SuppressWarnings("unchecked") private static T convertZonedDateTime(ZonedDateTime zdt, Class returnType) { - if (returnType == ZonedDateTime.class) { + if (ZonedDateTime.class == returnType) { return (T) zdt; - } else if (returnType == Instant.class) { + } else if (Instant.class == returnType) { return (T) zdt.toInstant(); - } else if (returnType.isAssignableFrom(Timestamp.class)) { + } else if (Timestamp.class.isAssignableFrom(returnType)) { return (T) Timestamp.from(zdt.toInstant()); - } else if (returnType.isAssignableFrom(java.util.Date.class)) { - return (T) java.util.Date.from(zdt.toInstant()); - } else if (returnType.isAssignableFrom(java.sql.Time.class)) { + } else if (java.sql.Time.class.isAssignableFrom(returnType)) { return (T) new java.sql.Time(zdt.toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli()); - } else if (returnType.isAssignableFrom(java.sql.Date.class)) { + } else if (java.sql.Date.class.isAssignableFrom(returnType)) { return (T) new java.sql.Date(zdt.toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli()); - } else if (returnType.isAssignableFrom(Long.class)) { + } else if (java.util.Date.class.isAssignableFrom(returnType)) { + return (T) java.util.Date.from(zdt.toInstant()); + } else if (Long.class.isAssignableFrom(returnType)) { return (T) Long.valueOf(zdt.toInstant().toEpochMilli()); - } else if (returnType.isAssignableFrom(Calendar.class)) { + } else if (Calendar.class.isAssignableFrom(returnType)) { return (T) GregorianCalendar.from(zdt); } else { throw new ClassCastException("Unsupported return type " + returnType.getSimpleName()); diff --git a/lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTest.java b/lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTest.java index 468e2623b1..befaaadd4b 100644 --- a/lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTest.java +++ b/lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTest.java @@ -20,8 +20,12 @@ import static org.junit.Assert.assertEquals; +import java.time.Instant; import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.TimeZone; import org.apache.olingo.commons.api.edm.EdmPrimitiveType; @@ -69,6 +73,26 @@ public void valueToString() throws Exception { expectTypeErrorInValueToString(instance, 0); } + @Test + public void valueToStringYear1000() throws Exception { + ZonedDateTime zdtYear1000 = LocalDate.of(1000, 1, 1).atStartOfDay(ZoneId.systemDefault()); + Instant instantYear1000 = zdtYear1000.toInstant(); + // GregorianCalendar.from(ZonedDateTime) will create a pure (proleptic) Gregorian calendar + Calendar calendarYear1000 = GregorianCalendar.from(zdtYear1000); + + assertEquals("1000-01-01", + instance.valueToString(calendarYear1000, null, null, null, null, null)); + assertEquals("1000-01-01", + instance.valueToString(instantYear1000.toEpochMilli(), null, null, null, null, null)); + assertEquals("1000-01-01", + instance.valueToString(java.util.Date.from(instantYear1000), null, null, null, null, null)); + // Using java.sql.Date.valueOf would result in the Julian instead of the (proleptic) Gregorian calendar being used + assertEquals("1000-01-01", + instance.valueToString(new java.sql.Date(instantYear1000.toEpochMilli()), null, null, null, null, null)); + assertEquals("1000-01-01", + instance.valueToString(zdtYear1000.toLocalDate(), null, null, null, null, null)); + } + @Test public void valueOfString() throws Exception { Calendar dateTime = Calendar.getInstance(); @@ -105,4 +129,25 @@ public void valueOfString() throws Exception { expectTypeErrorInValueOfString(instance, "2012-02-29"); } + + @Test + public void valueOfStringYear1000() throws Exception { + ZonedDateTime zdtYear1000 = LocalDate.of(1000, 1, 1).atStartOfDay(ZoneId.systemDefault()); + Instant instantYear1000 = zdtYear1000.toInstant(); + // GregorianCalendar.from(ZonedDateTime) will create a pure (proleptic) Gregorian calendar + Calendar calendarYear1000 = GregorianCalendar.from(zdtYear1000); + + assertEqualCalendar(calendarYear1000, + instance.valueOfString("1000-01-01", null, null, null, null, null, Calendar.class)); + assertEquals(Long.valueOf(instantYear1000.toEpochMilli()), + instance.valueOfString("1000-01-01", null, null, null, null, null, Long.class)); + assertEquals(java.util.Date.from(instantYear1000), + instance.valueOfString("1000-01-01", null, null, null, null, null, java.util.Date.class)); + assertEquals(zdtYear1000.toLocalDate(), + instance.valueOfString("1000-01-01", null, null, null, null, null, LocalDate.class)); + // Using java.sql.Date.valueOf would result in the Julian instead of the (proleptic) Gregorian calendar being used + java.util.Date dateValue = instance.valueOfString("1000-01-01", null, null, null, null, null, java.sql.Date.class); + assertEquals(java.sql.Date.class, dateValue.getClass()); + assertEquals(new java.sql.Date(instantYear1000.toEpochMilli()), dateValue); + } } diff --git a/lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffsetTest.java b/lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffsetTest.java index 3075dfd411..e76e926e26 100644 --- a/lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffsetTest.java +++ b/lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffsetTest.java @@ -23,7 +23,9 @@ import java.sql.Time; import java.sql.Timestamp; import java.time.Instant; +import java.time.LocalDate; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Calendar; @@ -184,9 +186,9 @@ public void valueOfStringToCalendar() throws Exception { @Test public void valueOfStringToTimestamp() throws Exception { - assertEquals(530000001, instance - .valueOfString("2012-02-29T01:02:03.530000001+11:00", null, null, 9, null, null, Timestamp.class) - .getNanos()); + java.util.Date dateValue = instance.valueOfString("2012-02-29T01:02:03.530000001+11:00", null, null, 9, null, null, Timestamp.class); + assertEquals(Timestamp.class, dateValue.getClass()); + assertEquals(530000001, ((Timestamp)dateValue).getNanos()); } @Test @@ -227,6 +229,18 @@ public void valueOfStringToJavaSqlDate() throws Exception { instance.valueOfString("1970-01-01T00:00:00.012", null, null, 3, null, null, java.sql.Date.class)); assertEquals(new java.sql.Date(0), instance.valueOfString("1970-01-01T00:00:00.12", null, null, 2, null, null, java.sql.Date.class)); + // String value without time zone information means UTC for EdmDateTimeOffset + java.util.Date dateValue = instance.valueOfString("1000-01-01T00:00:00", null, null, null, null, null, java.sql.Date.class); + assertEquals(java.sql.Date.class, dateValue.getClass()); + assertEquals(new java.sql.Date(toInstantAsUtc(LocalDate.of(1000, 1, 1)).toEpochMilli()), dateValue); + } + + @Test + public void valueOfStringToJavaUtilDate() throws Exception { + // String value without time zone information means UTC for EdmDateTimeOffset + java.util.Date dateValue = instance.valueOfString("1000-01-01T00:00:00", null, null, null, null, null, java.util.Date.class); + assertEquals(java.util.Date.class, dateValue.getClass()); + assertEquals(java.util.Date.from(toInstantAsUtc(LocalDate.of(1000, 1, 1))), dateValue); } @Test @@ -239,4 +253,7 @@ public void valueOfStringInvalidData() throws Exception { expectTypeErrorInValueOfString(instance, "2012-02-29T01:02:03Z"); } + private Instant toInstantAsUtc(LocalDate localDate) { + return localDate.atStartOfDay().atOffset(ZoneOffset.UTC).toInstant(); + } } diff --git a/lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java b/lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java index a6f380ae20..10b5001549 100644 --- a/lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java +++ b/lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java @@ -1158,9 +1158,9 @@ public void mappingTest() throws Exception { assertEquals(2, properties.size()); assertNotNull(entity.getProperty("PropertyDate").getValue()); - assertEquals(java.sql.Date.class, entity.getProperty("PropertyDate").getValue().getClass()); + assertEquals(Date.class, entity.getProperty("PropertyDate").getValue().getClass()); assertNotNull(entity.getProperty("PropertyDateTimeOffset").getValue()); - assertEquals(java.sql.Timestamp.class, entity.getProperty("PropertyDateTimeOffset").getValue().getClass()); + assertEquals(Date.class, entity.getProperty("PropertyDateTimeOffset").getValue().getClass()); } // ---------------------------------- Negative Tests -----------------------------------------------------------