Add NotificationService.registerListener() 76/97976/5
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 19 Oct 2021 12:08:56 +0000 (14:08 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 19 Oct 2021 16:12:35 +0000 (18:12 +0200)
We want to get rid of FooListener-based notifications, so that we
can use simple lambdas and proper composition. This takes the first
step towards that by allowing users to register a simple
@FunctionalInterface-based listener.

JIRA: MDSAL-700
Change-Id: Ica1ac23515c813366d5d66dc91c3183695f84ce7
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/NotificationService.java
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/AbstractDOMNotificationListenerAdapter.java [new file with mode: 0644]
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMNotificationListenerAdapter.java
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingDOMNotificationServiceAdapter.java
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/SingleBindingDOMNotificationAdapter.java [new file with mode: 0644]
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/osgi/OSGiNotificationService.java
binding/mdsal-binding-dom-adapter/src/test/java/org/opendaylight/mdsal/binding/dom/adapter/ForwardedNotificationAdapterTest.java

index 8d49fb5e5db3f074c0340ed8b537b5b98fc337e4..6ff74ca34159dd8a8d2b5ff8e8b9b2f28500b282 100644 (file)
@@ -7,8 +7,13 @@
  */
 package org.opendaylight.mdsal.binding.api;
 
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.EventListener;
+import java.util.concurrent.Executor;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.concepts.Registration;
+import org.opendaylight.yangtools.yang.binding.Notification;
 import org.opendaylight.yangtools.yang.binding.NotificationListener;
 
 /**
@@ -88,10 +93,58 @@ public interface NotificationService extends BindingService {
      * {@link NotificationListener}. The listener is registered for all notifications present in
      * the implemented interface.
      *
-     * <p>
+     * @param <T> NotificationListener type
      * @param listener the listener implementation that will receive notifications.
      * @return a {@link ListenerRegistration} instance that should be used to unregister the listener
      *         by invoking the {@link ListenerRegistration#close()} method when no longer needed.
      */
     <T extends NotificationListener> @NonNull ListenerRegistration<T> registerNotificationListener(@NonNull T listener);
+
+    /**
+     * Registers a {@link Listener} to receive callbacks for {@link Notification}s of a particular type.
+     *
+     * @param <N> Notification type
+     * @param type Notification type class
+     * @param listener The listener implementation that will receive notifications
+     * @param executor Executor to use for invoking the listener's methods
+     * @return a {@link Registration} instance that should be used to unregister the listener by invoking the
+     *        {@link Registration#close()} method when no longer needed
+     */
+    <N extends Notification> @NonNull Registration registerListener(Class<N> type, Listener<N> listener,
+        Executor executor);
+
+    /**
+     * Registers a {@link Listener} to receive callbacks for {@link Notification}s of a particular type.
+     *
+     * @implSpec
+     *     This method is equivalent to {@code registerListener(type, listener, MoreExecutors.directExecutor())}, i.e.
+     *     the listener will be invoked on some implementation-specific thread.
+     *
+     * @param <N> Notification type
+     * @param type Notification type class
+     * @param listener The listener implementation that will receive notifications
+     * @return a {@link Registration} instance that should be used to unregister the listener by invoking the
+     *        {@link Registration#close()} method when no longer needed
+     */
+    default <N extends Notification> @NonNull Registration registerListener(final Class<N> type,
+            final Listener<N> listener) {
+        return registerListener(type, listener, MoreExecutors.directExecutor());
+    }
+
+    /**
+     * Interface for listeners on global (YANG 1.0) notifications. Such notifications are identified by their generated
+     * interface which extends {@link Notification}. Each listener instance can listen to only a single notification
+     * type.
+     *
+     * @param N Notification type
+     */
+    @FunctionalInterface
+    interface Listener<N extends Notification> extends EventListener {
+        /**
+         * Process a global notification.
+         *
+         * @param notification Notification body
+         */
+        void onNotification(@NonNull N notification);
+    }
 }
diff --git a/binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/AbstractDOMNotificationListenerAdapter.java b/binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/AbstractDOMNotificationListenerAdapter.java
new file mode 100644 (file)
index 0000000..f85f2ab
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ * Copyright (c) 2021 PANTHEON.tech, s.r.o.
+ *
+ * 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 com.google.common.base.Verify.verifyNotNull;
+import static java.util.Objects.requireNonNull;
+
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.mdsal.dom.api.DOMEvent;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.mdsal.dom.api.DOMNotificationListener;
+import org.opendaylight.yangtools.yang.binding.Notification;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+
+abstract class AbstractDOMNotificationListenerAdapter implements DOMNotificationListener {
+    private final AdapterContext adapterContext;
+
+    AbstractDOMNotificationListenerAdapter(final AdapterContext adapterContext) {
+        this.adapterContext = requireNonNull(adapterContext);
+    }
+
+    @Override
+    public final void onNotification(final DOMNotification notification) {
+        onNotification(notification.getType(), verifyNotNull(deserialize(notification)));
+    }
+
+    abstract void onNotification(@NonNull Absolute domType, @NonNull Notification notification);
+
+    abstract Set<Absolute> getSupportedNotifications();
+
+    private Notification deserialize(final DOMNotification notification) {
+        if (notification instanceof LazySerializedDOMNotification) {
+            // TODO: This is a routed-back notification, for which we may end up losing event time here, but that is
+            //       okay, for now at least.
+            return ((LazySerializedDOMNotification) notification).getBindingData();
+        }
+
+        final CurrentAdapterSerializer serializer = adapterContext.currentSerializer();
+        return notification instanceof DOMEvent
+            ? serializer.fromNormalizedNodeNotification(notification.getType(), notification.getBody(),
+                ((DOMEvent) notification).getEventInstant())
+                : serializer.fromNormalizedNodeNotification(notification.getType(), notification.getBody());
+    }
+}
index 2785500fa2f9af350a37e07fbe673bea650110fb..cf09f350ceaabfe33a12ba355a56a79fcfb6a045 100644 (file)
@@ -18,51 +18,27 @@ import java.util.Map;
 import java.util.Set;
 import org.opendaylight.mdsal.binding.dom.adapter.invoke.NotificationListenerInvoker;
 import org.opendaylight.mdsal.binding.spec.reflect.BindingReflections;
-import org.opendaylight.mdsal.dom.api.DOMEvent;
-import org.opendaylight.mdsal.dom.api.DOMNotification;
-import org.opendaylight.mdsal.dom.api.DOMNotificationListener;
 import org.opendaylight.yangtools.yang.binding.Notification;
 import org.opendaylight.yangtools.yang.binding.NotificationListener;
-import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
 
-class BindingDOMNotificationListenerAdapter implements DOMNotificationListener {
-
-    private final AdapterContext adapterContext;
-    private final NotificationListener delegate;
+final class BindingDOMNotificationListenerAdapter extends AbstractDOMNotificationListenerAdapter {
     private final ImmutableMap<Absolute, NotificationListenerInvoker> invokers;
+    private final NotificationListener delegate;
 
     BindingDOMNotificationListenerAdapter(final AdapterContext adapterContext, final NotificationListener delegate) {
-        this.adapterContext = requireNonNull(adapterContext);
+        super(adapterContext);
         this.delegate = requireNonNull(delegate);
-        this.invokers = createInvokerMapFor(delegate.getClass());
+        invokers = createInvokerMapFor(delegate.getClass());
     }
 
     @Override
-    public void onNotification(final DOMNotification notification) {
-        final Notification baNotification = deserialize(notification);
-        final QName notificationQName = notification.getType().lastNodeIdentifier();
-        getInvoker(notification.getType()).invokeNotification(delegate, notificationQName, baNotification);
-    }
-
-    private Notification deserialize(final DOMNotification notification) {
-        if (notification instanceof LazySerializedDOMNotification) {
-            // TODO: This is a routed-back notification, for which we may end up losing event time here, but that is
-            //       okay, for now at least.
-            return ((LazySerializedDOMNotification) notification).getBindingData();
-        }
-
-        final CurrentAdapterSerializer serializer = adapterContext.currentSerializer();
-        return notification instanceof DOMEvent ? serializer.fromNormalizedNodeNotification(notification.getType(),
-            notification.getBody(), ((DOMEvent) notification).getEventInstant())
-                : serializer.fromNormalizedNodeNotification(notification.getType(), notification.getBody());
+    void onNotification(final Absolute domType, final Notification notification) {
+        invokers.get(domType).invokeNotification(delegate, domType.lastNodeIdentifier(), notification);
     }
 
-    private NotificationListenerInvoker getInvoker(final Absolute type) {
-        return invokers.get(type);
-    }
-
-    protected Set<Absolute> getSupportedNotifications() {
+    @Override
+    Set<Absolute> getSupportedNotifications() {
         return invokers.keySet();
     }
 
index 7f935e6aa108dd5739564afb230f23072223d1e3..ce26cbca98224edf2da4bae818bff06faff324e1 100644 (file)
@@ -13,12 +13,15 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ClassToInstanceMap;
 import com.google.common.collect.ImmutableSet;
 import java.util.Set;
+import java.util.concurrent.Executor;
 import org.opendaylight.mdsal.binding.api.NotificationService;
 import org.opendaylight.mdsal.binding.dom.adapter.BindingDOMAdapterBuilder.Factory;
 import org.opendaylight.mdsal.dom.api.DOMNotificationService;
 import org.opendaylight.mdsal.dom.api.DOMService;
 import org.opendaylight.yangtools.concepts.AbstractListenerRegistration;
 import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.concepts.Registration;
+import org.opendaylight.yangtools.yang.binding.Notification;
 import org.opendaylight.yangtools.yang.binding.NotificationListener;
 
 @VisibleForTesting
@@ -43,6 +46,17 @@ public class BindingDOMNotificationServiceAdapter implements NotificationService
         return new ListenerRegistrationImpl<>(listener, domRegistration);
     }
 
+    @Override
+    public <N extends Notification> Registration registerListener(final Class<N> type, final Listener<N> listener,
+            final Executor executor) {
+        final var domListener = new SingleBindingDOMNotificationAdapter<>(adapterContext, type, listener, executor);
+        return domNotifService.registerNotificationListener(domListener, domListener.getSupportedNotifications());
+    }
+
+    public DOMNotificationService getDomService() {
+        return domNotifService;
+    }
+
     private static class ListenerRegistrationImpl<T extends NotificationListener>
             extends AbstractListenerRegistration<T> {
         private final ListenerRegistration<?> listenerRegistration;
@@ -74,8 +88,4 @@ public class BindingDOMNotificationServiceAdapter implements NotificationService
             return ImmutableSet.of(DOMNotificationService.class);
         }
     }
-
-    public DOMNotificationService getDomService() {
-        return domNotifService;
-    }
 }
diff --git a/binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/SingleBindingDOMNotificationAdapter.java b/binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/SingleBindingDOMNotificationAdapter.java
new file mode 100644 (file)
index 0000000..0a7ead1
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 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.Set;
+import java.util.concurrent.Executor;
+import org.opendaylight.mdsal.binding.api.NotificationService.Listener;
+import org.opendaylight.mdsal.binding.spec.reflect.BindingReflections;
+import org.opendaylight.yangtools.yang.binding.Notification;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+
+final class SingleBindingDOMNotificationAdapter<N extends Notification> extends AbstractDOMNotificationListenerAdapter {
+    private final Listener<N> delegate;
+    private final Executor executor;
+    private final Class<N> type;
+
+    SingleBindingDOMNotificationAdapter(final AdapterContext adapterContext, final Class<N> type,
+            final Listener<N> delegate, final Executor executor) {
+        super(adapterContext);
+        this.type = requireNonNull(type);
+        this.delegate = requireNonNull(delegate);
+        this.executor = requireNonNull(executor);
+    }
+
+    @Override
+    void onNotification(final Absolute domType, final Notification notification) {
+        executor.execute(() -> delegate.onNotification(type.cast(notification)));
+    }
+
+    @Override
+    Set<Absolute> getSupportedNotifications() {
+        return Set.of(Absolute.of(BindingReflections.findQName(type)));
+    }
+}
index 85b0de45d81b7c12eaff9d78cd8428d7dc0909f0..0513f2efb2bc7a7f66963fb3f25c89dc22b0d37c 100644 (file)
@@ -9,8 +9,12 @@ package org.opendaylight.mdsal.binding.dom.adapter.osgi;
 
 import com.google.common.annotations.Beta;
 import java.util.Map;
+import java.util.concurrent.Executor;
 import org.opendaylight.mdsal.binding.api.NotificationService;
+import org.opendaylight.mdsal.binding.api.NotificationService.Listener;
 import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.concepts.Registration;
+import org.opendaylight.yangtools.yang.binding.Notification;
 import org.opendaylight.yangtools.yang.binding.NotificationListener;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -32,6 +36,12 @@ public final class OSGiNotificationService extends AbstractAdaptedService<Notifi
         return delegate().registerNotificationListener(listener);
     }
 
+    @Override
+    public <N extends Notification> Registration registerListener(final Class<N> type, final Listener<N> listener,
+            final Executor executor) {
+        return delegate().registerListener(type, listener, executor);
+    }
+
     @Activate
     void activate(final Map<String, ?> properties) {
         start(properties);
index 90e3ed96b358f8a56c2cafc87b6d0830dbccf8b8..d998bcbea3178c8d1ec138d97860755f21c7fabd 100644 (file)
@@ -9,10 +9,8 @@ package org.opendaylight.mdsal.binding.dom.adapter;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertSame;
 
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -20,9 +18,10 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import org.junit.Assert;
+import org.eclipse.jdt.annotation.NonNull;
 import org.junit.Test;
 import org.opendaylight.mdsal.binding.api.NotificationPublishService;
+import org.opendaylight.mdsal.binding.api.NotificationService.Listener;
 import org.opendaylight.mdsal.binding.dom.adapter.test.AbstractNotificationBrokerTest;
 import org.opendaylight.mdsal.binding.spec.reflect.BindingReflections;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.test.binding.rev140701.OpendaylightMdsalBindingTestListener;
@@ -30,101 +29,147 @@ import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.te
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.test.binding.rev140701.TwoLevelListChangedBuilder;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.test.binding.rev140701.two.level.list.TopLevelListBuilder;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.test.binding.rev140701.two.level.list.TopLevelListKey;
-import org.opendaylight.yangtools.concepts.ListenerRegistration;
 import org.opendaylight.yangtools.yang.binding.YangModuleInfo;
+import org.opendaylight.yangtools.yang.binding.util.BindingMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class ForwardedNotificationAdapterTest extends AbstractNotificationBrokerTest {
-
     private static final Logger LOG = LoggerFactory.getLogger(ForwardedNotificationAdapterTest.class);
 
     @Override
     protected Set<YangModuleInfo> getModuleInfos() throws Exception {
-        return ImmutableSet.of(BindingReflections.getModuleInfo(TwoLevelListChanged.class));
-
+        return Set.of(BindingReflections.getModuleInfo(TwoLevelListChanged.class));
     }
 
-    private static TwoLevelListChanged createTestData() {
-        final TopLevelListKey key = new TopLevelListKey("test");
-        return new TwoLevelListChangedBuilder()
-                .setTopLevelList(ImmutableMap.of(key, new TopLevelListBuilder().withKey(key).build()))
-                .build();
+    @Test
+    public void testPutSubscription() throws InterruptedException {
+        final var listener = new TestNotifListener(1);
+        try (var reg = getNotificationService().registerNotificationListener(listener)) {
+            assertPutSubscription(listener);
+        }
     }
 
     @Test
-    public void testNotifSubscription() throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final TwoLevelListChanged testData = createTestData();
+    public void testPutSubscriptionSimple() throws InterruptedException {
+        final var listener = new SimpleNotifListener(1);
+        try (var reg = getNotificationService().registerListener(TwoLevelListChanged.class, listener)) {
+            assertPutSubscription(listener);
+        }
+    }
 
-        final TestNotifListener testNotifListener = new TestNotifListener(latch);
-        final ListenerRegistration<TestNotifListener> listenerRegistration = getNotificationService()
-                .registerNotificationListener(testNotifListener);
+    private void assertPutSubscription(final AbstractNotifListener listener) throws InterruptedException {
+        final var testData = createTestData();
         getNotificationPublishService().putNotification(testData);
 
-        latch.await();
-        assertTrue(testNotifListener.getReceivedNotifications().size() == 1);
-        assertEquals(testData, testNotifListener.getReceivedNotifications().get(0));
+        final var received = listener.awaitNotifications();
+        assertEquals(1, received.size());
+        assertSame(testData, received.get(0));
+    }
 
-        listenerRegistration.close();
+    @Test
+    public void testOfferSubscription() throws InterruptedException {
+        final var listener = new TestNotifListener(1);
+        try (var reg = getNotificationService().registerNotificationListener(listener)) {
+            assertOfferNotification(listener);
+        }
     }
 
     @Test
-    public void testNotifSubscription2() throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final TwoLevelListChanged testData = createTestData();
+    public void testOfferSubscriptionSimple() throws InterruptedException {
+        final var listener = new SimpleNotifListener(1);
+        try (var reg = getNotificationService().registerListener(TwoLevelListChanged.class, listener)) {
+            assertOfferNotification(listener);
+        }
+    }
+
+    private void assertOfferNotification(final AbstractNotifListener listener) throws InterruptedException {
+        final var testData = createTestData();
 
-        final TestNotifListener testNotifListener = new TestNotifListener(latch);
-        final ListenerRegistration<TestNotifListener> listenerRegistration = getNotificationService()
-                .registerNotificationListener(testNotifListener);
         try {
             getNotificationPublishService().offerNotification(testData).get(1, TimeUnit.SECONDS);
         } catch (ExecutionException | TimeoutException e) {
-            LOG.error("Notification delivery failed", e);
-            Assert.fail("notification should be delivered");
+            throw new AssertionError("Notification should be delivered", e);
         }
 
-        latch.await();
-        assertTrue(testNotifListener.getReceivedNotifications().size() == 1);
-        assertEquals(testData, testNotifListener.getReceivedNotifications().get(0));
+        final var received = listener.awaitNotifications();
+        assertEquals(1, received.size());
+        assertSame(testData, received.get(0));
+    }
 
-        listenerRegistration.close();
+    @Test
+    public void testOfferTimedNotification() throws InterruptedException {
+        final var listener = new TestNotifListener(1);
+        try (var reg = getNotificationService().registerNotificationListener(listener)) {
+            assertOfferTimedNotification(listener);
+        }
     }
 
     @Test
-    public void testNotifSubscription3() throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final TwoLevelListChanged testData = createTestData();
+    public void testOfferTimedNotificationSimple() throws InterruptedException {
+        final var listener = new SimpleNotifListener(1);
+        try (var reg = getNotificationService().registerListener(TwoLevelListChanged.class, listener)) {
+            assertOfferTimedNotification(listener);
+        }
+    }
+
+    private void assertOfferTimedNotification(final AbstractNotifListener listener) throws InterruptedException {
+        final var testData = createTestData();
 
-        final TestNotifListener testNotifListener = new TestNotifListener(latch);
-        final ListenerRegistration<TestNotifListener> listenerRegistration = getNotificationService()
-                .registerNotificationListener(testNotifListener);
         assertNotSame(NotificationPublishService.REJECTED,
-                getNotificationPublishService().offerNotification(testData, 5, TimeUnit.SECONDS));
+            getNotificationPublishService().offerNotification(testData, 5, TimeUnit.SECONDS));
+
+        final var received = listener.awaitNotifications();
+        assertEquals(1, received.size());
+        assertSame(testData, received.get(0));
+    }
 
-        latch.await();
-        assertTrue(testNotifListener.getReceivedNotifications().size() == 1);
-        assertEquals(testData, testNotifListener.getReceivedNotifications().get(0));
 
-        listenerRegistration.close();
+    private static @NonNull TwoLevelListChanged createTestData() {
+        return new TwoLevelListChangedBuilder()
+            .setTopLevelList(BindingMap.of(new TopLevelListBuilder().withKey(new TopLevelListKey("test")).build()))
+            .build();
     }
 
-    private static class TestNotifListener implements OpendaylightMdsalBindingTestListener {
+    private abstract static class AbstractNotifListener {
         private final List<TwoLevelListChanged> receivedNotifications = new ArrayList<>();
         private final CountDownLatch latch;
 
-        TestNotifListener(final CountDownLatch latch) {
-            this.latch = latch;
+        AbstractNotifListener(final int expectedCount) {
+            latch = new CountDownLatch(expectedCount);
         }
 
-        @Override
-        public void onTwoLevelListChanged(final TwoLevelListChanged notification) {
+        final void receiveNotification(final TwoLevelListChanged notification) {
             receivedNotifications.add(notification);
             latch.countDown();
         }
 
-        public List<TwoLevelListChanged> getReceivedNotifications() {
+        final List<TwoLevelListChanged> awaitNotifications() throws InterruptedException {
+            latch.await();
             return receivedNotifications;
         }
     }
+
+    private static class SimpleNotifListener extends AbstractNotifListener implements Listener<TwoLevelListChanged> {
+        SimpleNotifListener(final int expectedCount) {
+            super(expectedCount);
+        }
+
+        @Override
+        public void onNotification(final TwoLevelListChanged notification) {
+            receiveNotification(notification);
+        }
+    }
+
+    private static class TestNotifListener extends AbstractNotifListener
+            implements OpendaylightMdsalBindingTestListener {
+        TestNotifListener(final int expectedCount) {
+            super(expectedCount);
+        }
+
+        @Override
+        public void onTwoLevelListChanged(final TwoLevelListChanged notification) {
+            receiveNotification(notification);
+        }
+    }
 }