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