Extend Websocket streams for data-less notifications 28/89928/13
authorNikhil Soni <nsoni@luminanetworks.com>
Mon, 25 May 2020 09:41:29 +0000 (15:11 +0530)
committerRobert Varga <nite@hq.sk>
Wed, 30 Sep 2020 09:34:56 +0000 (09:34 +0000)
A new filter named "odl-skip-notification-data", similar to "odl-leaf-nodes-only", is added in the subscription API.
Using this filter, Client can get notification without data.

JIRA: NETCONF-689
Change-Id: I0cec77f69cb141fabc9f839c9a91626d3c667655
Signed-off-by: Nikhil Soni <nsoni@luminanetworks.com>
16 files changed:
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/AbstractQueryParams.java
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapter.java
restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/ExpressionParserTest.java
restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapterTest.java
restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-create.json [new file with mode: 0644]
restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-del.json [new file with mode: 0644]
restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-update.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImpl.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/SubscribeToStreamUtil.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractQueryParams.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json [new file with mode: 0644]

index 6da4cba2547e4b9d24f387bfbe9bdadb457e9540..1f3710bd6f37c42e4f2a7c374eda1a6f381900af 100644 (file)
@@ -1050,6 +1050,8 @@ public final class RestconfImpl implements RestconfService {
         String filter = null;
         boolean leafNodesOnlyUsed = false;
         boolean leafNodesOnly = false;
+        boolean skipNotificationDataUsed = false;
+        boolean skipNotificationData = false;
 
         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
             switch (entry.getKey()) {
@@ -1085,6 +1087,15 @@ public final class RestconfImpl implements RestconfService {
                         throw new RestconfDocumentedException("Odl-leaf-nodes-only parameter can be used only once.");
                     }
                     break;
+                case "odl-skip-notification-data":
+                    if (!skipNotificationDataUsed) {
+                        skipNotificationDataUsed = true;
+                        skipNotificationData = Boolean.parseBoolean(entry.getValue().iterator().next());
+                    } else {
+                        throw new RestconfDocumentedException(
+                                "Odl-skip-notification-data parameter can be used only once.");
+                    }
+                    break;
                 default:
                     throw new RestconfDocumentedException("Bad parameter used with notifications: " + entry.getKey());
             }
@@ -1094,7 +1105,7 @@ public final class RestconfImpl implements RestconfService {
         }
         URI response = null;
         if (identifier.contains(DATA_SUBSCR)) {
-            response = dataSubs(identifier, uriInfo, start, stop, filter, leafNodesOnly);
+            response = dataSubs(identifier, uriInfo, start, stop, filter, leafNodesOnly, skipNotificationData);
         } else if (identifier.contains(NOTIFICATION_STREAM)) {
             response = notifStream(identifier, uriInfo, start, stop, filter);
         }
@@ -1180,7 +1191,7 @@ public final class RestconfImpl implements RestconfService {
 
         for (final NotificationListenerAdapter listener : listeners) {
             this.broker.registerToListenNotification(listener);
-            listener.setQueryParams(start, Optional.ofNullable(stop), Optional.ofNullable(filter), false);
+            listener.setQueryParams(start, Optional.ofNullable(stop), Optional.ofNullable(filter), false, false);
         }
 
         final UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder();
@@ -1219,7 +1230,7 @@ public final class RestconfImpl implements RestconfService {
      * @return {@link URI} of location
      */
     private URI dataSubs(final String identifier, final UriInfo uriInfo, final Instant start, final Instant stop,
-            final String filter, final boolean leafNodesOnly) {
+            final String filter, final boolean leafNodesOnly, final boolean skipNotificationData) {
         final String streamName = Notificator.createStreamNameFromUri(identifier);
         if (Strings.isNullOrEmpty(streamName)) {
             throw new RestconfDocumentedException("Stream name is empty.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
@@ -1230,7 +1241,8 @@ public final class RestconfImpl implements RestconfService {
             throw new RestconfDocumentedException("Stream was not found.", ErrorType.PROTOCOL,
                     ErrorTag.UNKNOWN_ELEMENT);
         }
-        listener.setQueryParams(start, Optional.ofNullable(stop), Optional.ofNullable(filter), leafNodesOnly);
+        listener.setQueryParams(start, Optional.ofNullable(stop), Optional.ofNullable(filter), leafNodesOnly,
+                skipNotificationData);
 
         final Map<String, String> paramToValues = resolveValuesFromUri(identifier);
         final LogicalDatastoreType datastore =
index 4327578d87db207b49eda14ad54662faa1d63cdb..469764628cfe452c39b4a6afd8959a9df3eea232 100644 (file)
@@ -54,6 +54,7 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
     private Instant stop = null;
     private String filter = null;
     private boolean leafNodesOnly = false;
+    private boolean skipNotificationData = false;
 
     @VisibleForTesting
     public final Instant getStart() {
@@ -71,14 +72,17 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
      *            indicate which subset of all possible events are of interest
      * @param leafNodesOnly
      *            if true, notifications will contain changes to leaf nodes only
+     * @param skipNotificationData
+     *            if true, notification will not contain changed data
      */
     @SuppressWarnings("checkstyle:hiddenField")
     public void setQueryParams(final Instant start, final Optional<Instant> stop, final Optional<String> filter,
-                               final boolean leafNodesOnly) {
+                               final boolean leafNodesOnly, final boolean skipNotificationData) {
         this.start = requireNonNull(start);
         this.stop = stop.orElse(null);
         this.filter = filter.orElse(null);
         this.leafNodesOnly = leafNodesOnly;
+        this.skipNotificationData = skipNotificationData;
     }
 
     /**
@@ -90,6 +94,15 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
         return leafNodesOnly;
     }
 
+    /**
+     * Check whether this query should notify changes without data.
+     *
+     * @return true if this query should notify about changes with  data
+     */
+    public boolean isSkipNotificationData() {
+        return skipNotificationData;
+    }
+
     @SuppressWarnings("checkstyle:IllegalCatch")
     <T extends BaseListenerInterface> boolean checkStartStop(final Instant now, final T listener) {
         if (this.stop != null) {
index 6e4d04d057f0386d5488ac4f8bbdbfc185868d4e..5d61cb52a317edfccab01f8aee05b45bf9840f7c 100644 (file)
@@ -49,6 +49,9 @@ import org.w3c.dom.Node;
 public class ListenerAdapter extends AbstractCommonSubscriber implements ClusteredDOMDataTreeChangeListener {
 
     private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
+    private static final String DATA_CHANGE_EVENT = "data-change-event";
+    private static final String PATH = "path";
+    private static final String OPERATION = "operation";
 
     private final ControllerContext controllerContext;
     private final YangInstanceIdentifier path;
@@ -173,8 +176,15 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements Cluster
                 continue;
             }
             YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath();
-            addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
-                    yiid.getParent(), schemaContext, dataSchemaContextTree);
+
+            boolean isSkipNotificationData = this.isSkipNotificationData();
+            if (isSkipNotificationData) {
+                createCreatedChangedDataChangeEventElementWithoutData(doc,
+                        dataChangedNotificationEventElement, dataTreeCandidate.getRootNode());
+            } else {
+                addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
+                        yiid.getParent(), schemaContext, dataSchemaContextTree);
+            }
         }
     }
 
@@ -252,27 +262,59 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements Cluster
      */
     private Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier dataPath,
             final Operation operation) {
-        final Element dataChangeEventElement = doc.createElement("data-change-event");
-        final Element pathElement = doc.createElement("path");
+        final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
+        final Element pathElement = doc.createElement(PATH);
         addPathAsValueToElement(dataPath, pathElement);
         dataChangeEventElement.appendChild(pathElement);
 
-        final Element operationElement = doc.createElement("operation");
+        final Element operationElement = doc.createElement(OPERATION);
         operationElement.setTextContent(operation.value);
         dataChangeEventElement.appendChild(operationElement);
 
         return dataChangeEventElement;
     }
 
+    /**
+     * Creates data change notification element without data element.
+     *
+     * @param doc
+     *       {@link Document}
+     * @param dataChangedNotificationEventElement
+     *       {@link Element}
+     * @param candidateNode
+     *       {@link DataTreeCandidateNode}
+     */
+    private void createCreatedChangedDataChangeEventElementWithoutData(final Document doc,
+            final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode) {
+        final Operation operation;
+        switch (candidateNode.getModificationType()) {
+            case APPEARED:
+            case SUBTREE_MODIFIED:
+            case WRITE:
+                operation = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
+                break;
+            case DELETE:
+            case DISAPPEARED:
+                operation = Operation.DELETED;
+                break;
+            case UNMODIFIED:
+            default:
+                return;
+        }
+        Node dataChangeEventElement = createDataChangeEventElement(doc, getPath(), operation);
+        dataChangedNotificationEventElement.appendChild(dataChangeEventElement);
+
+    }
+
     private Node createCreatedChangedDataChangeEventElement(final Document doc,
             final YangInstanceIdentifier eventPath, final NormalizedNode normalized, final Operation operation,
             final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
-        final Element dataChangeEventElement = doc.createElement("data-change-event");
-        final Element pathElement = doc.createElement("path");
+        final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
+        final Element pathElement = doc.createElement(PATH);
         addPathAsValueToElement(eventPath, pathElement);
         dataChangeEventElement.appendChild(pathElement);
 
-        final Element operationElement = doc.createElement("operation");
+        final Element operationElement = doc.createElement(OPERATION);
         operationElement.setTextContent(operation.value);
         dataChangeEventElement.appendChild(operationElement);
 
index 220537909cf16b25c896ee3ce0e85bacf9cda01a..8308da6443eb13102921f21f93adf032ef603099 100644 (file)
@@ -136,7 +136,7 @@ public class ExpressionParserTest {
         Mockito.when(path.getLastPathArgument()).thenReturn(pathValue);
         final ListenerAdapter listener = Notificator.createListener(path, "streamName", NotificationOutputType.JSON,
                 null);
-        listener.setQueryParams(Instant.now(), Optional.empty(), Optional.ofNullable(filter), false);
+        listener.setQueryParams(Instant.now(), Optional.empty(), Optional.ofNullable(filter), false, false);
 
         // FIXME: do not use reflection here
         final Class<?> superclass = listener.getClass().getSuperclass().getSuperclass();
index 194f756fcc6b7ac64eb223cfc0b9413a94df6380..453f077bee176524bda8c58f3870394dacd6b8e8 100644 (file)
@@ -51,6 +51,13 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
     private static final String JSON_NOTIF_CREATE = "/listener-adapter-test/notif-create.json";
     private static final String JSON_NOTIF_UPDATE = "/listener-adapter-test/notif-update.json";
     private static final String JSON_NOTIF_DEL = "/listener-adapter-test/notif-del.json";
+    private static final String JSON_NOTIF_WITHOUT_DATA_CREATE =
+            "/listener-adapter-test/notif-without-data-create.json";
+    private static final String JSON_NOTIF_WITHOUT_DATA_UPDATE =
+            "/listener-adapter-test/notif-without-data-update.json";
+    private static final String JSON_NOTIF_WITHOUT_DATA_DELETE =
+            "/listener-adapter-test/notif-without-data-del.json";
+
 
     private static final YangInstanceIdentifier PATCH_CONT_YIID =
             YangInstanceIdentifier.create(new YangInstanceIdentifier.NodeIdentifier(PatchCont.QNAME));
@@ -79,10 +86,10 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
         private String lastNotification = null;
 
         ListenerAdapterTester(final YangInstanceIdentifier path, final String streamName,
-                              final NotificationOutputTypeGrouping.NotificationOutputType outputType,
-                              final boolean leafNodesOnly) {
+                final NotificationOutputTypeGrouping.NotificationOutputType outputType,
+                final boolean leafNodesOnly, final boolean skipNotificationData) {
             super(path, streamName, outputType, controllerContext);
-            setQueryParams(EPOCH, Optional.empty(), Optional.empty(), leafNodesOnly);
+            setQueryParams(EPOCH, Optional.empty(), Optional.empty(), leafNodesOnly, skipNotificationData);
         }
 
         @Override
@@ -127,7 +134,7 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
     @Test
     public void testJsonNotifsLeaves() throws Exception {
         ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
-                                        NotificationOutputTypeGrouping.NotificationOutputType.JSON, true);
+                NotificationOutputTypeGrouping.NotificationOutputType.JSON, true, false);
         DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
                 .getInstance(DOMDataTreeChangeService.class);
         DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
@@ -156,7 +163,7 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
     @Test
     public void testJsonNotifs() throws Exception {
         ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
-                NotificationOutputTypeGrouping.NotificationOutputType.JSON, false);
+                NotificationOutputTypeGrouping.NotificationOutputType.JSON, false, false);
         DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
                 .getInstance(DOMDataTreeChangeService.class);
         DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
@@ -181,4 +188,33 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
         writeTransaction.commit();
         adapter.assertGot(getNotifJson(JSON_NOTIF_DEL));
     }
+
+    @Test
+    public void testJsonNotifsWithoutData() throws Exception {
+        ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+                NotificationOutputTypeGrouping.NotificationOutputType.JSON, false, true);
+        DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+                .getInstance(DOMDataTreeChangeService.class);
+        DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+        changeService.registerDataTreeChangeListener(root, adapter);
+
+        WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
+        MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
+        InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+                .child(MyList1.class, new MyList1Key("Althea"));
+        writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_WITHOUT_DATA_CREATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        builder = new MyList1Builder().withKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+        writeTransaction.mergeParentStructureMerge(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_WITHOUT_DATA_UPDATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+        writeTransaction.commit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_WITHOUT_DATA_DELETE));
+    }
 }
diff --git a/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-create.json b/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-create.json
new file mode 100644 (file)
index 0000000..6e4dadc
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "notification":{
+        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
+        "data-changed-notification":{
+            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
+            "data-change-event":{
+                "path":"/instance-identifier-patch-module:patch-cont",
+                "operation":"created"
+            }
+        },
+        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+    }
+}
diff --git a/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-del.json b/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-del.json
new file mode 100644 (file)
index 0000000..dc3f739
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "notification":{
+        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
+        "data-changed-notification":{
+            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
+            "data-change-event":{
+                "path":"/instance-identifier-patch-module:patch-cont",
+                "operation":"deleted"
+            }
+        },
+        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+    }
+}
diff --git a/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-update.json b/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-update.json
new file mode 100644 (file)
index 0000000..c22c956
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "notification":{
+        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
+        "data-changed-notification":{
+            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
+            "data-change-event":{
+                "path":"/instance-identifier-patch-module:patch-cont",
+                "operation":"updated"
+            }
+        },
+        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+    }
+}
index 04d8cc121d7c5fae06df930f9e21e9c92e709c6b..b69a6609dc4326129f163a51e8c3f0cd801652e1 100644 (file)
@@ -222,11 +222,14 @@ public class RestconfStreamsSubscriptionServiceImpl implements RestconfStreamsSu
         private final Instant start;
         private final Instant stop;
         private final String filter;
+        private final boolean skipNotificationData;
 
-        private NotificationQueryParams(final Instant start, final Instant stop, final String filter) {
+        private NotificationQueryParams(final Instant start, final Instant stop, final String filter,
+                final boolean skipNotificationData) {
             this.start = start == null ? Instant.now() : start;
             this.stop = stop;
             this.filter = filter;
+            this.skipNotificationData = skipNotificationData;
         }
 
         static NotificationQueryParams fromUriInfo(final UriInfo uriInfo) {
@@ -236,6 +239,8 @@ public class RestconfStreamsSubscriptionServiceImpl implements RestconfStreamsSu
             boolean stopTimeUsed = false;
             String filter = null;
             boolean filterUsed = false;
+            boolean skipNotificationDataUsed = false;
+            boolean skipNotificationData = false;
 
             for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
                 switch (entry.getKey()) {
@@ -261,6 +266,15 @@ public class RestconfStreamsSubscriptionServiceImpl implements RestconfStreamsSu
                             filter = entry.getValue().iterator().next();
                         }
                         break;
+                    case "odl-skip-notification-data":
+                        if (!skipNotificationDataUsed) {
+                            skipNotificationDataUsed = true;
+                            skipNotificationData = Boolean.parseBoolean(entry.getValue().iterator().next());
+                        } else {
+                            throw new RestconfDocumentedException(
+                                    "Odl-skip-notification-data parameter can be used only once.");
+                        }
+                        break;
                     default:
                         throw new RestconfDocumentedException(
                                 "Bad parameter used with notifications: " + entry.getKey());
@@ -270,7 +284,7 @@ public class RestconfStreamsSubscriptionServiceImpl implements RestconfStreamsSu
                 throw new RestconfDocumentedException("Stop-time parameter has to be used with start-time parameter.");
             }
 
-            return new NotificationQueryParams(start, stop, filter);
+            return new NotificationQueryParams(start, stop, filter, skipNotificationData);
         }
 
 
@@ -319,5 +333,14 @@ public class RestconfStreamsSubscriptionServiceImpl implements RestconfStreamsSu
         public Optional<String> getFilter() {
             return Optional.ofNullable(filter);
         }
+
+        /**
+         * Check whether this query should notify changes without data.
+         *
+         * @return true if this query should notify about changes with  data
+         */
+        public boolean isSkipNotificationData() {
+            return skipNotificationData;
+        }
     }
-}
\ No newline at end of file
+}
index b71de0454b009fb6abdb3f84b178c92ae306cdbb..9f1207f454640ea8835ad81fc1b7464ef8c180e1 100644 (file)
@@ -155,7 +155,7 @@ abstract class SubscribeToStreamUtil {
                 notificationQueryParams.getStart(),
                 notificationQueryParams.getStop().orElse(null),
                 notificationQueryParams.getFilter().orElse(null),
-                false);
+                false, notificationQueryParams.isSkipNotificationData());
         notificationListenerAdapter.get().setCloseVars(
                 handlersHolder.getTransactionChainHandler(), handlersHolder.getSchemaHandler());
         final NormalizedNode<?, ?> mapToStreams =
@@ -210,7 +210,7 @@ abstract class SubscribeToStreamUtil {
                 notificationQueryParams.getStart(),
                 notificationQueryParams.getStop().orElse(null),
                 notificationQueryParams.getFilter().orElse(null),
-                false);
+                false, notificationQueryParams.isSkipNotificationData());
         listener.get().setCloseVars(handlersHolder.getTransactionChainHandler(), handlersHolder.getSchemaHandler());
         registration(datastoreType, listener.get(), handlersHolder.getDomDataBrokerHandler().get());
 
index 15b217da1b6714b917974f769a99c9849d344987..9df8cb8e0c4d926a2c16134f44f621786bcd7281 100644 (file)
@@ -52,6 +52,7 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
     private Instant stop = null;
     private String filter = null;
     private boolean leafNodesOnly = false;
+    private boolean skipNotificationData = false;
 
     @VisibleForTesting
     public final Instant getStart() {
@@ -68,11 +69,12 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
      */
     @SuppressWarnings("checkstyle:hiddenField")
     public void setQueryParams(final Instant start, final Instant stop, final String filter,
-            final boolean leafNodesOnly) {
+            final boolean leafNodesOnly, final boolean skipNotificationData) {
         this.start = requireNonNull(start);
         this.stop = stop;
         this.filter = filter;
         this.leafNodesOnly = leafNodesOnly;
+        this.skipNotificationData = skipNotificationData;
     }
 
     /**
@@ -84,6 +86,15 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
         return leafNodesOnly;
     }
 
+    /**
+     * Check whether this query should notify changes without data.
+     *
+     * @return true if this query should notify about changes with  data
+     */
+    public boolean isSkipNotificationData() {
+        return skipNotificationData;
+    }
+
     @SuppressWarnings("checkstyle:IllegalCatch")
     <T extends BaseListenerInterface> boolean checkStartStop(final Instant now, final T listener) {
         if (this.stop != null) {
@@ -137,4 +148,4 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
         // FIXME: BUG-7956: xPath.setNamespaceContext(nsContext);
         return (boolean) xPath.compile(this.filter).evaluate(docOfXml, XPathConstants.BOOLEAN);
     }
-}
\ No newline at end of file
+}
index 201fa53016b93ddbd568d9a5a915ff532ac425da..d9d6eb3533182fa62a08b294d0d44c3dc75d859b 100644 (file)
@@ -50,6 +50,9 @@ import org.w3c.dom.Node;
 public class ListenerAdapter extends AbstractCommonSubscriber implements ClusteredDOMDataTreeChangeListener {
 
     private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
+    private static final String DATA_CHANGE_EVENT = "data-change-event";
+    private static final String PATH = "path";
+    private static final String OPERATION = "operation";
 
     private final YangInstanceIdentifier path;
     private final String streamName;
@@ -156,8 +159,14 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements Cluster
                 continue;
             }
             YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath();
-            addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
-                    yiid.getParent(), schemaContext, dataSchemaContextTree);
+            boolean isSkipNotificationData = this.isSkipNotificationData();
+            if (isSkipNotificationData) {
+                createCreatedChangedDataChangeEventElementWithoutData(doc, dataChangedNotificationEventElement,
+                        dataTreeCandidate.getRootNode(), schemaContext);
+            } else {
+                addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
+                        yiid.getParent(), schemaContext, dataSchemaContextTree);
+            }
         }
     }
 
@@ -207,7 +216,7 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements Cluster
                     break;
                 case DELETE:
                 case DISAPPEARED:
-                    node = createDataChangeEventElement(doc, yiid, schemaContext);
+                    node = createDataChangeEventElement(doc, yiid, schemaContext, Operation.DELETED);
                     break;
                 case UNMODIFIED:
                 default:
@@ -229,27 +238,60 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements Cluster
      *
      * @param doc           {@link Document}
      * @param schemaContext Schema context.
+     * @param operation  Operation value
      * @return {@link Node} represented by changed event element.
      */
     private static Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
-            final SchemaContext schemaContext) {
-        final Element dataChangeEventElement = doc.createElement("data-change-event");
-        final Element pathElement = doc.createElement("path");
+            final SchemaContext schemaContext, Operation operation) {
+        final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
+        final Element pathElement = doc.createElement(PATH);
         addPathAsValueToElement(eventPath, pathElement, schemaContext);
         dataChangeEventElement.appendChild(pathElement);
 
-        final Element operationElement = doc.createElement("operation");
-        operationElement.setTextContent(Operation.DELETED.value);
+        final Element operationElement = doc.createElement(OPERATION);
+        operationElement.setTextContent(operation.value);
         dataChangeEventElement.appendChild(operationElement);
 
         return dataChangeEventElement;
     }
 
+    /**
+     * Creates data change notification element without data element.
+     *
+     * @param doc
+     *       {@link Document}
+     * @param dataChangedNotificationEventElement
+     *       {@link Element}
+     * @param candidateNode
+     *       {@link DataTreeCandidateNode}
+     */
+    private void createCreatedChangedDataChangeEventElementWithoutData(final Document doc,
+            final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
+            final SchemaContext schemaContext) {
+        final Operation operation;
+        switch (candidateNode.getModificationType()) {
+            case APPEARED:
+            case SUBTREE_MODIFIED:
+            case WRITE:
+                operation = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
+                break;
+            case DELETE:
+            case DISAPPEARED:
+                operation = Operation.DELETED;
+                break;
+            case UNMODIFIED:
+            default:
+                return;
+        }
+        Node dataChangeEventElement = createDataChangeEventElement(doc, getPath(), schemaContext, operation);
+        dataChangedNotificationEventElement.appendChild(dataChangeEventElement);
+    }
+
     private Node createCreatedChangedDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
             final NormalizedNode<?, ?> normalized, final Operation operation, final SchemaContext schemaContext,
             final DataSchemaContextTree dataSchemaContextTree) {
-        final Element dataChangeEventElement = doc.createElement("data-change-event");
-        final Element pathElement = doc.createElement("path");
+        final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
+        final Element pathElement = doc.createElement(PATH);
         addPathAsValueToElement(eventPath, pathElement, schemaContext);
         dataChangeEventElement.appendChild(pathElement);
 
@@ -352,9 +394,9 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements Cluster
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
-                .add("path", path)
+                .add(PATH, path)
                 .add("stream-name", streamName)
                 .add("output-type", outputType)
                 .toString();
     }
-}
\ No newline at end of file
+}
index 6822850449311a2b90d03e2e1b8376e9c6ffa68f..0cbf42d7f17950bd22d000a7ff63311fff9ec529 100644 (file)
@@ -57,6 +57,12 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
     private static final String JSON_NOTIF_CREATE = "/listener-adapter-test/notif-create.json";
     private static final String JSON_NOTIF_UPDATE = "/listener-adapter-test/notif-update.json";
     private static final String JSON_NOTIF_DEL = "/listener-adapter-test/notif-del.json";
+    private static final String JSON_NOTIF_WITHOUT_DATA_CREATE =
+            "/listener-adapter-test/notif-without-data-create.json";
+    private static final String JSON_NOTIF_WITHOUT_DATA_UPDATE =
+            "/listener-adapter-test/notif-without-data-update.json";
+    private static final String JSON_NOTIF_WITHOUT_DATA_DELETE =
+            "/listener-adapter-test/notif-without-data-del.json";
 
     private static final YangInstanceIdentifier PATCH_CONT_YIID =
             YangInstanceIdentifier.create(new YangInstanceIdentifier.NodeIdentifier(PatchCont.QNAME));
@@ -96,9 +102,9 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
 
         ListenerAdapterTester(final YangInstanceIdentifier path, final String streamName,
                               final NotificationOutputTypeGrouping.NotificationOutputType outputType,
-                              final boolean leafNodesOnly) {
+                              final boolean leafNodesOnly, final boolean skipNotificationData) {
             super(path, streamName, outputType);
-            setQueryParams(EPOCH, null, null, leafNodesOnly);
+            setQueryParams(EPOCH, null, null, leafNodesOnly, skipNotificationData);
         }
 
         @Override
@@ -141,7 +147,7 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
     @Test
     public void testJsonNotifsLeaves() throws Exception {
         ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
-                                        NotificationOutputTypeGrouping.NotificationOutputType.JSON, true);
+                NotificationOutputTypeGrouping.NotificationOutputType.JSON, true, false);
         adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
 
         DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
@@ -172,7 +178,7 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
     @Test
     public void testJsonNotifs() throws Exception {
         ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
-                NotificationOutputTypeGrouping.NotificationOutputType.JSON, false);
+                NotificationOutputTypeGrouping.NotificationOutputType.JSON, false, false);
         adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
 
         DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
@@ -199,4 +205,34 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
         writeTransaction.commit();
         adapter.assertGot(getNotifJson(JSON_NOTIF_DEL));
     }
+
+    @Test
+    public void testJsonNotifsWithoutData() throws Exception {
+        ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+                NotificationOutputTypeGrouping.NotificationOutputType.JSON, false, true);
+        adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+        DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+                .getInstance(DOMDataTreeChangeService.class);
+        DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+        changeService.registerDataTreeChangeListener(root, adapter);
+        WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
+        MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
+        InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+                .child(MyList1.class, new MyList1Key("Althea"));
+        writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_WITHOUT_DATA_CREATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        builder = new MyList1Builder().withKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+        writeTransaction.mergeParentStructureMerge(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_WITHOUT_DATA_UPDATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+        writeTransaction.commit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_WITHOUT_DATA_DELETE));
+    }
 }
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json
new file mode 100644 (file)
index 0000000..6e4dadc
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "notification":{
+        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
+        "data-changed-notification":{
+            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
+            "data-change-event":{
+                "path":"/instance-identifier-patch-module:patch-cont",
+                "operation":"created"
+            }
+        },
+        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json
new file mode 100644 (file)
index 0000000..dc3f739
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "notification":{
+        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
+        "data-changed-notification":{
+            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
+            "data-change-event":{
+                "path":"/instance-identifier-patch-module:patch-cont",
+                "operation":"deleted"
+            }
+        },
+        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json
new file mode 100644 (file)
index 0000000..c22c956
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "notification":{
+        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
+        "data-changed-notification":{
+            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
+            "data-change-event":{
+                "path":"/instance-identifier-patch-module:patch-cont",
+                "operation":"updated"
+            }
+        },
+        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+    }
+}