introduce inject.guice.AutoWiringModule
authorMichael Vorburger <vorburger@redhat.com>
Sat, 22 Dec 2018 03:09:48 +0000 (04:09 +0100)
committerTom Pantelis <tompantelis@gmail.com>
Wed, 16 Jan 2019 20:46:00 +0000 (20:46 +0000)
It lets tests use classpath scanning based "auto-wiring" (à la Spring).

This comes out of https://github.com/vorburger/opendaylight-simple

Change-Id: I0c71be4930ec3158c1fb8213913fe693d4ccdec3
Signed-off-by: Michael Vorburger <vorburger@redhat.com>
inject-guice-testutils/src/main/java/org/opendaylight/infrautils/inject/guice/testutils/AbstractCheckedModule.java
inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/AbstractCheckedModule.java [new file with mode: 0644]
inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/AutoWiringModule.java [new file with mode: 0644]
inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/GuiceClassPathBinder.java [new file with mode: 0644]
inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTest.java [new file with mode: 0644]
inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestAnotherInterface.java [new file with mode: 0644]
inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestImplementation.java [new file with mode: 0644]
inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestNoInterfacesImplementation.java [new file with mode: 0644]
inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestTopInterface.java [new file with mode: 0644]
inject/pom.xml
inject/src/main/java/org/opendaylight/infrautils/inject/ClassPathScanner.java [new file with mode: 0644]

index f16fea993d78ec55675b6a7c097b710d1f8134d2..ddc0d1bb6488696ef4c534a1e7ac39f550d70f93 100644 (file)
@@ -16,8 +16,11 @@ import org.opendaylight.infrautils.inject.ModuleSetupRuntimeException;
  * throwing checked exceptions, which are caught and re-thrown as unchecked
  * {@link ModuleSetupRuntimeException}.
  *
+ * @deprecated Use org.opendaylight.infrautils.inject.guice.AbstractCheckedModule instead.
+ *
  * @author Michael Vorburger.ch
  */
+@Deprecated
 public abstract class AbstractCheckedModule extends AbstractModule {
 
     /**
diff --git a/inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/AbstractCheckedModule.java b/inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/AbstractCheckedModule.java
new file mode 100644 (file)
index 0000000..792a2fb
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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.infrautils.inject.guice;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Binder;
+import org.opendaylight.infrautils.inject.ModuleSetupRuntimeException;
+
+/**
+ * Convenience Guice module support class with configure method that allows
+ * throwing checked exceptions, which are caught and re-thrown as unchecked
+ * {@link ModuleSetupRuntimeException}.
+ *
+ * @author Michael Vorburger.ch
+ */
+public abstract class AbstractCheckedModule extends AbstractModule {
+
+    /**
+     * Configures a {@link Binder} via the exposed methods.
+     *
+     * @throws ModuleSetupRuntimeException if binding failed
+     */
+    @Override
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    protected final void configure() throws ModuleSetupRuntimeException {
+        try {
+            checkedConfigure();
+        } catch (Exception e) {
+            throw new ModuleSetupRuntimeException(e);
+        }
+    }
+
+    protected abstract void checkedConfigure() throws Exception;
+
+}
diff --git a/inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/AutoWiringModule.java b/inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/AutoWiringModule.java
new file mode 100644 (file)
index 0000000..3180f66
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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.infrautils.inject.guice;
+
+/**
+ * Guice Module with classpath scanning based autowiring.
+ *
+ * @author Michael Vorburger.ch
+ */
+public class AutoWiringModule extends AbstractCheckedModule {
+
+    protected final GuiceClassPathBinder classPathBinder;
+    private final String packagePrefix;
+
+    public AutoWiringModule(GuiceClassPathBinder classPathBinder, String packagePrefix) {
+        this.classPathBinder = classPathBinder;
+        this.packagePrefix = packagePrefix;
+    }
+
+    @Override
+    protected final void checkedConfigure() throws Exception {
+        classPathBinder.bindAllSingletons(packagePrefix, binder());
+        configureMore();
+    }
+
+    protected void configureMore() throws Exception {
+    }
+}
diff --git a/inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/GuiceClassPathBinder.java b/inject-guice/src/main/java/org/opendaylight/infrautils/inject/guice/GuiceClassPathBinder.java
new file mode 100644 (file)
index 0000000..ba2273a
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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.infrautils.inject.guice;
+
+import com.google.inject.Binder;
+import javax.inject.Singleton;
+import org.opendaylight.infrautils.inject.ClassPathScanner;
+
+/**
+ * Binds interfaces to implementations in Guice by scanning the classpath.
+ */
+public class GuiceClassPathBinder {
+    private final ClassPathScanner scanner;
+
+    public GuiceClassPathBinder(String prefix) {
+        this.scanner = new ClassPathScanner(prefix);
+    }
+
+    /**
+     * Binds all {@link Singleton} annotated classes discovered by scanning the class path to all their interfaces.
+     *
+     * @param prefix the package prefix of Singleton implementations to consider
+     * @param binder The binder to set up.
+     */
+    @SuppressWarnings("unchecked")
+    public void bindAllSingletons(String prefix, Binder binder) {
+        scanner.bindAllSingletons(prefix,
+            (contract, implementation) -> binder.bind(contract).to(implementation),
+            singleton -> binder.bind(singleton));
+    }
+}
diff --git a/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTest.java b/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTest.java
new file mode 100644 (file)
index 0000000..d449050
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2018 Red Hat, Inc. and others.
+ *
+ * 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.infrautils.inject.guice.test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+import org.opendaylight.infrautils.inject.ClassPathScanner;
+
+public class ClassPathScannerTest {
+
+    private static final String PREFIX = "org.opendaylight.infrautils.inject.guice.test";
+
+    @Test
+    public void testClasspathScanning() {
+        Set<Class<?>> singletons = new HashSet<>();
+        Map<Class<?>, Class<?>> bindings = new HashMap<>();
+        new ClassPathScanner(PREFIX).bindAllSingletons(PREFIX, bindings::put, singletons::add);
+        assertThat(bindings).containsExactly(
+                ClassPathScannerTestTopInterface.class, ClassPathScannerTestImplementation.class,
+                ClassPathScannerTestAnotherInterface.class, ClassPathScannerTestImplementation.class);
+        assertThat(singletons).containsExactly(ClassPathScannerTestNoInterfacesImplementation.class);
+    }
+
+    @Test
+    public void testClasspathExclusion() {
+        Set<Class<?>> singletons = new HashSet<>();
+        Map<Class<?>, Class<?>> bindings = new HashMap<>();
+        new ClassPathScanner(PREFIX).bindAllSingletons("nope", bindings::put, singletons::add);
+        assertThat(bindings).isEmpty();
+        assertThat(singletons).isEmpty();
+    }
+}
diff --git a/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestAnotherInterface.java b/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestAnotherInterface.java
new file mode 100644 (file)
index 0000000..c2cad0a
--- /dev/null
@@ -0,0 +1,11 @@
+/*
+ * Copyright © 2018 Red Hat, Inc. and others.
+ *
+ * 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.infrautils.inject.guice.test;
+
+public interface ClassPathScannerTestAnotherInterface {
+}
diff --git a/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestImplementation.java b/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestImplementation.java
new file mode 100644 (file)
index 0000000..de96d15
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * Copyright © 2018 Red Hat, Inc. and others.
+ *
+ * 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.infrautils.inject.guice.test;
+
+import javax.inject.Singleton;
+
+@Singleton
+public class ClassPathScannerTestImplementation
+        implements ClassPathScannerTestTopInterface, ClassPathScannerTestAnotherInterface {
+}
diff --git a/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestNoInterfacesImplementation.java b/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestNoInterfacesImplementation.java
new file mode 100644 (file)
index 0000000..0e891ee
--- /dev/null
@@ -0,0 +1,14 @@
+/*
+ * Copyright © 2018 Red Hat, Inc. and others.
+ *
+ * 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.infrautils.inject.guice.test;
+
+import javax.inject.Singleton;
+
+@Singleton
+public class ClassPathScannerTestNoInterfacesImplementation {
+}
diff --git a/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestTopInterface.java b/inject-guice/src/test/java/org/opendaylight/infrautils/inject/guice/test/ClassPathScannerTestTopInterface.java
new file mode 100644 (file)
index 0000000..8cb9bc2
--- /dev/null
@@ -0,0 +1,11 @@
+/*
+ * Copyright © 2018 Red Hat, Inc. and others.
+ *
+ * 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.infrautils.inject.guice.test;
+
+public interface ClassPathScannerTestTopInterface {
+}
index a54f4a2228d63bc6d673b73c48896d5ff1647321..866684448d7a2cf60fc6ec1febc5d4835b94fd16 100644 (file)
@@ -52,6 +52,9 @@
       <!-- FIXME: remove version with odlparent-4.0.8 -->
       <version>1.2</version>
     </dependency>
+    <dependency>
+      <groupId>io.github.classgraph</groupId>
+      <artifactId>classgraph</artifactId>
+    </dependency>
   </dependencies>
-
 </project>
diff --git a/inject/src/main/java/org/opendaylight/infrautils/inject/ClassPathScanner.java b/inject/src/main/java/org/opendaylight/infrautils/inject/ClassPathScanner.java
new file mode 100644 (file)
index 0000000..632269a
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Copyright © 2018 Red Hat, Inc. and others.
+ *
+ * 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.infrautils.inject;
+
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ClassInfo;
+import io.github.classgraph.ClassInfoList;
+import io.github.classgraph.ScanResult;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import javax.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class path scanner designed to be used with Guice. This provides a way for modules to request the bindings they
+ * need by scanning the class path.
+ */
+@SuppressWarnings("rawtypes")
+public class ClassPathScanner {
+    private static final Logger LOG = LoggerFactory.getLogger(ClassPathScanner.class);
+
+    private final Map<String, Class> implementations = new HashMap<>();
+    private final Set<Class<?>> singletons = new HashSet<>();
+
+    /**
+     * Create a class path scanner, scanning packages with the given prefix for {@literal @}Singleton annotated classes.
+     *
+     * @param prefix The package prefix.
+     */
+    public ClassPathScanner(String prefix) {
+        try (ScanResult scanResult =
+                 new ClassGraph()
+                     .enableClassInfo()
+                     .enableAnnotationInfo()
+                     .whitelistPackages(prefix)
+                     .scan()) {
+            Set<String> duplicateInterfaces = new HashSet<>();
+            for (ClassInfo singletonInfo : scanResult.getClassesWithAnnotation(Singleton.class.getName())) {
+                ClassInfoList interfaces = singletonInfo.getInterfaces();
+                if (interfaces.isEmpty()) {
+                    singletons.add(singletonInfo.loadClass());
+                } else {
+                    for (ClassInfo interfaceInfo : interfaces) {
+                        String interfaceName = interfaceInfo.getName();
+                        if (!duplicateInterfaces.contains(interfaceName)) {
+                            if (implementations.put(interfaceName, singletonInfo.loadClass()) != null) {
+                                LOG.debug("{} is declared multiple times, ignoring it", interfaceName);
+                                implementations.remove(interfaceName);
+                                duplicateInterfaces.add(interfaceName);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Binds all {@link Singleton} annotated classes discovered by scanning the class path to all their interfaces.
+     *
+     * @param prefix the package prefix of Singleton implementations to consider
+     * @param binder The binder (modeled as a generic consumer)
+     */
+    public void bindAllSingletons(String prefix, BiConsumer<Class, Class> binder, Consumer<Class> singletonConsumer) {
+        implementations.forEach((interfaceName, singletonClass) -> {
+            if (singletonClass.getName().startsWith(prefix)) {
+                try {
+                    Class interfaceClass = Class.forName(interfaceName);
+                    binder.accept(interfaceClass, singletonClass);
+                    // TODO later probably lower this info to debug, but for now it's very useful..
+                    LOG.info("Bound {} to {}", interfaceClass, singletonClass);
+                } catch (ClassNotFoundException e) {
+                    LOG.warn("ClassNotFoundException on Class.forName: {}", interfaceName, e);
+                }
+            }
+        });
+        singletons.stream().filter(singletonClass -> singletonClass.getName().startsWith(prefix))
+                .forEach(singletonClass -> singletonConsumer.accept(singletonClass));
+    }
+}