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