Convert rfc8040 ListenerAdapter to DOMDataTreeChangeListener 93/69093/4
authorTom Pantelis <tompantelis@gmail.com>
Sat, 3 Mar 2018 18:43:55 +0000 (13:43 -0500)
committerTom Pantelis <tompantelis@gmail.com>
Tue, 13 Mar 2018 18:51:50 +0000 (14:51 -0400)
DOMDataChangeListener is scheduled for removal - the bierman02
ListenerAdapter was already converted. Also modified the
ListenerAdapter to use the org.json lib instead of the jackson
lib as the UT revealed that the jackson lib doesn't convert
the XML to JSON correctly and the bierman02 ListenerAdapter
was recently converted to use org.json.

Change-Id: I9e6778a206f412bb8498297cdcb94b77ebba25dd
Signed-off-by: Tom Pantelis <tompantelis@gmail.com>
12 files changed:
restconf/restconf-nb-rfc8040/pom.xml
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/SubscribeToStreamUtil.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractCommonSubscriber.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/rests/services/impl/RestconfStreamsSubscriptionServiceImplTest.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json [new file with mode: 0644]

index e10030557f173ba19ff783c27683c21924bb4c9c..1ee8149e242e17415ae807ea7d2e6d07ffd73df2 100644 (file)
     <dependency>
       <groupId>org.opendaylight.netconf</groupId>
       <artifactId>restconf-common-models</artifactId>
-      <version>${project.version}</version>
-    </dependency>
+      </dependency>
     <dependency>
       <groupId>org.opendaylight.netconf</groupId>
       <artifactId>restconf-common</artifactId>
-      <version>${project.version}</version>
-    </dependency>
+      </dependency>
     <dependency>
       <groupId>org.opendaylight.netconf</groupId>
       <artifactId>ietf-yang-library</artifactId>
-      <version>${project.version}</version>
-    </dependency>
+      </dependency>
 
     <dependency>
       <groupId>org.opendaylight.yangtools</groupId>
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
-      <version>3.0</version>
     </dependency>
 
     <dependency>
       <artifactId>aaa-shiro-api</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+    </dependency>
+
     <!-- Testing Dependencies -->
     <dependency>
       <groupId>org.glassfish.jersey.test-framework.providers</groupId>
     <dependency>
       <groupId>org.glassfish.jersey.bundles.repackaged</groupId>
       <artifactId>jersey-guava</artifactId>
-      <version>2.6</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-binding-broker-impl</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-binding-broker-impl</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.skyscreamer</groupId>
+      <artifactId>jsonassert</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
index eb4b64f4aa4a286b66ec694dc65ab5bb48d424a1..c02cb5dc3eb2eadc43f6f0d06f2a2b05dc3feab6 100644 (file)
@@ -28,8 +28,9 @@ import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
 import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker;
-import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeService;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeIdentifier;
 import org.opendaylight.controller.md.sal.dom.api.DOMNotificationListener;
 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
@@ -324,16 +325,21 @@ public final class SubscribeToStreamUtil {
      * @param domDataBroker
      *             data broker for register data change listener
      */
-    @SuppressWarnings("deprecation")
     private static void registration(final LogicalDatastoreType ds, final DataChangeScope scope,
             final ListenerAdapter listener, final DOMDataBroker domDataBroker) {
         if (listener.isListening()) {
             return;
         }
 
-        final YangInstanceIdentifier path = listener.getPath();
-        final ListenerRegistration<DOMDataChangeListener> registration =
-                domDataBroker.registerDataChangeListener(ds, path, listener, scope);
+        final DOMDataTreeChangeService changeService = (DOMDataTreeChangeService)domDataBroker.getSupportedExtensions()
+                .get(DOMDataTreeChangeService.class);
+        if (changeService == null) {
+            throw new UnsupportedOperationException("DOMDataBroker does not support the DOMDataTreeChangeService");
+        }
+
+        final DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(ds, listener.getPath());
+        final ListenerRegistration<ListenerAdapter> registration =
+                                    changeService.registerDataTreeChangeListener(root, listener);
 
         listener.setRegistration(registration);
     }
index ea3ad270ff44d58bb33d2664d6dbba53aad78e05..6e2004eea4b571dc727043d476262dcd21278c25 100644 (file)
@@ -29,8 +29,8 @@ abstract class AbstractCommonSubscriber extends AbstractQueryParams implements B
 
     @SuppressWarnings("rawtypes")
     private EventBusChangeRecorder eventBusChangeRecorder;
-    @SuppressWarnings("rawtypes")
-    private ListenerRegistration registration;
+
+    private volatile ListenerRegistration<?> registration;
 
     /**
      * Creating {@link EventBus}.
@@ -51,8 +51,10 @@ abstract class AbstractCommonSubscriber extends AbstractQueryParams implements B
 
     @Override
     public final void close() throws Exception {
-        this.registration.close();
-        this.registration = null;
+        if (this.registration != null) {
+            this.registration.close();
+            this.registration = null;
+        }
 
         deleteDataInDS();
         unregister();
@@ -93,8 +95,7 @@ abstract class AbstractCommonSubscriber extends AbstractQueryParams implements B
      * @param registration
      *            DOMDataChangeListener registration
      */
-    @SuppressWarnings("rawtypes")
-    public void setRegistration(final ListenerRegistration registration) {
+    public void setRegistration(final ListenerRegistration<?> registration) {
         this.registration = registration;
     }
 
index 874ba1eaa8953431530e435316e6efbf164a4f00..0296e469d52e8a46fddf8fc74488449a7a253f0e 100644 (file)
@@ -7,19 +7,16 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.streams.listeners;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.dataformat.xml.XmlMapper;
 import com.google.common.base.Preconditions;
-import com.google.common.base.Throwables;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
+import java.util.Collection;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
+import java.util.Optional;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.transform.dom.DOMResult;
-import org.opendaylight.controller.md.sal.common.api.data.AsyncDataChangeEvent;
-import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
+import org.json.XML;
+import org.opendaylight.controller.md.sal.dom.api.ClusteredDOMDataTreeChangeListener;
 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;
@@ -30,6 +27,8 @@ 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;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
 import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
@@ -44,7 +43,7 @@ import org.w3c.dom.Node;
  * {@link ListenerAdapter} is responsible to track events, which occurred by
  * changing data in data source.
  */
-public class ListenerAdapter extends AbstractCommonSubscriber implements DOMDataChangeListener {
+public class ListenerAdapter extends AbstractCommonSubscriber implements ClusteredDOMDataTreeChangeListener {
 
     private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
 
@@ -52,8 +51,6 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
     private final String streamName;
     private final NotificationOutputType outputType;
 
-    private AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change;
-
     /**
      * Creates new {@link ListenerAdapter} listener specified by path and stream
      * name and register for subscribing.
@@ -77,10 +74,8 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
     }
 
     @Override
-    @SuppressWarnings("checkstyle:hiddenField")
-    public void onDataChanged(final AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change) {
-        this.change = change;
-        final String xml = prepareXml();
+    public void onDataTreeChanged(final Collection<DataTreeCandidate> dataTreeCandidates) {
+        final String xml = prepareXml(dataTreeCandidates);
         if (checkQueryParams(xml, this)) {
             prepareAndPostData(xml);
         }
@@ -118,13 +113,7 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
     private void prepareAndPostData(final String xml) {
         final Event event = new Event(EventType.NOTIFY);
         if (this.outputType.equals(NotificationOutputType.JSON)) {
-            try {
-                final JsonNode node = new XmlMapper().readTree(xml.getBytes(StandardCharsets.UTF_8));
-                event.setData(node.toString());
-            } catch (final IOException e) {
-                LOG.error("Error parsing XML {}", xml, e);
-                Throwables.propagate(e);
-            }
+            event.setData(XML.toJSONObject(xml).toString());
         } else {
             event.setData(xml);
         }
@@ -138,9 +127,11 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
     /**
      * Prepare data in printable form and transform it to String.
      *
+     * @param dataTreeCandidates the DataTreeCandidates to transform
+     *
      * @return Data in printable form.
      */
-    private String prepareXml() {
+    private String prepareXml(final Collection<DataTreeCandidate> dataTreeCandidates) {
         final SchemaContext schemaContext = schemaHandler.get();
         final DataSchemaContextTree dataContextTree = DataSchemaContextTree.from(schemaContext);
         final Document doc = createDocument();
@@ -149,7 +140,7 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
         final Element dataChangedNotificationEventElement = doc.createElementNS(
                 "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
 
-        addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, this.change,
+        addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, dataTreeCandidates,
                 schemaContext, dataContextTree);
         notificationElement.appendChild(dataChangedNotificationEventElement);
         return transformDoc(doc);
@@ -157,75 +148,83 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
 
     /**
      * Adds values to data changed notification event element.
-     *
-     * @param doc
-     *            {@link Document}
-     * @param dataChangedNotificationEventElement
-     *            {@link Element}
-     * @param change
-     *            {@link AsyncDataChangeEvent}
      */
     @SuppressWarnings("checkstyle:hiddenField")
     private void addValuesToDataChangedNotificationEventElement(final Document doc,
             final Element dataChangedNotificationEventElement,
-            final AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change,
+            final Collection<DataTreeCandidate> dataTreeCandidates,
             final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
 
-        addCreatedChangedValuesFromDataToElement(doc, change.getCreatedData().entrySet(),
-                dataChangedNotificationEventElement, Operation.CREATED, schemaContext, dataSchemaContextTree);
-
-        addCreatedChangedValuesFromDataToElement(doc, change.getUpdatedData().entrySet(),
-                dataChangedNotificationEventElement, Operation.UPDATED, schemaContext, dataSchemaContextTree);
-
-        addValuesFromDataToElement(doc, change.getRemovedPaths(), dataChangedNotificationEventElement,
-                Operation.DELETED, schemaContext, dataSchemaContextTree);
-    }
-
-    /**
-     * Adds values from data to element.
-     *
-     * @param doc
-     *            {@link Document}
-     * @param data
-     *            Set of {@link YangInstanceIdentifier}.
-     * @param element
-     *            {@link Element}
-     * @param operation
-     *            {@link Operation}
-     * @param schemaContext
-     *            schema context
-     * @param dataSchemaContextTree
-     *            data schema context tree
-     */
-    private void addValuesFromDataToElement(final Document doc, final Set<YangInstanceIdentifier> data,
-            final Element element, final Operation operation, final SchemaContext schemaContext,
-            final DataSchemaContextTree dataSchemaContextTree) {
-        if (data == null || data.isEmpty()) {
-            return;
-        }
-        for (final YangInstanceIdentifier yiid : data) {
-            if (!dataSchemaContextTree.getChild(yiid).isMixin()) {
-                final Node node = createDataChangeEventElement(doc, yiid, operation, schemaContext);
-                element.appendChild(node);
+        for (DataTreeCandidate dataTreeCandidate : dataTreeCandidates) {
+            DataTreeCandidateNode candidateNode = dataTreeCandidate.getRootNode();
+            if (candidateNode == null) {
+                continue;
             }
+            YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath();
+            addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
+                    yiid.getParent(), schemaContext, dataSchemaContextTree);
         }
     }
 
-    private void addCreatedChangedValuesFromDataToElement(final Document doc,
-            final Set<Entry<YangInstanceIdentifier, NormalizedNode<?, ?>>> data, final Element element,
-            final Operation operation, final SchemaContext schemaContext,
+    private void addNodeToDataChangeNotificationEventElement(final Document doc,
+            final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
+            final YangInstanceIdentifier parentYiid, final SchemaContext schemaContext,
             final DataSchemaContextTree dataSchemaContextTree) {
-        if (data == null || data.isEmpty()) {
+
+        Optional<NormalizedNode<?,?>> optionalNormalizedNode = Optional.empty();
+        switch (candidateNode.getModificationType()) {
+            case APPEARED:
+            case SUBTREE_MODIFIED:
+            case WRITE:
+                optionalNormalizedNode = candidateNode.getDataAfter();
+                break;
+            case DELETE:
+            case DISAPPEARED:
+                optionalNormalizedNode = candidateNode.getDataBefore();
+                break;
+            case UNMODIFIED:
+            default:
+                break;
+        }
+
+        if (!optionalNormalizedNode.isPresent()) {
+            LOG.error("No node present in notification for {}", candidateNode);
             return;
         }
-        for (final Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> entry : data) {
-            if (!dataSchemaContextTree.getChild(entry.getKey()).isMixin()
-                    && (!getLeafNodesOnly() || entry.getValue() instanceof LeafNode)) {
-                final Node node = createCreatedChangedDataChangeEventElement(doc, entry, operation, schemaContext,
-                        dataSchemaContextTree);
-                element.appendChild(node);
+
+        NormalizedNode<?,?> normalizedNode = optionalNormalizedNode.get();
+        YangInstanceIdentifier yiid = YangInstanceIdentifier.builder(parentYiid)
+                                                            .append(normalizedNode.getIdentifier()).build();
+
+        boolean isNodeMixin = dataSchemaContextTree.getChild(yiid).isMixin();
+        boolean isSkippedNonLeaf = getLeafNodesOnly() && !(normalizedNode instanceof LeafNode);
+        if (!isNodeMixin && !isSkippedNonLeaf) {
+            Node node = null;
+            switch (candidateNode.getModificationType()) {
+                case APPEARED:
+                case SUBTREE_MODIFIED:
+                case WRITE:
+                    Operation op = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
+                    node = createCreatedChangedDataChangeEventElement(doc, yiid, normalizedNode, op,
+                            schemaContext, dataSchemaContextTree);
+                    break;
+                case DELETE:
+                case DISAPPEARED:
+                    node = createDataChangeEventElement(doc, yiid, Operation.DELETED, schemaContext);
+                    break;
+                case UNMODIFIED:
+                default:
+                    break;
+            }
+            if (node != null) {
+                dataChangedNotificationEventElement.appendChild(node);
             }
         }
+
+        for (DataTreeCandidateNode childNode : candidateNode.getChildNodes()) {
+            addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, childNode,
+                                                                        yiid, schemaContext, dataSchemaContextTree);
+        }
     }
 
     /**
@@ -256,11 +255,10 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
     }
 
     private Node createCreatedChangedDataChangeEventElement(final Document doc,
-            final Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> entry, final Operation operation,
+            final YangInstanceIdentifier eventPath, 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 YangInstanceIdentifier eventPath = entry.getKey();
         addPathAsValueToElement(eventPath, pathElement, schemaContext);
         dataChangeEventElement.appendChild(pathElement);
 
@@ -270,7 +268,6 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData
 
         try {
             SchemaPath nodePath;
-            final NormalizedNode<?, ?> normalized = entry.getValue();
             if (normalized instanceof MapEntryNode || normalized instanceof UnkeyedListEntryNode) {
                 nodePath = dataSchemaContextTree.getChild(eventPath).getDataSchemaNode().getPath();
             } else {
index 24d9d8b9ee21f683ffd44ce6caa72188ac18fd54..bda60dd71de79ba8079e15971582182b5d91f86e 100644 (file)
@@ -17,6 +17,7 @@ import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
 import java.lang.reflect.Field;
 import java.net.URI;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -37,8 +38,8 @@ import org.mockito.MockitoAnnotations;
 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
 import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker;
-import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeService;
 import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction;
 import org.opendaylight.controller.md.sal.dom.api.DOMTransactionChain;
 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
@@ -79,8 +80,8 @@ public class RestconfStreamsSubscriptionServiceImplTest {
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        final TransactionChainHandler txHandler = Mockito.mock(TransactionChainHandler.class);
-        final DOMTransactionChain domTx = Mockito.mock(DOMTransactionChain.class);
+        final TransactionChainHandler txHandler = mock(TransactionChainHandler.class);
+        final DOMTransactionChain domTx = mock(DOMTransactionChain.class);
         Mockito.when(this.transactionHandler.get()).thenReturn(domTx);
         Mockito.when(txHandler.get()).thenReturn(domTx);
         final DOMDataWriteTransaction wTx = Mockito.mock(DOMDataWriteTransaction.class);
@@ -92,16 +93,23 @@ public class RestconfStreamsSubscriptionServiceImplTest {
                 Futures.immediateCheckedFuture(null);
         Mockito.when(rwTx.submit()).thenReturn(checkedFutureEmpty);
         Mockito.when(domTx.newReadWriteTransaction()).thenReturn(rwTx);
-        final CheckedFuture<Void, TransactionCommitFailedException> checked = Mockito.mock(CheckedFuture.class);
+        final CheckedFuture<Void, TransactionCommitFailedException> checked = mock(CheckedFuture.class);
         Mockito.when(wTx.submit()).thenReturn(checked);
         Mockito.when(checked.checkedGet()).thenReturn(null);
         this.schemaHandler = new SchemaContextHandler(txHandler);
 
         final DOMDataBroker dataBroker = mock(DOMDataBroker.class);
-        final ListenerRegistration<DOMDataChangeListener> listener = mock(ListenerRegistration.class);
+
+        DOMDataTreeChangeService dataTreeChangeService = mock(DOMDataTreeChangeService.class);
+        doReturn(mock(ListenerRegistration.class)).when(dataTreeChangeService)
+                .registerDataTreeChangeListener(any(), any());
+
+        doReturn(Collections.singletonMap(DOMDataTreeChangeService.class, dataTreeChangeService))
+                .when(dataBroker).getSupportedExtensions();
+
         doReturn(dataBroker).when(this.dataBrokerHandler).get();
-        doReturn(listener).when(dataBroker).registerDataChangeListener(any(), any(), any(), any());
-        final MultivaluedMap<String, String> map = Mockito.mock(MultivaluedMap.class);
+
+        final MultivaluedMap<String, String> map = mock(MultivaluedMap.class);
         final Set<Entry<String, List<String>>> set = new HashSet<>();
         Mockito.when(map.entrySet()).thenReturn(set);
         Mockito.when(this.uriInfo.getQueryParameters()).thenReturn(map);
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
new file mode 100644 (file)
index 0000000..1fb86d2
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2017 Red Hat, 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,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.restconf.nb.rfc8040.streams.listeners;
+
+import static java.time.Instant.EPOCH;
+import static org.junit.Assert.fail;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.WriteTransaction;
+import org.opendaylight.controller.md.sal.binding.test.AbstractConcurrentDataBrokerTest;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.controller.md.sal.common.api.data.TransactionChainListener;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeService;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeIdentifier;
+import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
+import org.opendaylight.restconf.nb.rfc8040.handlers.TransactionChainHandler;
+import org.opendaylight.yang.gen.v1.instance.identifier.patch.module.rev151121.PatchCont;
+import org.opendaylight.yang.gen.v1.instance.identifier.patch.module.rev151121.patch.cont.MyList1;
+import org.opendaylight.yang.gen.v1.instance.identifier.patch.module.rev151121.patch.cont.MyList1Builder;
+import org.opendaylight.yang.gen.v1.instance.identifier.patch.module.rev151121.patch.cont.MyList1Key;
+import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
+    private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapterTest.class);
+
+    private static final String JSON_NOTIF_LEAVES_CREATE = "/listener-adapter-test/notif-leaves-create.json";
+    private static final String JSON_NOTIF_LEAVES_UPDATE =  "/listener-adapter-test/notif-leaves-update.json";
+    private static final String JSON_NOTIF_LEAVES_DEL =  "/listener-adapter-test/notif-leaves-del.json";
+    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 YangInstanceIdentifier PATCH_CONT_YIID =
+            YangInstanceIdentifier.create(new YangInstanceIdentifier.NodeIdentifier(PatchCont.QNAME));
+
+    private DataBroker dataBroker;
+    private DOMDataBroker domDataBroker;
+    private TransactionChainHandler transactionChainHandler;
+    private SchemaContextHandler schemaContextHandler;
+
+    @Before
+    public void setUp() throws Exception {
+        dataBroker = getDataBroker();
+        domDataBroker = getDomBroker();
+        SchemaContext sc = YangParserTestUtils.parseYangResource(
+                "/instanceidentifier/yang/instance-identifier-patch-module.yang");
+
+        transactionChainHandler = new TransactionChainHandler(domDataBroker.createTransactionChain(
+                Mockito.mock(TransactionChainListener.class)));
+        schemaContextHandler = new SchemaContextHandler(transactionChainHandler);
+        SchemaContextHandler.setSchemaContext(sc);
+    }
+
+    class ListenerAdapterTester extends ListenerAdapter {
+
+        private volatile String lastNotification;
+        private CountDownLatch notificationLatch = new CountDownLatch(1);
+
+        ListenerAdapterTester(final YangInstanceIdentifier path, final String streamName,
+                              final NotificationOutputTypeGrouping.NotificationOutputType outputType,
+                              final boolean leafNodesOnly) {
+            super(path, streamName, outputType);
+            setQueryParams(EPOCH, Optional.empty(), Optional.empty(), leafNodesOnly);
+        }
+
+        @Override
+        protected void post(final Event event) {
+            this.lastNotification = event.getData();
+            notificationLatch.countDown();
+        }
+
+        public void assertGot(final String json) {
+            if (!Uninterruptibles.awaitUninterruptibly(notificationLatch, 5, TimeUnit.SECONDS)) {
+                fail("Timed out waiting for notification for: " + json);
+            }
+
+            LOG.info("lastNotification: {}", lastNotification);
+            String withFakeDate = withFakeDate(lastNotification);
+            LOG.info("Comparing: \n{}\n{}", json, withFakeDate);
+
+            JSONAssert.assertEquals(json, withFakeDate, false);
+            this.lastNotification = null;
+            notificationLatch = new CountDownLatch(1);
+        }
+    }
+
+    static String withFakeDate(final String in) {
+        JSONObject doc = new JSONObject(in);
+        JSONObject notification = doc.getJSONObject("notification");
+        if (notification == null) {
+            return in;
+        }
+        notification.put("eventTime", "someDate");
+        return doc.toString();
+    }
+
+    private String getNotifJson(final String path) throws IOException, URISyntaxException {
+        URL url = getClass().getResource(path);
+        byte[] bytes = Files.readAllBytes(Paths.get(url.toURI()));
+        return withFakeDate(new String(bytes, StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testJsonNotifsLeaves() throws Exception {
+        ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+                                        NotificationOutputTypeGrouping.NotificationOutputType.JSON, true);
+        adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+        DOMDataTreeChangeService changeService = (DOMDataTreeChangeService)
+                domDataBroker.getSupportedExtensions().get(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.put(LogicalDatastoreType.CONFIGURATION, iid, builder.build(), true);
+        writeTransaction.submit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_LEAVES_CREATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        builder = new MyList1Builder().setKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+        writeTransaction.merge(LogicalDatastoreType.CONFIGURATION, iid, builder.build(), true);
+        writeTransaction.submit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_LEAVES_UPDATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+        writeTransaction.submit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_LEAVES_DEL));
+    }
+
+    @Test
+    public void testJsonNotifs() throws Exception {
+        ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+                NotificationOutputTypeGrouping.NotificationOutputType.JSON, false);
+        adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+        DOMDataTreeChangeService changeService = (DOMDataTreeChangeService)
+                domDataBroker.getSupportedExtensions().get(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.put(LogicalDatastoreType.CONFIGURATION, iid, builder.build(), true);
+        writeTransaction.submit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_CREATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        builder = new MyList1Builder().setKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+        writeTransaction.merge(LogicalDatastoreType.CONFIGURATION, iid, builder.build(), true);
+        writeTransaction.submit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_UPDATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+        writeTransaction.submit();
+        adapter.assertGot(getNotifJson(JSON_NOTIF_DEL));
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json
new file mode 100644 (file)
index 0000000..31a7f1a
--- /dev/null
@@ -0,0 +1,55 @@
+{
+    "notification": {
+        "data-changed-notification": {
+            "data-change-event": [
+                {
+                    "data": {
+                        "my-leaf11": {
+                            "content": "Jed",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "created",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11"
+                },
+                {
+                    "data": {
+                        "name": {
+                            "content": "Althea",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "created",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                },
+                {
+                    "data": {
+                        "patch-cont": {
+                            "my-list1": {
+                                "my-leaf11": "Jed",
+                                "name": "Althea"
+                            },
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "created",
+                    "path": "/instance-identifier-patch-module:patch-cont"
+                },
+                {
+                    "data": {
+                        "my-list1": {
+                            "my-leaf11": "Jed",
+                            "name": "Althea",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "created",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']"
+                }
+            ],
+            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+        },
+        "eventTime": "2017-09-17T13:32:03.586+03:00",
+        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json
new file mode 100644 (file)
index 0000000..8c2af38
--- /dev/null
@@ -0,0 +1,36 @@
+{
+    "notification": {
+        "data-changed-notification": {
+            "data-change-event": [
+                {
+                    "data": {
+                        "patch-cont": {
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "updated",
+                    "path": "/instance-identifier-patch-module:patch-cont"
+                },
+                {
+                    "operation": "deleted",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']"
+                },
+                {
+                    "operation": "deleted",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                },
+                {
+                    "operation": "deleted",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12"
+                },
+                {
+                    "operation": "deleted",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11"
+                }
+            ],
+            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+        },
+        "eventTime": "2017-09-17T14:18:53.404+03:00",
+        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json
new file mode 100644 (file)
index 0000000..f685b89
--- /dev/null
@@ -0,0 +1,31 @@
+{
+    "notification": {
+        "data-changed-notification": {
+            "data-change-event": [
+                {
+                    "data": {
+                        "my-leaf11": {
+                            "content": "Jed",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "created",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11"
+                },
+                {
+                    "data": {
+                        "name": {
+                            "content": "Althea",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "created",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                }
+            ],
+            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+        },
+        "eventTime": "2017-09-17T11:23:10.323+03:00",
+        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json
new file mode 100644 (file)
index 0000000..fd1f1d8
--- /dev/null
@@ -0,0 +1,23 @@
+{
+    "notification": {
+        "data-changed-notification": {
+            "data-change-event": [
+                {
+                    "operation": "deleted",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11"
+                },
+                {
+                    "operation": "deleted",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                },
+                {
+                    "operation": "deleted",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12"
+                }
+            ],
+            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+        },
+        "eventTime": "2017-09-18T15:30:16.099+03:00",
+        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json
new file mode 100644 (file)
index 0000000..890d945
--- /dev/null
@@ -0,0 +1,31 @@
+{
+    "notification": {
+        "data-changed-notification": {
+            "data-change-event": [
+                {
+                    "data": {
+                        "my-leaf12": {
+                            "content": "Bertha",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "created",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12"
+                },
+                {
+                    "data": {
+                        "name": {
+                            "content": "Althea",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "updated",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                }
+            ],
+            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+        },
+        "eventTime": "2017-09-18T14:20:54.82+03:00",
+        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json
new file mode 100644 (file)
index 0000000..0ee0547
--- /dev/null
@@ -0,0 +1,57 @@
+{
+    "notification": {
+        "data-changed-notification": {
+            "data-change-event": [
+                {
+                    "data": {
+                        "patch-cont": {
+                            "my-list1": {
+                                "my-leaf11": "Jed",
+                                "my-leaf12": "Bertha",
+                                "name": "Althea"
+                            },
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "updated",
+                    "path": "/instance-identifier-patch-module:patch-cont"
+                },
+                {
+                    "data": {
+                        "my-list1": {
+                            "my-leaf11": "Jed",
+                            "my-leaf12": "Bertha",
+                            "name": "Althea",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "updated",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']"
+                },
+                {
+                    "data": {
+                        "my-leaf12": {
+                            "content": "Bertha",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "created",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12"
+                },
+                {
+                    "data": {
+                        "name": {
+                            "content": "Althea",
+                            "xmlns": "instance:identifier:patch:module"
+                        }
+                    },
+                    "operation": "updated",
+                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                }
+            ],
+            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+        },
+        "eventTime": "2017-09-18T15:52:25.213+03:00",
+        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+    }
+}