diff --git a/scm-webapp/src/main/java/sonia/scm/ResteasyAllInOneServletDispatcher.java b/scm-webapp/src/main/java/sonia/scm/ResteasyAllInOneServletDispatcher.java new file mode 100644 index 0000000000..6ea514bcb2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/ResteasyAllInOneServletDispatcher.java @@ -0,0 +1,70 @@ +package sonia.scm; + +import com.google.inject.Injector; +import org.jboss.resteasy.plugins.guice.ModuleProcessor; +import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher; +import org.jboss.resteasy.plugins.server.servlet.ListenerBootstrap; +import org.jboss.resteasy.spi.Registry; +import org.jboss.resteasy.spi.ResteasyDeployment; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +/** + * Resteasy initialization and dispatching. This servlet combines the initialization of + * {@link org.jboss.resteasy.plugins.guice.GuiceResteasyBootstrapServletContextListener} and the dispatching of + * {@link HttpServletDispatcher}. The combination is required to fix the initialization order. + */ +@Singleton +public class ResteasyAllInOneServletDispatcher extends HttpServletDispatcher { + + private static final Logger LOG = LoggerFactory.getLogger(ResteasyAllInOneServletDispatcher.class); + + private final Injector injector; + private ResteasyDeployment deployment; + + @Inject + public ResteasyAllInOneServletDispatcher(Injector injector) { + this.injector = injector; + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + LOG.info("init resteasy"); + + ServletContext servletContext = servletConfig.getServletContext(); + createDeployment(servletContext); + + ModuleProcessor processor = createModuleProcessor(); + processor.processInjector(injector); + + super.init(servletConfig); + } + + private void createDeployment(ServletContext servletContext) { + ListenerBootstrap config = new ListenerBootstrap(servletContext); + deployment = config.createDeployment(); + deployment.start(); + + servletContext.setAttribute(ResteasyDeployment.class.getName(), deployment); + } + + private ModuleProcessor createModuleProcessor() { + Registry registry = deployment.getRegistry(); + ResteasyProviderFactory providerFactory = deployment.getProviderFactory(); + return new ModuleProcessor(registry, providerFactory); + } + + @Override + public void destroy() { + LOG.info("destroy resteasy"); + super.destroy(); + deployment.stop(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java b/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java index a85c3b6d06..352efc28ab 100644 --- a/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java @@ -2,19 +2,17 @@ package sonia.scm; import com.google.common.collect.ImmutableMap; import com.google.inject.servlet.ServletModule; -import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher; import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters; -import javax.inject.Singleton; import java.util.Map; +/** + * Module to configure resteasy with guice. + */ public class ResteasyModule extends ServletModule { - @Override protected void configureServlets() { - bind(HttpServletDispatcher.class).in(Singleton.class); - Map initParams = ImmutableMap.of(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX, "/api"); - serve("/api/*").with(HttpServletDispatcher.class, initParams); + serve("/api/*").with(ResteasyAllInOneServletDispatcher.class, initParams); } } diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java deleted file mode 100644 index 92073a9e85..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm; - -import com.google.common.base.Throwables; -import com.google.common.collect.Lists; -import com.google.inject.Injector; -import com.google.inject.Module; -import com.google.inject.assistedinject.Assisted; -import org.apache.shiro.guice.web.ShiroWebModule; -import org.jboss.resteasy.plugins.guice.GuiceResteasyBootstrapServletContextListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.api.v2.resources.MapperModule; -import sonia.scm.cache.CacheManager; -import sonia.scm.debug.DebugModule; -import sonia.scm.filter.WebElementModule; -import sonia.scm.group.GroupManager; -import sonia.scm.plugin.DefaultPluginLoader; -import sonia.scm.plugin.ExtensionProcessor; -import sonia.scm.plugin.PluginWrapper; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.schedule.Scheduler; -import sonia.scm.user.UserManager; -import sonia.scm.util.IOUtil; - -import javax.inject.Inject; -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import java.util.List; -import java.util.Set; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmContextListener extends GuiceResteasyBootstrapServletContextListener -{ - - /** - * the logger for ScmContextListener - */ - private static final Logger LOG = LoggerFactory.getLogger(ScmContextListener.class); - - private final ClassLoader parent; - private final Set plugins; - private Injector injector; - - public interface Factory { - ScmContextListener create(ClassLoader parent, Set plugins); - } - - @Inject - public ScmContextListener(@Assisted ClassLoader parent, @Assisted Set plugins) - { - this.parent = parent; - this.plugins = plugins; - } - - public Set getPlugins() { - return plugins; - } - - @Override - public void contextInitialized(ServletContextEvent servletContextEvent) { - beforeInjectorCreation(); - super.contextInitialized(servletContextEvent); - afterInjectorCreation(servletContextEvent); - } - - private void beforeInjectorCreation() { - } - - private boolean hasStartupErrors() { - return SCMContext.getContext().getStartupError() != null; - } - - @Override - protected List getModules(ServletContext context) { - DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, parent, plugins); - - ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader()); - List moduleList = Lists.newArrayList(); - - moduleList.add(new ResteasyModule()); - moduleList.add(ShiroWebModule.guiceFilterModule()); - moduleList.add(new WebElementModule(pluginLoader)); - moduleList.add(new ScmServletModule(context, pluginLoader, overrides)); - moduleList.add( - new ScmSecurityModule(context, pluginLoader.getExtensionProcessor()) - ); - appendModules(pluginLoader.getExtensionProcessor(), moduleList); - moduleList.addAll(overrides.getModules()); - - if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT){ - moduleList.add(new DebugModule()); - } - moduleList.add(new MapperModule()); - - return moduleList; - } - - private void appendModules(ExtensionProcessor ep, List moduleList) { - for (Class module : ep.byExtensionPoint(Module.class)) { - try { - LOG.info("add module {}", module); - moduleList.add(module.newInstance()); - } catch (IllegalAccessException | InstantiationException ex) { - throw Throwables.propagate(ex); - } - } - } - - @Override - protected void withInjector(Injector injector) { - this.injector = injector; - } - - private void afterInjectorCreation(ServletContextEvent event) { - if (injector != null && !hasStartupErrors()) { - bindEagerSingletons(); - initializeServletContextListeners(event); - } - } - - private void bindEagerSingletons() { - injector.getInstance(EagerSingletonModule.class).initialize(injector); - } - - private void initializeServletContextListeners(ServletContextEvent event) { - injector.getInstance(ServletContextListenerHolder.class).contextInitialized(event); - } - - @Override - public void contextDestroyed(ServletContextEvent servletContextEvent) - { - if (injector != null &&!hasStartupErrors()) { - closeCloseables(); - destroyServletContextListeners(servletContextEvent); - } - - super.contextDestroyed(servletContextEvent); - } - - private void closeCloseables() { - injector.getInstance(CloseableModule.class).closeAll(); - } - - private void destroyServletContextListeners(ServletContextEvent event) { - injector.getInstance(ServletContextListenerHolder.class).contextDestroyed(event); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/ScmSecurityModule.java b/scm-webapp/src/main/java/sonia/scm/ScmSecurityModule.java index 527a8977af..3598967588 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmSecurityModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmSecurityModule.java @@ -81,8 +81,7 @@ public class ScmSecurityModule extends ShiroWebModule * @param servletContext * @param extensionProcessor */ - ScmSecurityModule(ServletContext servletContext, - ExtensionProcessor extensionProcessor) + public ScmSecurityModule(ServletContext servletContext, ExtensionProcessor extensionProcessor) { super(servletContext); this.extensionProcessor = extensionProcessor; diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 49ac0a3d59..63a9116716 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -59,8 +59,8 @@ import sonia.scm.net.ahc.ContentTransformer; import sonia.scm.net.ahc.DefaultAdvancedHttpClient; import sonia.scm.net.ahc.JsonContentTransformer; import sonia.scm.net.ahc.XmlContentTransformer; -import sonia.scm.plugin.DefaultPluginLoader; import sonia.scm.plugin.DefaultPluginManager; +import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginManager; import sonia.scm.repository.DefaultRepositoryManager; import sonia.scm.repository.DefaultRepositoryProvider; @@ -110,7 +110,6 @@ import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.DefaultAdministrationContext; import javax.net.ssl.SSLContext; -import javax.servlet.ServletContext; import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; @@ -179,9 +178,8 @@ public class ScmServletModule extends ServletModule //~--- constructors --------------------------------------------------------- - ScmServletModule(ServletContext servletContext, DefaultPluginLoader pluginLoader, ClassOverrides overrides) + public ScmServletModule(PluginLoader pluginLoader, ClassOverrides overrides) { - this.servletContext = servletContext; this.pluginLoader = pluginLoader; this.overrides = overrides; } @@ -206,10 +204,6 @@ public class ScmServletModule extends ServletModule RepositoryProvider.class, Repository.class).to( DefaultRepositoryProvider.class).in(RequestScoped.class); - // bind servlet context - bind(ServletContext.class).annotatedWith(Default.class).toInstance( - servletContext); - // bind event api bind(ScmEventBus.class).toInstance(ScmEventBus.getInstance()); @@ -408,8 +402,5 @@ public class ScmServletModule extends ServletModule private final ClassOverrides overrides; /** Field description */ - private final DefaultPluginLoader pluginLoader; - - /** Field description */ - private final ServletContext servletContext; + private final PluginLoader pluginLoader; } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/ApplicationModuleProvider.java b/scm-webapp/src/main/java/sonia/scm/boot/ApplicationModuleProvider.java new file mode 100644 index 0000000000..04ae39a52f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/ApplicationModuleProvider.java @@ -0,0 +1,78 @@ +package sonia.scm.boot; + +import com.google.common.base.Throwables; +import com.google.inject.Module; +import org.apache.shiro.guice.web.ShiroWebModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ClassOverrides; +import sonia.scm.ResteasyModule; +import sonia.scm.SCMContext; +import sonia.scm.ScmSecurityModule; +import sonia.scm.ScmServletModule; +import sonia.scm.Stage; +import sonia.scm.api.v2.resources.MapperModule; +import sonia.scm.debug.DebugModule; +import sonia.scm.filter.WebElementModule; +import sonia.scm.plugin.ExtensionProcessor; +import sonia.scm.plugin.PluginLoader; + +import javax.servlet.ServletContext; +import java.util.ArrayList; +import java.util.List; + +class ApplicationModuleProvider implements ModuleProvider { + + private static final Logger LOG = LoggerFactory.getLogger(ApplicationModuleProvider.class); + + private final ServletContext servletContext; + private final PluginLoader pluginLoader; + + ApplicationModuleProvider(ServletContext servletContext, PluginLoader pluginLoader) { + this.servletContext = servletContext; + this.pluginLoader = pluginLoader; + } + + @Override + public List createModules() { + ClassOverrides overrides = createClassOverrides(); + return createModules(overrides); + } + + private List createModules(ClassOverrides overrides) { + List moduleList = new ArrayList<>(); + moduleList.add(new ResteasyModule()); + moduleList.add(ShiroWebModule.guiceFilterModule()); + moduleList.add(new WebElementModule(pluginLoader)); + moduleList.add(new ScmServletModule(pluginLoader, overrides)); + moduleList.add( + new ScmSecurityModule(servletContext, pluginLoader.getExtensionProcessor()) + ); + appendModules(pluginLoader.getExtensionProcessor(), moduleList); + moduleList.addAll(overrides.getModules()); + + if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT){ + moduleList.add(new DebugModule()); + } + moduleList.add(new MapperModule()); + + return moduleList; + } + + private ClassOverrides createClassOverrides() { + ClassLoader uberClassLoader = pluginLoader.getUberClassLoader(); + return ClassOverrides.findOverrides(uberClassLoader); + } + + private void appendModules(ExtensionProcessor ep, List moduleList) { + for (Class module : ep.byExtensionPoint(Module.class)) { + try { + LOG.info("add module {}", module); + moduleList.add(module.newInstance()); + } catch (IllegalAccessException | InstantiationException ex) { + throw Throwables.propagate(ex); + } + } + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java index a134b44784..dcfc9cd178 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java @@ -82,7 +82,6 @@ public class BootstrapContextFilter extends GuiceFilter { super.destroy(); listener.contextDestroyed(new ServletContextEvent(filterConfig.getServletContext())); - ServletContextCleaner.cleanup(filterConfig.getServletContext()); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 68ec18e570..939e8f9f6c 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -29,111 +29,134 @@ package sonia.scm.boot; -import com.google.inject.AbstractModule; +import com.google.common.collect.ImmutableList; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; -import com.google.inject.assistedinject.FactoryModuleBuilder; +import com.google.inject.servlet.GuiceServletContextListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.CloseableModule; import sonia.scm.EagerSingletonModule; import sonia.scm.SCMContext; -import sonia.scm.ScmContextListener; import sonia.scm.ScmEventBusModule; import sonia.scm.ScmInitializerModule; import sonia.scm.plugin.PluginLoader; -import sonia.scm.plugin.PluginWrapper; -import sonia.scm.update.MigrationWizardContextListener; +import sonia.scm.update.MigrationWizardModuleProvider; import sonia.scm.update.UpdateEngine; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; import javax.servlet.http.HttpServletResponse; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; /** - * * @author Sebastian Sdorra */ -public class BootstrapContextListener implements ServletContextListener { +public class BootstrapContextListener extends GuiceServletContextListener { + + private static final Logger LOG = LoggerFactory.getLogger(BootstrapContextListener.class); private final ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); - private ServletContext context; - private ServletContextListener contextListener; + private InjectionLifeCycle injectionLifeCycle; @Override public void contextInitialized(ServletContextEvent sce) { - classLoaderLifeCycle.init(); + LOG.info("start scm-manager initialization"); context = sce.getServletContext(); + classLoaderLifeCycle.init(); + super.contextInitialized(sce); - createContextListener(); + Injector injector = (Injector) context.getAttribute(Injector.class.getName()); + injectionLifeCycle = new InjectionLifeCycle(injector); + injectionLifeCycle.initialize(); + } - contextListener.contextInitialized(sce); + @Override + protected Injector getInjector() { + Throwable startupError = SCMContext.getContext().getStartupError(); + if (startupError != null) { + return createStageOneInjector(SingleView.error(startupError)); + } else if (Versions.isTooOld()) { + return createStageOneInjector(SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT)); + } else { + try { + return createStageTwoInjector(); + } catch (Exception ex) { + return createStageOneInjector(SingleView.error(ex)); + } + } } @Override public void contextDestroyed(ServletContextEvent sce) { - contextListener.contextDestroyed(sce); + LOG.info("shutdown scm-manager context"); + + ServletContextCleaner.cleanup(context); + + injectionLifeCycle.shutdown(); + injectionLifeCycle = null; classLoaderLifeCycle.shutdown(); - - context = null; - contextListener = null; } - private void createContextListener() { - Throwable startupError = SCMContext.getContext().getStartupError(); - if (startupError != null) { - contextListener = SingleView.error(startupError); - } else if (Versions.isTooOld()) { - contextListener = SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT); - } else { - createMigrationOrNormalContextListener(); - Versions.writeNew(); - } - } - - private void createMigrationOrNormalContextListener() { + private Injector createStageTwoInjector() { PluginBootstrap pluginBootstrap = new PluginBootstrap(context, classLoaderLifeCycle); - Injector bootstrapInjector = createBootstrapInjector(pluginBootstrap.getPluginLoader()); - - startEitherMigrationOrNormalServlet(classLoaderLifeCycle.getBootstrapClassLoader(), pluginBootstrap.getPlugins(), pluginBootstrap.getPluginLoader(), bootstrapInjector); + ModuleProvider provider = createMigrationOrNormalModuleProvider(pluginBootstrap); + return createStageTwoInjector(provider, pluginBootstrap.getPluginLoader()); } - private void startEitherMigrationOrNormalServlet(ClassLoader cl, Set plugins, PluginLoader pluginLoader, Injector bootstrapInjector) { - MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector); + private ModuleProvider createMigrationOrNormalModuleProvider(PluginBootstrap pluginBootstrap) { + Injector bootstrapInjector = createBootstrapInjector(pluginBootstrap.getPluginLoader()); - if (wizardContextListener.wizardNecessary()) { - contextListener = wizardContextListener; + return startEitherMigrationOrApplication(pluginBootstrap.getPluginLoader(), bootstrapInjector); + } + + private ModuleProvider startEitherMigrationOrApplication(PluginLoader pluginLoader, Injector bootstrapInjector) { + MigrationWizardModuleProvider wizardModuleProvider = new MigrationWizardModuleProvider(bootstrapInjector); + + if (wizardModuleProvider.wizardNecessary()) { + return wizardModuleProvider; } else { processUpdates(pluginLoader, bootstrapInjector); - contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + + Versions.writeNew(); + + return new ApplicationModuleProvider(context, pluginLoader); } } + private Injector createStageOneInjector(ModuleProvider provider) { + return Guice.createInjector(provider.createModules()); + } - private MigrationWizardContextListener prepareWizardIfNeeded(Injector bootstrapInjector) { - return new MigrationWizardContextListener(bootstrapInjector); + private Injector createStageTwoInjector(ModuleProvider provider, PluginLoader pluginLoader) { + List modules = new ArrayList<>(createBootstrapModules(pluginLoader)); + modules.addAll(provider.createModules()); + return Guice.createInjector(modules); } private Injector createBootstrapInjector(PluginLoader pluginLoader) { - Module scmContextListenerModule = new ScmContextListenerModule(); - BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); - ScmInitializerModule scmInitializerModule = new ScmInitializerModule(); - EagerSingletonModule eagerSingletonModule = new EagerSingletonModule(); - CloseableModule closeableModule = new CloseableModule(); - ScmEventBusModule scmEventBusModule = new ScmEventBusModule(); + return Guice.createInjector(createBootstrapModules(pluginLoader)); + } - return Guice.createInjector( - bootstrapModule, - scmContextListenerModule, - scmEventBusModule, - scmInitializerModule, - eagerSingletonModule, - closeableModule + private List createBootstrapModules(PluginLoader pluginLoader) { + List modules = new ArrayList<>(createBaseModules()); + modules.add(new BootstrapModule(pluginLoader)); + return modules; + } + + private List createBaseModules() { + return ImmutableList.of( + new EagerSingletonModule(), + new ScmInitializerModule(), + new ScmEventBusModule(), + new ServletContextModule(), + new CloseableModule() ); } @@ -144,10 +167,4 @@ public class BootstrapContextListener implements ServletContextListener { updateEngine.update(); } - private static class ScmContextListenerModule extends AbstractModule { - @Override - protected void configure() { - install(new FactoryModuleBuilder().build(ScmContextListener.Factory.class)); - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/InjectionLifeCycle.java b/scm-webapp/src/main/java/sonia/scm/boot/InjectionLifeCycle.java new file mode 100644 index 0000000000..eda7a7575b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/InjectionLifeCycle.java @@ -0,0 +1,60 @@ +package sonia.scm.boot; + +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import sonia.scm.CloseableModule; +import sonia.scm.Default; +import sonia.scm.EagerSingletonModule; +import sonia.scm.ServletContextListenerHolder; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import java.util.Optional; + +class InjectionLifeCycle { + + private final Injector injector; + + InjectionLifeCycle(Injector injector) { + this.injector = injector; + } + + void initialize() { + initializeEagerSingletons(); + initializeServletContextListeners(); + } + + void shutdown() { + destroyServletContextListeners(); + closeRegisteredCloseables(); + } + + private void initializeServletContextListeners() { + ServletContextListenerHolder instance = injector.getInstance(ServletContextListenerHolder.class); + ServletContext context = injector.getInstance(Key.get(ServletContext.class, Default.class)); + instance.contextInitialized(new ServletContextEvent(context)); + } + + private void initializeEagerSingletons() { + findInstance(EagerSingletonModule.class).ifPresent(m -> m.initialize(injector)); + } + + private void closeRegisteredCloseables() { + findInstance(CloseableModule.class).ifPresent(CloseableModule::closeAll); + } + + private void destroyServletContextListeners() { + ServletContextListenerHolder instance = injector.getInstance(ServletContextListenerHolder.class); + ServletContext context = injector.getInstance(Key.get(ServletContext.class, Default.class)); + instance.contextDestroyed(new ServletContextEvent(context)); + } + + private Optional findInstance(Class clazz) { + Binding binding = injector.getExistingBinding(Key.get(clazz)); + if (binding != null) { + return Optional.of(binding.getProvider().get()); + } + return Optional.empty(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/ModuleProvider.java b/scm-webapp/src/main/java/sonia/scm/boot/ModuleProvider.java new file mode 100644 index 0000000000..d58cb264dd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/ModuleProvider.java @@ -0,0 +1,11 @@ +package sonia.scm.boot; + +import com.google.inject.Module; + +import java.util.Collection; + +public interface ModuleProvider { + + Collection createModules(); + +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/ServletContextModule.java b/scm-webapp/src/main/java/sonia/scm/boot/ServletContextModule.java new file mode 100644 index 0000000000..fdea45f1a4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/ServletContextModule.java @@ -0,0 +1,14 @@ +package sonia.scm.boot; + +import com.google.inject.servlet.ServletModule; +import sonia.scm.Default; + +import javax.servlet.ServletContext; + +class ServletContextModule extends ServletModule { + + @Override + protected void configureServlets() { + bind(ServletContext.class).annotatedWith(Default.class).toInstance(getServletContext()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java index 72e2c3522f..10cc42e4f2 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java @@ -1,11 +1,10 @@ package sonia.scm.boot; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.inject.Guice; -import com.google.inject.Injector; +import com.google.inject.Module; import com.google.inject.multibindings.Multibinder; -import com.google.inject.servlet.GuiceServletContextListener; import com.google.inject.servlet.ServletModule; import sonia.scm.Default; import sonia.scm.SCMContext; @@ -14,17 +13,16 @@ import sonia.scm.template.MustacheTemplateEngine; import sonia.scm.template.TemplateEngine; import sonia.scm.template.TemplateEngineFactory; -import javax.servlet.ServletContext; -import javax.servlet.ServletContextListener; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.util.Collection; final class SingleView { private SingleView() { } - static ServletContextListener error(Throwable throwable) { + static SingleViewModuleProvider error(Throwable throwable) { String error = Throwables.getStackTraceAsString(throwable); ViewController controller = new SimpleViewController("/templates/error.mustache", request -> { @@ -34,30 +32,30 @@ final class SingleView { ); return new View(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, model); }); - return new SingleViewContextListener(controller); + return new SingleViewModuleProvider(controller); } - static ServletContextListener view(String template, int sc) { + static SingleViewModuleProvider view(String template, int sc) { ViewController controller = new SimpleViewController(template, request -> { Object model = ImmutableMap.of( "contextPath", request.getContextPath() ); return new View(sc, model); }); - return new SingleViewContextListener(controller); + return new SingleViewModuleProvider(controller); } - private static class SingleViewContextListener extends GuiceServletContextListener { + private static class SingleViewModuleProvider implements ModuleProvider { private final ViewController controller; - private SingleViewContextListener(ViewController controller) { + private SingleViewModuleProvider(ViewController controller) { this.controller = controller; } @Override - protected Injector getInjector() { - return Guice.createInjector(new SingleViewModule(controller)); + public Collection createModules() { + return ImmutableList.of(new ServletContextModule(), new SingleViewModule(controller)); } } @@ -84,8 +82,6 @@ final class SingleView { MustacheTemplateEngine.class); bind(TemplateEngineFactory.class); - bind(ServletContext.class).annotatedWith(Default.class).toInstance(getServletContext()); - serve("/images/*", "/styles/*", "/favicon.ico").with(StaticResourceServlet.class); serve("/*").with(SingleViewServlet.class); } diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModuleProvider.java similarity index 52% rename from scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java rename to scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModuleProvider.java index 6929c8b10a..41c4cbf07b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModuleProvider.java @@ -1,14 +1,18 @@ package sonia.scm.update; import com.google.inject.Injector; -import com.google.inject.servlet.GuiceServletContextListener; +import com.google.inject.Module; +import sonia.scm.boot.ModuleProvider; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; -public class MigrationWizardContextListener extends GuiceServletContextListener { +import java.util.Collection; +import java.util.Collections; + +public class MigrationWizardModuleProvider implements ModuleProvider { private final Injector bootstrapInjector; - public MigrationWizardContextListener(Injector bootstrapInjector) { + public MigrationWizardModuleProvider(Injector bootstrapInjector) { this.bootstrapInjector = bootstrapInjector; } @@ -17,7 +21,7 @@ public class MigrationWizardContextListener extends GuiceServletContextListener } @Override - protected Injector getInjector() { - return bootstrapInjector.createChildInjector(new MigrationWizardModule()); + public Collection createModules() { + return Collections.singleton(new MigrationWizardModule()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/boot/InjectionLifeCycleTest.java b/scm-webapp/src/test/java/sonia/scm/boot/InjectionLifeCycleTest.java new file mode 100644 index 0000000000..cbd6ff9e06 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/InjectionLifeCycleTest.java @@ -0,0 +1,256 @@ +package sonia.scm.boot; + +import com.google.common.base.Strings; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.multibindings.Multibinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.CloseableModule; +import sonia.scm.Default; +import sonia.scm.EagerSingleton; +import sonia.scm.EagerSingletonModule; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class InjectionLifeCycleTest { + + @Mock + private ServletContext servletContext; + + @Test + void shouldInitializeEagerSingletons() { + Injector injector = initialize(new EagerSingletonModule(), new EagerModule()); + + Messenger messenger = injector.getInstance(Messenger.class); + assertThat(messenger.receive()).isEqualTo("eager baby!"); + } + + @Test + void shouldNotThrowAnExceptionWithoutEagerSingletons() { + Injector injector = initialize(new EagerSingletonModule()); + + Messenger messenger = injector.getInstance(Messenger.class); + assertThat(messenger.receive()).isNull(); + } + + @Test + void shouldInitializeServletContextListeners() { + Injector injector = initialize(new ServletContextListenerModule()); + + Messenger messenger = injector.getInstance(Messenger.class); + assertThat(messenger.receive()).isEqualTo("+4+2"); + } + + @Test + void shouldCallDestroyOnServletContextListeners() { + Injector injector = createInjector(servletContext, new ServletContextListenerModule()); + + InjectionLifeCycle lifeCycle = new InjectionLifeCycle(injector); + lifeCycle.shutdown(); + + Messenger messenger = injector.getInstance(Messenger.class); + assertThat(messenger.receive()).isEqualTo("-4-2"); + } + + @Test + void shouldCloseInstantiatedCloseables() { + Injector injector = createInjector(servletContext, new FortyTwoModule(), new CloseableModule()); + + injector.getInstance(Two.class); + injector.getInstance(Four.class); + + + InjectionLifeCycle lifeCycle = new InjectionLifeCycle(injector); + lifeCycle.shutdown(); + + Messenger messenger = injector.getInstance(Messenger.class); + assertThat(messenger.receive()).isEqualTo("42"); + } + + + private Injector initialize(Module... modules) { + Injector injector = createInjector(servletContext, modules); + + InjectionLifeCycle lifeCycle = new InjectionLifeCycle(injector); + lifeCycle.initialize(); + + return injector; + } + + public static class EagerModule extends AbstractModule { + + @Override + protected void configure() { + bind(ImEager.class); + } + } + + @EagerSingleton + public static class ImEager { + + @Inject + public ImEager(Messenger messenger) { + messenger.send("eager baby!"); + } + } + + public static class FortyTwoModule extends AbstractModule { + @Override + protected void configure() { + bind(Four.class); + bind(Two.class); + } + } + + @Singleton + public static class Four implements Closeable { + + private final Messenger messenger; + + @Inject + public Four(Messenger messenger) { + this.messenger = messenger; + } + + @Override + public void close() { + messenger.append("4"); + } + } + + @Singleton + public static class Two implements Closeable { + + private final Messenger messenger; + + @Inject + public Two(Messenger messenger) { + this.messenger = messenger; + } + + @Override + public void close() { + messenger.append("2"); + } + } + + static Injector createInjector(ServletContext context, Module... modules) { + List moduleList = new ArrayList<>(); + moduleList.add(new ServletContextModule(context)); + moduleList.addAll(Arrays.asList(modules)); + + return Guice.createInjector(moduleList); + } + + public static class ServletContextModule extends AbstractModule { + + private final ServletContext servletContext; + + ServletContextModule(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Override + protected void configure() { + bind(ServletContext.class).annotatedWith(Default.class).toInstance(servletContext); + } + + } + + public static class ServletContextListenerModule extends AbstractModule { + + @Override + protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), ServletContextListener.class); + multibinder.addBinding().to(AppendingFourServletContextListener.class); + multibinder.addBinding().to(AppendingTwoServletContextListener.class); + } + } + + public static class AppendingFourServletContextListener extends AppendingServletContextListener { + + @Inject + public AppendingFourServletContextListener(Messenger messenger) { + super(messenger); + } + + @Override + protected String getSign() { + return "4"; + } + } + + public static class AppendingTwoServletContextListener extends AppendingServletContextListener { + + @Inject + public AppendingTwoServletContextListener(Messenger messenger) { + super(messenger); + } + + @Override + protected String getSign() { + return "2"; + } + } + + public static abstract class AppendingServletContextListener implements ServletContextListener { + + private final Messenger messenger; + + @Inject + public AppendingServletContextListener(Messenger messenger) { + this.messenger = messenger; + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + send("+"); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + send("-"); + } + + private void send(String prefix) { + messenger.append(prefix + getSign()); + } + + protected abstract String getSign(); + } + + @Singleton + public static class Messenger { + + private String message; + + void send(String message) { + this.message = message; + } + + void append(String messageToAppend) { + send(Strings.nullToEmpty(message) + messageToAppend); + } + + String receive() { + return message; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/ServletContextModuleTest.java b/scm-webapp/src/test/java/sonia/scm/boot/ServletContextModuleTest.java new file mode 100644 index 0000000000..8539a94a2f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/ServletContextModuleTest.java @@ -0,0 +1,64 @@ +package sonia.scm.boot; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceFilter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.Default; + +import javax.inject.Inject; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServletContextModuleTest { + + @Mock + private ServletContext servletContext; + + private GuiceFilter guiceFilter; + + @BeforeEach + void setUpEnvironment() throws ServletException { + guiceFilter = new GuiceFilter(); + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getServletContext()).thenReturn(servletContext); + + guiceFilter.init(filterConfig); + } + + @AfterEach + void tearDownEnvironment() { + guiceFilter.destroy(); + } + + @Test + void shouldBeAbleToInjectServletContext() { + Injector injector = Guice.createInjector(new ServletContextModule()); + WebComponent instance = injector.getInstance(WebComponent.class); + assertThat(instance.context).isSameAs(servletContext); + } + + + public static class WebComponent { + + private ServletContext context; + + @Inject + public WebComponent(@Default ServletContext context) { + this.context = context; + } + + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java index 9dfebdc571..6da9536392 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java @@ -1,6 +1,5 @@ package sonia.scm.boot; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; diff --git a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java index f520aea504..b669d2c824 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java @@ -1,20 +1,17 @@ package sonia.scm.boot; +import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.servlet.GuiceFilter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @@ -26,22 +23,19 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class SingleViewTest { - @Mock - private ServletContext servletContext; - @Mock private HttpServletRequest request; - @Captor - private ArgumentCaptor captor; - private GuiceFilter guiceFilter; @BeforeEach void setUpGuiceFilter() throws ServletException { guiceFilter = new GuiceFilter(); + + ServletContext servletContext = mock(ServletContext.class); FilterConfig config = mock(FilterConfig.class); doReturn(servletContext).when(config).getServletContext(); + guiceFilter.init(config); } @@ -52,10 +46,10 @@ class SingleViewTest { @Test void shouldCreateViewControllerForView() { - ServletContextListener listener = SingleView.view("/my-template", 409); + ModuleProvider moduleProvider = SingleView.view("/my-template", 409); when(request.getContextPath()).thenReturn("/scm"); - ViewController instance = findViewController(listener); + ViewController instance = findViewController(moduleProvider); assertThat(instance.getTemplate()).isEqualTo("/my-template"); View view = instance.createView(request); @@ -64,17 +58,17 @@ class SingleViewTest { @Test void shouldCreateViewControllerForError() { - ServletContextListener listener = SingleView.error(new IOException("awesome io")); + ModuleProvider moduleProvider = SingleView.error(new IOException("awesome io")); when(request.getContextPath()).thenReturn("/scm"); - ViewController instance = findViewController(listener); + ViewController instance = findViewController(moduleProvider); assertErrorViewController(instance, "awesome io"); } @Test void shouldBindServlets() { - ServletContextListener listener = SingleView.error(new IOException("awesome io")); - Injector injector = findInjector(listener); + ModuleProvider moduleProvider = SingleView.error(new IOException("awesome io")); + Injector injector = Guice.createInjector(moduleProvider.createModules()); assertThat(injector.getInstance(StaticResourceServlet.class)).isNotNull(); assertThat(injector.getInstance(SingleViewServlet.class)).isNotNull(); @@ -94,18 +88,9 @@ class SingleViewTest { ); } - private ViewController findViewController(ServletContextListener listener) { - Injector injector = findInjector(listener); + private ViewController findViewController(ModuleProvider moduleProvider) { + Injector injector = Guice.createInjector(moduleProvider.createModules()); return injector.getInstance(ViewController.class); } - private Injector findInjector(ServletContextListener listener) { - listener.contextInitialized(new ServletContextEvent(servletContext)); - - verify(servletContext).setAttribute(anyString(), captor.capture()); - - return captor.getValue(); - } - - }