From 6012785f9f0f5771598a3d4d9d30016328c58781 Mon Sep 17 00:00:00 2001 From: Tom Markuske Date: Thu, 13 Feb 2025 12:29:16 +0100 Subject: [PATCH] Ensure millis precision in timestamps of outgoing messages ISO8601 does not specify that fractional seconds shall be omitted if given. Enforce that an on full seconds, the millis resolution is retained for outgoing messages and that incoming messages of any precision can be parsed. On incoming messages with... * ...seconds precision, assume that millis are zero. * ...millis precision, assume that micros are zero. * ...micros precision, assume that nanos are zero. This retains proper timestamp comparison. --- .../eu/chargetime/ocpp/JSONCommunicator.java | 14 ++++- .../ocpp/test/JSONCommunicatorTest.java | 57 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/OCPP-J/src/main/java/eu/chargetime/ocpp/JSONCommunicator.java b/OCPP-J/src/main/java/eu/chargetime/ocpp/JSONCommunicator.java index 15e937d0..01f3147f 100644 --- a/OCPP-J/src/main/java/eu/chargetime/ocpp/JSONCommunicator.java +++ b/OCPP-J/src/main/java/eu/chargetime/ocpp/JSONCommunicator.java @@ -9,6 +9,8 @@ import java.lang.reflect.Type; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.ResolverStyle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +48,16 @@ public class JSONCommunicator extends Communicator { private static final Logger logger = LoggerFactory.getLogger(JSONCommunicator.class); + /** + * a derivation of ISO_INSTANT that always serializes to three millisecond digits, even if zero + */ + public static final DateTimeFormatter ISO_INSTANT_WITH_MILLIS_PRECISION = + new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendInstant(3) + .toFormatter() + .withResolverStyle(ResolverStyle.STRICT); + private static final int INDEX_MESSAGEID = 0; private static final int TYPENUMBER_CALL = 2; private static final int INDEX_CALL_ACTION = 2; @@ -94,7 +106,7 @@ private static class ZonedDateTimeSerializer @Override public JsonElement serialize( ZonedDateTime zonedDateTime, Type type, JsonSerializationContext jsonSerializationContext) { - return new JsonPrimitive(zonedDateTime.format(DateTimeFormatter.ISO_INSTANT)); + return new JsonPrimitive(zonedDateTime.format(ISO_INSTANT_WITH_MILLIS_PRECISION)); } @Override diff --git a/ocpp-v1_6/src/test/java/eu/chargetime/ocpp/test/JSONCommunicatorTest.java b/ocpp-v1_6/src/test/java/eu/chargetime/ocpp/test/JSONCommunicatorTest.java index 0436cf22..da2b4b70 100644 --- a/ocpp-v1_6/src/test/java/eu/chargetime/ocpp/test/JSONCommunicatorTest.java +++ b/ocpp-v1_6/src/test/java/eu/chargetime/ocpp/test/JSONCommunicatorTest.java @@ -102,6 +102,22 @@ public void unpackPayload_aCalendarPayload_returnsTestModelWithACalendar() throw assertThat(model.getCalendarTest().compareTo(someDate), is(0)); } + @Test + public void unpackPayload_aCalendarPayload_parsesNoFractionalAsZeroFractional() throws Exception { + // Given + String aCalendar = "2016-04-28T07:16:11Z"; + String payload = "{\"calendarTest\":\"%s\"}"; + + ZonedDateTime someDate = ZonedDateTime.parse("2016-04-28T07:16:11.000Z"); + + // When + TestModel model = + communicator.unpackPayload(String.format(payload, aCalendar), TestModel.class); + + // Then + assertThat(model.getCalendarTest().compareTo(someDate), is(0)); + } + @Test public void unpackPayload_anIntegerPayload_returnsTestModelWithAnInteger() throws Exception { // Given @@ -276,6 +292,24 @@ public void pack_bootNotificationRequest_returnsBootNotificationRequestPayload() assertThat(payload, equalTo(expected)); } + @Test + public void pack_bootNotificationConfirmation_limitsTimeFractionalDigitsToThree() + throws Exception { + // Given + String expected = + "{\"currentTime\":\"2016-04-28T06:41:13.123Z\",\"interval\":300,\"status\":\"Accepted\"}"; + BootNotificationConfirmation confirmation = new BootNotificationConfirmation(); + confirmation.setCurrentTime(createDateTimeInNanos(1461825673123456789L)); // will be truncated + confirmation.setInterval(300); + confirmation.setStatus(RegistrationStatus.Accepted); + + // When + Object payload = communicator.packPayload(confirmation); + + // Then + assertThat(payload, equalTo(expected)); + } + @Test public void pack_bootNotificationConfirmation_returnsBootNotificationConfirmationPayload() throws Exception { @@ -294,6 +328,24 @@ public void pack_bootNotificationConfirmation_returnsBootNotificationConfirmatio assertThat(payload, equalTo(expected)); } + @Test + public void pack_bootNotificationConfirmation_alwaysFormatsTimeWithThreeFractionalDigits() + throws Exception { + // Given + String expected = + "{\"currentTime\":\"2016-04-28T06:41:13.000Z\",\"interval\":300,\"status\":\"Accepted\"}"; + BootNotificationConfirmation confirmation = new BootNotificationConfirmation(); + confirmation.setCurrentTime(createDateTimeInMillis(1461825673000L)); // will not be truncated + confirmation.setInterval(300); + confirmation.setStatus(RegistrationStatus.Accepted); + + // When + Object payload = communicator.packPayload(confirmation); + + // Then + assertThat(payload, equalTo(expected)); + } + @Test public void disconnect_disconnects() { // When @@ -319,4 +371,9 @@ public void sendError_transmitsError() throws Exception { private ZonedDateTime createDateTimeInMillis(long dateInMillis) { return Instant.ofEpochMilli(dateInMillis).atOffset(ZoneOffset.UTC).toZonedDateTime(); } + + private ZonedDateTime createDateTimeInNanos(long dateInNanos) { + return Instant.ofEpochSecond(dateInNanos / 1000000000, dateInNanos % 1000000000) + .atOffset(ZoneOffset.UTC).toZonedDateTime(); + } }