eeeccc6cc460f303c877037b9e133adb0a7f193d
[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.EffectiveModelContext;
37 import org.opendaylight.yangtools.yang.model.api.Module;
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 EffectiveModelContext 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 EffectiveModelContext 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 EffectiveModelContext 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.isEmpty()) {
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 EffectiveModelContext 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.findChild(eventPath).orElseThrow().getDataSchemaNode().getPath();
325             } else {
326                 nodePath = dataSchemaContextTree.findChild(eventPath).orElseThrow().getDataSchemaNode().getPath()
327                     .getParent();
328             }
329             final DOMResult domResult = writeNormalizedNode(normalized, schemaContext, nodePath);
330             final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
331             final Element dataElement = doc.createElement("data");
332             dataElement.appendChild(result);
333             dataChangeEventElement.appendChild(dataElement);
334         } catch (final IOException e) {
335             LOG.error("Error in writer ", e);
336         } catch (final XMLStreamException e) {
337             LOG.error("Error processing stream", e);
338         }
339
340         return dataChangeEventElement;
341     }
342
343     /**
344      * Adds path as value to element.
345      *
346      * @param dataPath
347      *            Path to data in data store.
348      * @param element
349      *            {@link Element}
350      */
351     @SuppressWarnings("rawtypes")
352     private void addPathAsValueToElement(final YangInstanceIdentifier dataPath, final Element element) {
353         final YangInstanceIdentifier normalizedPath = controllerContext.toXpathRepresentation(dataPath);
354         final StringBuilder textContent = new StringBuilder();
355
356         for (final PathArgument pathArgument : normalizedPath.getPathArguments()) {
357             if (pathArgument instanceof YangInstanceIdentifier.AugmentationIdentifier) {
358                 continue;
359             }
360             textContent.append("/");
361             writeIdentifierWithNamespacePrefix(element, textContent, pathArgument.getNodeType());
362             if (pathArgument instanceof NodeIdentifierWithPredicates) {
363                 for (final Entry<QName, Object> entry : ((NodeIdentifierWithPredicates) pathArgument).entrySet()) {
364                     final QName keyValue = entry.getKey();
365                     final String predicateValue = String.valueOf(entry.getValue());
366                     textContent.append("[");
367                     writeIdentifierWithNamespacePrefix(element, textContent, keyValue);
368                     textContent.append("='");
369                     textContent.append(predicateValue);
370                     textContent.append("'");
371                     textContent.append("]");
372                 }
373             } else if (pathArgument instanceof NodeWithValue) {
374                 textContent.append("[.='");
375                 textContent.append(((NodeWithValue) pathArgument).getValue());
376                 textContent.append("'");
377                 textContent.append("]");
378             }
379         }
380         element.setTextContent(textContent.toString());
381     }
382
383     /**
384      * Writes identifier that consists of prefix and QName.
385      *
386      * @param element
387      *            {@link Element}
388      * @param textContent
389      *            StringBuilder
390      * @param qualifiedName
391      *            QName
392      */
393     private void writeIdentifierWithNamespacePrefix(final Element element, final StringBuilder textContent,
394             final QName qualifiedName) {
395         final Module module = controllerContext.getGlobalSchema().findModule(qualifiedName.getModule())
396                 .get();
397
398         textContent.append(module.getName());
399         textContent.append(":");
400         textContent.append(qualifiedName.getLocalName());
401     }
402
403     /**
404      * Consists of three types {@link Operation#CREATED},
405      * {@link Operation#UPDATED} and {@link Operation#DELETED}.
406      */
407     private enum Operation {
408         CREATED("created"), UPDATED("updated"), DELETED("deleted");
409
410         private final String value;
411
412         Operation(final String value) {
413             this.value = value;
414         }
415     }
416 }