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