Add simple data listeners 80/105080/10
authorOleksandr Panasiuk <oleksandr.panasiuk@pantheon.tech>
Tue, 28 Mar 2023 09:36:14 +0000 (12:36 +0300)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 21 Jun 2023 10:14:12 +0000 (12:14 +0200)
DataTreeChangeListener is a rather complex contract, where users
sometimes want to receive only the latest state or the delta of the
state.

Add DataListener, which reports the current value and
DataChangeListener, which reports changes.

JIRA: MDSAL-813
Change-Id: I7db024f76709b9d4afcc0db5cbca3f1d35218e3f
Signed-off-by: Oleksandr Panasiuk <oleksandr.panasiuk@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataChangeListener.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataChangeListenerAdapter.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataListener.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataListenerAdapter.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataTreeChangeService.java
binding/mdsal-binding-api/src/test/java/org/opendaylight/mdsal/binding/api/DataTreeChangeServiceWildcardedTest.java [new file with mode: 0644]
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMDataChangeListenerAdapter.java [new file with mode: 0644]
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMDataListenerAdapter.java [new file with mode: 0644]
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMDataTreeChangeServiceAdapter.java
binding/mdsal-binding-dom-adapter/src/test/java/org/opendaylight/mdsal/binding/dom/adapter/DataListenerTest.java [new file with mode: 0644]
binding/mdsal-binding-test-model/src/main/yang/mdsal813.yang [new file with mode: 0644]

diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataChangeListener.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataChangeListener.java
new file mode 100644 (file)
index 0000000..1692f4e
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.binding.api;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * Interface implemented by classes interested in receiving data changes.
+ * It provides a comparison on before-value and after-value.
+ */
+@Beta
+@FunctionalInterface
+public interface DataChangeListener<T extends DataObject> {
+    /**
+     * Invoked when there was data change for the supplied path, which was used
+     * to register the listener.
+     *
+     * <p>
+     * Note: When invoking the method {@link DataTreeChangeListener#onInitialData} initial data == null, null.
+     * @param previousValue data before.
+     * @param currentValue data after.
+     */
+    void dataChanged(@Nullable T previousValue, @Nullable T currentValue);
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataChangeListenerAdapter.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataChangeListenerAdapter.java
new file mode 100644 (file)
index 0000000..6ed578d
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.binding.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ForwardingObject;
+import com.google.common.collect.Iterables;
+import java.util.Collection;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+final class DataChangeListenerAdapter<T extends DataObject> extends ForwardingObject
+        implements ClusteredDataTreeChangeListener<T> {
+    private final DataChangeListener<T> delegate;
+
+    DataChangeListenerAdapter(final DataChangeListener<T> delegate) {
+        this.delegate = requireNonNull(delegate);
+    }
+
+    @Override
+    public void onDataTreeChanged(final Collection<DataTreeModification<T>> changes) {
+        delegate.dataChanged(changes.iterator().next().getRootNode().getDataBefore(),
+            Iterables.getLast(changes).getRootNode().getDataAfter());
+    }
+
+    @Override
+    public void onInitialData() {
+        delegate.dataChanged(null, null);
+    }
+
+    @Override
+    protected DataChangeListener<T> delegate() {
+        return delegate;
+    }
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataListener.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataListener.java
new file mode 100644 (file)
index 0000000..3a72f64
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.binding.api;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * Interface implemented by classes interested in receiving the last data change.
+ */
+@Beta
+@FunctionalInterface
+public interface DataListener<T extends DataObject> {
+
+    /**
+     * Invoked when there was data change for the supplied path, which was used to register listener.
+     * @param data last state.
+     */
+    void dataChangedTo(@Nullable T data);
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataListenerAdapter.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/DataListenerAdapter.java
new file mode 100644 (file)
index 0000000..1cae318
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.binding.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ForwardingObject;
+import com.google.common.collect.Iterables;
+import java.util.Collection;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+final class DataListenerAdapter<T extends DataObject> extends ForwardingObject
+        implements ClusteredDataTreeChangeListener<T> {
+    private final DataListener<T> delegate;
+
+    DataListenerAdapter(final DataListener<T> delegate) {
+        this.delegate = requireNonNull(delegate);
+    }
+
+    @Override
+    public void onDataTreeChanged(final Collection<DataTreeModification<T>> changes) {
+        delegate.dataChangedTo(Iterables.getLast(changes).getRootNode().getDataAfter());
+    }
+
+    @Override
+    public void onInitialData() {
+        delegate.dataChangedTo(null);
+    }
+
+    @Override
+    protected DataListener<T> delegate() {
+        return delegate;
+    }
+}
index 6558ec8b273f77cc6d4c5dd65ac47abe5fa24f6a..7803e8787677b6929357d36cb2b7ce3366badea0 100644 (file)
@@ -9,7 +9,9 @@ package org.opendaylight.mdsal.binding.api;
 
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.concepts.Registration;
 import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 
 /**
  * A {@link BindingService} which allows users to register for changes to a subtree.
@@ -55,4 +57,81 @@ public interface DataTreeChangeService extends BindingService {
      */
     <T extends DataObject, L extends DataTreeChangeListener<T>> @NonNull ListenerRegistration<L>
             registerDataTreeChangeListener(@NonNull DataTreeIdentifier<T> treeId, @NonNull L listener);
+
+    /**
+     * Registers a {@link DataTreeChangeListener} to receive
+     * notifications when data changes under a given path in the conceptual data tree.
+     *
+     * <p>
+     * You are able to register for notifications  for any node or subtree
+     * which can be represented using {@link DataTreeIdentifier}.
+     *
+     * <p>
+     * This method returns a {@link Registration} object. To
+     * "unregister" your listener for changes call the {@link Registration#close()}
+     * method on the returned object.
+     *
+     * <p>
+     * You MUST explicitly unregister your listener when you no longer want to receive
+     * notifications. This is especially true in OSGi environments, where failure to
+     * do so during bundle shutdown can lead to stale listeners being still registered.
+     * @implSpec This method provides {@link DataListenerAdapter} as listener during
+     *      the registration of {@link DataTreeChangeListener}. This would allow users
+     *      to know the last state of data instead of getting details about what changed
+     *      in the entire tree.
+     * @param treeId Data tree identifier of the subtree which should be watched for
+     *            changes.
+     * @param listener Listener instance which is being registered
+     * @return Listener registration object, which may be used to unregister
+     *         your listener using {@link Registration#close()} to stop
+     *         delivery of change events.
+     */
+    default <T extends DataObject> @NonNull Registration registerDataListener(
+            final @NonNull DataTreeIdentifier<T> treeId, final @NonNull DataListener<T> listener) {
+        return registerDataTreeChangeListener(checkNotWildcard(treeId), new DataListenerAdapter<>(listener));
+    }
+
+    /**
+     * Registers a {@link DataTreeChangeListener} to receive
+     * notifications about the last data state when it changes under a given path in the conceptual data
+     * tree.
+     *
+     * <p>
+     * You are able to register for notifications  for any node or subtree
+     * which can be represented using {@link DataTreeIdentifier}.
+     *
+     * <p>
+     * This method returns a {@link Registration} object. To
+     * "unregister" your listener for changes call the {@link Registration#close()}
+     * method on the returned object.
+     *
+     * <p>
+     * You MUST explicitly unregister your listener when you no longer want to receive
+     * notifications. This is especially true in OSGi environments, where failure to
+     * do so during bundle shutdown can lead to stale listeners being still registered.
+     *
+     * @implSpec This method provides {@link DataChangeListenerAdapter} as listener during
+     *      the registration of {@link DataTreeChangeListener}, which provides a comparison
+     *      of before-value and after-value.
+     *
+     * @param treeId Data tree identifier of the subtree which should be watched for
+     *            changes.
+     * @param listener Listener instance which is being registered
+     * @return Listener registration object, which may be used to unregister
+     *         your listener using {@link Registration#close()} to stop
+     *         delivery of change events.
+     */
+    default <T extends DataObject> @NonNull Registration registerDataChangeListener(
+            final @NonNull DataTreeIdentifier<T> treeId, final @NonNull DataChangeListener<T> listener) {
+        return registerDataTreeChangeListener(checkNotWildcard(treeId), new DataChangeListenerAdapter<>(listener));
+    }
+
+    private static <T extends DataObject> @NonNull DataTreeIdentifier<T> checkNotWildcard(
+            final DataTreeIdentifier<T> treeId) {
+        final @NonNull InstanceIdentifier<T> instanceIdentifier = treeId.getRootIdentifier();
+        if (instanceIdentifier.isWildcarded()) {
+            throw new IllegalArgumentException("Cannot register listener for wildcard " + instanceIdentifier);
+        }
+        return treeId;
+    }
 }
\ No newline at end of file
diff --git a/binding/mdsal-binding-api/src/test/java/org/opendaylight/mdsal/binding/api/DataTreeChangeServiceWildcardedTest.java b/binding/mdsal-binding-api/src/test/java/org/opendaylight/mdsal/binding/api/DataTreeChangeServiceWildcardedTest.java
new file mode 100644 (file)
index 0000000..55ad0a4
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.binding.api;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.yang.gen.v1.mdsal813.norev.RegisterListenerTest;
+import org.opendaylight.yang.gen.v1.mdsal813.norev.register.listener.test.Item;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+public class DataTreeChangeServiceWildcardedTest {
+
+    DataBroker dataBroker;
+
+    DataListener<Item> listener;
+
+    DataChangeListener<Item> changeListener;
+
+    @Before
+    public void setUp() {
+        dataBroker = mock(DataBroker.class);
+        listener = mock(DataListener.class);
+        changeListener = mock(DataChangeListener.class);
+    }
+
+    @Test
+    public void testThrowExceptionOnRegister() {
+        final InstanceIdentifier<Item> instanceIdentifier = InstanceIdentifier.builder(RegisterListenerTest.class)
+            .child(Item.class).build();
+        final DataTreeIdentifier<Item> itemsDataTreeIdentifier = DataTreeIdentifier.create(
+            LogicalDatastoreType.OPERATIONAL, instanceIdentifier);
+
+        doCallRealMethod().when(dataBroker).registerDataListener(any(), any());
+        final var dataListenerException = assertThrows(IllegalArgumentException.class,
+            () -> dataBroker.registerDataListener(itemsDataTreeIdentifier, listener));
+        assertTrue(dataListenerException.getMessage().contains("Cannot register listener for wildcard"));
+
+        doCallRealMethod().when(dataBroker).registerDataChangeListener(any(), any());
+        final var dataListenerChangeException = assertThrows(IllegalArgumentException.class,
+            () -> dataBroker.registerDataChangeListener(itemsDataTreeIdentifier, changeListener));
+        assertTrue(dataListenerChangeException.getMessage().contains("Cannot register listener for wildcard"));
+    }
+
+}
diff --git a/binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMDataChangeListenerAdapter.java b/binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMDataChangeListenerAdapter.java
new file mode 100644 (file)
index 0000000..371a56a
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.binding.dom.adapter;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.mdsal.binding.api.DataChangeListener;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingAugmentationCodecTreeNode;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingDataObjectCodecTreeNode;
+import org.opendaylight.mdsal.binding.dom.codec.api.CommonDataObjectCodecTreeNode;
+import org.opendaylight.mdsal.dom.api.ClusteredDOMDataTreeChangeListener;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
+
+final class BindingDOMDataChangeListenerAdapter<T extends DataObject> implements ClusteredDOMDataTreeChangeListener {
+    private final AdapterContext adapterContext;
+    private final DataChangeListener<T> listener;
+
+    BindingDOMDataChangeListenerAdapter(final AdapterContext adapterContext, final DataChangeListener<T> listener) {
+        this.adapterContext = requireNonNull(adapterContext);
+        this.listener = requireNonNull(listener);
+    }
+
+    @Override
+    public void onDataTreeChanged(final List<DataTreeCandidate> changes) {
+        final var first = changes.get(0);
+        final var serializer = adapterContext.currentSerializer();
+        final var codec = serializer.getSubtreeCodec(serializer.coerceInstanceIdentifier(first.getRootPath()));
+
+        listener.dataChanged(deserialize(codec, first.getRootNode().dataBefore()),
+            deserialize(codec, changes.get(changes.size() - 1).getRootNode().dataAfter()));
+    }
+
+    @Override
+    public void onInitialData() {
+        listener.dataChanged(null, null);
+    }
+
+    @SuppressWarnings("unchecked")
+    private @Nullable T deserialize(final CommonDataObjectCodecTreeNode<?> codec, final NormalizedNode data) {
+        if (data == null) {
+            return null;
+        } else if (codec instanceof BindingDataObjectCodecTreeNode<?> dataObject) {
+            return (T) dataObject.deserialize(data);
+        } else if (codec instanceof BindingAugmentationCodecTreeNode<?> augmentation) {
+            return (T) augmentation.filterFrom(data);
+        } else {
+            throw new IllegalStateException("Unhandled codec " + codec);
+        }
+    }
+}
diff --git a/binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMDataListenerAdapter.java b/binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMDataListenerAdapter.java
new file mode 100644 (file)
index 0000000..03f716c
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.binding.dom.adapter;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.mdsal.binding.api.DataListener;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingAugmentationCodecTreeNode;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingDataObjectCodecTreeNode;
+import org.opendaylight.mdsal.dom.api.ClusteredDOMDataTreeChangeListener;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
+
+final class BindingDOMDataListenerAdapter<T extends DataObject> implements ClusteredDOMDataTreeChangeListener {
+    private final AdapterContext adapterContext;
+    private final DataListener<T> listener;
+
+    BindingDOMDataListenerAdapter(final AdapterContext adapterContext, final DataListener<T> listener) {
+        this.adapterContext = requireNonNull(adapterContext);
+        this.listener = requireNonNull(listener);
+    }
+
+    @Override
+    public void onDataTreeChanged(final List<DataTreeCandidate> changes) {
+        final var last = changes.get(changes.size() - 1);
+        final var after = last.getRootNode().dataAfter();
+        listener.dataChangedTo(after == null ? null : deserialize(last.getRootPath(), after));
+    }
+
+    @Override
+    public void onInitialData() {
+        listener.dataChangedTo(null);
+    }
+
+    @SuppressWarnings("unchecked")
+    private T deserialize(final YangInstanceIdentifier path, final @NonNull NormalizedNode data) {
+        final var serializer = adapterContext.currentSerializer();
+        final var codec = serializer.getSubtreeCodec(serializer.coerceInstanceIdentifier(path));
+        if (codec instanceof BindingDataObjectCodecTreeNode<?> dataObject) {
+            return (T) dataObject.deserialize(data);
+        } else if (codec instanceof BindingAugmentationCodecTreeNode<?> augmentation) {
+            return (T) augmentation.filterFrom(data);
+        } else {
+            throw new IllegalStateException("Unhandled codec " + codec);
+        }
+    }
+}
index ea84fdc514cf2e8e1cda2427fc6cd3ac9b2b96a9..7326a0501f2667e96e4829f18d788bb82fa263cc 100644 (file)
@@ -7,7 +7,10 @@
  */
 package org.opendaylight.mdsal.binding.dom.adapter;
 
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.mdsal.binding.api.ClusteredDataTreeChangeListener;
+import org.opendaylight.mdsal.binding.api.DataChangeListener;
+import org.opendaylight.mdsal.binding.api.DataListener;
 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
 import org.opendaylight.mdsal.binding.api.DataTreeChangeService;
 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
@@ -15,6 +18,7 @@ import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeService;
 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
 import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.concepts.Registration;
 import org.opendaylight.yangtools.yang.binding.Augmentation;
 import org.opendaylight.yangtools.yang.binding.DataObject;
 
@@ -51,8 +55,30 @@ final class BindingDOMDataTreeChangeServiceAdapter extends AbstractBindingAdapte
         return new BindingDataTreeChangeListenerRegistration<>(listener, domReg);
     }
 
-    private DOMDataTreeIdentifier toDomTreeIdentifier(final DataTreeIdentifier<?> treeId) {
+    @Override
+    public <T extends DataObject> Registration registerDataListener(final DataTreeIdentifier<T> treeId,
+            final DataListener<T> listener) {
+        return getDelegate().registerDataTreeChangeListener(toDomTreeInstance(treeId),
+            new BindingDOMDataListenerAdapter<>(adapterContext(), listener));
+    }
+
+    @Override
+    public <T extends DataObject> Registration registerDataChangeListener(final DataTreeIdentifier<T> treeId,
+            final DataChangeListener<T> listener) {
+        return getDelegate().registerDataTreeChangeListener(toDomTreeInstance(treeId),
+            new BindingDOMDataChangeListenerAdapter<>(adapterContext(), listener));
+    }
+
+    private @NonNull DOMDataTreeIdentifier toDomTreeIdentifier(final DataTreeIdentifier<?> treeId) {
         return new DOMDataTreeIdentifier(treeId.getDatastoreType(),
             currentSerializer().toYangInstanceIdentifier(treeId.getRootIdentifier()));
     }
+
+    private @NonNull DOMDataTreeIdentifier toDomTreeInstance(final DataTreeIdentifier<?> treeId) {
+        final var instanceIdentifier = treeId.getRootIdentifier();
+        if (instanceIdentifier.isWildcarded()) {
+            throw new IllegalArgumentException("Cannot register listener for wildcard " + instanceIdentifier);
+        }
+        return toDomTreeIdentifier(treeId);
+    }
 }
diff --git a/binding/mdsal-binding-dom-adapter/src/test/java/org/opendaylight/mdsal/binding/dom/adapter/DataListenerTest.java b/binding/mdsal-binding-dom-adapter/src/test/java/org/opendaylight/mdsal/binding/dom/adapter/DataListenerTest.java
new file mode 100644 (file)
index 0000000..a347b04
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.binding.dom.adapter;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.opendaylight.mdsal.binding.api.DataBroker;
+import org.opendaylight.mdsal.binding.api.DataChangeListener;
+import org.opendaylight.mdsal.binding.api.DataListener;
+import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
+import org.opendaylight.mdsal.binding.api.WriteTransaction;
+import org.opendaylight.mdsal.binding.dom.adapter.test.AbstractDataBrokerTest;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.yang.gen.v1.mdsal813.norev.RegisterListenerTest;
+import org.opendaylight.yang.gen.v1.mdsal813.norev.RegisterListenerTestBuilder;
+import org.opendaylight.yang.gen.v1.mdsal813.norev.register.listener.test.Item;
+import org.opendaylight.yang.gen.v1.mdsal813.norev.register.listener.test.ItemBuilder;
+import org.opendaylight.yang.gen.v1.mdsal813.norev.register.listener.test.ItemKey;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.opendaylight.yangtools.yang.binding.util.BindingMap;
+import org.opendaylight.yangtools.yang.common.Uint32;
+
+public class DataListenerTest extends AbstractDataBrokerTest {
+    DataBroker dataBroker;
+
+    DataListener<Item> listener;
+
+    DataChangeListener<Item> changeListener;
+
+    @Before
+    public void setUp() {
+        dataBroker = getDataBroker();
+        listener = mock(DataListener.class);
+        changeListener = mock(DataChangeListener.class);
+    }
+
+    @Test
+    public void testThrowExceptionOnRegister() {
+        final InstanceIdentifier<Item> instanceIdentifier = InstanceIdentifier.builder(RegisterListenerTest.class)
+            .child(Item.class).build();
+        final DataTreeIdentifier<Item> itemsDataTreeIdentifier = DataTreeIdentifier.create(
+            LogicalDatastoreType.OPERATIONAL,
+            instanceIdentifier);
+
+        final Throwable dataListenerException = assertThrows(IllegalArgumentException.class,
+            () -> dataBroker.registerDataListener(itemsDataTreeIdentifier, listener));
+        assertTrue(dataListenerException.getMessage().contains("Cannot register listener for wildcard"));
+
+        final Throwable dataListenerChangeException = assertThrows(IllegalArgumentException.class,
+            () -> dataBroker.registerDataChangeListener(itemsDataTreeIdentifier, changeListener));
+        assertTrue(dataListenerChangeException.getMessage().contains("Cannot register listener for wildcard"));
+    }
+
+    @Test
+    public void testRegisterDataListener() {
+        final Item item = writeItems();
+        final InstanceIdentifier<Item> instanceIdentifier = InstanceIdentifier.builder(RegisterListenerTest.class)
+            .child(Item.class, new ItemKey(item.key())).build();
+
+        dataBroker.registerDataListener(
+            DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, instanceIdentifier), listener);
+
+        verify(listener, timeout(100)).dataChangedTo(item);
+    }
+
+    @Test
+    public void testRegisterDataChangeListener() {
+        final Item item = writeItems();
+        final InstanceIdentifier<Item> instanceIdentifier = InstanceIdentifier.builder(RegisterListenerTest.class)
+            .child(Item.class, new ItemKey(item.key())).build();
+
+        dataBroker.registerDataChangeListener(
+            DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, instanceIdentifier), changeListener);
+
+        verify(changeListener, timeout(100)).dataChanged(null, item);
+    }
+
+    private Item writeItems() {
+        final WriteTransaction writeTransaction = getDataBroker().newWriteOnlyTransaction();
+        final Item wildcardItem = new ItemBuilder().setText("name").setNumber(Uint32.valueOf(43)).build();
+        final RegisterListenerTestBuilder builder = new RegisterListenerTestBuilder().setItem(
+            BindingMap.of(wildcardItem));
+        writeTransaction.put(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.builder(
+            RegisterListenerTest.class).build(), builder.build());
+        assertCommit(writeTransaction.commit());
+        return wildcardItem;
+    }
+}
diff --git a/binding/mdsal-binding-test-model/src/main/yang/mdsal813.yang b/binding/mdsal-binding-test-model/src/main/yang/mdsal813.yang
new file mode 100644 (file)
index 0000000..618d923
--- /dev/null
@@ -0,0 +1,17 @@
+module mdsal813 {
+  namespace mdsal813;
+  prefix mdsal813;
+
+  container register-listener-test {
+    list item {
+      key "text number";
+      leaf text {
+
+        type string;
+      }
+      leaf number {
+        type uint32;
+      }
+    }
+  }
+}
\ No newline at end of file