2 * Copyright (c) 2014, 2016 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.netconf.sal.streams.listeners;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
13 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
14 import java.io.IOException;
15 import java.time.Instant;
16 import java.util.Collection;
17 import java.util.List;
18 import java.util.Map.Entry;
19 import java.util.Optional;
20 import javax.xml.stream.XMLStreamException;
21 import javax.xml.transform.dom.DOMResult;
23 import org.opendaylight.mdsal.dom.api.ClusteredDOMDataTreeChangeListener;
24 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
25 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
26 import org.opendaylight.yangtools.yang.common.QName;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
31 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
32 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
33 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
34 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
35 import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
36 import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidateNode;
37 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
38 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
39 import org.opendaylight.yangtools.yang.model.api.Module;
40 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.w3c.dom.Document;
44 import org.w3c.dom.Element;
45 import org.w3c.dom.Node;
48 * {@link ListenerAdapter} is responsible to track events, which occurred by
49 * changing data in data source.
51 public class ListenerAdapter extends AbstractCommonSubscriber implements ClusteredDOMDataTreeChangeListener {
53 private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
54 private static final String DATA_CHANGE_EVENT = "data-change-event";
55 private static final String PATH = "path";
56 private static final String OPERATION = "operation";
58 private final ControllerContext controllerContext;
59 private final YangInstanceIdentifier path;
60 private final String streamName;
61 private final NotificationOutputType outputType;
64 * Creates new {@link ListenerAdapter} listener specified by path and stream
65 * name and register for subscribing.
68 * Path to data in data store.
70 * The name of the stream.
72 * Type of output on notification (JSON, XML)
74 @SuppressFBWarnings(value = "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR", justification = "non-final for testing")
75 ListenerAdapter(final YangInstanceIdentifier path, final String streamName,
76 final NotificationOutputType outputType, final ControllerContext controllerContext) {
77 this.outputType = requireNonNull(outputType);
78 this.path = requireNonNull(path);
79 checkArgument(streamName != null && !streamName.isEmpty());
80 this.streamName = streamName;
81 this.controllerContext = controllerContext;
86 public void onInitialData() {
91 public void onDataTreeChanged(final List<DataTreeCandidate> dataTreeCandidates) {
92 final Instant now = Instant.now();
93 if (!checkStartStop(now, this)) {
97 final String xml = prepareXml(dataTreeCandidates);
98 if (checkFilter(xml)) {
99 prepareAndPostData(xml);
104 * Gets the name of the stream.
106 * @return The name of the stream.
109 public String getStreamName() {
114 public String getOutputType() {
115 return outputType.getName();
119 * Get path pointed to data in data store.
121 * @return Path pointed to data in data store.
123 public YangInstanceIdentifier getPath() {
128 * Prepare data of notification and data to client.
132 private void prepareAndPostData(final String xml) {
133 final Event event = new Event(EventType.NOTIFY);
134 if (outputType.equals(NotificationOutputType.JSON)) {
135 event.setData(XML.toJSONObject(xml).toString());
143 * Tracks events of data change by customer.
147 * Prepare data in printable form and transform it to String.
149 * @return Data in printable form.
151 private String prepareXml(final Collection<DataTreeCandidate> candidates) {
152 final EffectiveModelContext schemaContext = controllerContext.getGlobalSchema();
153 final DataSchemaContextTree dataContextTree = DataSchemaContextTree.from(schemaContext);
154 final Document doc = createDocument();
155 final Element notificationElement = basePartDoc(doc);
157 final Element dataChangedNotificationEventElement = doc.createElementNS(
158 "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
160 addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, candidates,
161 schemaContext, dataContextTree);
162 notificationElement.appendChild(dataChangedNotificationEventElement);
163 return transformDoc(doc);
167 * Adds values to data changed notification event element.
171 * @param dataChangedNotificationEventElement
173 * @param dataTreeCandidates
174 * {@link DataTreeCandidate}
176 private void addValuesToDataChangedNotificationEventElement(final Document doc,
177 final Element dataChangedNotificationEventElement,
178 final Collection<DataTreeCandidate> dataTreeCandidates,
179 final EffectiveModelContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
181 for (DataTreeCandidate dataTreeCandidate : dataTreeCandidates) {
182 DataTreeCandidateNode candidateNode = dataTreeCandidate.getRootNode();
183 if (candidateNode == null) {
186 YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath();
188 boolean isSkipNotificationData = this.isSkipNotificationData();
189 if (isSkipNotificationData) {
190 createCreatedChangedDataChangeEventElementWithoutData(doc,
191 dataChangedNotificationEventElement, dataTreeCandidate.getRootNode());
193 addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
194 yiid.getParent(), schemaContext, dataSchemaContextTree);
199 private void addNodeToDataChangeNotificationEventElement(final Document doc,
200 final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
201 final YangInstanceIdentifier parentYiid, final EffectiveModelContext schemaContext,
202 final DataSchemaContextTree dataSchemaContextTree) {
204 Optional<NormalizedNode> optionalNormalizedNode = Optional.empty();
205 switch (candidateNode.getModificationType()) {
207 case SUBTREE_MODIFIED:
209 optionalNormalizedNode = candidateNode.getDataAfter();
213 optionalNormalizedNode = candidateNode.getDataBefore();
220 if (optionalNormalizedNode.isEmpty()) {
221 LOG.error("No node present in notification for {}", candidateNode);
225 NormalizedNode normalizedNode = optionalNormalizedNode.get();
226 YangInstanceIdentifier yiid = YangInstanceIdentifier.builder(parentYiid)
227 .append(normalizedNode.getIdentifier()).build();
229 boolean isNodeMixin = controllerContext.isNodeMixin(yiid);
230 boolean isSkippedNonLeaf = getLeafNodesOnly() && !(normalizedNode instanceof LeafNode);
231 if (!isNodeMixin && !isSkippedNonLeaf) {
233 switch (candidateNode.getModificationType()) {
235 case SUBTREE_MODIFIED:
237 Operation op = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
238 node = createCreatedChangedDataChangeEventElement(doc, yiid, normalizedNode, op,
239 schemaContext, dataSchemaContextTree);
243 node = createDataChangeEventElement(doc, yiid, Operation.DELETED);
250 dataChangedNotificationEventElement.appendChild(node);
254 for (DataTreeCandidateNode childNode : candidateNode.getChildNodes()) {
255 addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, childNode,
256 yiid, schemaContext, dataSchemaContextTree);
261 * Creates changed event element from data.
266 * Path to data in data store.
269 * @return {@link Node} node represented by changed event element.
271 private Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier dataPath,
272 final Operation operation) {
273 final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
274 final Element pathElement = doc.createElement(PATH);
275 addPathAsValueToElement(dataPath, pathElement);
276 dataChangeEventElement.appendChild(pathElement);
278 final Element operationElement = doc.createElement(OPERATION);
279 operationElement.setTextContent(operation.value);
280 dataChangeEventElement.appendChild(operationElement);
282 return dataChangeEventElement;
286 * Creates data change notification element without data element.
290 * @param dataChangedNotificationEventElement
292 * @param candidateNode
293 * {@link DataTreeCandidateNode}
295 private void createCreatedChangedDataChangeEventElementWithoutData(final Document doc,
296 final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode) {
297 final Operation operation;
298 switch (candidateNode.getModificationType()) {
300 case SUBTREE_MODIFIED:
302 operation = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
306 operation = Operation.DELETED;
312 Node dataChangeEventElement = createDataChangeEventElement(doc, getPath(), operation);
313 dataChangedNotificationEventElement.appendChild(dataChangeEventElement);
317 private Node createCreatedChangedDataChangeEventElement(final Document doc,
318 final YangInstanceIdentifier eventPath, final NormalizedNode normalized, final Operation operation,
319 final EffectiveModelContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
320 final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
321 final Element pathElement = doc.createElement(PATH);
322 addPathAsValueToElement(eventPath, pathElement);
323 dataChangeEventElement.appendChild(pathElement);
325 final Element operationElement = doc.createElement(OPERATION);
326 operationElement.setTextContent(operation.value);
327 dataChangeEventElement.appendChild(operationElement);
329 final SchemaInferenceStack stack = dataSchemaContextTree.enterPath(eventPath).orElseThrow().stack();
330 if (!(normalized instanceof MapEntryNode) && !(normalized instanceof UnkeyedListEntryNode)
331 && !stack.isEmpty()) {
335 final var inference = stack.toInference();
338 final DOMResult domResult = writeNormalizedNode(normalized, inference);
339 final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
340 final Element dataElement = doc.createElement("data");
341 dataElement.appendChild(result);
342 dataChangeEventElement.appendChild(dataElement);
343 } catch (final IOException e) {
344 LOG.error("Error in writer ", e);
345 } catch (final XMLStreamException e) {
346 LOG.error("Error processing stream", e);
349 return dataChangeEventElement;
353 * Adds path as value to element.
356 * Path to data in data store.
360 @SuppressWarnings("rawtypes")
361 private void addPathAsValueToElement(final YangInstanceIdentifier dataPath, final Element element) {
362 final YangInstanceIdentifier normalizedPath = controllerContext.toXpathRepresentation(dataPath);
363 final StringBuilder textContent = new StringBuilder();
365 for (final PathArgument pathArgument : normalizedPath.getPathArguments()) {
366 if (pathArgument instanceof YangInstanceIdentifier.AugmentationIdentifier) {
369 textContent.append("/");
370 writeIdentifierWithNamespacePrefix(element, textContent, pathArgument.getNodeType());
371 if (pathArgument instanceof NodeIdentifierWithPredicates) {
372 for (final Entry<QName, Object> entry : ((NodeIdentifierWithPredicates) pathArgument).entrySet()) {
373 final QName keyValue = entry.getKey();
374 final String predicateValue = String.valueOf(entry.getValue());
375 textContent.append("[");
376 writeIdentifierWithNamespacePrefix(element, textContent, keyValue);
377 textContent.append("='");
378 textContent.append(predicateValue);
379 textContent.append("'");
380 textContent.append("]");
382 } else if (pathArgument instanceof NodeWithValue) {
383 textContent.append("[.='");
384 textContent.append(((NodeWithValue) pathArgument).getValue());
385 textContent.append("'");
386 textContent.append("]");
389 element.setTextContent(textContent.toString());
393 * Writes identifier that consists of prefix and QName.
399 * @param qualifiedName
402 private void writeIdentifierWithNamespacePrefix(final Element element, final StringBuilder textContent,
403 final QName qualifiedName) {
404 final Module module = controllerContext.getGlobalSchema().findModule(qualifiedName.getModule())
407 textContent.append(module.getName());
408 textContent.append(":");
409 textContent.append(qualifiedName.getLocalName());
413 * Consists of three types {@link Operation#CREATED},
414 * {@link Operation#UPDATED} and {@link Operation#DELETED}.
416 private enum Operation {
417 CREATED("created"), UPDATED("updated"), DELETED("deleted");
419 private final String value;
421 Operation(final String value) {