package ru.vyarus.dropwizard.guice.module.installer; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import com.google.common.collect.Lists; import com.google.inject.AbstractModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; import ru.vyarus.dropwizard.guice.module.context.info.impl.ExtensionItemInfoImpl; import ru.vyarus.dropwizard.guice.module.context.option.Options; import ru.vyarus.dropwizard.guice.module.context.stat.Stat; import ru.vyarus.dropwizard.guice.module.installer.install.binding.BindingInstaller; import ru.vyarus.dropwizard.guice.module.installer.install.binding.LazyBinding; import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsHolder; import ru.vyarus.dropwizard.guice.module.installer.internal.FeatureInstallerExecutor; import ru.vyarus.dropwizard.guice.module.installer.option.WithOptions; import ru.vyarus.dropwizard.guice.module.installer.order.OrderComparator; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClassVisitor; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding; import java.util.List; import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.InstallersTime; /** * Module performs auto configuration using classpath scanning or manually predefined installers and beans. * First search provided packages for registered installers * {@link FeatureInstaller}. Then scan classpath one more time with installers to apply extensions. * <p> * Feature installers can be disabled from bundle config. * <p> * NOTE: Classpath scan will load all found classes in configured packages. Try to reduce scan scope * as much as possible. * * @author Vyacheslav Rusakov * @since 31.08.2014 */ public class InstallerModule extends AbstractModule { private static final OrderComparator COMPARATOR = new OrderComparator(); private final Logger logger = LoggerFactory.getLogger(InstallerModule.class); private final ClasspathScanner scanner; private final ConfigurationContext context; public InstallerModule(final ClasspathScanner scanner, final ConfigurationContext context) { this.scanner = scanner; this.context = context; } @Override @SuppressWarnings("unchecked") protected void configure() { // called just after injector creation to process instance installers bind(FeatureInstallerExecutor.class).asEagerSingleton(); final Stopwatch timer = context.stat().timer(InstallersTime); final List<Class<? extends FeatureInstaller>> installerClasses = findInstallers(); final List<FeatureInstaller> installers = prepareInstallers(installerClasses); timer.stop(); final ExtensionsHolder holder = new ExtensionsHolder(installers, context.stat()); bind(ExtensionsHolder.class).toInstance(holder); resolveExtensions(holder); context.finalizeConfiguration(); } /** * Performs classpath scan to find all classes implementing or use only manually configured installers. * {@link FeatureInstaller}. * * @return list of found installers or empty list */ @SuppressWarnings("unchecked") private List<Class<? extends FeatureInstaller>> findInstallers() { if (scanner != null) { final List<Class<? extends FeatureInstaller>> installers = Lists.newArrayList(); scanner.scan(new ClassVisitor() { @Override public void visit(final Class<?> type) { if (FeatureUtils.is(type, FeatureInstaller.class)) { installers.add((Class<? extends FeatureInstaller>) type); } } }); context.registerInstallersFromScan(installers); } final List<Class<? extends FeatureInstaller>> installers = context.getInstallers(); installers.removeAll(context.getDisabledInstallers()); installers.sort(COMPARATOR); logger.debug("Found {} installers", installers.size()); return installers; } /** * Instantiate all found installers using default constructor. * * @param installerClasses found installer classes * @return list of installer instances */ private List<FeatureInstaller> prepareInstallers( final List<Class<? extends FeatureInstaller>> installerClasses) { final List<FeatureInstaller> installers = Lists.newArrayList(); // different instance then used in guice context, but it's just an accessor object final Options options = new Options(context.options()); for (Class<? extends FeatureInstaller> installerClass : installerClasses) { try { final FeatureInstaller installer = installerClass.newInstance(); installers.add(installer); if (WithOptions.class.isAssignableFrom(installerClass)) { ((WithOptions) installer).setOptions(options); } if (logger.isTraceEnabled()) { logger.trace("Registered installer: {}", FeatureUtils.getInstallerExtName(installerClass)); } } catch (Exception e) { throw new IllegalStateException("Failed to register installer " + installerClass.getName(), e); } } return installers; } /** * Performs one more classpath scan to search for extensions or simply install manually provided extension classes. * * @param holder holder to store found extension classes until injector creation */ private void resolveExtensions(final ExtensionsHolder holder) { final Stopwatch timer = context.stat().timer(Stat.ExtensionsRecognitionTime); final List<Class<?>> manual = context.getExtensions(); for (Class<?> type : manual) { Preconditions.checkState(processType(type, holder, false), "No installer found for extension %s", type.getName()); } if (scanner != null) { scanner.scan(new ClassVisitor() { @Override public void visit(final Class<?> type) { if (manual.contains(type)) { // avoid duplicate extension installation, but register it's appearance in auto scan scope context.getOrRegisterExtension(type, true); } else { processType(type, holder, true); } } }); } timer.stop(); } private boolean processType(final Class<?> type, final ExtensionsHolder holder, final boolean fromScan) { final boolean lazy = type.isAnnotationPresent(LazyBinding.class); final Class<? extends FeatureInstaller> installer = bindExtension(type, lazy, holder); final boolean recognized = installer != null; if (recognized) { final ExtensionItemInfoImpl info = context.getOrRegisterExtension(type, fromScan); info.setLazy(lazy); info.setHk2Managed(JerseyBinding.isHK2Managed(type)); info.setInstalledBy(installer); } return recognized; } /** * Only one installer could manage extension. If extension could be matched by multiple installers, * then first matched installer wins (note that installers are ordered). * * @param type class to analyze * @return matched installer or null if no matching installer found */ @SuppressWarnings("unchecked") private Class<? extends FeatureInstaller> bindExtension( final Class<?> type, final boolean lazy, final ExtensionsHolder holder) { Class<? extends FeatureInstaller> recognized = null; for (FeatureInstaller installer : holder.getInstallers()) { if (installer.matches(type)) { final Class<? extends FeatureInstaller> installerClass = installer.getClass(); if (logger.isTraceEnabled()) { logger.trace("{} extension found: {}", FeatureUtils.getInstallerExtName(installerClass), type.getName()); } holder.register(installerClass, type); if (installer instanceof BindingInstaller) { ((BindingInstaller) installer).install(binder(), type, lazy); } else if (!lazy) { // if installer isn't install binding manually, lazy simply disable registration binder().bind(type); } recognized = installerClass; break; } } return recognized; } }