Merge "Bug 6645 - 2 digits milliseconds can not be parsed in notification eventTime"
authorJakub Morvay <jmorvay@cisco.com>
Tue, 7 Mar 2017 18:17:48 +0000 (18:17 +0000)
committerGerrit Code Review <gerrit@opendaylight.org>
Tue, 7 Mar 2017 18:17:48 +0000 (18:17 +0000)
netconf/netconf-notifications-api/src/main/java/org/opendaylight/netconf/notifications/NetconfNotification.java
netconf/netconf-notifications-api/src/test/java/org/opendaylight/netconf/notifications/NetconfNotificationTest.java
netconf/netconf-notifications-impl/src/test/java/org/opendaylight/netconf/notifications/impl/NetconfNotificationManagerTest.java
netconf/netconf-notifications-impl/src/test/java/org/opendaylight/netconf/notifications/impl/ops/NotificationsTransformUtilTest.java
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/NetconfToNotificationTest.java

index 852682a15e2abc7024cc10b869f0b017ce39d961..67b4069ac8b27fa7aee6000688fb507445eac0e0 100644 (file)
@@ -9,9 +9,21 @@
 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;
 
@@ -20,13 +32,130 @@ 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";
 
     /**
@@ -75,7 +204,6 @@ public final class NetconfNotification extends NetconfMessage {
     }
 
     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);
     }
 }
index bb3da161979f924e2decd9e32ada694bae91a6cf..bc2f0a10876930342c819cea0c3308f607a26c26 100644 (file)
@@ -11,7 +11,6 @@ package org.opendaylight.netconf.notifications;
 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;
@@ -54,8 +53,8 @@ public class NetconfNotificationTest {
 
         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());
 
index c838506ff50c0a89e4f96a203d4100800afb136c..98c5e6bc5e77b02533f73b945ea711d397816b04 100644 (file)
@@ -8,6 +8,7 @@
 
 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;
@@ -16,8 +17,13 @@ import static org.mockito.Mockito.verify;
 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;
@@ -35,6 +41,7 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.not
 
 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;
 
@@ -43,28 +50,90 @@ public class NetconfNotificationManagerTest {
         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);
         }
     }
 
index cb5e9d544118033dc7f849b738303eb43988700d..84d77ef16381040e6ba933bbfdf1a36cbc092902 100644 (file)
@@ -12,7 +12,6 @@ import static org.junit.Assert.assertTrue;
 
 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;
@@ -36,9 +35,12 @@ public class NotificationsTransformUtilTest {
             "<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
index 36aa41a8b1b5ed54263f555272eff7ec5ed2b1c4..2b1476f8220394f953d25f76814e09f858ce48d6 100644 (file)
@@ -13,8 +13,7 @@ import com.google.common.collect.ImmutableList;
 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;
@@ -316,33 +315,6 @@ public class NetconfMessageTransformUtil {
         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();
@@ -364,10 +336,12 @@ public class NetconfMessageTransformUtil {
         }
 
         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);
         }
index 732f86e691001291ad80d10d1a7f1cf3db90bf53..da1497b101adcec6112ad956e59243782d5f3ecb 100644 (file)
@@ -14,7 +14,6 @@ import static org.junit.Assert.assertTrue;
 
 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;
@@ -86,7 +85,7 @@ public class NetconfToNotificationTest {
         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());
     }
 }