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