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