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() {