Moving datastore related utils from mdsalutil to new "genius.tools"
authorFaseela K <faseela.k@ericsson.com>
Wed, 21 Mar 2018 12:43:48 +0000 (18:13 +0530)
committerFaseela K <faseela.k@ericsson.com>
Mon, 9 Apr 2018 08:09:12 +0000 (13:39 +0530)
There are several requirements coming in where users are asking for a
simple feature with only datastore related utils. Currently all the utils
are parked in mdsalutil which has even openflowplugin related utils, and is embedded
in a heavier genius feature. This patch aims to move of these heavily used utils
to a new module, and add new features to expose the same.

Change-Id: I7d59273851c51b02fe43afcb1525c94ced1c5088
Signed-off-by: Faseela K <faseela.k@ericsson.com>
14 files changed:
api/pom.xml [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractAsyncDataTreeChangeListener.java [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractClusteredAsyncDataTreeChangeListener.java [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractClusteredSyncDataTreeChangeListener.java [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractDataTreeChangeListener.java [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractSyncDataTreeChangeListener.java [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/DataTreeChangeListenerActions.java [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/package-info.java [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/metrics/DataStoreMetrics.java [new file with mode: 0644]
api/src/main/java/org/opendaylight/genius/tools/mdsal/rpc/FutureRpcResults.java [new file with mode: 0644]
pom.xml [new file with mode: 0644]
testutils/pom.xml [new file with mode: 0644]
testutils/src/main/java/org/opendaylight/genius/tools/mdsal/testutils/TestFutureRpcResults.java [new file with mode: 0644]
testutils/src/test/java/org/opendaylight/genius/tools/mdsal/rpc/FutureRpcResultsTest.java [new file with mode: 0644]

diff --git a/api/pom.xml b/api/pom.xml
new file mode 100644 (file)
index 0000000..b18d411
--- /dev/null
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright © 2018 Ericsson India Global Services Pvt Ltd. 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
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.opendaylight.genius</groupId>
+    <artifactId>binding-parent</artifactId>
+    <version>0.5.0-SNAPSHOT</version>
+    <relativePath>../../commons/binding-parent</relativePath>
+  </parent>
+
+  <groupId>org.opendaylight.genius</groupId>
+  <artifactId>tools-api</artifactId>
+  <version>0.5.0-SNAPSHOT</version>
+  <packaging>bundle</packaging>
+  <!-- <name> formatting is used by autorelease to parse and notify projects on
+       build failure. Please do not modify this unless you have a good reason. -->
+  <name>ODL :: genius :: ${project.artifactId}</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>util</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-binding-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.infrautils</groupId>
+      <artifactId>inject</artifactId>
+      <version>${genius.infrautils.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.infrautils</groupId>
+      <artifactId>metrics-api</artifactId>
+      <version>${genius.infrautils.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.infrautils</groupId>
+      <artifactId>infrautils-util</artifactId>
+      <version>${genius.infrautils.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.immutables</groupId>
+      <artifactId>value</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.ops4j.pax.cdi</groupId>
+      <artifactId>pax-cdi-api</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+    <!-- Dependencies with <scope>test -->
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>testutils</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.awaitility</groupId>
+      <artifactId>awaitility</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.mdsal</groupId>
+      <artifactId>mdsal-binding-test-utils</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.infrautils</groupId>
+      <artifactId>infrautils-testutils</artifactId>
+      <version>${genius.infrautils.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava-testlib</artifactId>
+    </dependency>
+    <dependency>
+       <groupId>org.opendaylight.controller</groupId>
+       <artifactId>sal-binding-broker-impl</artifactId>
+       <scope>test</scope>
+     </dependency>
+     <dependency>
+       <groupId>org.opendaylight.controller</groupId>
+       <artifactId>sal-binding-broker-impl</artifactId>
+       <scope>test</scope>
+       <type>test-jar</type>
+     </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>test-jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractAsyncDataTreeChangeListener.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractAsyncDataTreeChangeListener.java
new file mode 100644 (file)
index 0000000..9fc7882
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2018 Ericsson S.A. 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.genius.tools.mdsal.listener;
+
+import java.util.Collection;
+import java.util.concurrent.ExecutorService;
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.infrautils.metrics.MetricProvider;
+import org.opendaylight.yangtools.util.concurrent.SpecialExecutors;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Abstract class providing some common functionality to specific listeners. This listener launches the received
+ * notifications by using an {@link ExecutorService}.
+ *
+ * <p>The {@link ExecutorService} passed to the constructor will depend on the use case. Here we have some examples:
+ *
+ * <p>- If the listener is fast enough and non-blocking: MoreExecutors.directExecutor might be used, or even better,
+ * just use the {@link AbstractSyncDataTreeChangeListener}.
+ *
+ * <p>- If the listener is heavy or could be blocked: use a multi-threaded executor.
+ * We recommend using one of the factory methods in {@link SpecialExecutors}.
+ * You could and probably should share such an Executor among several listeners in your project.
+ *
+ * <p>- If the listener needs to preserve the order of notifications, then (only) use a single thread executor typically
+ * an {@link org.opendaylight.infrautils.utils.concurrent.Executors#newSingleThreadExecutor(String, org.slf4j.Logger)}.
+ *
+ * <p>- If there are multiple listeners: they could even share an Executor as the ones in {@link SpecialExecutors},
+ *
+ * <p>Subclasses are also encouraged to, in addition to passing the ExecutorService for use in
+ * production (by Blueprint wiring) based on above via super(), expose a public constructor letting tests specify
+ * an alternative ExecutorService; this is useful e.g. to inject infrautils' AwaitableExecutorService for testing.
+ *
+ * @param <T> type of the data object the listener is registered to.
+ *
+ * @author David Suárez (david.suarez.fuentes@gmail.com)
+ */
+public abstract class AbstractAsyncDataTreeChangeListener<T extends DataObject> extends
+        AbstractDataTreeChangeListener<T> {
+
+    private final ExecutorService executorService;
+
+    @Inject
+    public AbstractAsyncDataTreeChangeListener(DataBroker dataBroker, DataTreeIdentifier<T> dataTreeIdentifier,
+                                               ExecutorService executorService) {
+        super(dataBroker, dataTreeIdentifier);
+        this.executorService = executorService;
+    }
+
+    @Inject
+    public AbstractAsyncDataTreeChangeListener(DataBroker dataBroker, LogicalDatastoreType datastoreType,
+                                               InstanceIdentifier<T> instanceIdentifier,
+                                               ExecutorService executorService) {
+        super(dataBroker, datastoreType, instanceIdentifier);
+        this.executorService = executorService;
+    }
+
+    @Inject
+    public AbstractAsyncDataTreeChangeListener(DataBroker dataBroker,
+                                               LogicalDatastoreType datastoreType,
+                                               InstanceIdentifier<T> instanceIdentifier,
+                                               ExecutorService executorService,
+                                               MetricProvider metricProvider) {
+        super(dataBroker, datastoreType, instanceIdentifier, metricProvider);
+        this.executorService = executorService;
+    }
+
+    @Override
+    public final void onDataTreeChanged(@Nonnull Collection<DataTreeModification<T>> collection) {
+        executorService.execute(() -> super.onDataTreeChanged(collection,
+                getDataStoreMetrics()));
+    }
+
+    /**
+     * Returns the ExecutorService provided when constructing this instance. If the subclass owns the
+     * ExecutorService, it should be shut down when closing the listener using this getter.
+     *
+     * @return executor service
+     */
+    protected ExecutorService getExecutorService() {
+        return executorService;
+    }
+}
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractClusteredAsyncDataTreeChangeListener.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractClusteredAsyncDataTreeChangeListener.java
new file mode 100644 (file)
index 0000000..af5c4bd
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2018 Ericsson S.A. 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.genius.tools.mdsal.listener;
+
+import java.util.Collection;
+import java.util.concurrent.ExecutorService;
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.infrautils.metrics.MetricProvider;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Abstract class providing some common functionality to specific listeners. This is the clustered version of the
+ * {@link AbstractAsyncDataTreeChangeListener}.  Please carefully consider whether you really need an async listener,
+ * or if {@link AbstractClusteredSyncDataTreeChangeListener} wouldn't work just fine for your use case.
+ *
+ * @param <T> type of the data object the listener is registered to.
+ *
+ * @see AbstractAsyncDataTreeChangeListener
+ *
+ * @author David Suárez (david.suarez.fuentes@gmail.com)
+ */
+public abstract class AbstractClusteredAsyncDataTreeChangeListener<T extends DataObject> extends
+        AbstractDataTreeChangeListener<T> implements ClusteredDataTreeChangeListener<T> {
+
+    private final ExecutorService executorService;
+
+    @Inject
+    public AbstractClusteredAsyncDataTreeChangeListener(DataBroker dataBroker, DataTreeIdentifier<T> dataTreeIdentifier,
+                                                        ExecutorService executorService) {
+        super(dataBroker, dataTreeIdentifier);
+        this.executorService = executorService;
+    }
+
+    @Inject
+    public AbstractClusteredAsyncDataTreeChangeListener(DataBroker dataBroker, LogicalDatastoreType datastoreType,
+                                                        InstanceIdentifier<T> instanceIdentifier,
+                                                        ExecutorService executorService) {
+        super(dataBroker, datastoreType, instanceIdentifier);
+        this.executorService = executorService;
+    }
+
+    @Inject
+    public AbstractClusteredAsyncDataTreeChangeListener(DataBroker dataBroker,
+                                                        LogicalDatastoreType datastoreType,
+                                                        InstanceIdentifier<T> instanceIdentifier,
+                                                        ExecutorService executorService,
+                                                        MetricProvider metricProvider) {
+        super(dataBroker, datastoreType, instanceIdentifier, metricProvider);
+        this.executorService = executorService;
+    }
+
+    @Override
+    public final void onDataTreeChanged(@Nonnull Collection<DataTreeModification<T>> collection) {
+        executorService.execute(() -> super.onDataTreeChanged(collection,
+                getDataStoreMetrics()));
+    }
+
+    /**
+     * Returns the ExecutorService provided when constructing this instance. If the subclass owns the
+     * ExecutorService, it should be shut down when closing the listener using this getter.
+     *
+     * @return executor service
+     */
+    protected ExecutorService getExecutorService() {
+        return executorService;
+    }
+}
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractClusteredSyncDataTreeChangeListener.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractClusteredSyncDataTreeChangeListener.java
new file mode 100644 (file)
index 0000000..4556a61
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018 Ericsson S.A. 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.genius.tools.mdsal.listener;
+
+import java.util.Collection;
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+
+import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.infrautils.metrics.MetricProvider;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Abstract class providing some common functionality to specific listeners. This is the clustered version of the
+ * {@link AbstractSyncDataTreeChangeListener}.
+ *
+ * @param <T> type of the data object the listener is registered to.
+ *
+ * @see AbstractSyncDataTreeChangeListener
+ *
+ * @author David Suárez (david.suarez.fuentes@gmail.com)
+ */
+public abstract class AbstractClusteredSyncDataTreeChangeListener<T extends DataObject> extends
+        AbstractDataTreeChangeListener<T> implements ClusteredDataTreeChangeListener<T> {
+
+    @Inject
+    public AbstractClusteredSyncDataTreeChangeListener(DataBroker dataBroker,
+                                                       DataTreeIdentifier<T> dataTreeIdentifier) {
+        super(dataBroker, dataTreeIdentifier);
+    }
+
+    @Inject
+    public AbstractClusteredSyncDataTreeChangeListener(DataBroker dataBroker, LogicalDatastoreType datastoreType,
+                                                       InstanceIdentifier<T> instanceIdentifier) {
+        super(dataBroker, datastoreType, instanceIdentifier);
+    }
+
+    @Inject
+    public AbstractClusteredSyncDataTreeChangeListener(DataBroker dataBroker,
+                                                       LogicalDatastoreType datastoreType,
+                                                       InstanceIdentifier<T> instanceIdentifier,
+                                                       MetricProvider metricProvider) {
+        super(dataBroker, datastoreType, instanceIdentifier, metricProvider);
+    }
+
+    @Override
+    public final void onDataTreeChanged(@Nonnull Collection<DataTreeModification<T>> collection) {
+        super.onDataTreeChanged(collection, getDataStoreMetrics());
+    }
+}
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractDataTreeChangeListener.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractDataTreeChangeListener.java
new file mode 100644 (file)
index 0000000..9b3a563
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2018 Ericsson S.A. 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.genius.tools.mdsal.listener;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeChangeListener;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.genius.tools.mdsal.metrics.DataStoreMetrics;
+import org.opendaylight.infrautils.metrics.MetricProvider;
+import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Abstract class providing some common functionality to abstract listeners. This class is not designed to be
+ * extended by the specific listeners, that's why it is package-private. It provides subclasses with access to the
+ * {@link DataBroker} passed as constructor argument, listener registration/de-registration and a shutdown method to
+ * be implemented if needed by the subclasses (e.g. shutting down services, closing resources, etc.)
+ *
+ * @param <T> type of the data object the listener is registered to.
+ * @author David Suárez (david.suarez.fuentes@gmail.com)
+ */
+abstract class AbstractDataTreeChangeListener<T extends DataObject> implements DataTreeChangeListener<T>,
+        DataTreeChangeListenerActions<T>, AutoCloseable {
+
+    private final DataBroker dataBroker;
+    private final DataTreeIdentifier<T> dataTreeIdentifier;
+    private ListenerRegistration<AbstractDataTreeChangeListener<T>> dataChangeListenerRegistration;
+    private DataStoreMetrics dataStoreMetrics;
+
+    @Inject
+    AbstractDataTreeChangeListener(DataBroker dataBroker, DataTreeIdentifier<T> dataTreeIdentifier) {
+        this.dataBroker = dataBroker;
+        this.dataTreeIdentifier = dataTreeIdentifier;
+    }
+
+    @Inject
+    AbstractDataTreeChangeListener(DataBroker dataBroker, LogicalDatastoreType datastoreType,
+                                   InstanceIdentifier<T> instanceIdentifier) {
+        this(dataBroker, new DataTreeIdentifier<>(datastoreType, instanceIdentifier));
+    }
+
+    @Inject
+    AbstractDataTreeChangeListener(DataBroker dataBroker,
+                                   LogicalDatastoreType datastoreType,
+                                   InstanceIdentifier<T> instanceIdentifier,
+                                   MetricProvider metricProvider) {
+        this(dataBroker, new DataTreeIdentifier<>(datastoreType, instanceIdentifier));
+        this.dataStoreMetrics = new DataStoreMetrics(metricProvider, getClass());
+    }
+
+    @PostConstruct
+    public void register() {
+        this.dataChangeListenerRegistration = dataBroker.registerDataTreeChangeListener(dataTreeIdentifier, this);
+    }
+
+    protected DataBroker getDataBroker() {
+        return dataBroker;
+    }
+
+    protected DataStoreMetrics getDataStoreMetrics() {
+        return dataStoreMetrics;
+    }
+
+    @Override
+    @PreDestroy
+    public void close() {
+        if (dataChangeListenerRegistration != null) {
+            dataChangeListenerRegistration.close();
+        }
+    }
+
+    @Override
+    @Deprecated
+    public void add(@Nonnull T newDataObject) {
+        // TODO: to be removed after all listeners migrated to use the new methods
+    }
+
+    @Override
+    @Deprecated
+    public void remove(@Nonnull T removedDataObject) {
+        // TODO: to be removed after all listeners migrated to use the new methods
+    }
+
+    @Override
+    @Deprecated
+    public void update(@Nonnull T originalDataObject, @Nonnull T updatedDataObject) {
+        // TODO: to be removed after all listeners migrated to use the new methods
+    }
+}
+
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractSyncDataTreeChangeListener.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/AbstractSyncDataTreeChangeListener.java
new file mode 100644 (file)
index 0000000..ee758d9
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2018 Ericsson S.A. 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.genius.tools.mdsal.listener;
+
+import java.util.Collection;
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.infrautils.metrics.MetricProvider;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Abstract class providing some common functionality to specific listeners. This should be the most common parent
+ * class for any listener that does not need to be either cluster-aware (see
+ * {@link AbstractClusteredSyncDataTreeChangeListener}) or to process notifications asynchronously
+ * (see {@link AbstractAsyncDataTreeChangeListener}) or even both characteristics (see
+ * {@link AbstractClusteredAsyncDataTreeChangeListener}).
+ *
+ * @param <T> type of the data object the listener is registered to.
+ *
+ * @author David Suárez (david.suarez.fuentes@gmail.com)
+ */
+public abstract class AbstractSyncDataTreeChangeListener<T extends DataObject> extends
+        AbstractDataTreeChangeListener<T> {
+
+    @Inject
+    public AbstractSyncDataTreeChangeListener(DataBroker dataBroker, DataTreeIdentifier<T> dataTreeIdentifier) {
+        super(dataBroker, dataTreeIdentifier);
+    }
+
+    @Inject
+    public AbstractSyncDataTreeChangeListener(DataBroker dataBroker, LogicalDatastoreType datastoreType,
+                                              InstanceIdentifier<T> instanceIdentifier) {
+        super(dataBroker, datastoreType, instanceIdentifier);
+    }
+
+    @Inject
+    public AbstractSyncDataTreeChangeListener(DataBroker dataBroker, LogicalDatastoreType datastoreType,
+                                              InstanceIdentifier<T> instanceIdentifier,
+                                              MetricProvider metricProvider) {
+        super(dataBroker, datastoreType, instanceIdentifier, metricProvider);
+    }
+
+    @Override
+    public final void onDataTreeChanged(@Nonnull Collection<DataTreeModification<T>> collection) {
+        super.onDataTreeChanged(collection, getDataStoreMetrics());
+    }
+}
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/DataTreeChangeListenerActions.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/DataTreeChangeListenerActions.java
new file mode 100644 (file)
index 0000000..7734c3c
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2018 Ericsson, S.A. 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.genius.tools.mdsal.listener;
+
+import java.util.Collection;
+import javax.annotation.Nonnull;
+import javax.inject.Singleton;
+import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
+import org.opendaylight.genius.tools.mdsal.metrics.DataStoreMetrics;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Interface to be implemented by classes interested in receiving notifications
+ * about data tree changes. It implements a default method to handle the data
+ * tree modifications. Those notifications will be forwarded to the appropriate
+ * methods (add, update, remove) depending on their action type. The listeners
+ * implementing this interface will need to be annotated as {@link Singleton}.
+ *
+ * @param <T> type of the data object the listener is registered to.
+ * @author David Suárez (david.suarez.fuentes@gmail.com)
+ */
+interface DataTreeChangeListenerActions<T extends DataObject> {
+
+    /**
+     * Default method invoked upon data tree change, in turn it calls the
+     * appropriate method (add, update, remove) depending on the type of change.
+     *
+     * @param changes          collection of changes
+     * @param dataStoreMetrics data store metrics
+     */
+    default void onDataTreeChanged(@Nonnull Collection<DataTreeModification<T>> changes,
+                                   DataStoreMetrics dataStoreMetrics) {
+        // This code is also in DataTreeEventCallbackRegistrarImpl and any changes should be applied there as well
+        for (final DataTreeModification<T> dataTreeModification : changes) {
+            final InstanceIdentifier<T> instanceIdentifier = dataTreeModification.getRootPath().getRootIdentifier();
+            final DataObjectModification<T> dataObjectModification = dataTreeModification.getRootNode();
+            final T dataBefore = dataObjectModification.getDataBefore();
+            final T dataAfter = dataObjectModification.getDataAfter();
+
+            switch (dataObjectModification.getModificationType()) {
+                case SUBTREE_MODIFIED:
+                    if (dataStoreMetrics != null) {
+                        dataStoreMetrics.incrementUpdated();
+                    }
+                    update(instanceIdentifier, dataBefore, dataAfter);
+                    break;
+                case DELETE:
+                    if (dataStoreMetrics != null) {
+                        dataStoreMetrics.incrementDeleted();
+                    }
+                    remove(instanceIdentifier, dataBefore);
+                    break;
+                case WRITE:
+                    if (dataBefore == null) {
+                        if (dataStoreMetrics != null) {
+                            dataStoreMetrics.incrementAdded();
+                        }
+                        add(instanceIdentifier, dataAfter);
+                    } else {
+                        if (dataStoreMetrics != null) {
+                            dataStoreMetrics.incrementUpdated();
+                        }
+                        update(instanceIdentifier, dataBefore, dataAfter);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Invoked when a new data object is added.
+     *
+     * @param instanceIdentifier instance id for this data object
+     * @param newDataObject      newly added object
+     */
+    default void add(@Nonnull InstanceIdentifier<T> instanceIdentifier, @Nonnull T newDataObject) {
+        add(newDataObject);
+    }
+
+    /**
+     * Invoked when a new data object added.
+     *
+     * @param newDataObject newly added object
+     */
+    @Deprecated
+    void add(@Nonnull T newDataObject);
+
+    /**
+     * Invoked when the data object has been removed.
+     *
+     * @param instanceIdentifier instance id for this data object
+     * @param removedDataObject  existing object being removed
+     */
+    default void remove(@Nonnull InstanceIdentifier<T> instanceIdentifier, @Nonnull T removedDataObject) {
+        remove(removedDataObject);
+    }
+
+    /**
+     * Invoked when the data object has been removed.
+     *
+     * @param removedDataObject existing object being removed
+     */
+    @Deprecated
+    void remove(@Nonnull T removedDataObject);
+
+    /**
+     * Invoked when there is a change in the data object.
+     *
+     * @param instanceIdentifier instance id for this data object
+     * @param originalDataObject existing object being modified
+     * @param updatedDataObject  modified data object
+     */
+    default void update(@Nonnull InstanceIdentifier<T> instanceIdentifier, @Nonnull T originalDataObject,
+                        @Nonnull T updatedDataObject) {
+        update(originalDataObject, updatedDataObject);
+    }
+
+    /**
+     * Invoked when there is a change in the data object.
+     *
+     * @param originalDataObject existing object being modified
+     * @param updatedDataObject  modified data object
+     */
+    @Deprecated
+    void update(@Nonnull T originalDataObject, @Nonnull T updatedDataObject);
+}
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/package-info.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/listener/package-info.java
new file mode 100644 (file)
index 0000000..9a1220c
--- /dev/null
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2018 Ericsson S.A. 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
+ */
+/**
+ * Incubator package for a listeners framework. This code might be proposed into
+ * upstream projects once merged and proven to be useful here for a while.
+ *
+ * @author David Suárez (david.suarez.fuentes@gmail.com)
+ */
+package org.opendaylight.genius.tools.mdsal.listener;
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/metrics/DataStoreMetrics.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/metrics/DataStoreMetrics.java
new file mode 100644 (file)
index 0000000..5253356
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2018 Ericsson India Global Services Pvt Ltd. 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.genius.tools.mdsal.metrics;
+
+import org.opendaylight.infrautils.metrics.Meter;
+import org.opendaylight.infrautils.metrics.MetricDescriptor;
+import org.opendaylight.infrautils.metrics.MetricProvider;
+
+public class DataStoreMetrics {
+
+    private final MetricProvider metricProvider;
+    private final Class<?> clazz;
+    private final Meter added;
+    private final Meter updated;
+    private final Meter deleted;
+
+    public DataStoreMetrics(MetricProvider metricProvider, Class<?> clazz) {
+        this.metricProvider = metricProvider;
+        this.clazz = clazz;
+        this.added = initCounter("_added");
+        this.updated = initCounter("_updated");
+        this.deleted = initCounter("_deleted");
+    }
+
+    public void incrementAdded() {
+        added.mark();
+    }
+
+    public void incrementUpdated() {
+        updated.mark();
+    }
+
+    public void incrementDeleted() {
+        deleted.mark();
+    }
+
+    private Meter initCounter(String type) {
+        String className = clazz.getSimpleName();
+        // expects the form org.opendaylight.project.module
+        String project = clazz.getName().split("\\.")[2];
+        String module = clazz.getName().split("\\.")[3];
+        return metricProvider.newMeter(new MetricDescriptor() {
+            @Override
+            public Object anchor() {
+                return this;
+            }
+
+            @Override
+            public String project() {
+                return project;
+            }
+
+            @Override
+            public String module() {
+                return module;
+            }
+
+            @Override
+            public String id() {
+                return className + type;
+            }
+        });
+    }
+}
diff --git a/api/src/main/java/org/opendaylight/genius/tools/mdsal/rpc/FutureRpcResults.java b/api/src/main/java/org/opendaylight/genius/tools/mdsal/rpc/FutureRpcResults.java
new file mode 100644 (file)
index 0000000..1f3d646
--- /dev/null
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2018 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.genius.tools.mdsal.rpc;
+
+import static org.opendaylight.yangtools.yang.common.RpcError.ErrorType.APPLICATION;
+
+import com.google.common.annotations.Beta;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import edu.umd.cs.findbugs.annotations.SuppressWarnings;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import javax.annotation.CheckReturnValue;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+import org.opendaylight.infrautils.utils.StackTraces;
+import org.opendaylight.yangtools.concepts.Builder;
+import org.opendaylight.yangtools.yang.common.OperationFailedException;
+import org.opendaylight.yangtools.yang.common.RpcError;
+import org.opendaylight.yangtools.yang.common.RpcResult;
+import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
+import org.slf4j.Logger;
+
+/**
+ * Utility to simplify correctly handling transformation of Future of RpcResult to return.
+ *
+ * @author Michael Vorburger.ch
+ */
+@Beta
+public final class FutureRpcResults {
+
+    // NB: The FutureRpcResultsTest unit test for this util is in mdsalutil-testutils's src/test, not this project's
+
+    // TODO Once matured in genius, this class could be proposed to org.opendaylight.yangtools.yang.common
+    // (This was proposed in Oct on yangtools-dev list, but there little interest due to plans to change RpcResult.)
+
+    private FutureRpcResults() {}
+
+    /**
+     * Create a Builder for a ListenableFuture to Future&lt;RpcResult&lt;O&gt;&gt; transformer. By default, the future
+     * will log success or failure, with configurable log levels; the caller can also add handlers for success and/or
+     * failure.
+     *
+     * <p>The RPC's method name is automatically obtained using {@link StackTraces}.  This has some cost, which in
+     * the overall scheme of a typical RPC is typically negligible, but on a highly optimized fast path could
+     * theoretically be an issue; if you see this method as a hot spot in a profiler, then (only) use the
+     * alternative signature where you manually pass the String rpcMethodName.
+     *
+     * @param logger the slf4j Logger of the caller
+     * @param input the RPC input DataObject of the caller (may be null)
+     * @param callable the Callable (typically lambda) creating a ListenableFuture.  Note that the
+     *        functional interface Callable's call() method declares throws Exception, so your lambda
+     *        does not have to do any exception handling (specifically it does NOT have to catch and
+     *        wrap any exception into a failed Future); this utility does that for you.
+     *
+     * @return a new Builder
+     */
+    @CheckReturnValue
+    public static <I, O> FutureRpcResultBuilder<I, O> fromListenableFuture(Logger logger,
+            @Nullable I input, Callable<ListenableFuture<O>> callable) {
+        return new FutureRpcResultBuilder<>(logger, StackTraces.getCallersCallerMethodName(), input, callable);
+    }
+
+    /**
+     * Create a Builder for a ListenableFuture to Future&lt;RpcResult&lt;O&gt;&gt; transformer. By default, the future
+     * will log success or failure, with configurable log levels; the caller can also add handlers for success and/or
+     * failure.
+     *
+     * @param logger the slf4j Logger of the caller
+     * @param rpcMethodName Java method name (without "()") of the RPC operation, used for logging
+     * @param input the RPC input DataObject of the caller (may be null)
+     * @param callable the Callable (typically lambda) creating a ListenableFuture.  Note that the
+     *        functional interface Callable's call() method declares throws Exception, so your lambda
+     *        does not have to do any exception handling (specifically it does NOT have to catch and
+     *        wrap any exception into a failed Future); this utility does that for you.
+     *
+     * @return a new FutureRpcResultBuilder
+     */
+    @CheckReturnValue
+    public static <I, O> FutureRpcResultBuilder<I, O> fromListenableFuture(Logger logger, String rpcMethodName,
+            @Nullable I input, Callable<ListenableFuture<O>> callable) {
+        return new FutureRpcResultBuilder<>(logger, rpcMethodName, input, callable);
+    }
+
+    public enum LogLevel {
+        ERROR, WARN, INFO, DEBUG, TRACE,
+        /**
+         * Note that when using LogLevel NONE for failures, then you should set a
+         * {@link FutureRpcResultBuilder#onFailure(Consumer)} which does better logging,
+         * or be 100% sure that all callers of the RPC check the returned Future RpcResult appropriately;
+         * otherwise you will lose error messages.
+         */
+        NONE;
+        @SuppressWarnings({"SLF4J_UNKNOWN_ARRAY","SLF4J_FORMAT_SHOULD_BE_CONST"})
+        public void log(Logger logger, String format, Object... arguments) {
+            switch (this) {
+                case NONE:
+                    break;
+                case TRACE:
+                    logger.trace(format, arguments);
+                    break;
+                case DEBUG:
+                    logger.debug(format, arguments);
+                    break;
+                case INFO:
+                    logger.info(format, arguments);
+                    break;
+                case WARN:
+                    logger.warn(format, arguments);
+                    break;
+                default: // including ERROR
+                    logger.error(format, arguments);
+                    break;
+            }
+        }
+    }
+
+    @CheckReturnValue
+    public static <I, O> FutureRpcResultBuilder<I, O> fromBuilder(Logger logger, String rpcMethodName,
+            @Nullable I input, Callable<Builder<O>> builder) {
+        Callable<ListenableFuture<O>> callable = () -> Futures.immediateFuture(builder.call().build());
+        return fromListenableFuture(logger, rpcMethodName, input, callable);
+    }
+
+    @CheckReturnValue
+    public static <I, O> FutureRpcResultBuilder<I, O> fromBuilder(Logger logger, @Nullable I input,
+            Callable<Builder<O>> builder) {
+        Callable<ListenableFuture<O>> callable = () -> Futures.immediateFuture(builder.call().build());
+        return fromListenableFuture(logger, StackTraces.getCallersCallerMethodName(), input, callable);
+    }
+
+    @NotThreadSafe
+    public static final class FutureRpcResultBuilder<I, O> implements Builder<Future<RpcResult<O>>> {
+
+        private static final Function<Throwable, String> DEFAULT_ERROR_MESSAGE_FUNCTION = Throwable::getMessage;
+        private static final Consumer<Throwable> DEFAULT_ON_FAILURE = throwable -> { };
+        private final Consumer<O> defaultOnSuccess = result -> { };
+
+        // fixed (final) builder values
+        private final Logger logger;
+        private final String rpcMethodName;
+        @Nullable private final I input;
+        private final Callable<ListenableFuture<O>> callable;
+
+        // optional builder values, which can be overridden by users
+        private Function<Throwable, String> rpcErrorMessageFunction = DEFAULT_ERROR_MESSAGE_FUNCTION;
+        private Consumer<O> onSuccessConsumer = defaultOnSuccess;
+        private Consumer<Throwable> onFailureConsumer = DEFAULT_ON_FAILURE;
+
+        // defaulted builder values, which can be overridden by users
+        private LogLevel onEnterLogLevel = LogLevel.TRACE;
+        private LogLevel onSuccessLogLevel = LogLevel.DEBUG;
+        private LogLevel onFailureLogLevel = LogLevel.ERROR;
+
+        private FutureRpcResultBuilder(Logger logger, String rpcMethodName, @Nullable I input,
+                Callable<ListenableFuture<O>> callable) {
+            this.logger = logger;
+            this.rpcMethodName = rpcMethodName;
+            this.input = input;
+            this.callable = callable;
+        }
+
+        /**
+         * Builds the Future RpcResult.
+         *
+         * @return Future RpcResult. Note that this will NEVER be a failed Future; any
+         *         errors are reported as !{@link RpcResult#isSuccessful()}, with
+         *         details in {@link RpcResult#getErrors()}, and not the Future itself.
+         */
+        @Override
+        @CheckReturnValue
+        @SuppressWarnings("checkstyle:IllegalCatch")
+        public Future<RpcResult<O>> build() {
+            SettableFuture<RpcResult<O>> futureRpcResult = SettableFuture.create();
+            FutureCallback<O> callback = new FutureCallback<O>() {
+                @Override
+                public void onSuccess(O result) {
+                    onSuccessLogLevel.log(logger, "RPC {}() successful; input = {}, output = {}", rpcMethodName,
+                            input, result);
+                    onSuccessConsumer.accept(result);
+                    futureRpcResult.set(RpcResultBuilder.success(result).build());
+                }
+
+                @Override
+                public void onFailure(Throwable cause) {
+                    onFailureLogLevel.log(logger, "RPC {}() failed; input = {}", rpcMethodName, input, cause);
+                    onFailureConsumer.accept(cause);
+                    RpcResultBuilder<O> rpcResultBuilder =  RpcResultBuilder.failed();
+                    if (cause instanceof OperationFailedException) {
+                        // NB: This looses (not not propagate) the cause, and only preserves the error list
+                        // But we did log the cause above, so it can still be found.
+                        rpcResultBuilder.withRpcErrors(((OperationFailedException) cause).getErrorList());
+                    } else {
+                        rpcResultBuilder.withError(APPLICATION, rpcErrorMessageFunction.apply(cause), cause);
+                    }
+                    futureRpcResult.set(rpcResultBuilder.build());
+                }
+            };
+            try {
+                onEnterLogLevel.log(logger, "RPC {}() entered; input = {}", rpcMethodName, input);
+                Futures.addCallback(callable.call(), callback, MoreExecutors.directExecutor());
+            } catch (Exception cause) {
+                callback.onFailure(cause);
+            }
+            return futureRpcResult;
+        }
+
+        /**
+         * Sets a custom on-failure action, for a given exception.
+         */
+        public FutureRpcResultBuilder<I,O> onFailure(Consumer<Throwable> newOnFailureConsumer) {
+            if (onFailureConsumer != DEFAULT_ON_FAILURE) {
+                throw new IllegalStateException("onFailure can only be set once");
+            }
+            this.onFailureConsumer = newOnFailureConsumer;
+            return this;
+        }
+
+        /**
+         * Sets a custom on-failure SLF4J logging level, in case of an exception. The log message mentions the RPC
+         * method name, the provided input, the exception and its stack trace (depending on logger settings).
+         * By default, it is {@code LOG.error}. Setting {@code NONE} will disable this logging.
+         */
+        public FutureRpcResultBuilder<I,O> onFailureLogLevel(LogLevel level) {
+            this.onFailureLogLevel = level;
+            return this;
+        }
+
+        /**
+         * Sets a custom on-success SLF4J logging level. The log message mentions the RPC method name, the provided
+         * input, and the resulting output.
+         * By default, it is {@code LOG.debug}. Setting {@code NONE} will disable this logging.
+         */
+        public FutureRpcResultBuilder<I,O> onSuccessLogLevel(LogLevel level) {
+            this.onSuccessLogLevel = level;
+            return this;
+        }
+
+        /**
+         * Sets a custom on-enter SLF4J logging level. The log message mentions the RPC method name and the provided
+         * input.
+         * By default, it is {@code LOG.trace}. Setting {@code NONE} will disable this logging.
+         */
+        public FutureRpcResultBuilder<I,O> onEnterLogLevel(LogLevel level) {
+            this.onEnterLogLevel = level;
+            return this;
+        }
+
+        /**
+         * Set a custom {@link RpcError} message function, for a given exception.
+         * By default, the message is just {@link Throwable#getMessage()}.
+         */
+        public FutureRpcResultBuilder<I,O> withRpcErrorMessage(Function<Throwable, String> newRpcErrorMessageFunction) {
+            if (rpcErrorMessageFunction != DEFAULT_ERROR_MESSAGE_FUNCTION) {
+                throw new IllegalStateException("rpcErrorMessage can only be set once");
+            }
+            this.rpcErrorMessageFunction = newRpcErrorMessageFunction;
+            return this;
+        }
+
+        /**
+         * Sets a custom on-success action, for a given output.
+         */
+        public FutureRpcResultBuilder<I,O> onSuccess(Consumer<O> newOnSuccessFunction) {
+            if (onSuccessConsumer != defaultOnSuccess) {
+                throw new IllegalStateException("onSuccess can only be set once");
+            }
+            this.onSuccessConsumer = newOnSuccessFunction;
+            return this;
+        }
+
+    }
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644 (file)
index 0000000..5d2b7df
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (c) 2018 Ericsson India Global Services Pvt Ltd. 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 INTERNAL
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.opendaylight.odlparent</groupId>
+    <artifactId>odlparent-lite</artifactId>
+    <version>3.0.2</version>
+    <relativePath/>
+  </parent>
+
+  <groupId>org.opendaylight.genius</groupId>
+  <artifactId>tools-aggregator</artifactId>
+  <version>0.5.0-SNAPSHOT</version>
+  <!-- <name> formatting is used by autorelease to parse and notify projects on
+       build failure. Please do not modify this unless you have a good reason. -->
+  <name>ODL :: genius :: ${project.artifactId}</name>
+  <packaging>pom</packaging>
+
+  <modules>
+    <module>api</module>
+    <module>testutils</module>
+  </modules>
+
+  <!-- DO NOT install or deploy the repo root pom as it's only needed to initiate a build -->
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-install-plugin</artifactId>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/testutils/pom.xml b/testutils/pom.xml
new file mode 100644 (file)
index 0000000..04dcf58
--- /dev/null
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (c) 2018 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
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.opendaylight.genius</groupId>
+    <artifactId>binding-parent</artifactId>
+    <version>0.5.0-SNAPSHOT</version>
+    <relativePath>../../commons/binding-parent</relativePath>
+  </parent>
+
+  <groupId>org.opendaylight.genius</groupId>
+  <artifactId>tools-testutils</artifactId>
+  <!-- <name> formatting is used by autorelease to parse and notify projects on
+       build failure. Please do not modify this unless you have a good reason. -->
+  <name>ODL :: genius :: ${project.artifactId}</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.opendaylight.genius</groupId>
+      <artifactId>tools-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <!-- mdsalutil-api (test-jar) needs this, but cannot transitively inherit it (because it is and has to be <scope>test there) -->
+      <artifactId>testutils</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.mycila.guice.extensions</groupId>
+      <artifactId>mycila-guice-jsr250</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-binding-broker-impl</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-binding-broker-impl</artifactId>
+      <type>test-jar</type>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.ops4j.pax.cdi</groupId>
+      <artifactId>pax-cdi-api</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.infrautils</groupId>
+      <artifactId>infrautils-testutils</artifactId>
+      <version>1.4.0-SNAPSHOT</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/testutils/src/main/java/org/opendaylight/genius/tools/mdsal/testutils/TestFutureRpcResults.java b/testutils/src/main/java/org/opendaylight/genius/tools/mdsal/testutils/TestFutureRpcResults.java
new file mode 100644 (file)
index 0000000..d6348d1
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2017 - 2018 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.genius.tools.mdsal.testutils;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeoutException;
+import org.opendaylight.genius.tools.mdsal.rpc.FutureRpcResults;
+import org.opendaylight.yangtools.yang.common.RpcError;
+import org.opendaylight.yangtools.yang.common.RpcError.ErrorType;
+import org.opendaylight.yangtools.yang.common.RpcResult;
+
+/**
+ * Assertion utilities for {@link FutureRpcResults}.
+ *
+ * @author Michael Vorburger.ch
+ */
+@SuppressFBWarnings("BC_UNCONFIRMED_CAST") // see https://wiki.opendaylight.org/view/BestPractices/Coding_Guidelines#Unchecked.2Funconfirmed_cast_from_com.google.common.truth.Subject_to_com.google.common.truth.BooleanSubject_etc.
+public final class TestFutureRpcResults {
+
+    private TestFutureRpcResults() { }
+
+    private static <T> T getResult(RpcResult<T> rpcResult) {
+        assertThat(rpcResult.isSuccessful()).named("rpcResult.isSuccessful").isTrue();
+        T result = rpcResult.getResult();
+        assertThat(result).named("result").isNotNull();
+        return result;
+    }
+
+    public static <T> T getResult(Future<RpcResult<T>> futureRpcResult)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        return getResult(futureRpcResult.get(1, MINUTES));
+    }
+
+    public static void assertVoidRpcSuccess(Future<RpcResult<Void>> futureRpcResult)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        RpcResult<Void> rpcResult = futureRpcResult.get(1, MINUTES);
+        assertThat(rpcResult.isSuccessful()).isTrue();
+        assertThat(rpcResult.getErrors()).isEmpty();
+    }
+
+    public static <T> void assertRpcErrorWithoutCausesOrMessages(Future<RpcResult<T>> futureRpcResult)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        RpcResult<T> rpcResult = futureRpcResult.get(1, MINUTES);
+        assertThat(rpcResult.isSuccessful()).named("rpcResult.isSuccessful").isFalse();
+        assertThat(rpcResult.getErrors()).named("rpcResult.errors").isEmpty();
+    }
+
+    public static <T> void assertRpcErrorCause(Future<RpcResult<T>> futureRpcResult, Class<?> expectedExceptionClass,
+            String expectedRpcErrorMessage) throws InterruptedException, ExecutionException, TimeoutException {
+        assertRpcErrorCause(futureRpcResult.get(1, MINUTES), expectedExceptionClass, expectedRpcErrorMessage);
+    }
+
+    private static <T> void assertRpcErrorCause(RpcResult<T> rpcResult, Class<?> expected1stExceptionClass,
+            String expected1stRpcErrorMessage) {
+        assertThat(rpcResult.isSuccessful()).named("rpcResult.isSuccessful").isFalse();
+        Collection<RpcError> errors = rpcResult.getErrors();
+        assertThat(errors).named("rpcResult.errors").hasSize(1);
+        RpcError error1 = errors.iterator().next();
+        assertThat(error1.getErrorType()).named("rpcResult.errors[0].errorType").isEqualTo(ErrorType.APPLICATION);
+        assertThat(error1.getMessage()).named("rpcResult.errors[0].message").isEqualTo(expected1stRpcErrorMessage);
+        if (error1.getCause() != null) {
+            // Check needed because FutureRpcResults does not propagate cause if OperationFailedException
+            assertThat(error1.getCause()).named("rpcResult.errors[0].cause").isInstanceOf(expected1stExceptionClass);
+        }
+    }
+
+}
diff --git a/testutils/src/test/java/org/opendaylight/genius/tools/mdsal/rpc/FutureRpcResultsTest.java b/testutils/src/test/java/org/opendaylight/genius/tools/mdsal/rpc/FutureRpcResultsTest.java
new file mode 100644 (file)
index 0000000..2049b97
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * 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.genius.tools.mdsal.rpc;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static org.opendaylight.genius.tools.mdsal.rpc.FutureRpcResults.LogLevel.NONE;
+
+import com.google.common.truth.Truth;
+import com.google.common.util.concurrent.Futures;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Rule;
+import org.junit.Test;
+import org.opendaylight.genius.tools.mdsal.rpc.FutureRpcResults.LogLevel;
+import org.opendaylight.genius.tools.mdsal.testutils.TestFutureRpcResults;
+import org.opendaylight.infrautils.testutils.LogCaptureRule;
+import org.opendaylight.infrautils.testutils.LogRule;
+import org.opendaylight.yangtools.yang.common.RpcResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Unit Test for {@link FutureRpcResults}.
+ *
+ * @author Michael Vorburger.ch
+ */
+public class FutureRpcResultsTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(FutureRpcResultsTest.class);
+
+    public @Rule LogRule logRule = new LogRule();
+    public @Rule LogCaptureRule logCaptureRule = new LogCaptureRule();
+
+    @Test
+    public void testListenableFutureSuccess() throws Exception {
+        Future<RpcResult<String>> future = FutureRpcResults.fromListenableFuture(
+                LOG, null, () -> immediateFuture("hello, world")).build();
+        Truth.assertThat(TestFutureRpcResults.getResult(future)).isEqualTo("hello, world");
+    }
+
+    @Test
+    public void testFailedListenableFuture() throws Exception {
+        logCaptureRule.expectError("RPC testFailedListenableFuture() failed; input = null");
+        TestFutureRpcResults.assertRpcErrorCause(FutureRpcResults.fromListenableFuture(LOG, null, () ->
+                immediateFailedFuture(new IllegalArgumentException("boum"))).build(),
+                    IllegalArgumentException.class, "boum");
+    }
+
+    @Test
+    public void testFromListenableFutureException() throws Exception {
+        logCaptureRule.expectError("RPC testFromListenableFutureException() failed; input = null");
+        TestFutureRpcResults.assertRpcErrorCause(FutureRpcResults.fromListenableFuture(
+            LOG, null, () -> {
+                throw new IllegalArgumentException("bam");
+            }).build(), IllegalArgumentException.class, "bam");
+    }
+
+    @Test
+    public void testFromListenableFutureExceptionWarnInsteadError() throws Exception {
+        TestFutureRpcResults.assertRpcErrorCause(FutureRpcResults.fromListenableFuture(
+            LOG, "testFromListenableFutureException", null, () -> {
+                throw new IllegalArgumentException("bam");
+            }).onFailureLogLevel(LogLevel.WARN).build(), IllegalArgumentException.class, "bam");
+    }
+
+    @Test
+    public void testFromListenableFutureExceptionNoLog() throws Exception {
+        TestFutureRpcResults.assertRpcErrorCause(FutureRpcResults.fromListenableFuture(
+            LOG, "testFromListenableFutureException", null, () -> {
+                throw new IllegalArgumentException("bam");
+            }).onFailureLogLevel(NONE).build(), IllegalArgumentException.class, "bam");
+    }
+
+    @Test
+    public void testFromListenableFutureExceptionAlsoLog() throws Exception {
+        final AtomicBoolean afterLogActionCalled = new AtomicBoolean(false);
+        logCaptureRule.expectError("RPC testFromListenableFutureException() failed; input = null");
+        TestFutureRpcResults.assertRpcErrorCause(FutureRpcResults.fromListenableFuture(
+            LOG, "testFromListenableFutureException", null, () -> {
+                throw new IllegalArgumentException("bam");
+            }).onFailure(e -> afterLogActionCalled.set(true)).build(), IllegalArgumentException.class, "bam");
+        assertThat(afterLogActionCalled.get()).isTrue();
+    }
+
+    @Test
+    public void testFromListenableFutureExceptionCustomMessage() throws Exception {
+        logCaptureRule.expectError("RPC testFromListenableFutureExceptionCustomMessage() failed; input = null");
+        TestFutureRpcResults.assertRpcErrorCause(FutureRpcResults.fromListenableFuture(LOG, null, () -> {
+            throw new IllegalArgumentException("bam");
+        }).withRpcErrorMessage(e -> "tra la la").build(), IllegalArgumentException.class, "tra la la");
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testExtraOnFailureThrowsException() throws Exception {
+        FutureRpcResults.fromListenableFuture(LOG, null, () -> Futures.immediateFuture(null)).onFailure(failure -> {
+        }).onFailure(failure -> {
+        });
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testExtraOnSuccessThrowsException() throws Exception {
+        FutureRpcResults.fromListenableFuture(LOG, null, () -> Futures.immediateFuture(null)).onSuccess(result -> {
+        }).onSuccess(result -> {
+        });
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testExtraWithRpcErrorMessageThrowsException() throws Exception {
+        FutureRpcResults.fromListenableFuture(LOG, null, () -> Futures.immediateFuture(null)).withRpcErrorMessage(
+            error -> null).withRpcErrorMessage(error -> null);
+    }
+
+}