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 static java.time.format.DateTimeFormatter.*;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
import java.time.ZonedDateTime;
|
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
|
* "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 />
|
* Inc.</a> licensed under the Apache License, Version 2.0. <br />
|
||||||
* Formatted and modified.
|
* Formatted and modified.
|
||||||
@@ -40,133 +35,22 @@ import java.util.TimeZone;
|
|||||||
*/
|
*/
|
||||||
class DateTimeFormatter {
|
class DateTimeFormatter {
|
||||||
|
|
||||||
private static final FormatterImpl FORMATTER_IMPL;
|
private static final int DATE_STRING_LENGTH = "1970-01-01".length();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static long parse(final String dateTimeString) {
|
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) {
|
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 {
|
private DateTimeFormatter() {
|
||||||
long parse(String dateTimeString);
|
|
||||||
|
|
||||||
String format(long dateTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.DateFormat;
|
||||||
import java.text.ParsePosition;
|
import java.text.ParsePosition;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -27,7 +29,6 @@ import java.util.HashMap;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -42,7 +43,6 @@ import org.springframework.data.spel.EvaluationContextProvider;
|
|||||||
import org.springframework.expression.EvaluationContext;
|
import org.springframework.expression.EvaluationContext;
|
||||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Base64Utils;
|
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.NumberUtils;
|
import org.springframework.util.NumberUtils;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
@@ -957,7 +957,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
|
|||||||
}
|
}
|
||||||
verifyToken(JsonTokenType.RIGHT_PAREN);
|
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);
|
return new BsonBinary(subTypeToken.getValue(Integer.class).byteValue(), bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1080,28 +1080,14 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
verifyToken(JsonTokenType.RIGHT_PAREN);
|
verifyToken(JsonTokenType.RIGHT_PAREN);
|
||||||
String[] patterns = { "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm:ssz", "yyyy-MM-dd'T'HH:mm:ss.SSSz" };
|
|
||||||
|
|
||||||
SimpleDateFormat format = new SimpleDateFormat(patterns[0], Locale.ENGLISH);
|
String dateTimeString = token.getValue(String.class);
|
||||||
ParsePosition pos = new ParsePosition(0);
|
|
||||||
String s = token.getValue(String.class);
|
|
||||||
|
|
||||||
if (s.endsWith("Z")) {
|
try {
|
||||||
s = s.substring(0, s.length() - 1) + "GMT-00:00";
|
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() {
|
private BsonBinary visitHexDataConstructor() {
|
||||||
@@ -1219,7 +1205,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
|
|||||||
byte type;
|
byte type;
|
||||||
if (firstNestedKey.equals("base64")) {
|
if (firstNestedKey.equals("base64")) {
|
||||||
verifyToken(JsonTokenType.COLON);
|
verifyToken(JsonTokenType.COLON);
|
||||||
data = Base64Utils.decodeFromString(readStringFromExtendedJson());
|
data = Base64.getDecoder().decode(readStringFromExtendedJson());
|
||||||
verifyToken(JsonTokenType.COMMA);
|
verifyToken(JsonTokenType.COMMA);
|
||||||
verifyString("subType");
|
verifyString("subType");
|
||||||
verifyToken(JsonTokenType.COLON);
|
verifyToken(JsonTokenType.COLON);
|
||||||
@@ -1230,7 +1216,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
|
|||||||
verifyToken(JsonTokenType.COMMA);
|
verifyToken(JsonTokenType.COMMA);
|
||||||
verifyString("base64");
|
verifyString("base64");
|
||||||
verifyToken(JsonTokenType.COLON);
|
verifyToken(JsonTokenType.COLON);
|
||||||
data = Base64Utils.decodeFromString(readStringFromExtendedJson());
|
data = Base64.getDecoder().decode(readStringFromExtendedJson());
|
||||||
} else {
|
} else {
|
||||||
throw new JsonParseException("Unexpected key for $binary: " + firstNestedKey);
|
throw new JsonParseException("Unexpected key for $binary: " + firstNestedKey);
|
||||||
}
|
}
|
||||||
@@ -1258,7 +1244,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
|
|||||||
byte type;
|
byte type;
|
||||||
|
|
||||||
if (firstKey.equals("$binary")) {
|
if (firstKey.equals("$binary")) {
|
||||||
data = Base64Utils.decodeFromString(readStringFromExtendedJson());
|
data = Base64.getDecoder().decode(readStringFromExtendedJson());
|
||||||
verifyToken(JsonTokenType.COMMA);
|
verifyToken(JsonTokenType.COMMA);
|
||||||
verifyString("$type");
|
verifyString("$type");
|
||||||
verifyToken(JsonTokenType.COLON);
|
verifyToken(JsonTokenType.COLON);
|
||||||
@@ -1268,7 +1254,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
|
|||||||
verifyToken(JsonTokenType.COMMA);
|
verifyToken(JsonTokenType.COMMA);
|
||||||
verifyString("$binary");
|
verifyString("$binary");
|
||||||
verifyToken(JsonTokenType.COLON);
|
verifyToken(JsonTokenType.COLON);
|
||||||
data = Base64Utils.decodeFromString(readStringFromExtendedJson());
|
data = Base64.getDecoder().decode(readStringFromExtendedJson());
|
||||||
}
|
}
|
||||||
verifyToken(JsonTokenType.END_OBJECT);
|
verifyToken(JsonTokenType.END_OBJECT);
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,38 @@ class ParameterBindingJsonReaderUnitTests {
|
|||||||
assertThat(target).isEqualTo(Document.parse("{ 'end_date' : { $gte : { $date : " + time + " } } } "));
|
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
|
@Test // DATAMONGO-2418
|
||||||
void shouldNotAccessSpElEvaluationContextWhenNoSpElPresentInBindableTarget() {
|
void shouldNotAccessSpElEvaluationContextWhenNoSpElPresentInBindableTarget() {
|
||||||
|
|
||||||
@@ -486,7 +518,6 @@ class ParameterBindingJsonReaderUnitTests {
|
|||||||
assertThat(target).isEqualTo(new Document("parent", null));
|
assertThat(target).isEqualTo(new Document("parent", null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test // GH-4089
|
@Test // GH-4089
|
||||||
void retainsSpelArgumentTypeViaArgumentIndex() {
|
void retainsSpelArgumentTypeViaArgumentIndex() {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user