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