diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java
index 30735069f..98f08d66e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java
@@ -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 MongoDB
* Inc. licensed under the Apache License, Version 2.0.
* 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);
- }
-
- private interface FormatterImpl {
- 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();
- }
- }
+ return ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateTime), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME);
}
- 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() {
- @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() {
}
-
- private DateTimeFormatter() {}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java
index b8258529c..c7c3c6076 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java
@@ -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";
- }
-
- 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();
- }
+ try {
+ return DateTimeFormatter.parse(dateTimeString);
+ } catch (DateTimeParseException e) {
+ throw new JsonParseException("Failed to parse string as a date: " + dateTimeString, e);
}
- 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);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java
index cf2dceed7..8519f4f76 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java
@@ -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() {