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