2 * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.netconf.api.messages;
10 import static java.util.Objects.requireNonNull;
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;
35 * Special kind of netconf message that contains a timestamp.
37 public final class NotificationMessage extends NetconfMessage {
38 public static final @NonNull String ELEMENT_NAME = "notification";
40 private static final Logger LOG = LoggerFactory.getLogger(NotificationMessage.class);
43 * Used for unknown/un-parse-able event-times.
45 // FIXME: we should differentiate unknown and invalid event times
46 public static final Instant UNKNOWN_EVENT_TIME = Instant.EPOCH;
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]'.
53 private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
56 * Provide a {@link String} representation of a {@link Instant} object,
57 * using the time-zone offset for UTC, {@code ZoneOffset.UTC}.
59 public static final Function<Instant, String> RFC3339_DATE_FORMATTER = date ->
60 DATE_TIME_FORMATTER.format(date.atOffset(ZoneOffset.UTC));
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()}.
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
74 public static final Function<String, Instant> RFC3339_DATE_PARSER = time -> {
76 return ZonedDateTime.parse(time, DATE_TIME_FORMATTER).toInstant();
77 } catch (DateTimeParseException exception) {
78 final var res = handlePotentialLeapSecond(time);
87 * Check whether the input {@link String} is representing a time compliant with the ISO
88 * format and having a leap second; e.g. formatted as 23:59:60. If that's the case, a simple
89 * conversion is applied, replacing the second-of-minute of 60 with 59.
91 * @param time {@link String} representation of a time
92 * @return {@code null} if time isn't ISO compliant or if the time doesn't have a leap second else an
93 * {@link Instant} as per as the RFC3339_DATE_PARSER.
95 private static Instant handlePotentialLeapSecond(final String time) {
96 // Parse the string from offset 0, so we get the whole value.
98 final var parsed = DATE_TIME_FORMATTER.parseUnresolved(time, new ParsePosition(offset));
100 if (parsed == null) {
104 int secondOfMinute = getFieldFromTemporalAccessor(parsed, ChronoField.SECOND_OF_MINUTE);
105 final int hourOfDay = getFieldFromTemporalAccessor(parsed, ChronoField.HOUR_OF_DAY);
106 final int minuteOfHour = getFieldFromTemporalAccessor(parsed, ChronoField.MINUTE_OF_HOUR);
108 // Check whether the input time has leap second. As the leap second can only
109 // occur at 23:59:60, we can be very strict, and don't interpret an incorrect
110 // value as leap second.
111 if (secondOfMinute != 60 || minuteOfHour != 59 || hourOfDay != 23) {
115 LOG.trace("Received time contains leap second, adjusting by replacing the second-of-minute of 60 with 59 {}",
118 // Applying simple conversion replacing the second-of-minute of 60 with 59.
122 final int year = getFieldFromTemporalAccessor(parsed, ChronoField.YEAR);
123 final int monthOfYear = getFieldFromTemporalAccessor(parsed, ChronoField.MONTH_OF_YEAR);
124 final int dayOfMonth = getFieldFromTemporalAccessor(parsed, ChronoField.DAY_OF_MONTH);
125 final int nanoOfSecond = getFieldFromTemporalAccessor(parsed, ChronoField.NANO_OF_SECOND);
126 final int offsetSeconds = getFieldFromTemporalAccessor(parsed, ChronoField.OFFSET_SECONDS);
128 final var currentTime = LocalDateTime.of(year, monthOfYear, dayOfMonth, hourOfDay, minuteOfHour, secondOfMinute,
130 final var dateTimeWithZoneOffset = currentTime.atOffset(ZoneOffset.ofTotalSeconds(offsetSeconds));
131 return RFC3339_DATE_PARSER.apply(dateTimeWithZoneOffset.toString());
135 * Get value associated with {@code ChronoField}.
137 * @param accessor The {@link TemporalAccessor}
138 * @param field The {@link ChronoField} to get
139 * @return the value associated with the {@link ChronoField} for the given {@link TemporalAccessor} if present,
142 private static int getFieldFromTemporalAccessor(final TemporalAccessor accessor, final ChronoField field) {
143 return accessor.isSupported(field) ? (int) accessor.getLong(field) : 0;
146 private final @NonNull Instant eventTime;
148 private NotificationMessage(final Document notificationContent, final Instant eventTime) {
149 super(notificationContent);
150 this.eventTime = requireNonNull(eventTime);
154 * Create new NotificationMessage with provided document. Only to be used if we know that the document represents a
155 * valid NotificationMessage.
157 static @NonNull NotificationMessage ofChecked(final Document document) throws DocumentedException {
158 final var eventTime = document.getDocumentElement().getElementsByTagNameNS(NamespaceURN.NOTIFICATION,
159 XmlNetconfConstants.EVENT_TIME);
160 if (eventTime.getLength() < 1) {
161 throw new DocumentedException("Missing event-time", ErrorType.PROTOCOL, ErrorTag.MISSING_ELEMENT,
162 ErrorSeverity.ERROR, ImmutableMap.of());
164 return new NotificationMessage(document, RFC3339_DATE_PARSER.apply(eventTime.item(0).getTextContent()));
168 * Create new notification with provided timestamp.
170 public static @NonNull NotificationMessage ofNotificationContent(final Document notificationContent,
171 final Instant eventTime) {
172 return new NotificationMessage(wrapNotification(notificationContent, eventTime), eventTime);
176 * Create new notification and capture the timestamp in the method call.
178 public static @NonNull NotificationMessage ofNotificationContent(final Document notificationContent) {
179 return ofNotificationContent(notificationContent, Instant.now());
183 * Get the time of the event.
185 * @return notification event time
187 public @NonNull Instant getEventTime() {
191 private static Document wrapNotification(final Document notificationContent, final Instant eventTime) {
192 requireNonNull(notificationContent);
193 requireNonNull(eventTime);
195 final var baseNotification = notificationContent.getDocumentElement();
196 final var entireNotification = notificationContent.createElementNS(NamespaceURN.NOTIFICATION, ELEMENT_NAME);
197 entireNotification.appendChild(baseNotification);
199 final var eventTimeElement = notificationContent.createElementNS(NamespaceURN.NOTIFICATION,
200 XmlNetconfConstants.EVENT_TIME);
201 eventTimeElement.setTextContent(RFC3339_DATE_FORMATTER.apply(eventTime));
202 entireNotification.appendChild(eventTimeElement);
204 notificationContent.appendChild(entireNotification);
205 return notificationContent;