Skip to content

Commit 2520e49

Browse files
committed
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
1 parent 9f9c490 commit 2520e49

File tree

5 files changed

+104
-34
lines changed

5 files changed

+104
-34
lines changed

lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDate.java

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,25 @@ protected <T> T internalValueOfString(final String value, final Boolean isNullab
5959
}
6060

6161
// appropriate types
62-
if (returnType.isAssignableFrom(LocalDate.class)) {
62+
if (LocalDate.class.isAssignableFrom(returnType)) {
6363
return (T) date;
64-
} else if (returnType.isAssignableFrom(java.sql.Date.class)) {
65-
return (T) java.sql.Date.valueOf(date);
64+
} else if (java.sql.Date.class.isAssignableFrom(returnType)) {
65+
/*
66+
Using java.sql.Date.valueOf would result in the Julian instead of the (proleptic) Gregorian calendar
67+
being used for historical dates (before the Julian-Gregorian cutover date)
68+
*/
69+
return (T) new java.sql.Date(toZonedDateTime(date).toInstant().toEpochMilli());
6670
}
6771

6872
// inappropriate types, which need to be supported for backward compatibility
69-
ZonedDateTime zdt = LocalDateTime.of(date, LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault());
70-
if (returnType.isAssignableFrom(Calendar.class)) {
71-
return (T) GregorianCalendar.from(zdt);
72-
} else if (returnType.isAssignableFrom(Long.class)) {
73-
return (T) Long.valueOf(zdt.toInstant().toEpochMilli());
74-
} else if (returnType.isAssignableFrom(java.sql.Date.class)) {
75-
throw new EdmPrimitiveTypeException("The value type " + returnType + " is not supported.");
76-
} else if (returnType.isAssignableFrom(java.sql.Timestamp.class)) {
77-
return (T) java.sql.Timestamp.from(zdt.toInstant());
78-
} else if (returnType.isAssignableFrom(java.util.Date.class)) {
79-
return (T) java.util.Date.from(zdt.toInstant());
73+
if (Calendar.class.isAssignableFrom(returnType)) {
74+
return (T) GregorianCalendar.from(toZonedDateTime(date));
75+
} else if (Long.class.isAssignableFrom(returnType)) {
76+
return (T) Long.valueOf(toZonedDateTime(date).toInstant().toEpochMilli());
77+
} else if (java.sql.Timestamp.class.isAssignableFrom(returnType)) {
78+
return (T) java.sql.Timestamp.from(toZonedDateTime(date).toInstant());
79+
} else if (java.util.Date.class.isAssignableFrom(returnType)) {
80+
return (T) java.util.Date.from(toZonedDateTime(date).toInstant());
8081
} else {
8182
throw new EdmPrimitiveTypeException("The value type " + returnType + " is not supported.");
8283
}
@@ -89,7 +90,11 @@ protected <T> String internalValueToString(final T value, final Boolean isNullab
8990
if (value instanceof LocalDate) {
9091
return value.toString();
9192
} else if (value instanceof java.sql.Date) {
92-
return value.toString();
93+
/*
94+
Using java.sql.Date.toString would result in the Julian instead of the (proleptic) Gregorian calendar
95+
being used for historical dates (before the Julian-Gregorian cutover date)
96+
*/
97+
return toLocalDateString(((java.sql.Date) value).getTime());
9398
}
9499

95100
// inappropriate types, which need to be supported for backward compatibility
@@ -98,17 +103,20 @@ protected <T> String internalValueToString(final T value, final Boolean isNullab
98103
return calendar.toZonedDateTime().toLocalDate().toString();
99104
}
100105

101-
long millis;
102106
if (value instanceof Long) {
103-
millis = (Long) value;
107+
return toLocalDateString((Long) value);
104108
} else if (value instanceof java.util.Date) {
105-
millis = ((java.util.Date) value).getTime();
109+
return toLocalDateString(((java.util.Date) value).getTime());
106110
} else {
107111
throw new EdmPrimitiveTypeException("The value type " + value.getClass() + " is not supported.");
108112
}
113+
}
109114

110-
ZonedDateTime zdt = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault());
115+
private ZonedDateTime toZonedDateTime(LocalDate localDate) {
116+
return LocalDateTime.of(localDate, LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault());
117+
}
111118

112-
return zdt.toLocalDate().toString();
119+
private String toLocalDateString(long millis) {
120+
return Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate().toString();
113121
}
114122
}

lib/commons-core/src/main/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffset.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,21 @@ private static ZonedDateTime parseZonedDateTime(final String value) {
9090

9191
@SuppressWarnings("unchecked")
9292
private static <T> T convertZonedDateTime(ZonedDateTime zdt, Class<T> returnType) {
93-
if (returnType == ZonedDateTime.class) {
93+
if (ZonedDateTime.class == returnType) {
9494
return (T) zdt;
95-
} else if (returnType == Instant.class) {
95+
} else if (Instant.class == returnType) {
9696
return (T) zdt.toInstant();
97-
} else if (returnType.isAssignableFrom(Timestamp.class)) {
97+
} else if (Timestamp.class.isAssignableFrom(returnType)) {
9898
return (T) Timestamp.from(zdt.toInstant());
99-
} else if (returnType.isAssignableFrom(java.util.Date.class)) {
100-
return (T) java.util.Date.from(zdt.toInstant());
101-
} else if (returnType.isAssignableFrom(java.sql.Time.class)) {
99+
} else if (java.sql.Time.class.isAssignableFrom(returnType)) {
102100
return (T) new java.sql.Time(zdt.toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli());
103-
} else if (returnType.isAssignableFrom(java.sql.Date.class)) {
101+
} else if (java.sql.Date.class.isAssignableFrom(returnType)) {
104102
return (T) new java.sql.Date(zdt.toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli());
105-
} else if (returnType.isAssignableFrom(Long.class)) {
103+
} else if (java.util.Date.class.isAssignableFrom(returnType)) {
104+
return (T) java.util.Date.from(zdt.toInstant());
105+
} else if (Long.class.isAssignableFrom(returnType)) {
106106
return (T) Long.valueOf(zdt.toInstant().toEpochMilli());
107-
} else if (returnType.isAssignableFrom(Calendar.class)) {
107+
} else if (Calendar.class.isAssignableFrom(returnType)) {
108108
return (T) GregorianCalendar.from(zdt);
109109
} else {
110110
throw new ClassCastException("Unsupported return type " + returnType.getSimpleName());

lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020

2121
import static org.junit.Assert.assertEquals;
2222

23+
import java.time.Instant;
2324
import java.time.LocalDate;
25+
import java.time.ZoneId;
26+
import java.time.ZonedDateTime;
2427
import java.util.Calendar;
28+
import java.util.GregorianCalendar;
2529
import java.util.TimeZone;
2630

2731
import org.apache.olingo.commons.api.edm.EdmPrimitiveType;
@@ -69,6 +73,26 @@ public void valueToString() throws Exception {
6973
expectTypeErrorInValueToString(instance, 0);
7074
}
7175

76+
@Test
77+
public void valueToStringYear1000() throws Exception {
78+
ZonedDateTime zdtYear1000 = LocalDate.of(1000, 1, 1).atStartOfDay(ZoneId.systemDefault());
79+
Instant instantYear1000 = zdtYear1000.toInstant();
80+
// GregorianCalendar.from(ZonedDateTime) will create a pure (proleptic) Gregorian calendar
81+
Calendar calendarYear1000 = GregorianCalendar.from(zdtYear1000);
82+
83+
assertEquals("1000-01-01",
84+
instance.valueToString(calendarYear1000, null, null, null, null, null));
85+
assertEquals("1000-01-01",
86+
instance.valueToString(instantYear1000.toEpochMilli(), null, null, null, null, null));
87+
assertEquals("1000-01-01",
88+
instance.valueToString(java.util.Date.from(instantYear1000), null, null, null, null, null));
89+
// Using java.sql.Date.valueOf would result in the Julian instead of the (proleptic) Gregorian calendar being used
90+
assertEquals("1000-01-01",
91+
instance.valueToString(new java.sql.Date(instantYear1000.toEpochMilli()), null, null, null, null, null));
92+
assertEquals("1000-01-01",
93+
instance.valueToString(zdtYear1000.toLocalDate(), null, null, null, null, null));
94+
}
95+
7296
@Test
7397
public void valueOfString() throws Exception {
7498
Calendar dateTime = Calendar.getInstance();
@@ -105,4 +129,25 @@ public void valueOfString() throws Exception {
105129

106130
expectTypeErrorInValueOfString(instance, "2012-02-29");
107131
}
132+
133+
@Test
134+
public void valueOfStringYear1000() throws Exception {
135+
ZonedDateTime zdtYear1000 = LocalDate.of(1000, 1, 1).atStartOfDay(ZoneId.systemDefault());
136+
Instant instantYear1000 = zdtYear1000.toInstant();
137+
// GregorianCalendar.from(ZonedDateTime) will create a pure (proleptic) Gregorian calendar
138+
Calendar calendarYear1000 = GregorianCalendar.from(zdtYear1000);
139+
140+
assertEqualCalendar(calendarYear1000,
141+
instance.valueOfString("1000-01-01", null, null, null, null, null, Calendar.class));
142+
assertEquals(Long.valueOf(instantYear1000.toEpochMilli()),
143+
instance.valueOfString("1000-01-01", null, null, null, null, null, Long.class));
144+
assertEquals(java.util.Date.from(instantYear1000),
145+
instance.valueOfString("1000-01-01", null, null, null, null, null, java.util.Date.class));
146+
assertEquals(zdtYear1000.toLocalDate(),
147+
instance.valueOfString("1000-01-01", null, null, null, null, null, LocalDate.class));
148+
// Using java.sql.Date.valueOf would result in the Julian instead of the (proleptic) Gregorian calendar being used
149+
java.util.Date dateValue = instance.valueOfString("1000-01-01", null, null, null, null, null, java.sql.Date.class);
150+
assertEquals(java.sql.Date.class, dateValue.getClass());
151+
assertEquals(new java.sql.Date(instantYear1000.toEpochMilli()), dateValue);
152+
}
108153
}

lib/commons-core/src/test/java/org/apache/olingo/commons/core/edm/primitivetype/EdmDateTimeOffsetTest.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import java.sql.Time;
2424
import java.sql.Timestamp;
2525
import java.time.Instant;
26+
import java.time.LocalDate;
2627
import java.time.ZoneId;
28+
import java.time.ZoneOffset;
2729
import java.time.ZonedDateTime;
2830
import java.time.temporal.ChronoUnit;
2931
import java.util.Calendar;
@@ -184,9 +186,9 @@ public void valueOfStringToCalendar() throws Exception {
184186

185187
@Test
186188
public void valueOfStringToTimestamp() throws Exception {
187-
assertEquals(530000001, instance
188-
.valueOfString("2012-02-29T01:02:03.530000001+11:00", null, null, 9, null, null, Timestamp.class)
189-
.getNanos());
189+
java.util.Date dateValue = instance.valueOfString("2012-02-29T01:02:03.530000001+11:00", null, null, 9, null, null, Timestamp.class);
190+
assertEquals(Timestamp.class, dateValue.getClass());
191+
assertEquals(530000001, ((Timestamp)dateValue).getNanos());
190192
}
191193

192194
@Test
@@ -227,6 +229,18 @@ public void valueOfStringToJavaSqlDate() throws Exception {
227229
instance.valueOfString("1970-01-01T00:00:00.012", null, null, 3, null, null, java.sql.Date.class));
228230
assertEquals(new java.sql.Date(0),
229231
instance.valueOfString("1970-01-01T00:00:00.12", null, null, 2, null, null, java.sql.Date.class));
232+
// String value without time zone information means UTC for EdmDateTimeOffset
233+
java.util.Date dateValue = instance.valueOfString("1000-01-01T00:00:00", null, null, null, null, null, java.sql.Date.class);
234+
assertEquals(java.sql.Date.class, dateValue.getClass());
235+
assertEquals(new java.sql.Date(toInstantAsUtc(LocalDate.of(1000, 1, 1)).toEpochMilli()), dateValue);
236+
}
237+
238+
@Test
239+
public void valueOfStringToJavaUtilDate() throws Exception {
240+
// String value without time zone information means UTC for EdmDateTimeOffset
241+
java.util.Date dateValue = instance.valueOfString("1000-01-01T00:00:00", null, null, null, null, null, java.util.Date.class);
242+
assertEquals(java.util.Date.class, dateValue.getClass());
243+
assertEquals(java.util.Date.from(toInstantAsUtc(LocalDate.of(1000, 1, 1))), dateValue);
230244
}
231245

232246
@Test
@@ -239,4 +253,7 @@ public void valueOfStringInvalidData() throws Exception {
239253
expectTypeErrorInValueOfString(instance, "2012-02-29T01:02:03Z");
240254
}
241255

256+
private Instant toInstantAsUtc(LocalDate localDate) {
257+
return localDate.atStartOfDay().atOffset(ZoneOffset.UTC).toInstant();
258+
}
242259
}

lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,9 +1158,9 @@ public void mappingTest() throws Exception {
11581158
assertEquals(2, properties.size());
11591159

11601160
assertNotNull(entity.getProperty("PropertyDate").getValue());
1161-
assertEquals(java.sql.Date.class, entity.getProperty("PropertyDate").getValue().getClass());
1161+
assertEquals(Date.class, entity.getProperty("PropertyDate").getValue().getClass());
11621162
assertNotNull(entity.getProperty("PropertyDateTimeOffset").getValue());
1163-
assertEquals(java.sql.Timestamp.class, entity.getProperty("PropertyDateTimeOffset").getValue().getClass());
1163+
assertEquals(Date.class, entity.getProperty("PropertyDateTimeOffset").getValue().getClass());
11641164
}
11651165

11661166
// ---------------------------------- Negative Tests -----------------------------------------------------------

0 commit comments

Comments
 (0)