From 13667036f7a30c5e7aca796ef6e2a3b0926c679a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sevket=20G=C3=B6kay?= Date: Sun, 3 Dec 2023 12:28:01 +0100 Subject: [PATCH] change DateTimeFormatter (closes #13) --- pom.xml | 12 +++ .../idsg/ocpp/jaxb/JodaDateTimeConverter.java | 63 +++++++++++- .../ocpp/jaxb/JodaDateTimeConverterTest.java | 97 +++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 src/test/java/de/rwth/idsg/ocpp/jaxb/JodaDateTimeConverterTest.java diff --git a/pom.xml b/pom.xml index 1f6f2b8..f9e4046 100644 --- a/pom.xml +++ b/pom.xml @@ -207,6 +207,18 @@ validation-api 2.0.1.Final + + org.junit.jupiter + junit-jupiter-engine + 5.10.1 + test + + + org.junit.jupiter + junit-jupiter-params + 5.10.1 + test + diff --git a/src/main/java/de/rwth/idsg/ocpp/jaxb/JodaDateTimeConverter.java b/src/main/java/de/rwth/idsg/ocpp/jaxb/JodaDateTimeConverter.java index d06c9ca..e43cc89 100644 --- a/src/main/java/de/rwth/idsg/ocpp/jaxb/JodaDateTimeConverter.java +++ b/src/main/java/de/rwth/idsg/ocpp/jaxb/JodaDateTimeConverter.java @@ -1,9 +1,13 @@ package de.rwth.idsg.ocpp.jaxb; import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.DateTimeFormatterBuilder; import javax.xml.bind.annotation.adapters.XmlAdapter; +import static org.joda.time.format.ISODateTimeFormat.date; + /** * Joda-Time and XSD represent data and time information according to ISO 8601. * @@ -12,12 +16,14 @@ */ public class JodaDateTimeConverter extends XmlAdapter { + private static final DateTimeFormatter formatter = dateTimeParser(); + @Override public DateTime unmarshal(String v) throws Exception { if (isNullOrEmpty(v)) { return null; } else { - return new DateTime(v); + return DateTime.parse(v, formatter); } } @@ -36,4 +42,59 @@ public String marshal(DateTime v) throws Exception { private static boolean isNullOrEmpty(String string) { return string == null || string.isEmpty(); } + + /** + * A custom DateTimeFormatter that follows the strictness and flexibility of XSD:dateTime (ISO 8601). + * This exact composition (with optional fields) is not present under {@link org.joda.time.format.ISODateTimeFormat}. + */ + private static DateTimeFormatter dateTimeParser() { + return new DateTimeFormatterBuilder() + .append(date()) + .appendLiteral('T') + .append(hourElement()) + .append(minuteElement()) + .append(secondElement()) + .appendOptional(fractionElement().getParser()) + .appendOptional(offsetElement().getParser()) + .toFormatter(); + } + + // ------------------------------------------------------------------------- + // Copy-paste from "private" methods in ISODateTimeFormat + // ------------------------------------------------------------------------- + + private static DateTimeFormatter hourElement() { + return new DateTimeFormatterBuilder() + .appendHourOfDay(2) + .toFormatter(); + } + + private static DateTimeFormatter minuteElement() { + return new DateTimeFormatterBuilder() + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter(); + } + + private static DateTimeFormatter secondElement() { + return new DateTimeFormatterBuilder() + .appendLiteral(':') + .appendSecondOfMinute(2) + .toFormatter(); + } + + private static DateTimeFormatter fractionElement() { + return new DateTimeFormatterBuilder() + .appendLiteral('.') + // Support parsing up to nanosecond precision even though + // those extra digits will be dropped. + .appendFractionOfSecond(3, 9) + .toFormatter(); + } + + private static DateTimeFormatter offsetElement() { + return new DateTimeFormatterBuilder() + .appendTimeZoneOffset("Z", true, 2, 4) + .toFormatter(); + } } diff --git a/src/test/java/de/rwth/idsg/ocpp/jaxb/JodaDateTimeConverterTest.java b/src/test/java/de/rwth/idsg/ocpp/jaxb/JodaDateTimeConverterTest.java new file mode 100644 index 0000000..6e95ac2 --- /dev/null +++ b/src/test/java/de/rwth/idsg/ocpp/jaxb/JodaDateTimeConverterTest.java @@ -0,0 +1,97 @@ +package de.rwth.idsg.ocpp.jaxb; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.TimeZone; +import java.util.stream.Stream; + +public class JodaDateTimeConverterTest { + + private final JodaDateTimeConverter converter = new JodaDateTimeConverter(); + + static { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + DateTimeZone.setDefault(DateTimeZone.forID("UTC")); + } + + // ------------------------------------------------------------------------- + // Marshal + // ------------------------------------------------------------------------- + + @Test + public void testMarshallNullInput() throws Exception { + String val = converter.marshal(null); + Assertions.assertNull(val); + } + + @ParameterizedTest + @MethodSource("provideValidInput") + public void testMarshallEmptyInput(String val, String expected) throws Exception { + DateTime input = converter.unmarshal(val); + String output = converter.marshal(input); + Assertions.assertEquals(expected, output); + } + + // ------------------------------------------------------------------------- + // Unmarshal + // ------------------------------------------------------------------------- + + @Test + public void testUnmarshallNullInput() throws Exception { + converter.unmarshal(null); + } + + @Test + public void testUnmarshallEmptyInput() throws Exception { + converter.unmarshal(""); + } + + @ParameterizedTest + @MethodSource("provideValidInput") + public void testUnmarshalValid(String val) throws Exception { + converter.unmarshal(val); + } + + @ParameterizedTest + @MethodSource("provideInvalidInput") + public void testUnmarshalInvalid(String val) { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + converter.unmarshal(val); + }); + } + + /** + * First argument is used for marshaling only. + * Both arguments are used for unmarshaling: We use the second as the expected output of formatting. + */ + private static Stream provideValidInput() { + return Stream.of( + Arguments.of("2022-06-30T01:20:52", "2022-06-30T01:20:52.000Z"), + Arguments.of("2022-06-30T01:20:52+02:00", "2022-06-29T23:20:52.000Z"), + Arguments.of("2022-06-30T01:20:52Z", "2022-06-30T01:20:52.000Z"), + Arguments.of("2022-06-30T01:20:52+00:00", "2022-06-30T01:20:52.000Z"), + Arguments.of("2022-06-30T01:20:52.126", "2022-06-30T01:20:52.126Z"), + Arguments.of("2022-06-30T01:20:52.126+05:00", "2022-06-29T20:20:52.126Z"), + Arguments.of("2018-11-13T20:20:39+00:00", "2018-11-13T20:20:39.000Z"), + Arguments.of("-2022-06-30T01:20:52", "-2022-06-30T01:20:52.000Z") + ); + } + + private static Stream provideInvalidInput() { + return Stream.of( + Arguments.of("-1"), + Arguments.of("10000"), // https://github.com/steve-community/steve/issues/1292 + Arguments.of("text"), + Arguments.of("2022-06-30"), // no time + Arguments.of("2022-06-30T01:20"), // seconds are required + Arguments.of("2022-06-30T25:20:34"), // hour out of range + Arguments.of("22-06-30T25:20:34") // year not YYYY-format + ); + } +}