* @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
+ }
}
*/
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;
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 {
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();
+ }
}
* @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
+ }
}
--- /dev/null
+/*
+ * 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());
+ }
+}
*/
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;
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;
<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);
}
}
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);
* 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
listenerChanges.asMap().entrySet()) {
notifyListener(entry.getKey(), entry.getValue());
}
+
+ return !listenerChanges.isEmpty();
}
}