Split Restconf implementations (draft02 and RFC) - features
[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.Map;
13 import java.util.Map.Entry;
14 import java.util.Set;
15 import javax.xml.stream.XMLStreamException;
16 import javax.xml.transform.dom.DOMResult;
17 import org.json.XML;
18 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataChangeEvent;
19 import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
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.util.DataSchemaContextTree;
32 import org.opendaylight.yangtools.yang.model.api.Module;
33 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
34 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.w3c.dom.Document;
38 import org.w3c.dom.Element;
39 import org.w3c.dom.Node;
40
41 /**
42  * {@link ListenerAdapter} is responsible to track events, which occurred by
43  * changing data in data source.
44  */
45 public class ListenerAdapter extends AbstractCommonSubscriber implements DOMDataChangeListener {
46
47     private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
48
49     private final YangInstanceIdentifier path;
50     private final String streamName;
51     private final NotificationOutputType outputType;
52
53     private AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change;
54
55     /**
56      * Creates new {@link ListenerAdapter} listener specified by path and stream
57      * name and register for subscribing.
58      *
59      * @param path
60      *            Path to data in data store.
61      * @param streamName
62      *            The name of the stream.
63      * @param outputType
64      *            Type of output on notification (JSON, XML)
65      */
66     ListenerAdapter(final YangInstanceIdentifier path, final String streamName,
67             final NotificationOutputType outputType) {
68         super();
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     }
75
76     @Override
77     public void onDataChanged(final AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change) {
78         this.change = change;
79         final String xml = prepareXml();
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() {
134         final SchemaContext schemaContext = ControllerContext.getInstance().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, this.change,
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 change
156      *            {@link AsyncDataChangeEvent}
157      */
158     private void addValuesToDataChangedNotificationEventElement(final Document doc,
159             final Element dataChangedNotificationEventElement,
160             final AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change,
161             final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
162
163         addCreatedChangedValuesFromDataToElement(doc, change.getCreatedData().entrySet(),
164                 dataChangedNotificationEventElement, Operation.CREATED, schemaContext, dataSchemaContextTree);
165
166         addCreatedChangedValuesFromDataToElement(doc, change.getUpdatedData().entrySet(),
167                 dataChangedNotificationEventElement, Operation.UPDATED, schemaContext, dataSchemaContextTree);
168
169         addValuesFromDataToElement(doc, change.getRemovedPaths(), dataChangedNotificationEventElement,
170                 Operation.DELETED);
171     }
172
173     /**
174      * Adds values from data to element.
175      *
176      * @param doc
177      *            {@link Document}
178      * @param data
179      *            Set of {@link YangInstanceIdentifier}.
180      * @param element
181      *            {@link Element}
182      * @param operation
183      *            {@link Operation}
184      */
185     private void addValuesFromDataToElement(final Document doc, final Set<YangInstanceIdentifier> data,
186             final Element element, final Operation operation) {
187         if ((data == null) || data.isEmpty()) {
188             return;
189         }
190         for (final YangInstanceIdentifier path : data) {
191             if (!ControllerContext.getInstance().isNodeMixin(path)) {
192                 final Node node = createDataChangeEventElement(doc, path, operation);
193                 element.appendChild(node);
194             }
195         }
196     }
197
198     private void addCreatedChangedValuesFromDataToElement(final Document doc,
199             final Set<Entry<YangInstanceIdentifier, NormalizedNode<?, ?>>> data, final Element element,
200             final Operation operation, final SchemaContext schemaContext,
201             final DataSchemaContextTree dataSchemaContextTree) {
202         if ((data == null) || data.isEmpty()) {
203             return;
204         }
205         for (final Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> entry : data) {
206             if (!ControllerContext.getInstance().isNodeMixin(entry.getKey())
207                     && (!getLeafNodesOnly() || entry.getValue() instanceof LeafNode)) {
208                 final Node node = createCreatedChangedDataChangeEventElement(doc, entry, operation, schemaContext,
209                         dataSchemaContextTree);
210                 element.appendChild(node);
211             }
212         }
213     }
214
215     /**
216      * Creates changed event element from data.
217      *
218      * @param doc
219      *            {@link Document}
220      * @param path
221      *            Path to data in data store.
222      * @param operation
223      *            {@link Operation}
224      * @return {@link Node} node represented by changed event element.
225      */
226     private Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier path,
227             final Operation operation) {
228         final Element dataChangeEventElement = doc.createElement("data-change-event");
229         final Element pathElement = doc.createElement("path");
230         addPathAsValueToElement(path, pathElement);
231         dataChangeEventElement.appendChild(pathElement);
232
233         final Element operationElement = doc.createElement("operation");
234         operationElement.setTextContent(operation.value);
235         dataChangeEventElement.appendChild(operationElement);
236
237         return dataChangeEventElement;
238     }
239
240     private Node createCreatedChangedDataChangeEventElement(final Document doc,
241             final Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> entry, final Operation operation,
242             final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
243         final Element dataChangeEventElement = doc.createElement("data-change-event");
244         final Element pathElement = doc.createElement("path");
245         final YangInstanceIdentifier path = entry.getKey();
246         addPathAsValueToElement(path, pathElement);
247         dataChangeEventElement.appendChild(pathElement);
248
249         final Element operationElement = doc.createElement("operation");
250         operationElement.setTextContent(operation.value);
251         dataChangeEventElement.appendChild(operationElement);
252
253         try {
254             SchemaPath nodePath;
255             final NormalizedNode<?, ?> normalized = entry.getValue();
256             if ((normalized instanceof MapEntryNode) || (normalized instanceof UnkeyedListEntryNode)) {
257                 nodePath = dataSchemaContextTree.getChild(path).getDataSchemaNode().getPath();
258             } else {
259                 nodePath = dataSchemaContextTree.getChild(path).getDataSchemaNode().getPath().getParent();
260             }
261             final DOMResult domResult = writeNormalizedNode(normalized, schemaContext, nodePath);
262             final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
263             final Element dataElement = doc.createElement("data");
264             dataElement.appendChild(result);
265             dataChangeEventElement.appendChild(dataElement);
266         } catch (final IOException e) {
267             LOG.error("Error in writer ", e);
268         } catch (final XMLStreamException e) {
269             LOG.error("Error processing stream", e);
270         }
271
272         return dataChangeEventElement;
273     }
274
275     /**
276      * Adds path as value to element.
277      *
278      * @param path
279      *            Path to data in data store.
280      * @param element
281      *            {@link Element}
282      */
283     @SuppressWarnings("rawtypes")
284     private void addPathAsValueToElement(final YangInstanceIdentifier path, final Element element) {
285         final YangInstanceIdentifier normalizedPath = ControllerContext.getInstance().toXpathRepresentation(path);
286         final StringBuilder textContent = new StringBuilder();
287
288         for (final PathArgument pathArgument : normalizedPath.getPathArguments()) {
289             if (pathArgument instanceof YangInstanceIdentifier.AugmentationIdentifier) {
290                 continue;
291             }
292             textContent.append("/");
293             writeIdentifierWithNamespacePrefix(element, textContent, pathArgument.getNodeType());
294             if (pathArgument instanceof NodeIdentifierWithPredicates) {
295                 final Map<QName, Object> predicates = ((NodeIdentifierWithPredicates) pathArgument).getKeyValues();
296                 for (final QName keyValue : predicates.keySet()) {
297                     final String predicateValue = String.valueOf(predicates.get(keyValue));
298                     textContent.append("[");
299                     writeIdentifierWithNamespacePrefix(element, textContent, keyValue);
300                     textContent.append("='");
301                     textContent.append(predicateValue);
302                     textContent.append("'");
303                     textContent.append("]");
304                 }
305             } else if (pathArgument instanceof NodeWithValue) {
306                 textContent.append("[.='");
307                 textContent.append(((NodeWithValue) pathArgument).getValue());
308                 textContent.append("'");
309                 textContent.append("]");
310             }
311         }
312         element.setTextContent(textContent.toString());
313     }
314
315     /**
316      * Writes identifier that consists of prefix and QName.
317      *
318      * @param element
319      *            {@link Element}
320      * @param textContent
321      *            StringBuilder
322      * @param qualifiedName
323      *            QName
324      */
325     private static void writeIdentifierWithNamespacePrefix(final Element element, final StringBuilder textContent,
326             final QName qualifiedName) {
327         final Module module = ControllerContext.getInstance().getGlobalSchema()
328                 .findModuleByNamespaceAndRevision(qualifiedName.getNamespace(), qualifiedName.getRevision());
329
330         textContent.append(module.getName());
331         textContent.append(":");
332         textContent.append(qualifiedName.getLocalName());
333     }
334
335     /**
336      * Consists of three types {@link Operation#CREATED},
337      * {@link Operation#UPDATED} and {@link Operation#DELETED}.
338      */
339     private enum Operation {
340         CREATED("created"), UPDATED("updated"), DELETED("deleted");
341
342         private final String value;
343
344         Operation(final String value) {
345             this.value = value;
346         }
347     }
348 }