From 1982b130c5b7b4342c1b9bb107b717f44ce4b7af Mon Sep 17 00:00:00 2001 From: Nikhil Soni Date: Mon, 25 May 2020 15:11:29 +0530 Subject: [PATCH] Extend Websocket streams for data-less notifications 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 --- .../sal/restconf/impl/RestconfImpl.java | 20 ++++-- .../listeners/AbstractQueryParams.java | 15 ++++- .../streams/listeners/ListenerAdapter.java | 58 +++++++++++++--- .../impl/test/ExpressionParserTest.java | 2 +- .../listeners/ListenerAdapterTest.java | 46 +++++++++++-- .../notif-without-data-create.json | 13 ++++ .../notif-without-data-del.json | 13 ++++ .../notif-without-data-update.json | 13 ++++ ...estconfStreamsSubscriptionServiceImpl.java | 29 +++++++- .../services/impl/SubscribeToStreamUtil.java | 4 +- .../listeners/AbstractQueryParams.java | 15 ++++- .../streams/listeners/ListenerAdapter.java | 66 +++++++++++++++---- .../listeners/ListenerAdapterTest.java | 44 +++++++++++-- .../notif-without-data-create.json | 13 ++++ .../notif-without-data-del.json | 13 ++++ .../notif-without-data-update.json | 13 ++++ 16 files changed, 335 insertions(+), 42 deletions(-) create mode 100644 restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-create.json create mode 100644 restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-del.json create mode 100644 restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-update.json create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json diff --git a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java index 6da4cba254..1f3710bd6f 100644 --- a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java +++ b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java @@ -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> 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 paramToValues = resolveValuesFromUri(identifier); final LogicalDatastoreType datastore = diff --git a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/AbstractQueryParams.java b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/AbstractQueryParams.java index 4327578d87..469764628c 100644 --- a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/AbstractQueryParams.java +++ b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/AbstractQueryParams.java @@ -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 stop, final Optional 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") boolean checkStartStop(final Instant now, final T listener) { if (this.stop != null) { diff --git a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapter.java b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapter.java index 6e4d04d057..5d61cb52a3 100644 --- a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapter.java +++ b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapter.java @@ -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); diff --git a/restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/ExpressionParserTest.java b/restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/ExpressionParserTest.java index 220537909c..8308da6443 100644 --- a/restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/ExpressionParserTest.java +++ b/restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/ExpressionParserTest.java @@ -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(); diff --git a/restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapterTest.java b/restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapterTest.java index 194f756fcc..453f077bee 100644 --- a/restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapterTest.java +++ b/restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapterTest.java @@ -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 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 index 0000000000..6e4dadc36e --- /dev/null +++ b/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-create.json @@ -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 index 0000000000..dc3f739779 --- /dev/null +++ b/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-del.json @@ -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 index 0000000000..c22c956216 --- /dev/null +++ b/restconf/restconf-nb-bierman02/src/test/resources/listener-adapter-test/notif-without-data-update.json @@ -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" + } +} diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImpl.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImpl.java index 04d8cc121d..b69a6609dc 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImpl.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImpl.java @@ -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> 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 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 +} diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/SubscribeToStreamUtil.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/SubscribeToStreamUtil.java index b71de0454b..9f1207f454 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/SubscribeToStreamUtil.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/SubscribeToStreamUtil.java @@ -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()); diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractQueryParams.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractQueryParams.java index 15b217da1b..9df8cb8e0c 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractQueryParams.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractQueryParams.java @@ -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") 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 +} diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java index 201fa53016..d9d6eb3533 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java @@ -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 +} diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java index 6822850449..0cbf42d7f1 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java @@ -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 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 index 0000000000..6e4dadc36e --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json @@ -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 index 0000000000..dc3f739779 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json @@ -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 index 0000000000..c22c956216 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json @@ -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" + } +} -- 2.36.6