Update date time parser.

See: #3750
Original pull request: #4334
This commit is contained in:
Christoph Strobl
2023-03-20 11:25:45 +01:00
committed by Mark Paluch
parent d54f2e47b0
commit 79c6427cc9
3 changed files with 58 additions and 157 deletions

View File

@@ -17,19 +17,14 @@ package org.springframework.data.mongodb.util.json;
import static java.time.format.DateTimeFormatter.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;
import java.util.Calendar;
import java.util.TimeZone;
/**
* JsonBuffer implementation borrowed from <a href=
* DateTimeFormatter implementation borrowed from <a href=
* "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/json/DateTimeFormatter.java">MongoDB
* Inc.</a> licensed under the Apache License, Version 2.0. <br />
* Formatted and modified.
@@ -40,133 +35,22 @@ import java.util.TimeZone;
*/
class DateTimeFormatter {
private static final FormatterImpl FORMATTER_IMPL;
static {
FormatterImpl dateTimeHelper;
try {
dateTimeHelper = loadDateTimeFormatter(
"org.springframework.data.mongodb.util.json.DateTimeFormatter$Java8DateTimeFormatter");
} catch (LinkageError e) {
// this is expected if running on a release prior to Java 8: fallback to JAXB.
dateTimeHelper = loadDateTimeFormatter(
"org.springframework.data.mongodb.util.json.DateTimeFormatter$JaxbDateTimeFormatter");
}
FORMATTER_IMPL = dateTimeHelper;
}
private static FormatterImpl loadDateTimeFormatter(final String className) {
try {
return (FormatterImpl) Class.forName(className).getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
// this is unexpected as it means the class itself is not found
throw new ExceptionInInitializerError(e);
} catch (InstantiationException e) {
// this is unexpected as it means the class can't be instantiated
throw new ExceptionInInitializerError(e);
} catch (IllegalAccessException e) {
// this is unexpected as it means the no-args constructor isn't accessible
throw new ExceptionInInitializerError(e);
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
} catch (InvocationTargetException e) {
throw new ExceptionInInitializerError(e);
}
}
private static final int DATE_STRING_LENGTH = "1970-01-01".length();
static long parse(final String dateTimeString) {
return FORMATTER_IMPL.parse(dateTimeString);
// ISO_OFFSET_DATE_TIME will not parse date strings consisting of just year-month-day, so use ISO_LOCAL_DATE for
// those
if (dateTimeString.length() == DATE_STRING_LENGTH) {
return LocalDate.parse(dateTimeString, ISO_LOCAL_DATE).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
} else {
return ISO_OFFSET_DATE_TIME.parse(dateTimeString, Instant::from).toEpochMilli();
}
}
static String format(final long dateTime) {
return FORMATTER_IMPL.format(dateTime);
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateTime), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME);
}
private interface FormatterImpl {
long parse(String dateTimeString);
String format(long dateTime);
private DateTimeFormatter() {
}
// Reflective use of DatatypeConverter avoids a compile-time dependency on the java.xml.bind module in Java 9
static class JaxbDateTimeFormatter implements FormatterImpl {
private static final Method DATATYPE_CONVERTER_PARSE_DATE_TIME_METHOD;
private static final Method DATATYPE_CONVERTER_PRINT_DATE_TIME_METHOD;
static {
try {
DATATYPE_CONVERTER_PARSE_DATE_TIME_METHOD = Class.forName("jakarta.xml.bind.DatatypeConverter")
.getDeclaredMethod("parseDateTime", String.class);
DATATYPE_CONVERTER_PRINT_DATE_TIME_METHOD = Class.forName("jakarta.xml.bind.DatatypeConverter")
.getDeclaredMethod("printDateTime", Calendar.class);
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public long parse(final String dateTimeString) {
try {
return ((Calendar) DATATYPE_CONVERTER_PARSE_DATE_TIME_METHOD.invoke(null, dateTimeString)).getTimeInMillis();
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw (RuntimeException) e.getCause();
}
}
@Override
public String format(final long dateTime) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(dateTime);
calendar.setTimeZone(TimeZone.getTimeZone("Z"));
try {
return (String) DATATYPE_CONVERTER_PRINT_DATE_TIME_METHOD.invoke(null, calendar);
} catch (IllegalAccessException e) {
throw new IllegalStateException();
} catch (InvocationTargetException e) {
throw (RuntimeException) e.getCause();
}
}
}
static class Java8DateTimeFormatter implements FormatterImpl {
// if running on Java 8 or above then java.time.format.DateTimeFormatter will be available and initialization will
// succeed.
// Otherwise it will fail.
static {
try {
Class.forName("java.time.format.DateTimeFormatter");
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public long parse(final String dateTimeString) {
try {
return ISO_OFFSET_DATE_TIME.parse(dateTimeString, new TemporalQuery<Instant>() {
@Override
public Instant queryFrom(final TemporalAccessor temporal) {
return Instant.from(temporal);
}
}).toEpochMilli();
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
@Override
public String format(final long dateTime) {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateTime), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME);
}
}
private DateTimeFormatter() {}
}

View File

@@ -20,6 +20,8 @@ import static java.lang.String.*;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeParseException;
import java.util.Base64;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
@@ -27,7 +29,6 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -42,7 +43,6 @@ import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Base64Utils;
import org.springframework.util.ClassUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
@@ -957,7 +957,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
}
verifyToken(JsonTokenType.RIGHT_PAREN);
byte[] bytes = Base64Utils.decodeFromString(bytesToken.getValue(String.class));
byte[] bytes = Base64.getDecoder().decode(bytesToken.getValue(String.class));
return new BsonBinary(subTypeToken.getValue(Integer.class).byteValue(), bytes);
}
@@ -1080,28 +1080,14 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
}
verifyToken(JsonTokenType.RIGHT_PAREN);
String[] patterns = { "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm:ssz", "yyyy-MM-dd'T'HH:mm:ss.SSSz" };
String dateTimeString = token.getValue(String.class);
SimpleDateFormat format = new SimpleDateFormat(patterns[0], Locale.ENGLISH);
ParsePosition pos = new ParsePosition(0);
String s = token.getValue(String.class);
if (s.endsWith("Z")) {
s = s.substring(0, s.length() - 1) + "GMT-00:00";
try {
return DateTimeFormatter.parse(dateTimeString);
} catch (DateTimeParseException e) {
throw new JsonParseException("Failed to parse string as a date: " + dateTimeString, e);
}
for (final String pattern : patterns) {
format.applyPattern(pattern);
format.setLenient(true);
pos.setIndex(0);
Date date = format.parse(s, pos);
if (date != null && pos.getIndex() == s.length()) {
return date.getTime();
}
}
throw new JsonParseException("Invalid date format.");
}
private BsonBinary visitHexDataConstructor() {
@@ -1219,7 +1205,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
byte type;
if (firstNestedKey.equals("base64")) {
verifyToken(JsonTokenType.COLON);
data = Base64Utils.decodeFromString(readStringFromExtendedJson());
data = Base64.getDecoder().decode(readStringFromExtendedJson());
verifyToken(JsonTokenType.COMMA);
verifyString("subType");
verifyToken(JsonTokenType.COLON);
@@ -1230,7 +1216,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
verifyToken(JsonTokenType.COMMA);
verifyString("base64");
verifyToken(JsonTokenType.COLON);
data = Base64Utils.decodeFromString(readStringFromExtendedJson());
data = Base64.getDecoder().decode(readStringFromExtendedJson());
} else {
throw new JsonParseException("Unexpected key for $binary: " + firstNestedKey);
}
@@ -1258,7 +1244,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
byte type;
if (firstKey.equals("$binary")) {
data = Base64Utils.decodeFromString(readStringFromExtendedJson());
data = Base64.getDecoder().decode(readStringFromExtendedJson());
verifyToken(JsonTokenType.COMMA);
verifyString("$type");
verifyToken(JsonTokenType.COLON);
@@ -1268,7 +1254,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
verifyToken(JsonTokenType.COMMA);
verifyString("$binary");
verifyToken(JsonTokenType.COLON);
data = Base64Utils.decodeFromString(readStringFromExtendedJson());
data = Base64.getDecoder().decode(readStringFromExtendedJson());
}
verifyToken(JsonTokenType.END_OBJECT);

View File

@@ -211,6 +211,38 @@ class ParameterBindingJsonReaderUnitTests {
assertThat(target).isEqualTo(Document.parse("{ 'end_date' : { $gte : { $date : " + time + " } } } "));
}
@Test // GH-3750
public void shouldParseISODate() {
String json = "{ 'value' : ISODate(\"1970-01-01T00:00:00Z\") }";
Date value = parse(json).get("value", Date.class);
assertThat(value.getTime()).isZero();
}
@Test // GH-3750
public void shouldParseISODateWith24HourTimeSpecification() {
String json = "{ 'value' : ISODate(\"2013-10-04T12:07:30.443Z\") }";
Date value = parse(json).get("value", Date.class);
assertThat(value.getTime()).isEqualTo(1380888450443L);
}
@Test // GH-3750
public void shouldParse$date() {
String json = "{ 'value' : { \"$date\" : \"2015-04-16T14:55:57.626Z\" } }";
Date value = parse(json).get("value", Date.class);
assertThat(value.getTime()).isEqualTo(1429196157626L);
}
@Test // GH-3750
public void shouldParse$dateWithTimeOffset() {
String json = "{ 'value' :{ \"$date\" : \"2015-04-16T16:55:57.626+02:00\" } }";
Date value = parse(json).get("value", Date.class);
assertThat(value.getTime()).isEqualTo(1429196157626L);
}
@Test // DATAMONGO-2418
void shouldNotAccessSpElEvaluationContextWhenNoSpElPresentInBindableTarget() {
@@ -486,7 +518,6 @@ class ParameterBindingJsonReaderUnitTests {
assertThat(target).isEqualTo(new Document("parent", null));
}
@Test // GH-4089
void retainsSpelArgumentTypeViaArgumentIndex() {