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