Report non-present initial data in (DOM)DataTreeChangeListener 70/77070/7
authorhan <han.jie@zte.com.cn>
Thu, 18 Oct 2018 03:08:34 +0000 (11:08 +0800)
committerRobert Varga <nite@hq.sk>
Wed, 31 Oct 2018 09:43:15 +0000 (09:43 +0000)
Add a default method 'onInitialData' with no-op in
DOMDataTreeChangeListener', which would be invoked only once during
registration of the listener if there was no data in the conceptual
data tree for the supplied path, which was used to register this
listener, and after this 'onDataTreeChanged' would always be
invoked for data changes.

Users not caring about this event could leave it as default with no-op.

JIRA: MDSAL-217
Change-Id: I3a2c02c76b8c37ac7624dd1646692bd783a3468d
Signed-off-by: Jie Han <han.jie@zte.com.cn>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataTreeChangeListener.java
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMDataTreeChangeListenerAdapter.java
dom/mdsal-dom-api/src/main/java/org/opendaylight/mdsal/dom/api/DOMDataTreeChangeListener.java
dom/mdsal-dom-broker/src/test/java/org/opendaylight/mdsal/dom/broker/DOMDataTreeChangeListenerTest.java [new file with mode: 0644]
dom/mdsal-dom-inmemory-datastore/src/main/java/org/opendaylight/mdsal/dom/store/inmemory/InMemoryDOMStoreTreeChangePublisher.java
dom/mdsal-dom-inmemory-datastore/src/test/java/org/opendaylight/mdsal/dom/store/inmemory/InMemoryDOMDataStoreFactoryTest.java
dom/mdsal-dom-spi/src/main/java/org/opendaylight/mdsal/dom/spi/store/AbstractDOMStoreTreeChangePublisher.java

index b6ae850e36945996b615461db125fc7dd2efae0b..3e066757d6a27353a35b597a42ab9cd7243f9612 100644 (file)
@@ -41,4 +41,16 @@ public interface DataTreeChangeListener<T extends DataObject> extends EventListe
      * @param changes Collection of change events, may not be null or empty.
      */
     void onDataTreeChanged(@NonNull Collection<DataTreeModification<T>> changes);
+
+    /**
+     * Invoked only once during registration of the listener if there was no data in the conceptual data tree
+     * for the supplied path, which was used to register this listener, and after this {@link #onDataTreeChanged}
+     * would always be invoked for data changes.
+     *
+     * <p>
+     * Users not care about this event could leave it as default with no-op.
+     */
+    default void onInitialData() {
+        //no-op
+    }
 }
index c2b11900df050cedaf2f69b5e9db2c73f943513d..046536511f67b5bbb51e4fcc6f4c748a01f87a44 100644 (file)
@@ -7,7 +7,8 @@
  */
 package org.opendaylight.mdsal.binding.dom.adapter;
 
-import com.google.common.base.Preconditions;
+import static java.util.Objects.requireNonNull;
+
 import java.util.Collection;
 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
 import org.opendaylight.mdsal.binding.api.DataTreeModification;
@@ -17,10 +18,8 @@ import org.opendaylight.yangtools.yang.binding.DataObject;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
 
 /**
- * Adapter wrapping Binding {@link DataTreeChangeListener} and exposing
- * it as {@link DOMDataTreeChangeListener} and translated DOM events
- * to their Binding equivalent.
- *
+ * Adapter wrapping Binding {@link DataTreeChangeListener} and exposing it as {@link DOMDataTreeChangeListener}
+ * and translated DOM events to their Binding equivalent.
  */
 class BindingDOMDataTreeChangeListenerAdapter<T extends DataObject> implements DOMDataTreeChangeListener {
 
@@ -30,15 +29,20 @@ class BindingDOMDataTreeChangeListenerAdapter<T extends DataObject> implements D
 
     BindingDOMDataTreeChangeListenerAdapter(final BindingToNormalizedNodeCodec codec,
             final DataTreeChangeListener<T> listener, final LogicalDatastoreType store) {
-        this.codec = Preconditions.checkNotNull(codec);
-        this.listener = Preconditions.checkNotNull(listener);
-        this.store = Preconditions.checkNotNull(store);
+        this.codec = requireNonNull(codec);
+        this.listener = requireNonNull(listener);
+        this.store = requireNonNull(store);
     }
 
     @Override
     public void onDataTreeChanged(final Collection<DataTreeCandidate> domChanges) {
-        final Collection<DataTreeModification<T>> bindingChanges
-                = LazyDataTreeModification.from(codec, domChanges, store);
+        final Collection<DataTreeModification<T>> bindingChanges = LazyDataTreeModification.from(codec, domChanges,
+            store);
         listener.onDataTreeChanged(bindingChanges);
     }
+
+    @Override
+    public void onInitialData() {
+        listener.onInitialData();
+    }
 }
index 2ae4f780dbb322495302c18321193dce865ea62b..7068210858a2d1036450b2269a50b37368976b64 100644 (file)
@@ -36,4 +36,16 @@ public interface DOMDataTreeChangeListener extends EventListener {
      * @throws NullPointerException if {@code changes} is null
      */
     void onDataTreeChanged(@NonNull Collection<DataTreeCandidate> changes);
+
+    /**
+     * Invoked only once during registration of the listener if there was no data in the conceptual data tree
+     * for the supplied path, which was used to register this listener, and after this {@link #onDataTreeChanged}
+     * would always be invoked for data changes.
+     *
+     * <p>
+     * Users not care about this event could leave it as default with no-op.
+     */
+    default void onInitialData() {
+        //no-op
+    }
 }
diff --git a/dom/mdsal-dom-broker/src/test/java/org/opendaylight/mdsal/dom/broker/DOMDataTreeChangeListenerTest.java b/dom/mdsal-dom-broker/src/test/java/org/opendaylight/mdsal/dom/broker/DOMDataTreeChangeListenerTest.java
new file mode 100644 (file)
index 0000000..60a220f
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2018 ZTE, 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.mdsal.dom.broker;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeListener;
+import org.opendaylight.mdsal.dom.broker.util.TestModel;
+import org.opendaylight.mdsal.dom.spi.store.DOMStoreReadWriteTransaction;
+import org.opendaylight.mdsal.dom.spi.store.DOMStoreThreePhaseCommitCohort;
+import org.opendaylight.mdsal.dom.store.inmemory.InMemoryDOMDataStore;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.data.impl.schema.builder.impl.ImmutableContainerNodeBuilder;
+
+public class DOMDataTreeChangeListenerTest {
+
+    private InMemoryDOMDataStore domStore;
+
+    @Before
+    public void setUp() {
+        domStore = new InMemoryDOMDataStore("Mdsal217", MoreExecutors.newDirectExecutorService());
+        domStore.onGlobalContextUpdated(TestModel.createTestContext());
+    }
+
+    @Test
+    public void receiveOnDataInitialEventForEmptyRoot() throws InterruptedException, ExecutionException {
+        final DOMDataTreeChangeListener listener = mock(DOMDataTreeChangeListener.class);
+        doNothing().when(listener).onInitialData();
+
+        domStore.registerTreeChangeListener(YangInstanceIdentifier.EMPTY, listener);
+        verify(listener, timeout(1000)).onInitialData();
+    }
+
+    @Test
+    public void receiveOnDataInitialEventForNonExistingData() throws InterruptedException, ExecutionException {
+        final DOMDataTreeChangeListener listener = mock(DOMDataTreeChangeListener.class);
+        final ArgumentCaptor<Collection> candidateCapture = ArgumentCaptor.forClass(Collection.class);
+        doNothing().when(listener).onInitialData();
+        doNothing().when(listener).onDataTreeChanged(any());
+
+        domStore.registerTreeChangeListener(TestModel.TEST_PATH, listener);
+        verify(listener, times(1)).onInitialData();
+
+        final NormalizedNode<?, ?> testNode = ImmutableNodes.containerNode(TestModel.TEST_QNAME);
+        DOMStoreReadWriteTransaction writeTx = domStore.newReadWriteTransaction();
+        assertNotNull(writeTx);
+        writeTx.write(TestModel.TEST_PATH, testNode);
+        DOMStoreThreePhaseCommitCohort cohort = writeTx.ready();
+        cohort.preCommit().get();
+        cohort.commit().get();
+
+        verify(listener, timeout(1000)).onDataTreeChanged(candidateCapture.capture());
+        final DataTreeCandidate candidate = (DataTreeCandidate) Iterables.getOnlyElement(candidateCapture.getValue());
+        assertEquals(TestModel.TEST_PATH, candidate.getRootPath());
+    }
+
+    @Test
+    public void receiveOnDataTreeChangedEventForPreExistingEmptyData() throws InterruptedException, ExecutionException {
+        final DOMDataTreeChangeListener listener = mock(DOMDataTreeChangeListener.class);
+        final ArgumentCaptor<Collection> candidateCapture = ArgumentCaptor.forClass(Collection.class);
+        doNothing().when(listener).onDataTreeChanged(any());
+
+        final NormalizedNode<?, ?> testNode = ImmutableNodes.containerNode(TestModel.TEST_QNAME);
+
+        DOMStoreReadWriteTransaction writeTx = domStore.newReadWriteTransaction();
+        assertNotNull(writeTx);
+        writeTx.write(TestModel.TEST_PATH, testNode);
+        DOMStoreThreePhaseCommitCohort cohort = writeTx.ready();
+        cohort.preCommit().get();
+        cohort.commit().get();
+
+        domStore.registerTreeChangeListener(TestModel.TEST_PATH, listener);
+
+        verify(listener, timeout(1000)).onDataTreeChanged(candidateCapture.capture());
+        final DataTreeCandidate candidate = (DataTreeCandidate) Iterables.getOnlyElement(candidateCapture.getValue());
+        assertEquals(TestModel.TEST_PATH, candidate.getRootPath());
+    }
+
+    @Test
+    public void receiveOnDataTreeChangeEventForPreExistingData() throws InterruptedException, ExecutionException {
+        final DOMDataTreeChangeListener listener = mock(DOMDataTreeChangeListener.class);
+        final ArgumentCaptor<Collection> candidateCapture = ArgumentCaptor.forClass(Collection.class);
+        doNothing().when(listener).onDataTreeChanged(any());
+
+        final ContainerNode testNode = ImmutableContainerNodeBuilder.create()
+                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.TEST_QNAME))
+                .addChild(ImmutableNodes.mapNodeBuilder(TestModel.OUTER_LIST_QNAME)
+                        .addChild(ImmutableNodes.mapEntry(TestModel.OUTER_LIST_QNAME,
+                                TestModel.ID_QNAME, 1)).build()).build();
+        DOMStoreReadWriteTransaction writeTx = domStore.newReadWriteTransaction();
+        assertNotNull(writeTx);
+        writeTx.write(TestModel.TEST_PATH, testNode);
+        DOMStoreThreePhaseCommitCohort cohort = writeTx.ready();
+        cohort.preCommit().get();
+        cohort.commit().get();
+
+        domStore.registerTreeChangeListener(TestModel.TEST_PATH, listener);
+
+        verify(listener, timeout(1000)).onDataTreeChanged(candidateCapture.capture());
+        final DataTreeCandidate firstItem = (DataTreeCandidate) candidateCapture.getValue().iterator().next();
+        assertEquals(TestModel.TEST_PATH, firstItem.getRootPath());
+    }
+}
index 81cc57d0cf0b32c94fd07e6124ca5f555375cf61..9b8e25cd30b48f27a413220a724b68d9ac37c058 100644 (file)
@@ -7,6 +7,8 @@
  */
 package org.opendaylight.mdsal.dom.store.inmemory;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import java.util.Optional;
@@ -19,6 +21,7 @@ import org.opendaylight.yangtools.concepts.ListenerRegistration;
 import org.opendaylight.yangtools.util.concurrent.QueuedNotificationManager;
 import org.opendaylight.yangtools.util.concurrent.QueuedNotificationManager.BatchedInvoker;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidates;
@@ -71,23 +74,40 @@ final class InMemoryDOMStoreTreeChangePublisher extends AbstractDOMStoreTreeChan
     <L extends DOMDataTreeChangeListener> ListenerRegistration<L> registerTreeChangeListener(
             final YangInstanceIdentifier treeId, final L listener, final DataTreeSnapshot snapshot) {
         final AbstractDOMDataTreeChangeListenerRegistration<L> reg = registerTreeChangeListener(treeId, listener);
+        final Optional<NormalizedNode<?, ?>> preExistingData = snapshot.readNode(YangInstanceIdentifier.EMPTY);
+        if (!preExistingData.isPresent()) {
+            listener.onInitialData();
+            return reg;
+        }
 
-        final Optional<NormalizedNode<?, ?>> node = snapshot.readNode(YangInstanceIdentifier.EMPTY);
-        if (node.isPresent()) {
-            final DataTreeCandidate candidate = DataTreeCandidates.fromNormalizedNode(
-                    YangInstanceIdentifier.EMPTY, node.get());
+        final NormalizedNode<?, ?> data = preExistingData.get();
+        if (treeId.isEmpty()) {
+            checkState(data instanceof DataContainerNode, "Unexpected root node %s", data);
+            if (((DataContainerNode) data).getValue().isEmpty()) {
+                // If we are listening on root of data tree we still get empty normalized node, root is always present,
+                // we should filter this out separately and notify it by 'onInitialData()' once.
+                // Otherwise, it is just a valid data node with empty value which also should be notified by
+                // "onDataTreeChanged(Collection<DataTreeCandidate>)".
+                listener.onInitialData();
+                return reg;
+            }
+        }
 
-            InMemoryDOMStoreTreeChangePublisher publisher =
-                    new InMemoryDOMStoreTreeChangePublisher(notificationManager);
-            publisher.registerTreeChangeListener(treeId, listener);
-            publisher.publishChange(candidate);
+        final DataTreeCandidate candidate = DataTreeCandidates.fromNormalizedNode(YangInstanceIdentifier.EMPTY, data);
+        final InMemoryDOMStoreTreeChangePublisher publisher = new InMemoryDOMStoreTreeChangePublisher(
+            notificationManager);
+        publisher.registerTreeChangeListener(treeId, listener);
+        if (!publisher.publishChange(candidate)) {
+            // There is no data in the conceptual data tree then
+            // notify with 'onInitialData()'.
+            listener.onInitialData();
         }
 
         return reg;
     }
 
-    synchronized void publishChange(final @NonNull DataTreeCandidate candidate) {
+    synchronized boolean publishChange(final @NonNull DataTreeCandidate candidate) {
         // Runs synchronized with registrationRemoved()
-        processCandidateTree(candidate);
+        return processCandidateTree(candidate);
     }
 }
index 6f8531a6cefe32a302933930ac8de4174fe685dc..bf70cea74c343a14193ecad47a85321fbc7fb54e 100644 (file)
@@ -40,6 +40,7 @@ public class InMemoryDOMDataStoreFactoryTest {
         final DOMDataTreeChangeListener domDataTreeChangeListener = mock(DOMDataTreeChangeListener.class);
         doReturn("testListener").when(domDataTreeChangeListener).toString();
         doNothing().when(domDataTreeChangeListener).onDataTreeChanged(any());
+        doNothing().when(domDataTreeChangeListener).onInitialData();
         inMemoryDOMDataStore.onGlobalContextUpdated(TestModel.createTestContext());
         inMemoryDOMDataStore.registerTreeChangeListener(YangInstanceIdentifier.EMPTY, domDataTreeChangeListener);
 
index 63cb45a66c2443e6fe98a350fc6c1de48319a232..8bc0499f6bd1a7aca8bf1fd094baec752f06e4b5 100644 (file)
@@ -65,12 +65,13 @@ public abstract class AbstractDOMStoreTreeChangePublisher
      * Process a candidate tree with respect to registered listeners.
      *
      * @param candidate candidate three which needs to be processed
+     * @return true if at least one listener was notified or false.
      */
-    protected final void processCandidateTree(final @NonNull DataTreeCandidate candidate) {
+    protected final boolean processCandidateTree(final @NonNull DataTreeCandidate candidate) {
         final DataTreeCandidateNode node = candidate.getRootNode();
         if (node.getModificationType() == ModificationType.UNMODIFIED) {
             LOG.debug("Skipping unmodified candidate {}", candidate);
-            return;
+            return false;
         }
 
         try (RegistrationTreeSnapshot<AbstractDOMDataTreeChangeListenerRegistration<?>> snapshot
@@ -84,6 +85,8 @@ public abstract class AbstractDOMStoreTreeChangePublisher
                     listenerChanges.asMap().entrySet()) {
                 notifyListener(entry.getKey(), entry.getValue());
             }
+
+            return !listenerChanges.isEmpty();
         }
     }