package org.opendaylight.netconf.notifications;
import com.google.common.base.Preconditions;
-import java.text.SimpleDateFormat;
+import java.text.ParsePosition;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
import java.util.Date;
+import java.util.function.Function;
import org.opendaylight.netconf.api.NetconfMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
*/
public final class NetconfNotification extends NetconfMessage {
+ private static final Logger LOG = LoggerFactory.getLogger(NetconfNotification.class);
+
public static final String NOTIFICATION = "notification";
public static final String NOTIFICATION_NAMESPACE = "urn:ietf:params:netconf:capability:notification:1.0";
- public static final String RFC3339_DATE_FORMAT_BLUEPRINT = "yyyy-MM-dd'T'HH:mm:ssXXX";
- // The format with milliseconds is a bit fragile, it cannot be used for timestamps without millis (thats why its a separate format)
- // + it might not work properly with more than 6 digits
- // TODO try to find a better solution with Java8
- public static final String RFC3339_DATE_FORMAT_WITH_MILLIS_BLUEPRINT = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX";
+
+ /**
+ * The ISO-like date-time formatter that formats or parses a date-time with
+ * the offset and zone if available, such as '2011-12-03T10:15:30',
+ * '2011-12-03T10:15:30+01:00' or '2011-12-03T10:15:30+01:00[Europe/Paris]'.
+ */
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
+
+ /**
+ * Provide a {@link String} representation of a {@link Date} object,
+ * using the time-zone offset for UTC, {@code ZoneOffset.UTC}.
+ */
+ public static final Function<Date, String> RFC3339_DATE_FORMATTER = date ->
+ DATE_TIME_FORMATTER.format(date.toInstant().atOffset(ZoneOffset.UTC));
+
+ /**
+ * Parse a {@link String} object into a {@link Date} using the time-zone
+ * offset for UTC, {@code ZoneOffset.UTC}, and the system default time-zone,
+ * {@code ZoneId.systemDefault()}.
+ * <p>
+ * While parsing, if an exception occurs, we try to handle it as if it is due
+ * to a leap second. If that's the case, a simple conversion is applied, replacing
+ * the second-of-minute of 60 with 59.
+ * If that's not the case, we propagate the {@link DateTimeParseException} to the
+ * caller.
+ * </p>
+ */
+ public static final Function<String, Date> RFC3339_DATE_PARSER = time -> {
+ try {
+ final ZonedDateTime localDateTime = ZonedDateTime.parse(time, DATE_TIME_FORMATTER);
+ final int startAt = 0;
+ final TemporalAccessor parsed = DATE_TIME_FORMATTER.parse(time, new ParsePosition(startAt));
+ final int nanoOfSecond = getFieldFromTemporalAccessor(parsed, ChronoField.NANO_OF_SECOND);
+ final long reminder = nanoOfSecond % 1000000;
+
+ // Log warn in case we rounded the fraction of a second. We need to create a string from the
+ // value that was cut. Example -> 1.123750 -> Value that was cut 75
+ if (reminder != 0) {
+ final StringBuilder reminderBuilder = new StringBuilder(String.valueOf(reminder));
+
+ //add zeros in case we have number like 123056 to make sure 056 is displayed
+ while (reminderBuilder.length() < 6) {
+ reminderBuilder.insert(0, '0');
+ }
+
+ //delete zeros from end to make sure that number like 1.123750 will show value cut 75.
+ while (reminderBuilder.charAt(reminderBuilder.length() - 1) == '0') {
+ reminderBuilder.deleteCharAt(reminderBuilder.length() - 1);
+ }
+ LOG.warn("Fraction of second is cut to three digits. Value that was cut {}", reminderBuilder.toString());
+ }
+
+ return Date.from(Instant.from(localDateTime));
+ } catch (DateTimeParseException exception) {
+ Date res = handlePotentialLeapSecond(time);
+ if (res != null) {
+ return res;
+ }
+ throw exception;
+ }
+ };
+
+ /**
+ * Check whether the input {@link String} is representing a time compliant with the ISO
+ * format and having a leap second; e.g. formatted as 23:59:60. If that's the case, a simple
+ * conversion is applied, replacing the second-of-minute of 60 with 59.
+ *
+ * @param time {@link String} representation of a time
+ * @return {@code null} if time isn't ISO compliant or if the time doesn't have a leap second
+ * else a {@link Date} as per as the RFC3339_DATE_PARSER.
+ */
+ private static Date handlePotentialLeapSecond(final String time) {
+ // Parse the string from offset 0, so we get the whole value.
+ final int offset = 0;
+ final TemporalAccessor parsed = DATE_TIME_FORMATTER.parseUnresolved(time, new ParsePosition(offset));
+ // Bail fast
+ if (parsed == null) {
+ return null;
+ }
+
+ int secondOfMinute = getFieldFromTemporalAccessor(parsed, ChronoField.SECOND_OF_MINUTE);
+ final int hourOfDay = getFieldFromTemporalAccessor(parsed, ChronoField.HOUR_OF_DAY);
+ final int minuteOfHour = getFieldFromTemporalAccessor(parsed, ChronoField.MINUTE_OF_HOUR);
+
+ // Check whether the input time has leap second. As the leap second can only
+ // occur at 23:59:60, we can be very strict, and don't interpret an incorrect
+ // value as leap second.
+ if (secondOfMinute != 60 || minuteOfHour != 59 || hourOfDay != 23) {
+ return null;
+ }
+
+ LOG.trace("Received time contains leap second, adjusting by replacing the second-of-minute of 60 with 59 {}", time);
+
+ // Applying simple conversion replacing the second-of-minute of 60 with 59.
+
+ secondOfMinute = 59;
+
+ final int year = getFieldFromTemporalAccessor(parsed, ChronoField.YEAR);
+ final int monthOfYear = getFieldFromTemporalAccessor(parsed, ChronoField.MONTH_OF_YEAR);
+ final int dayOfMonth = getFieldFromTemporalAccessor(parsed, ChronoField.DAY_OF_MONTH);
+ final int nanoOfSecond = getFieldFromTemporalAccessor(parsed, ChronoField.NANO_OF_SECOND);
+ final int offsetSeconds = getFieldFromTemporalAccessor(parsed, ChronoField.OFFSET_SECONDS);
+
+ final LocalDateTime currentTime = LocalDateTime.of(year, monthOfYear, dayOfMonth, hourOfDay, minuteOfHour,
+ secondOfMinute, nanoOfSecond);
+ final OffsetDateTime dateTimeWithZoneOffset = currentTime.atOffset(ZoneOffset.ofTotalSeconds(offsetSeconds));
+
+ return RFC3339_DATE_PARSER.apply(dateTimeWithZoneOffset.toString());
+ }
+
+ /**
+ * @param accessor The {@link TemporalAccessor}
+ * @param field The {@link ChronoField} to get
+ * @return the value associated with the {@link ChronoField} for the given {@link TemporalAccessor} if present,
+ * else 0.
+ */
+ private static int getFieldFromTemporalAccessor(final TemporalAccessor accessor, final ChronoField field) {
+ return accessor.isSupported(field) ? (int) accessor.getLong(field) : 0;
+ }
+
public static final String EVENT_TIME = "eventTime";
/**
}
private static String getSerializedEventTime(final Date eventTime) {
- // SimpleDateFormat is not threadsafe, cannot be in a constant
- return new SimpleDateFormat(RFC3339_DATE_FORMAT_BLUEPRINT).format(eventTime);
+ return RFC3339_DATE_FORMATTER.apply(eventTime);
}
}
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import java.text.SimpleDateFormat;
import java.util.Date;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
final Element eventTimeElement = (Element) childNodes.item(0);
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
- assertEquals(eventTime.getTime(), sdf.parse(eventTimeElement.getTextContent()).getTime());
+ assertEquals(eventTime.getTime(), NetconfNotification.RFC3339_DATE_PARSER
+ .apply(eventTimeElement.getTextContent()).getTime());
assertEquals(eventTime, netconfNotification.getEventTime());
package org.opendaylight.netconf.notifications.impl;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.collect.Lists;
-import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
public class NetconfNotificationManagerTest {
+ public static final String RFC3339_DATE_FORMAT_WITH_MILLIS_BLUEPRINT = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
@Mock
private NetconfNotificationRegistry notificationRegistry;
MockitoAnnotations.initMocks(this);
}
+ @Test
+ public void testEventTime() throws Exception {
+ //Testing values with SimpleDateFormat
+ final ArrayList<String> checkWith = Lists.newArrayList(
+ "2001-07-04T12:08:56.235-07:00",
+ "2015-10-23T09:42:27.671+00:00",
+ "1970-01-01T17:17:22.229+00:00",
+ "1937-01-01T12:00:27.870+00:20",
+ "2015-06-30T23:59:59.000+00:00",
+ "1996-12-19T16:39:57.000-08:00",
+ "2015-10-23T09:42:27.000+00:00",
+ "2015-10-23T09:42:27.200+00:00",
+ "1985-04-12T23:20:50.520+00:00",
+ // Values with leap second
+ "2001-07-04T23:59:59.235-07:00",
+ "1990-12-31T23:59:59.000-08:00",
+ "2015-10-23T23:59:59.671+00:00",
+ "1970-01-01T23:59:59.229+00:00",
+ "1937-01-01T23:59:59.870+00:20",
+ "1990-12-31T23:59:59.000+00:00",
+ "2015-10-23T23:59:59.200+00:00",
+ "1985-04-12T23:59:59.520+00:00");
+ final Iterator<String> iterator = checkWith.iterator();
+
+ // Testing correct values
+ for (final String time : Lists.newArrayList(
+ "2001-07-04T12:08:56.235-07:00",
+ "2015-10-23T09:42:27.67175+00:00",
+ "1970-01-01T17:17:22.229568+00:00",
+ "1937-01-01T12:00:27.87+00:20",
+ "2015-06-30T23:59:59Z",
+ "1996-12-19T16:39:57-08:00",
+ "2015-10-23T09:42:27Z",
+ "2015-10-23T09:42:27.200001Z",
+ "1985-04-12T23:20:50.52Z",
+ // Values with leap second
+ "2001-07-04T23:59:60.235-07:00",
+ "1990-12-31T23:59:60-08:00",
+ "2015-10-23T23:59:60.67175+00:00",
+ "1970-01-01T23:59:60.229568+00:00",
+ "1937-01-01T23:59:60.87+00:20",
+ "1990-12-31T23:59:60Z",
+ "2015-10-23T23:59:60.20001Z",
+ "1985-04-12T23:59:60.52Z"
+ )) {
+ try {
+ final Date apply = NetconfNotification.RFC3339_DATE_PARSER.apply(time);
+ final Date parse = new SimpleDateFormat(RFC3339_DATE_FORMAT_WITH_MILLIS_BLUEPRINT).parse(iterator.next());
+ assertEquals(parse.getTime(), apply.getTime());
+ // Testing that we're consistent from formatting to parsing.
+ final String dateString = NetconfNotification.RFC3339_DATE_FORMATTER.apply(apply);
+ final Date date1 = NetconfNotification.RFC3339_DATE_PARSER.apply(dateString);
+ final String dateString1 = NetconfNotification.RFC3339_DATE_FORMATTER.apply(date1);
+ Assert.assertEquals(apply, date1);
+ Assert.assertEquals(dateString, dateString1);
+ } catch (final DateTimeParseException e) {
+ fail("Failed to parse time value = " + time + " " + e);
+ throw e;
+ }
+ }
- @Test public void testEventTime() throws Exception {
- final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
- NetconfNotification.RFC3339_DATE_FORMAT_BLUEPRINT);
- final SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat(
- NetconfNotification.RFC3339_DATE_FORMAT_WITH_MILLIS_BLUEPRINT);
-
- for (String time : Lists.newArrayList(
- "2001-07-04T12:08:56.235-07:00",
- "2015-10-23T09:42:27.67175+00:00",
- "1970-01-01T17:17:22.229568+00:00",
- "1937-01-01T12:00:27.87+00:20",
- "1990-12-31T15:59:60-08:00",
- "1990-12-31T23:59:60Z",
- "1996-12-19T16:39:57-08:00"
-// ,"1985-04-12T23:20:50.52Z"
+ // Testing that we're consistent from formatting to parsing.
+ final Date date0 = Date.from(Instant.ofEpochMilli(0));
+ final String dateString0 = NetconfNotification.RFC3339_DATE_FORMATTER.apply(date0);
+ final Date date1 = NetconfNotification.RFC3339_DATE_PARSER.apply(dateString0);
+ final String dateString1 = NetconfNotification.RFC3339_DATE_FORMATTER.apply(date1);
+ Assert.assertEquals(date0, date1);
+ Assert.assertEquals(dateString0, dateString1);
+
+ // Testing wrong values
+ for (final String time : Lists.newArrayList(
+ "0",
+ "205-10-23T09:42:27.67175+00:00",
+ "1970-01-01T17:60:22.229568+00:00",
+ "1937-01-01T32:00:27.87+00:20",
+ "2060-13-31T15:59:90-08:00",
+ "1990-12-31T23:58:60Z"
)) {
try {
- simpleDateFormat.parse(time);
- } catch (ParseException e) {
- simpleDateFormat2.parse(time);
+ NetconfNotification.RFC3339_DATE_PARSER.apply(time);
+ } catch (final DateTimeParseException e) {
+ continue;
}
+ fail("Should have thrown an exception; value= " + time);
}
}
import com.google.common.collect.Lists;
import java.io.IOException;
-import java.text.SimpleDateFormat;
import java.util.Date;
import org.custommonkey.xmlunit.DetailedDiff;
import org.custommonkey.xmlunit.Diff;
"<added-capability>uri1</added-capability>" +
"</netconf-capability-change>";
- private static final String expectedNotification = "<notification xmlns=\"urn:ietf:params:netconf:capability:notification:1.0\">" +
- innerNotification +
- "<eventTime>" + new SimpleDateFormat(NetconfNotification.RFC3339_DATE_FORMAT_BLUEPRINT).format(DATE) + "</eventTime>" +
+ private static final String expectedNotification =
+ "<notification xmlns=\"urn:ietf:params:netconf:capability:notification:1.0\">"
+ + innerNotification
+ + "<eventTime>"
+ + NetconfNotification.RFC3339_DATE_FORMATTER.apply(DATE)
+ + "</eventTime>" +
"</notification>";
@Test
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.net.URI;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
+import java.time.format.DateTimeParseException;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Collections;
return SchemaPath.create(true, rpc);
}
- private static final ThreadLocal<SimpleDateFormat> EVENT_TIME_FORMAT = new ThreadLocal<SimpleDateFormat>() {
- @Override
- protected SimpleDateFormat initialValue() {
-
- final SimpleDateFormat withMillis = new SimpleDateFormat(
- NetconfNotification.RFC3339_DATE_FORMAT_WITH_MILLIS_BLUEPRINT);
-
- return new SimpleDateFormat(NetconfNotification.RFC3339_DATE_FORMAT_BLUEPRINT) {
- private static final long serialVersionUID = 1L;
-
- @Override public Date parse(final String source) throws ParseException {
- try {
- return super.parse(source);
- } catch (final ParseException e) {
- // In case of failure, try to parse with milliseconds
- return withMillis.parse(source);
- }
- }
- };
- }
-
- @Override
- public void set(final SimpleDateFormat value) {
- throw new UnsupportedOperationException();
- }
- };
-
public static Map.Entry<Date, XmlElement> stripNotification(final NetconfMessage message) {
final XmlElement xmlElement = XmlElement.fromDomDocument(message.getDocument());
final List<XmlElement> childElements = xmlElement.getChildElements();
}
try {
- return new AbstractMap.SimpleEntry<>(EVENT_TIME_FORMAT.get().parse(eventTimeElement.getTextContent()), notificationElement);
+ return new AbstractMap.SimpleEntry<>(
+ NetconfNotification.RFC3339_DATE_PARSER.apply(eventTimeElement.getTextContent()),
+ notificationElement);
} catch (final DocumentedException e) {
throw new IllegalArgumentException("Notification payload does not contain " + EVENT_TIME + " " + message);
- } catch (final ParseException e) {
+ } catch (final DateTimeParseException e) {
LOG.warn("Unable to parse event time from {}. Setting time to {}", eventTimeElement, NetconfNotification.UNKNOWN_EVENT_TIME, e);
return new AbstractMap.SimpleEntry<>(NetconfNotification.UNKNOWN_EVENT_TIME, notificationElement);
}
import com.google.common.collect.Iterables;
import java.io.InputStream;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
assertNotNull(root);
assertEquals(6, Iterables.size(root.getValue()));
assertEquals("user-visited-page", root.getNodeType().getLocalName());
- assertEquals(new SimpleDateFormat(NetconfNotification.RFC3339_DATE_FORMAT_WITH_MILLIS_BLUEPRINT).parse("2015-10-23T09:42:27.67175+00:00"),
+ assertEquals(NetconfNotification.RFC3339_DATE_PARSER.apply("2015-10-23T09:42:27.67175+00:00"),
((DOMEvent) domNotification).getEventTime());
}
}