Merge "Option to receive only leaf nodes in websocket notifs"
authorTomas Cere <tcere@cisco.com>
Thu, 30 Mar 2017 11:43:46 +0000 (11:43 +0000)
committerGerrit Code Review <gerrit@opendaylight.org>
Thu, 30 Mar 2017 11:43:46 +0000 (11:43 +0000)
restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/AbstractQueryParams.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/ListenerAdapter.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/SubscribeToStreamUtil.java
restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/ExpressionParserTest.java
restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/websockets/test/RestStreamTest.java [moved from restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/websockets/test/RestStream.java with 74% similarity]

index d28fcea107cb34af0b2bc6564cc4399b5e3e9a09..359e4de1b9c4f2953bac1494d9a4173e15ae0e59 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2015 Brocade Communication Systems, Inc., Cisco Systems, Inc. and others.  All rights reserved.
+ * Copyright (c) 2014 - 2016 Brocade Communication Systems, Inc., Cisco Systems, Inc. and others.  All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
@@ -1087,10 +1087,12 @@ public class RestconfImpl implements RestconfService {
     public NormalizedNodeContext subscribeToStream(final String identifier, final UriInfo uriInfo) {
         boolean startTime_used = false;
         boolean stopTime_used = false;
-        boolean filter_used = false;
         Instant start = Instant.now();
         Instant stop = null;
+        boolean filter_used = false;
         String filter = null;
+        boolean leafNodesOnly_used = false;
+        boolean leafNodesOnly = false;
 
         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
             switch (entry.getKey()) {
@@ -1118,6 +1120,14 @@ public class RestconfImpl implements RestconfService {
                         throw new RestconfDocumentedException("Filter parameter can be used only once.");
                     }
                     break;
+                case "odl-leaf-nodes-only":
+                    if (!leafNodesOnly_used) {
+                        leafNodesOnly_used = true;
+                        leafNodesOnly = Boolean.parseBoolean(entry.getValue().iterator().next());
+                    } else {
+                        throw new RestconfDocumentedException("Odl-leaf-nodes-only parameter can be used only once.");
+                    }
+                    break;
                 default:
                     throw new RestconfDocumentedException("Bad parameter used with notifications: " + entry.getKey());
             }
@@ -1127,7 +1137,7 @@ public class RestconfImpl implements RestconfService {
         }
         URI response = null;
         if (identifier.contains(DATA_SUBSCR)) {
-            response = dataSubs(identifier, uriInfo, start, stop, filter);
+            response = dataSubs(identifier, uriInfo, start, stop, filter, leafNodesOnly);
         } else if (identifier.contains(NOTIFICATION_STREAM)) {
             response = notifStream(identifier, uriInfo, start, stop, filter);
         }
@@ -1211,7 +1221,7 @@ public class RestconfImpl implements RestconfService {
 
         for (final NotificationListenerAdapter listener : listeners) {
             this.broker.registerToListenNotification(listener);
-            listener.setQueryParams(start, java.util.Optional.ofNullable(stop), java.util.Optional.ofNullable(filter));
+            listener.setQueryParams(start, java.util.Optional.ofNullable(stop), java.util.Optional.ofNullable(filter), false);
         }
 
         final UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder();
@@ -1244,7 +1254,7 @@ public 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 String filter, boolean leafNodesOnly) {
         final String streamName = Notificator.createStreamNameFromUri(identifier);
         if (Strings.isNullOrEmpty(streamName)) {
             throw new RestconfDocumentedException("Stream name is empty.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
@@ -1255,7 +1265,7 @@ public class RestconfImpl implements RestconfService {
             throw new RestconfDocumentedException("Stream was not found.", ErrorType.PROTOCOL,
                     ErrorTag.UNKNOWN_ELEMENT);
         }
-        listener.setQueryParams(start, java.util.Optional.ofNullable(stop), java.util.Optional.ofNullable(filter));
+        listener.setQueryParams(start, java.util.Optional.ofNullable(stop), java.util.Optional.ofNullable(filter), leafNodesOnly);
 
         final Map<String, String> paramToValues = resolveValuesFromUri(identifier);
         final LogicalDatastoreType datastore =
index 67abe769a0210c3e063579d55cc576c6495ca16e..c86b1ed64c975bb10ab721d9cc91176e960668bd 100644 (file)
@@ -51,6 +51,7 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
     private Instant start = null;
     private Instant stop = null;
     private String filter = null;
+    private boolean leafNodesOnly = false;
 
     @VisibleForTesting
     public final Instant getStart() {
@@ -66,11 +67,23 @@ abstract class AbstractQueryParams extends AbstractNotificationsData {
      *            - stop-time of getting notification
      * @param filter
      *            - indicate which subset of all possible events are of interest
+     * @param leafNodesOnly
+     *            - if true, notifications will contain changes to leaf nodes only
      */
-    public void setQueryParams(final Instant start, final Optional<Instant> stop, final Optional<String> filter) {
+    public void setQueryParams(final Instant start, final Optional<Instant> stop, final Optional<String> filter, final boolean leafNodesOnly) {
         this.start = Preconditions.checkNotNull(start);
         this.stop = stop.orElse(null);
         this.filter = filter.orElse(null);
+        this.leafNodesOnly = leafNodesOnly;
+    }
+
+    /**
+     * Check whether this query should only notify about leaf node changes
+     *
+     * @return true if this query should only notify about leaf node changes
+     */
+    public boolean getLeafNodesOnly() {
+        return leafNodesOnly;
     }
 
     /**
index 0b2c1dd34d8811b2f6e79ba1b2754d29ff32ecd5..92c2c7b2c9531c76e6ba0eca3986a66b5916c5d1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ * Copyright (c) 2014, 2016 Cisco Systems, Inc. and others.  All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
@@ -19,12 +19,14 @@ import org.json.XML;
 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataChangeEvent;
 import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
+import org.opendaylight.restconf.parser.builder.YangInstanceIdentifierDeserializer;
 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
@@ -208,7 +210,8 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
             return;
         }
         for (final Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> entry : data) {
-            if (!ControllerContext.getInstance().isNodeMixin(entry.getKey())) {
+            if (!ControllerContext.getInstance().isNodeMixin(entry.getKey()) &&
+                                                            (!getLeafNodesOnly() || entry.getValue() instanceof LeafNode)) {
                 final Node node = createCreatedChangedDataChangeEventElement(doc, entry, operation, schemaContext,
                         dataSchemaContextTree);
                 element.appendChild(node);
index 8786f67639b164f038de68ea5344bbffb04dc29a..5b8a5255df03dc5ea78b4554486fffeb3f45ea54 100644 (file)
@@ -127,7 +127,7 @@ public final class SubscribeToStreamUtil {
         for (final NotificationListenerAdapter listener : listeners) {
             registerToListenNotification(listener, handlersHolder.getNotificationServiceHandler());
             listener.setQueryParams(notificationQueryParams.getStart(), notificationQueryParams.getStop(),
-                    notificationQueryParams.getFilter());
+                    notificationQueryParams.getFilter(), false);
             listener.setCloseVars(handlersHolder.getTransactionChainHandler(), handlersHolder.getSchemaHandler());
             final NormalizedNode mapToStreams =
                     RestconfMappingNodeUtil.mapYangNotificationStreamByIetfRestconfMonitoring(listener.getSchemaPath().getLastComponent(),
@@ -215,7 +215,7 @@ public final class SubscribeToStreamUtil {
         Preconditions.checkNotNull(listener, "Listener doesn't exist : " + streamName);
 
         listener.setQueryParams(notificationQueryParams.getStart(), notificationQueryParams.getStop(),
-                notificationQueryParams.getFilter());
+                notificationQueryParams.getFilter(), false);
         listener.setCloseVars(handlersHolder.getTransactionChainHandler(), handlersHolder.getSchemaHandler());
 
         registration(ds, scope, listener, handlersHolder.getDomDataBrokerHandler().get());
index 9fd7e0ac44010561e274a8ff1e04ab5354fce885..cf9b345618fda2e5cfa61e2f3c13d4a36321c97c 100644 (file)
@@ -135,7 +135,7 @@ public class ExpressionParserTest {
         final PathArgument pathValue = NodeIdentifier.create(QName.create("module", "2016-14-12", "localName"));
         Mockito.when(path.getLastPathArgument()).thenReturn(pathValue);
         final ListenerAdapter listener = Notificator.createListener(path, "streamName", NotificationOutputType.JSON);
-        listener.setQueryParams(Instant.now(), Optional.empty(), Optional.ofNullable(filter));
+        listener.setQueryParams(Instant.now(), Optional.empty(), Optional.ofNullable(filter), false);
 
         // FIXME: do not use reflection here
         final Class<?> superclass = listener.getClass().getSuperclass().getSuperclass();
@@ -15,12 +15,15 @@ import static org.mockito.Mockito.mock;
 import java.io.FileNotFoundException;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
+import java.util.logging.Level;
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.Application;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
 import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -39,7 +42,7 @@ import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
-public class RestStream extends JerseyTest {
+public class RestStreamTest extends JerseyTest {
 
     private static BrokerFacade brokerFacade;
     private static RestconfImpl restconfImpl;
@@ -71,10 +74,22 @@ public class RestStream extends JerseyTest {
     }
 
     @Test
-    @Ignore // FIXME : find problem with codec
+    @Ignore // Sporadic failures where jersey does not correctly pass post data to XmlNormalizedNodeBodyReader.readFrom
     public void testCallRpcCallGet() throws UnsupportedEncodingException, InterruptedException {
+        createAndSubscribe(null);
+    }
+
+    @Test
+    @Ignore // Sporadic failures where jersey does not correctly pass post data to XmlNormalizedNodeBodyReader.readFrom
+    public void testCallRpcCallGetLeaves() throws UnsupportedEncodingException, InterruptedException {
+        createAndSubscribe("odl-leaf-nodes-only", "true");
+    }
+
+    private void createAndSubscribe(String queryParamName, Object... values)
+                                                throws UnsupportedEncodingException, InterruptedException {
         String uri = "/operations/sal-remote:create-data-change-event-subscription";
-        final Response responseWithStreamName = post(uri, MediaType.APPLICATION_XML, getRpcInput());
+        String rpcInput = getRpcInput();
+        final Response responseWithStreamName = post(uri, MediaType.APPLICATION_XML, rpcInput);
         final Document xmlResponse = responseWithStreamName.readEntity(Document.class);
         assertNotNull(xmlResponse);
         final Element outputElement = xmlResponse.getDocumentElement();
@@ -82,21 +97,25 @@ public class RestStream extends JerseyTest {
 
         final Node streamNameElement = outputElement.getFirstChild();
         assertEquals("stream-name",streamNameElement.getLocalName());
-        assertEquals("ietf-interfaces:interfaces/ietf-interfaces:interface/eth0/datastore=CONFIGURATION/scope=BASE",streamNameElement.getTextContent());
+        assertEquals("data-change-event-subscription/ietf-interfaces:interfaces/ietf-interfaces:interface/eth0/datastore=CONFIGURATION/scope=BASE",streamNameElement.getTextContent());
 
-        uri = "/streams/stream/ietf-interfaces:interfaces/ietf-interfaces:interface/eth0/datastore=CONFIGURATION/scope=BASE";
-        final Response responseWithRedirectionUri = get(uri, MediaType.APPLICATION_XML);
+        uri = "/streams/stream/data-change-event-subscription/ietf-interfaces:interfaces/ietf-interfaces:interface/eth0/datastore=CONFIGURATION/scope=BASE";
+        final Response responseWithRedirectionUri = get(uri, MediaType.APPLICATION_XML, null);
         final URI websocketServerUri = responseWithRedirectionUri.getLocation();
         assertNotNull(websocketServerUri);
-        assertTrue(websocketServerUri.toString().matches(".*http://localhost:[\\d]+/ietf-interfaces:interfaces/ietf-interfaces:interface/eth0.*"));
+        assertTrue(websocketServerUri.toString().matches(".*ws://localhost:[\\d]+/data-change-event-subscription/ietf-interfaces:interfaces/ietf-interfaces:interface/eth0.*"));
     }
 
     private Response post(final String uri, final String mediaType, final String data) {
         return target(uri).request(mediaType).post(Entity.entity(data, mediaType));
     }
 
-    private Response get(final String uri, final String mediaType) {
-        return target(uri).request(mediaType).get();
+    private Response get(final String uri, final String mediaType, String queryParam, Object... values) {
+        if (queryParam != null) {
+            return target(uri).queryParam(queryParam, values).request(mediaType).get();
+        } else {
+            return target(uri).request(mediaType).get();
+        }
     }
 
     private static String getRpcInput() {