6afae02e7dcdc40289f68c21c0eb58727fcec1fe
[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 com.google.common.collect.ImmutableMap;
13 import java.text.ParsePosition;
14 import java.time.Instant;
15 import java.time.LocalDateTime;
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.DocumentedException;
25 import org.opendaylight.netconf.api.NamespaceURN;
26 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
27 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
28 import org.opendaylight.yangtools.yang.common.ErrorTag;
29 import org.opendaylight.yangtools.yang.common.ErrorType;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32 import org.w3c.dom.Document;
33
34 /**
35  * Special kind of netconf message that contains a timestamp.
36  */
37 public final class NotificationMessage extends NetconfMessage {
38     public static final @NonNull String ELEMENT_NAME = "notification";
39
40     private static final Logger LOG = LoggerFactory.getLogger(NotificationMessage.class);
41
42     /**
43      * Used for unknown/un-parse-able event-times.
44      */
45     // FIXME: we should differentiate unknown and invalid event times
46     public static final Instant UNKNOWN_EVENT_TIME = Instant.EPOCH;
47
48     /**
49      * The ISO-like date-time formatter that formats or parses a date-time with
50      * the offset and zone if available, such as '2011-12-03T10:15:30',
51      * '2011-12-03T10:15:30+01:00' or '2011-12-03T10:15:30+01:00[Europe/Paris]'.
52      */
53     private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
54
55     /**
56      * Provide a {@link String} representation of a {@link Instant} object,
57      * using the time-zone offset for UTC, {@code ZoneOffset.UTC}.
58      */
59     public static final Function<Instant, String> RFC3339_DATE_FORMATTER = date ->
60             DATE_TIME_FORMATTER.format(date.atOffset(ZoneOffset.UTC));
61
62     /**
63      * Parse a {@link String} object into a {@link Instant} using the time-zone
64      * offset for UTC, {@code ZoneOffset.UTC}, and the system default time-zone,
65      * {@code ZoneId.systemDefault()}.
66      * <p>
67      *     While parsing, if an exception occurs, we try to handle it as if it is due
68      *     to a leap second. If that's the case, a simple conversion is applied, replacing
69      *     the second-of-minute of 60 with 59.
70      *     If that's not the case, we propagate the {@link DateTimeParseException} to the
71      *     caller.
72      * </p>
73      */
74     public static final Function<String, Instant> RFC3339_DATE_PARSER = time -> {
75         try {
76             final var localDateTime = ZonedDateTime.parse(time, DATE_TIME_FORMATTER);
77             final int startAt = 0;
78             final var parsed = DATE_TIME_FORMATTER.parse(time, new ParsePosition(startAt));
79             final int nanoOfSecond = getFieldFromTemporalAccessor(parsed, ChronoField.NANO_OF_SECOND);
80             final long reminder = nanoOfSecond % 1000000;
81
82             // Log warn in case we rounded the fraction of a second. We need to create a string from the
83             // value that was cut. Example -> 1.123750 -> Value that was cut 75
84             if (reminder != 0) {
85                 final var reminderBuilder = new StringBuilder(String.valueOf(reminder));
86
87                 //add zeros in case we have number like 123056 to make sure 056 is displayed
88                 while (reminderBuilder.length() < 6) {
89                     reminderBuilder.insert(0, '0');
90                 }
91
92                 //delete zeros from end to make sure that number like 1.123750 will show value cut 75.
93                 while (reminderBuilder.charAt(reminderBuilder.length() - 1) == '0') {
94                     reminderBuilder.deleteCharAt(reminderBuilder.length() - 1);
95                 }
96                 LOG.warn("Fraction of second is cut to three digits. Value that was cut {}",
97                         reminderBuilder.toString());
98             }
99
100             return Instant.from(localDateTime);
101         } catch (DateTimeParseException exception) {
102             final var res = handlePotentialLeapSecond(time);
103             if (res != null) {
104                 return res;
105             }
106             throw exception;
107         }
108     };
109
110     /**
111      * Check whether the input {@link String} is representing a time compliant with the ISO
112      * format and having a leap second; e.g. formatted as 23:59:60. If that's the case, a simple
113      * conversion is applied, replacing the second-of-minute of 60 with 59.
114      *
115      * @param time {@link String} representation of a time
116      * @return {@code null} if time isn't ISO compliant or if the time doesn't have a leap second else an
117      *         {@link Instant} as per as the RFC3339_DATE_PARSER.
118      */
119     private static Instant handlePotentialLeapSecond(final String time) {
120         // Parse the string from offset 0, so we get the whole value.
121         final int offset = 0;
122         final var parsed = DATE_TIME_FORMATTER.parseUnresolved(time, new ParsePosition(offset));
123         // Bail fast
124         if (parsed == null) {
125             return null;
126         }
127
128         int secondOfMinute = getFieldFromTemporalAccessor(parsed, ChronoField.SECOND_OF_MINUTE);
129         final int hourOfDay = getFieldFromTemporalAccessor(parsed, ChronoField.HOUR_OF_DAY);
130         final int minuteOfHour = getFieldFromTemporalAccessor(parsed, ChronoField.MINUTE_OF_HOUR);
131
132         // Check whether the input time has leap second. As the leap second can only
133         // occur at 23:59:60, we can be very strict, and don't interpret an incorrect
134         // value as leap second.
135         if (secondOfMinute != 60 || minuteOfHour != 59 || hourOfDay != 23) {
136             return null;
137         }
138
139         LOG.trace("Received time contains leap second, adjusting by replacing the second-of-minute of 60 with 59 {}",
140                 time);
141
142         // Applying simple conversion replacing the second-of-minute of 60 with 59.
143
144         secondOfMinute = 59;
145
146         final int year = getFieldFromTemporalAccessor(parsed, ChronoField.YEAR);
147         final int monthOfYear = getFieldFromTemporalAccessor(parsed, ChronoField.MONTH_OF_YEAR);
148         final int dayOfMonth = getFieldFromTemporalAccessor(parsed, ChronoField.DAY_OF_MONTH);
149         final int nanoOfSecond = getFieldFromTemporalAccessor(parsed, ChronoField.NANO_OF_SECOND);
150         final int offsetSeconds = getFieldFromTemporalAccessor(parsed, ChronoField.OFFSET_SECONDS);
151
152         final var currentTime = LocalDateTime.of(year, monthOfYear, dayOfMonth, hourOfDay, minuteOfHour, secondOfMinute,
153             nanoOfSecond);
154         final var dateTimeWithZoneOffset = currentTime.atOffset(ZoneOffset.ofTotalSeconds(offsetSeconds));
155         return RFC3339_DATE_PARSER.apply(dateTimeWithZoneOffset.toString());
156     }
157
158     /**
159      * Get value associated with {@code ChronoField}.
160      *
161      * @param accessor The {@link TemporalAccessor}
162      * @param field The {@link ChronoField} to get
163      * @return the value associated with the {@link ChronoField} for the given {@link TemporalAccessor} if present,
164      *     else 0.
165      */
166     private static int getFieldFromTemporalAccessor(final TemporalAccessor accessor, final ChronoField field) {
167         return accessor.isSupported(field) ? (int) accessor.getLong(field) : 0;
168     }
169
170     private final @NonNull Instant eventTime;
171
172     private NotificationMessage(final Document notificationContent, final Instant eventTime) {
173         super(notificationContent);
174         this.eventTime = requireNonNull(eventTime);
175     }
176
177     /**
178      * Create new NotificationMessage with provided document. Only to be used if we know that the document represents a
179      * valid NotificationMessage.
180      */
181     static @NonNull NotificationMessage ofChecked(final Document document) throws DocumentedException {
182         final var eventTime = document.getDocumentElement().getElementsByTagNameNS(NamespaceURN.NOTIFICATION,
183             XmlNetconfConstants.EVENT_TIME);
184         if (eventTime.getLength() < 1) {
185             throw new DocumentedException("Missing event-time", ErrorType.PROTOCOL, ErrorTag.MISSING_ELEMENT,
186                 ErrorSeverity.ERROR, ImmutableMap.of());
187         }
188         return new NotificationMessage(document, RFC3339_DATE_PARSER.apply(eventTime.item(0).getTextContent()));
189     }
190
191     /**
192      * Create new notification with provided timestamp.
193      */
194     public static @NonNull NotificationMessage ofNotificationContent(final Document notificationContent,
195             final Instant eventTime) {
196         return new NotificationMessage(wrapNotification(notificationContent, eventTime), eventTime);
197     }
198
199     /**
200      * Create new notification and capture the timestamp in the method call.
201      */
202     public static @NonNull NotificationMessage ofNotificationContent(final Document notificationContent) {
203         return ofNotificationContent(notificationContent, Instant.now());
204     }
205
206     /**
207      * Get the time of the event.
208      *
209      * @return notification event time
210      */
211     public @NonNull Instant getEventTime() {
212         return eventTime;
213     }
214
215     private static Document wrapNotification(final Document notificationContent, final Instant eventTime) {
216         requireNonNull(notificationContent);
217         requireNonNull(eventTime);
218
219         final var baseNotification = notificationContent.getDocumentElement();
220         final var entireNotification = notificationContent.createElementNS(NamespaceURN.NOTIFICATION, ELEMENT_NAME);
221         entireNotification.appendChild(baseNotification);
222
223         final var eventTimeElement = notificationContent.createElementNS(NamespaceURN.NOTIFICATION,
224             XmlNetconfConstants.EVENT_TIME);
225         eventTimeElement.setTextContent(RFC3339_DATE_FORMATTER.apply(eventTime));
226         entireNotification.appendChild(eventTimeElement);
227
228         notificationContent.appendChild(entireNotification);
229         return notificationContent;
230     }
231 }