Remove ResolveEnumUtil
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / services / impl / RestconfStreamsSubscriptionServiceImpl.java
1 /*
2  * Copyright (c) 2016 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.restconf.nb.rfc8040.rests.services.impl;
9
10 import static com.google.common.base.Preconditions.checkState;
11
12 import java.net.URI;
13 import java.time.Instant;
14 import java.time.format.DateTimeFormatter;
15 import java.time.format.DateTimeFormatterBuilder;
16 import java.time.format.DateTimeParseException;
17 import java.time.temporal.ChronoField;
18 import java.time.temporal.TemporalAccessor;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Map.Entry;
23 import java.util.Optional;
24 import javax.ws.rs.Path;
25 import javax.ws.rs.core.UriInfo;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
28 import org.opendaylight.mdsal.dom.api.DOMNotificationService;
29 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
30 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
31 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
32 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
33 import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
34 import org.opendaylight.restconf.nb.rfc8040.handlers.TransactionChainHandler;
35 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSubscriptionService;
36 import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants;
37 import org.opendaylight.restconf.nb.rfc8040.streams.Configuration;
38 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.types.rev130715.DateAndTime;
39 import org.opendaylight.yangtools.yang.common.QName;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
42 import org.opendaylight.yangtools.yang.data.impl.schema.builder.impl.ImmutableLeafNodeBuilder;
43 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.Module;
46 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
47 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * Implementation of {@link RestconfStreamsSubscriptionService}.
53  */
54 @Path("/")
55 public class RestconfStreamsSubscriptionServiceImpl implements RestconfStreamsSubscriptionService {
56     private static final Logger LOG = LoggerFactory.getLogger(RestconfStreamsSubscriptionServiceImpl.class);
57     private static final QName LOCATION_QNAME =
58         QName.create("subscribe:to:notification", "2016-10-28", "location").intern();
59     private static final NodeIdentifier LOCATION_NODEID = NodeIdentifier.create(LOCATION_QNAME);
60     private static final QName NOTIFI_QNAME = QName.create(LOCATION_QNAME, "notifi").intern();
61     private static final YangInstanceIdentifier LOCATION_PATH =
62         YangInstanceIdentifier.create(NodeIdentifier.create(NOTIFI_QNAME), LOCATION_NODEID);
63
64     private final SubscribeToStreamUtil streamUtils;
65     private final HandlersHolder handlersHolder;
66
67     /**
68      * Initialize holder of handlers with holders as parameters.
69      *
70      * @param dataBroker {@link DOMDataBroker}
71      * @param notificationService {@link DOMNotificationService}
72      * @param schemaHandler
73      *             handler of {@link SchemaContext}
74      * @param transactionChainHandler
75      *             handler of {@link DOMTransactionChain}
76      * @param configuration
77      *             configuration for restconf {@link Configuration}}
78      */
79     public RestconfStreamsSubscriptionServiceImpl(final DOMDataBroker dataBroker,
80             final DOMNotificationService notificationService, final SchemaContextHandler schemaHandler,
81             final TransactionChainHandler transactionChainHandler, final Configuration configuration) {
82         this.handlersHolder = new HandlersHolder(dataBroker, notificationService,
83                 transactionChainHandler, schemaHandler);
84         streamUtils = configuration.isUseSSE() ? SubscribeToStreamUtil.serverSentEvents()
85                 : SubscribeToStreamUtil.webSockets();
86     }
87
88     @Override
89     public NormalizedNodeContext subscribeToStream(final String identifier, final UriInfo uriInfo) {
90         final NotificationQueryParams notificationQueryParams = NotificationQueryParams.fromUriInfo(uriInfo);
91
92         final URI response;
93         if (identifier.contains(RestconfStreamsConstants.DATA_SUBSCRIPTION)) {
94             response = streamUtils.subscribeToDataStream(identifier, uriInfo, notificationQueryParams, handlersHolder);
95         } else if (identifier.contains(RestconfStreamsConstants.NOTIFICATION_STREAM)) {
96             response = streamUtils.subscribeToYangStream(identifier, uriInfo, notificationQueryParams, handlersHolder);
97         } else {
98             final String msg = "Bad type of notification of sal-remote";
99             LOG.warn(msg);
100             throw new RestconfDocumentedException(msg);
101         }
102
103         // prepare new header with location
104         final Map<String, Object> headers = new HashMap<>();
105         headers.put("Location", response);
106
107         // prepare node with value of location
108         return new NormalizedNodeContext(prepareIIDSubsStreamOutput(handlersHolder.getSchemaHandler()),
109             ImmutableLeafNodeBuilder.create()
110                 .withNodeIdentifier(LOCATION_NODEID)
111                 .withValue(response.toString())
112                 .build(), headers);
113     }
114
115     /**
116      * Prepare InstanceIdentifierContext for Location leaf.
117      *
118      * @param schemaHandler Schema context handler.
119      * @return InstanceIdentifier of Location leaf.
120      */
121     private static InstanceIdentifierContext<?> prepareIIDSubsStreamOutput(final SchemaContextHandler schemaHandler) {
122         final Optional<Module> module = schemaHandler.get().findModule(NOTIFI_QNAME.getModule());
123         checkState(module.isPresent());
124         final DataSchemaNode notify = module.get().dataChildByName(NOTIFI_QNAME);
125         checkState(notify instanceof ContainerSchemaNode, "Unexpected non-container %s", notify);
126         final DataSchemaNode location = ((ContainerSchemaNode) notify).dataChildByName(LOCATION_QNAME);
127         checkState(location != null, "Missing location");
128
129         return new InstanceIdentifierContext<SchemaNode>(LOCATION_PATH, location, null, schemaHandler.get());
130     }
131
132     /**
133      * Holder of all handlers for notifications.
134      */
135     // FIXME: why do we even need this class?!
136     public static final class HandlersHolder {
137         private final DOMDataBroker dataBroker;
138         private final DOMNotificationService notificationService;
139         private final TransactionChainHandler transactionChainHandler;
140         private final SchemaContextHandler schemaHandler;
141
142         private HandlersHolder(final DOMDataBroker dataBroker, final DOMNotificationService notificationService,
143                 final TransactionChainHandler transactionChainHandler, final SchemaContextHandler schemaHandler) {
144             this.dataBroker = dataBroker;
145             this.notificationService = notificationService;
146             this.transactionChainHandler = transactionChainHandler;
147             this.schemaHandler = schemaHandler;
148         }
149
150         /**
151          * Get {@link DOMDataBroker}.
152          *
153          * @return the dataBroker
154          */
155         public DOMDataBroker getDataBroker() {
156             return this.dataBroker;
157         }
158
159         /**
160          * Get {@link DOMNotificationService}.
161          *
162          * @return the notificationService
163          */
164         public DOMNotificationService getNotificationServiceHandler() {
165             return this.notificationService;
166         }
167
168         /**
169          * Get {@link TransactionChainHandler}.
170          *
171          * @return the transactionChainHandler
172          */
173         public TransactionChainHandler getTransactionChainHandler() {
174             return this.transactionChainHandler;
175         }
176
177         /**
178          * Get {@link SchemaContextHandler}.
179          *
180          * @return the schemaHandler
181          */
182         public SchemaContextHandler getSchemaHandler() {
183             return this.schemaHandler;
184         }
185     }
186
187     /**
188      * Parser and holder of query paramteres from uriInfo for notifications.
189      */
190     public static final class NotificationQueryParams {
191         private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
192                 .appendValue(ChronoField.YEAR, 4).appendLiteral('-')
193                 .appendValue(ChronoField.MONTH_OF_YEAR, 2).appendLiteral('-')
194                 .appendValue(ChronoField.DAY_OF_MONTH, 2).appendLiteral('T')
195                 .appendValue(ChronoField.HOUR_OF_DAY, 2).appendLiteral(':')
196                 .appendValue(ChronoField.MINUTE_OF_HOUR, 2).appendLiteral(':')
197                 .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
198                 .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
199                 .appendOffset("+HH:MM", "Z").toFormatter();
200
201         private final Instant start;
202         private final Instant stop;
203         private final String filter;
204         private final boolean skipNotificationData;
205
206         private NotificationQueryParams(final Instant start, final Instant stop, final String filter,
207                 final boolean skipNotificationData) {
208             this.start = start == null ? Instant.now() : start;
209             this.stop = stop;
210             this.filter = filter;
211             this.skipNotificationData = skipNotificationData;
212         }
213
214         static NotificationQueryParams fromUriInfo(final UriInfo uriInfo) {
215             Instant start = null;
216             boolean startTimeUsed = false;
217             Instant stop = null;
218             boolean stopTimeUsed = false;
219             String filter = null;
220             boolean filterUsed = false;
221             boolean skipNotificationDataUsed = false;
222             boolean skipNotificationData = false;
223
224             for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
225                 switch (entry.getKey()) {
226                     case "start-time":
227                         if (!startTimeUsed) {
228                             startTimeUsed = true;
229                             start = parseDateFromQueryParam(entry);
230                         } else {
231                             throw new RestconfDocumentedException("Start-time parameter can be used only once.");
232                         }
233                         break;
234                     case "stop-time":
235                         if (!stopTimeUsed) {
236                             stopTimeUsed = true;
237                             stop = parseDateFromQueryParam(entry);
238                         } else {
239                             throw new RestconfDocumentedException("Stop-time parameter can be used only once.");
240                         }
241                         break;
242                     case "filter":
243                         if (!filterUsed) {
244                             filterUsed = true;
245                             filter = entry.getValue().iterator().next();
246                         }
247                         break;
248                     case "odl-skip-notification-data":
249                         if (!skipNotificationDataUsed) {
250                             skipNotificationDataUsed = true;
251                             skipNotificationData = Boolean.parseBoolean(entry.getValue().iterator().next());
252                         } else {
253                             throw new RestconfDocumentedException(
254                                     "Odl-skip-notification-data parameter can be used only once.");
255                         }
256                         break;
257                     default:
258                         throw new RestconfDocumentedException(
259                                 "Bad parameter used with notifications: " + entry.getKey());
260                 }
261             }
262             if (!startTimeUsed && stopTimeUsed) {
263                 throw new RestconfDocumentedException("Stop-time parameter has to be used with start-time parameter.");
264             }
265
266             return new NotificationQueryParams(start, stop, filter, skipNotificationData);
267         }
268
269
270         /**
271          * Parse input of query parameters - start-time or stop-time - from {@link DateAndTime} format
272          * to {@link Instant} format.
273          *
274          * @param entry Start-time or stop-time as string in {@link DateAndTime} format.
275          * @return Parsed {@link Instant} by entry.
276          */
277         private static Instant parseDateFromQueryParam(final Entry<String, List<String>> entry) {
278             final DateAndTime event = new DateAndTime(entry.getValue().iterator().next());
279             final String value = event.getValue();
280             final TemporalAccessor accessor;
281             try {
282                 accessor = FORMATTER.parse(value);
283             } catch (final DateTimeParseException e) {
284                 throw new RestconfDocumentedException("Cannot parse of value in date: " + value, e);
285             }
286             return Instant.from(accessor);
287         }
288
289         /**
290          * Get start-time query parameter.
291          *
292          * @return start-time
293          */
294         public @NonNull Instant getStart() {
295             return start;
296         }
297
298         /**
299          * Get stop-time query parameter.
300          *
301          * @return stop-time
302          */
303         public Optional<Instant> getStop() {
304             return Optional.ofNullable(stop);
305         }
306
307         /**
308          * Get filter query parameter.
309          *
310          * @return filter
311          */
312         public Optional<String> getFilter() {
313             return Optional.ofNullable(filter);
314         }
315
316         /**
317          * Check whether this query should notify changes without data.
318          *
319          * @return true if this query should notify about changes with  data
320          */
321         public boolean isSkipNotificationData() {
322             return skipNotificationData;
323         }
324     }
325 }