committed by
Mark Paluch
parent
d54f2e47b0
commit
79c6427cc9
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user