Clean up NotificationMessage
[netconf.git] / protocol / netconf-api / src / main / java / org / opendaylight / netconf / api / messages / NotificationMessage.java
1 /*
2  * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.api.messages;
9
10 import static java.util.Objects.requireNonNull;
11
12 import java.text.ParsePosition;
13 import java.time.Instant;
14 import java.time.LocalDateTime;
15 import java.time.OffsetDateTime;
16 import java.time.ZoneOffset;
17 import java.time.ZonedDateTime;
18 import java.time.format.DateTimeFormatter;
19 import java.time.format.DateTimeParseException;
20 import java.time.temporal.ChronoField;
21 import java.time.temporal.TemporalAccessor;
22 import java.util.function.Function;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.opendaylight.netconf.api.NetconfMessage;
25 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28 import org.w3c.dom.Document;
29 import org.w3c.dom.Element;
30
31 /**
32  * Special kind of netconf message that contains a timestamp.
33  */
34 public final class NotificationMessage extends NetconfMessage {
35     private static final Logger LOG = LoggerFactory.getLogger(NotificationMessage.class);
36     private static final String NOTIFICATION_TAG = "notification";
37     private static final String EVENT_TIME_TAG = "eventTime";
38
39     /**
40      * Used for unknown/un-parse-able event-times.
41      */
42     // FIXME: we should differentiate unknown and invalid event times
43     public static final Instant UNKNOWN_EVENT_TIME = Instant.EPOCH;
44
45     /**
46      * The ISO-like date-time formatter that formats or parses a date-time with
47      * the offset and zone if available, such as '2011-12-03T10:15:30',
48      * '2011-12-03T10:15:30+01:00' or '2011-12-03T10:15:30+01:00[Europe/Paris]'.
49      */
50     private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
51
52     /**
53      * Provide a {@link String} representation of a {@link Instant} object,
54      * using the time-zone offset for UTC, {@code ZoneOffset.UTC}.
55      */
56     public static final Function<Instant, String> RFC3339_DATE_FORMATTER = date ->
57             DATE_TIME_FORMATTER.format(date.atOffset(ZoneOffset.UTC));
58
59     /**
60      * Parse a {@link String} object into a {@link Instant} using the time-zone
61      * offset for UTC, {@code ZoneOffset.UTC}, and the system default time-zone,
62      * {@code ZoneId.systemDefault()}.
63      * <p>
64      *     While parsing, if an exception occurs, we try to handle it as if it is due
65      *     to a leap second. If that's the case, a simple conversion is applied, replacing
66      *     the second-of-minute of 60 with 59.
67      *     If that's not the case, we propagate the {@link DateTimeParseException} to the
68      *     caller.
69      * </p>
70      */
71     public static final Function<String, Instant> RFC3339_DATE_PARSER = time -> {
72         try {
73             final ZonedDateTime localDateTime = ZonedDateTime.parse(time, DATE_TIME_FORMATTER);
74             final int startAt = 0;
75             final TemporalAccessor parsed = DATE_TIME_FORMATTER.parse(time, new ParsePosition(startAt));
76             final int nanoOfSecond = getFieldFromTemporalAccessor(parsed, ChronoField.NANO_OF_SECOND);
77             final long reminder = nanoOfSecond % 1000000;
78
79             // Log warn in case we rounded the fraction of a second. We need to create a string from the
80             // value that was cut. Example -> 1.123750 -> Value that was cut 75
81             if (reminder != 0) {
82                 final StringBuilder reminderBuilder = new StringBuilder(String.valueOf(reminder));
83
84                 //add zeros in case we have number like 123056 to make sure 056 is displayed
85                 while (reminderBuilder.length() < 6) {
86                     reminderBuilder.insert(0, '0');
87                 }
88
89                 //delete zeros from end to make sure that number like 1.123750 will show value cut 75.
90                 while (reminderBuilder.charAt(reminderBuilder.length() - 1) == '0') {
91                     reminderBuilder.deleteCharAt(reminderBuilder.length() - 1);
92                 }
93                 LOG.warn("Fraction of second is cut to three digits. Value that was cut {}",
94                         reminderBuilder.toString());
95             }
96
97             return Instant.from(localDateTime);
98         } catch (DateTimeParseException exception) {
99             final var res = handlePotentialLeapSecond(time);
100             if (res != null) {
101                 return res;
102             }
103             throw exception;
104         }
105     };
106
107     /**
108      * Check whether the input {@link String} is representing a time compliant with the ISO
109      * format and having a leap second; e.g. formatted as 23:59:60. If that's the case, a simple
110      * conversion is applied, replacing the second-of-minute of 60 with 59.
111      *
112      * @param time {@link String} representation of a time
113      * @return {@code null} if time isn't ISO compliant or if the time doesn't have a leap second else an
114      *         {@link Instant} as per as the RFC3339_DATE_PARSER.
115      */
116     private static Instant handlePotentialLeapSecond(final String time) {
117         // Parse the string from offset 0, so we get the whole value.
118         final int offset = 0;
119         final TemporalAccessor parsed = DATE_TIME_FORMATTER.parseUnresolved(time, new ParsePosition(offset));
120         // Bail fast
121         if (parsed == null) {
122             return null;
123         }
124
125         int secondOfMinute = getFieldFromTemporalAccessor(parsed, ChronoField.SECOND_OF_MINUTE);
126         final int hourOfDay = getFieldFromTemporalAccessor(parsed, ChronoField.HOUR_OF_DAY);
127         final int minuteOfHour = getFieldFromTemporalAccessor(parsed, ChronoField.MINUTE_OF_HOUR);
128
129         // Check whether the input time has leap second. As the leap second can only
130         // occur at 23:59:60, we can be very strict, and don't interpret an incorrect
131         // value as leap second.
132         if (secondOfMinute != 60 || minuteOfHour != 59 || hourOfDay != 23) {
133             return null;
134         }
135
136         LOG.trace("Received time contains leap second, adjusting by replacing the second-of-minute of 60 with 59 {}",
137                 time);
138
139         // Applying simple conversion replacing the second-of-minute of 60 with 59.
140
141         secondOfMinute = 59;
142
143         final int year = getFieldFromTemporalAccessor(parsed, ChronoField.YEAR);
144         final int monthOfYear = getFieldFromTemporalAccessor(parsed, ChronoField.MONTH_OF_YEAR);
145         final int dayOfMonth = getFieldFromTemporalAccessor(parsed, ChronoField.DAY_OF_MONTH);
146         final int nanoOfSecond = getFieldFromTemporalAccessor(parsed, ChronoField.NANO_OF_SECOND);
147         final int offsetSeconds = getFieldFromTemporalAccessor(parsed, ChronoField.OFFSET_SECONDS);
148
149         final LocalDateTime currentTime = LocalDateTime.of(year, monthOfYear, dayOfMonth, hourOfDay, minuteOfHour,
150                 secondOfMinute, nanoOfSecond);
151         final OffsetDateTime dateTimeWithZoneOffset = currentTime.atOffset(ZoneOffset.ofTotalSeconds(offsetSeconds));
152
153         return RFC3339_DATE_PARSER.apply(dateTimeWithZoneOffset.toString());
154     }
155
156     /**
157      * Get value asociated with {@code ChronoField}.
158      *
159      * @param accessor The {@link TemporalAccessor}
160      * @param field The {@link ChronoField} to get
161      * @return the value associated with the {@link ChronoField} for the given {@link TemporalAccessor} if present,
162      *     else 0.
163      */
164     private static int getFieldFromTemporalAccessor(final TemporalAccessor accessor, final ChronoField field) {
165         return accessor.isSupported(field) ? (int) accessor.getLong(field) : 0;
166     }
167
168     private final @NonNull Instant eventTime;
169
170     /**
171      * Create new notification and capture the timestamp in the constructor.
172      */
173     public NotificationMessage(final Document notificationContent) {
174         this(notificationContent, Instant.now());
175     }
176
177     /**
178      * Create new notification with provided timestamp.
179      */
180     public NotificationMessage(final Document notificationContent, final Instant eventTime) {
181         super(wrapNotification(notificationContent, eventTime));
182         this.eventTime = requireNonNull(eventTime);
183     }
184
185     /**
186      * Get the time of the event.
187      *
188      * @return notification event time
189      */
190     public @NonNull Instant getEventTime() {
191         return eventTime;
192     }
193
194     private static Document wrapNotification(final Document notificationContent, final Instant eventTime) {
195         requireNonNull(notificationContent);
196         requireNonNull(eventTime);
197
198         final Element baseNotification = notificationContent.getDocumentElement();
199         final Element entireNotification = notificationContent.createElementNS(
200             XmlNetconfConstants.URN_IETF_PARAMS_NETCONF_CAPABILITY_NOTIFICATION_1_0, NOTIFICATION_TAG);
201         entireNotification.appendChild(baseNotification);
202
203         final Element eventTimeElement = notificationContent.createElementNS(
204             XmlNetconfConstants.URN_IETF_PARAMS_NETCONF_CAPABILITY_NOTIFICATION_1_0, EVENT_TIME_TAG);
205         eventTimeElement.setTextContent(RFC3339_DATE_FORMATTER.apply(eventTime));
206         entireNotification.appendChild(eventTimeElement);
207
208         notificationContent.appendChild(entireNotification);
209         return notificationContent;
210     }
211 }