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>
* 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 {
/**
--- /dev/null
+/*
+ * 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;
+
+}
--- /dev/null
+/*
+ * 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 {
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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 {
+}
--- /dev/null
+/*
+ * 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 {
+}
--- /dev/null
+/*
+ * 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 {
+}
--- /dev/null
+/*
+ * 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 {
+}
<!-- 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>
--- /dev/null
+/*
+ * 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));
+ }
+}