Extend Websocket streams for data-less notifications
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / streams / listeners / ListenerAdapter.java
1 /*
2  * Copyright (c) 2014, 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.streams.listeners;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12 import static java.util.Objects.requireNonNull;
13
14 import com.google.common.base.MoreObjects;
15 import java.io.IOException;
16 import java.time.Instant;
17 import java.util.Collection;
18 import java.util.Map.Entry;
19 import java.util.Optional;
20 import javax.xml.stream.XMLStreamException;
21 import javax.xml.transform.dom.DOMResult;
22 import org.json.XML;
23 import org.opendaylight.mdsal.dom.api.ClusteredDOMDataTreeChangeListener;
24 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
25 import org.opendaylight.yangtools.yang.common.QName;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
30 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
31 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
32 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
33 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
34 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
35 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
36 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
37 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
38 import org.opendaylight.yangtools.yang.model.api.Module;
39 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
40 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.w3c.dom.Document;
44 import org.w3c.dom.Element;
45 import org.w3c.dom.Node;
46
47 /**
48  * {@link ListenerAdapter} is responsible to track events, which occurred by changing data in data source.
49  */
50 public class ListenerAdapter extends AbstractCommonSubscriber implements ClusteredDOMDataTreeChangeListener {
51
52     private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
53     private static final String DATA_CHANGE_EVENT = "data-change-event";
54     private static final String PATH = "path";
55     private static final String OPERATION = "operation";
56
57     private final YangInstanceIdentifier path;
58     private final String streamName;
59     private final NotificationOutputType outputType;
60
61     /**
62      * Creates new {@link ListenerAdapter} listener specified by path and stream name and register for subscribing.
63      *
64      * @param path       Path to data in data store.
65      * @param streamName The name of the stream.
66      * @param outputType Type of output on notification (JSON, XML).
67      */
68     ListenerAdapter(final YangInstanceIdentifier path, final String streamName,
69             final NotificationOutputType outputType) {
70         setLocalNameOfPath(path.getLastPathArgument().getNodeType().getLocalName());
71
72         this.outputType = requireNonNull(outputType);
73         this.path = requireNonNull(path);
74         this.streamName = requireNonNull(streamName);
75         checkArgument(!streamName.isEmpty());
76     }
77
78     @Override
79     public void onDataTreeChanged(final Collection<DataTreeCandidate> dataTreeCandidates) {
80         final Instant now = Instant.now();
81         if (!checkStartStop(now, this)) {
82             return;
83         }
84
85         final String xml = prepareXml(dataTreeCandidates);
86         if (checkFilter(xml)) {
87             prepareAndPostData(xml);
88         }
89     }
90
91     /**
92      * Gets the name of the stream.
93      *
94      * @return The name of the stream.
95      */
96     @Override
97     public String getStreamName() {
98         return this.streamName;
99     }
100
101     @Override
102     public String getOutputType() {
103         return this.outputType.getName();
104     }
105
106     /**
107      * Get path pointed to data in data store.
108      *
109      * @return Path pointed to data in data store.
110      */
111     public YangInstanceIdentifier getPath() {
112         return this.path;
113     }
114
115     /**
116      * Prepare data of notification and data to client.
117      *
118      * @param xml XML-formatted data.
119      */
120     private void prepareAndPostData(final String xml) {
121         if (this.outputType.equals(NotificationOutputType.JSON)) {
122             post(XML.toJSONObject(xml).toString());
123         } else {
124             post(xml);
125         }
126     }
127
128     /**
129      * Prepare data in printable form and transform it to String.
130      *
131      * @param dataTreeCandidates Data-tree candidates to be transformed.
132      * @return Data in printable form.
133      */
134     private String prepareXml(final Collection<DataTreeCandidate> dataTreeCandidates) {
135         final SchemaContext schemaContext = schemaHandler.get();
136         final DataSchemaContextTree dataContextTree = DataSchemaContextTree.from(schemaContext);
137         final Document doc = createDocument();
138         final Element notificationElement = basePartDoc(doc);
139
140         final Element dataChangedNotificationEventElement = doc.createElementNS(
141                 "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
142
143         addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, dataTreeCandidates,
144                 schemaContext, dataContextTree);
145         notificationElement.appendChild(dataChangedNotificationEventElement);
146         return transformDoc(doc);
147     }
148
149     /**
150      * Adds values to data changed notification event element.
151      */
152     private void addValuesToDataChangedNotificationEventElement(final Document doc,
153             final Element dataChangedNotificationEventElement, final Collection<DataTreeCandidate> dataTreeCandidates,
154             final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
155
156         for (DataTreeCandidate dataTreeCandidate : dataTreeCandidates) {
157             DataTreeCandidateNode candidateNode = dataTreeCandidate.getRootNode();
158             if (candidateNode == null) {
159                 continue;
160             }
161             YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath();
162             boolean isSkipNotificationData = this.isSkipNotificationData();
163             if (isSkipNotificationData) {
164                 createCreatedChangedDataChangeEventElementWithoutData(doc, dataChangedNotificationEventElement,
165                         dataTreeCandidate.getRootNode(), schemaContext);
166             } else {
167                 addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
168                         yiid.getParent(), schemaContext, dataSchemaContextTree);
169             }
170         }
171     }
172
173     private void addNodeToDataChangeNotificationEventElement(final Document doc,
174             final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
175             final YangInstanceIdentifier parentYiid, final SchemaContext schemaContext,
176             final DataSchemaContextTree dataSchemaContextTree) {
177
178         Optional<NormalizedNode<?, ?>> optionalNormalizedNode = Optional.empty();
179         switch (candidateNode.getModificationType()) {
180             case APPEARED:
181             case SUBTREE_MODIFIED:
182             case WRITE:
183                 optionalNormalizedNode = candidateNode.getDataAfter();
184                 break;
185             case DELETE:
186             case DISAPPEARED:
187                 optionalNormalizedNode = candidateNode.getDataBefore();
188                 break;
189             case UNMODIFIED:
190             default:
191                 break;
192         }
193
194         if (!optionalNormalizedNode.isPresent()) {
195             LOG.error("No node present in notification for {}", candidateNode);
196             return;
197         }
198
199         NormalizedNode<?, ?> normalizedNode = optionalNormalizedNode.get();
200         YangInstanceIdentifier yiid = YangInstanceIdentifier.builder(parentYiid)
201                 .append(normalizedNode.getIdentifier()).build();
202
203         final Optional<DataSchemaContextNode<?>> childrenSchemaNode = dataSchemaContextTree.findChild(yiid);
204         checkState(childrenSchemaNode.isPresent());
205         boolean isNodeMixin = childrenSchemaNode.get().isMixin();
206         boolean isSkippedNonLeaf = getLeafNodesOnly() && !(normalizedNode instanceof LeafNode);
207         if (!isNodeMixin && !isSkippedNonLeaf) {
208             Node node = null;
209             switch (candidateNode.getModificationType()) {
210                 case APPEARED:
211                 case SUBTREE_MODIFIED:
212                 case WRITE:
213                     Operation op = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
214                     node = createCreatedChangedDataChangeEventElement(doc, yiid, normalizedNode, op,
215                             schemaContext, dataSchemaContextTree);
216                     break;
217                 case DELETE:
218                 case DISAPPEARED:
219                     node = createDataChangeEventElement(doc, yiid, schemaContext, Operation.DELETED);
220                     break;
221                 case UNMODIFIED:
222                 default:
223                     break;
224             }
225             if (node != null) {
226                 dataChangedNotificationEventElement.appendChild(node);
227             }
228         }
229
230         for (DataTreeCandidateNode childNode : candidateNode.getChildNodes()) {
231             addNodeToDataChangeNotificationEventElement(
232                     doc, dataChangedNotificationEventElement, childNode, yiid, schemaContext, dataSchemaContextTree);
233         }
234     }
235
236     /**
237      * Creates data-changed event element from data.
238      *
239      * @param doc           {@link Document}
240      * @param schemaContext Schema context.
241      * @param operation  Operation value
242      * @return {@link Node} represented by changed event element.
243      */
244     private static Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
245             final SchemaContext schemaContext, Operation operation) {
246         final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
247         final Element pathElement = doc.createElement(PATH);
248         addPathAsValueToElement(eventPath, pathElement, schemaContext);
249         dataChangeEventElement.appendChild(pathElement);
250
251         final Element operationElement = doc.createElement(OPERATION);
252         operationElement.setTextContent(operation.value);
253         dataChangeEventElement.appendChild(operationElement);
254
255         return dataChangeEventElement;
256     }
257
258     /**
259      * Creates data change notification element without data element.
260      *
261      * @param doc
262      *       {@link Document}
263      * @param dataChangedNotificationEventElement
264      *       {@link Element}
265      * @param candidateNode
266      *       {@link DataTreeCandidateNode}
267      */
268     private void createCreatedChangedDataChangeEventElementWithoutData(final Document doc,
269             final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
270             final SchemaContext schemaContext) {
271         final Operation operation;
272         switch (candidateNode.getModificationType()) {
273             case APPEARED:
274             case SUBTREE_MODIFIED:
275             case WRITE:
276                 operation = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
277                 break;
278             case DELETE:
279             case DISAPPEARED:
280                 operation = Operation.DELETED;
281                 break;
282             case UNMODIFIED:
283             default:
284                 return;
285         }
286         Node dataChangeEventElement = createDataChangeEventElement(doc, getPath(), schemaContext, operation);
287         dataChangedNotificationEventElement.appendChild(dataChangeEventElement);
288     }
289
290     private Node createCreatedChangedDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
291             final NormalizedNode<?, ?> normalized, final Operation operation, final SchemaContext schemaContext,
292             final DataSchemaContextTree dataSchemaContextTree) {
293         final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
294         final Element pathElement = doc.createElement(PATH);
295         addPathAsValueToElement(eventPath, pathElement, schemaContext);
296         dataChangeEventElement.appendChild(pathElement);
297
298         final Element operationElement = doc.createElement("operation");
299         operationElement.setTextContent(operation.value);
300         dataChangeEventElement.appendChild(operationElement);
301
302         try {
303             SchemaPath nodePath;
304             final Optional<DataSchemaContextNode<?>> childrenSchemaNode = dataSchemaContextTree.findChild(eventPath);
305             checkState(childrenSchemaNode.isPresent());
306             if (normalized instanceof MapEntryNode || normalized instanceof UnkeyedListEntryNode) {
307                 nodePath = childrenSchemaNode.get().getDataSchemaNode().getPath();
308             } else {
309                 nodePath = childrenSchemaNode.get().getDataSchemaNode().getPath().getParent();
310             }
311             final DOMResult domResult = writeNormalizedNode(normalized, schemaContext, nodePath);
312             final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
313             final Element dataElement = doc.createElement("data");
314             dataElement.appendChild(result);
315             dataChangeEventElement.appendChild(dataElement);
316         } catch (final IOException e) {
317             LOG.error("Error in writer ", e);
318         } catch (final XMLStreamException e) {
319             LOG.error("Error processing stream", e);
320         }
321
322         return dataChangeEventElement;
323     }
324
325     /**
326      * Adds path as value to element.
327      *
328      * @param eventPath     Path to data in data store.
329      * @param element       {@link Element}
330      * @param schemaContext Schema context.
331      */
332     private static void addPathAsValueToElement(final YangInstanceIdentifier eventPath, final Element element,
333             final SchemaContext schemaContext) {
334         final StringBuilder textContent = new StringBuilder();
335
336         for (final PathArgument pathArgument : eventPath.getPathArguments()) {
337             if (pathArgument instanceof YangInstanceIdentifier.AugmentationIdentifier) {
338                 continue;
339             }
340             textContent.append("/");
341             writeIdentifierWithNamespacePrefix(textContent, pathArgument.getNodeType(), schemaContext);
342             if (pathArgument instanceof NodeIdentifierWithPredicates) {
343                 for (final Entry<QName, Object> entry : ((NodeIdentifierWithPredicates) pathArgument).entrySet()) {
344                     final QName keyValue = entry.getKey();
345                     final String predicateValue = String.valueOf(entry.getValue());
346                     textContent.append("[");
347                     writeIdentifierWithNamespacePrefix(textContent, keyValue, schemaContext);
348                     textContent.append("='").append(predicateValue).append("']");
349                 }
350             } else if (pathArgument instanceof NodeWithValue) {
351                 textContent.append("[.='").append(((NodeWithValue<?>) pathArgument).getValue()).append("']");
352             }
353         }
354         element.setTextContent(textContent.toString());
355     }
356
357     /**
358      * Writes identifier that consists of prefix and QName.
359      *
360      * @param textContent   Text builder that should be supplemented by QName and its modules name.
361      * @param qualifiedName QName of the element.
362      * @param schemaContext Schema context that holds modules which should contain module specified in QName.
363      */
364     private static void writeIdentifierWithNamespacePrefix(final StringBuilder textContent, final QName qualifiedName,
365             final SchemaContext schemaContext) {
366         final Optional<Module> module = schemaContext.findModule(qualifiedName.getModule());
367         if (module.isPresent()) {
368             textContent.append(module.get().getName());
369             textContent.append(":");
370             textContent.append(qualifiedName.getLocalName());
371         } else {
372             LOG.error("Cannot write identifier with namespace prefix in data-change listener adapter: "
373                     + "Cannot find module in schema context for input QName {}.", qualifiedName);
374             throw new IllegalStateException(String.format("Cannot find module in schema context for input QName %s.",
375                     qualifiedName));
376         }
377     }
378
379     /**
380      * Consists of three types {@link Operation#CREATED}, {@link Operation#UPDATED} and {@link Operation#DELETED}.
381      */
382     private enum Operation {
383         CREATED("created"),
384         UPDATED("updated"),
385         DELETED("deleted");
386
387         private final String value;
388
389         Operation(final String value) {
390             this.value = value;
391         }
392     }
393
394     @Override
395     public String toString() {
396         return MoreObjects.toStringHelper(this)
397                 .add(PATH, path)
398                 .add("stream-name", streamName)
399                 .add("output-type", outputType)
400                 .toString();
401     }
402 }