From 4c91ef010058742940399b3e7e42d6c67cc45600 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 17:04:32 +0200 Subject: [PATCH 1/7] make PluginLoader dependency of MustacheTemplateEngine optional --- .../scm/template/MustacheTemplateEngine.java | 36 +++++++++++-------- .../scm/template/ServletMustacheFactory.java | 23 +++++------- .../template/MustacheTemplateEngineTest.java | 34 ++++++++++++++---- 3 files changed, 57 insertions(+), 36 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java b/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java index 06bd8b0f62..170c0cd641 100644 --- a/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java +++ b/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java @@ -37,27 +37,22 @@ package sonia.scm.template; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheException; - import com.google.common.base.Throwables; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.inject.Inject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.Default; import sonia.scm.plugin.PluginLoader; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.ServletContext; import java.io.IOException; import java.io.Reader; - import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; -import javax.servlet.ServletContext; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -67,6 +62,14 @@ import javax.servlet.ServletContext; public class MustacheTemplateEngine implements TemplateEngine { + /** + * Used to implement optional injection for the PluginLoader. + * @see Optional Injection + */ + static class PluginLoaderHolder { + @Inject(optional = true) PluginLoader pluginLoader; + } + /** Field description */ public static final TemplateType TYPE = new TemplateType("mustache", "Mustache", "mustache"); @@ -87,13 +90,12 @@ public class MustacheTemplateEngine implements TemplateEngine * * * @param context - * @param pluginLoader + * @param pluginLoaderHolder */ @Inject - public MustacheTemplateEngine(@Default ServletContext context, - PluginLoader pluginLoader) + public MustacheTemplateEngine(@Default ServletContext context, PluginLoaderHolder pluginLoaderHolder) { - factory = new ServletMustacheFactory(context, pluginLoader); + factory = new ServletMustacheFactory(context, createClassLoader(pluginLoaderHolder.pluginLoader)); ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(THREAD_NAME).build(); @@ -101,6 +103,13 @@ public class MustacheTemplateEngine implements TemplateEngine factory.setExecutorService(Executors.newCachedThreadPool(threadFactory)); } + private ClassLoader createClassLoader(PluginLoader pluginLoader) { + if (pluginLoader == null) { + return Thread.currentThread().getContextClassLoader(); + } + return pluginLoader.getUberClassLoader(); + } + //~--- get methods ---------------------------------------------------------- /** @@ -112,12 +121,9 @@ public class MustacheTemplateEngine implements TemplateEngine * * @return * - * @throws IOException */ @Override - public Template getTemplate(String templateIdentifier, Reader reader) - throws IOException - { + public Template getTemplate(String templateIdentifier, Reader reader) { if (logger.isTraceEnabled()) { logger.trace("try to create mustache template from reader with id {}", diff --git a/scm-webapp/src/main/java/sonia/scm/template/ServletMustacheFactory.java b/scm-webapp/src/main/java/sonia/scm/template/ServletMustacheFactory.java index c6e5b82441..24c8ff21c9 100644 --- a/scm-webapp/src/main/java/sonia/scm/template/ServletMustacheFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/template/ServletMustacheFactory.java @@ -36,22 +36,17 @@ package sonia.scm.template; //~--- non-JDK imports -------------------------------------------------------- import com.github.mustachejava.DefaultMustacheFactory; - import com.google.common.base.Charsets; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.plugin.PluginLoader; - -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.ServletContext; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import javax.servlet.ServletContext; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -73,13 +68,12 @@ public class ServletMustacheFactory extends DefaultMustacheFactory * * * @param servletContext - * @param pluginLoader + * @param classLoader */ - public ServletMustacheFactory(ServletContext servletContext, - PluginLoader pluginLoader) + public ServletMustacheFactory(ServletContext servletContext, ClassLoader classLoader) { this.servletContext = servletContext; - this.pluginLoader = pluginLoader; + this.classLoader = classLoader; } //~--- get methods ---------------------------------------------------------- @@ -116,7 +110,7 @@ public class ServletMustacheFactory extends DefaultMustacheFactory resourceName = resourceName.substring(1); } - is = pluginLoader.getUberClassLoader().getResourceAsStream(resourceName); + is = classLoader.getResourceAsStream(resourceName); } if (is != null) @@ -144,9 +138,8 @@ public class ServletMustacheFactory extends DefaultMustacheFactory //~--- fields --------------------------------------------------------------- - /** Field description */ - private final PluginLoader pluginLoader; - /** Field description */ private ServletContext servletContext; + + private ClassLoader classLoader; } diff --git a/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java b/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java index be0c33f098..131db6e44d 100644 --- a/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java +++ b/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java @@ -35,16 +35,21 @@ package sonia.scm.template; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.collect.ImmutableMap; +import org.assertj.core.api.Assertions; +import org.junit.Test; import sonia.scm.plugin.PluginLoader; -import static org.mockito.Mockito.*; +import javax.servlet.ServletContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ -import java.io.InputStream; - -import javax.servlet.ServletContext; - /** * * @author Sebastian Sdorra @@ -68,7 +73,10 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase when(loader.getUberClassLoader()).thenReturn( Thread.currentThread().getContextClassLoader()); - return new MustacheTemplateEngine(context, loader); + MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder(); + holder.pluginLoader = loader; + + return new MustacheTemplateEngine(context, holder); } //~--- get methods ---------------------------------------------------------- @@ -116,4 +124,18 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase return MustacheTemplateEngineTest.class.getResourceAsStream( "/sonia/scm/template/".concat(resource).concat(".mustache")); } + + @Test + public void testCreateEngineWithoutPluginLoader() throws IOException { + ServletContext context = mock(ServletContext.class); + MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder(); + MustacheTemplateEngine engine = new MustacheTemplateEngine(context, holder); + + Template template = engine.getTemplate(getTemplateResource()); + + StringWriter writer = new StringWriter(); + template.execute(writer, ImmutableMap.of("name", "World")); + + Assertions.assertThat(writer.toString()).isEqualTo("Hello World!"); + } } From df3e4395b0227d97d0b81a33c2b33441979e030e Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 18:23:21 +0200 Subject: [PATCH 2/7] remove unused template from v1 --- .../main/resources/templates/error.mustache | 14 ++ .../templates/repository-root.mustache | 102 ------------ .../main/resources/templates/support.mustache | 150 ------------------ 3 files changed, 14 insertions(+), 252 deletions(-) create mode 100644 scm-webapp/src/main/resources/templates/error.mustache delete mode 100644 scm-webapp/src/main/resources/templates/repository-root.mustache delete mode 100644 scm-webapp/src/main/resources/templates/support.mustache diff --git a/scm-webapp/src/main/resources/templates/error.mustache b/scm-webapp/src/main/resources/templates/error.mustache new file mode 100644 index 0000000000..ade00d002b --- /dev/null +++ b/scm-webapp/src/main/resources/templates/error.mustache @@ -0,0 +1,14 @@ +{{< layout}} + + {{$title}}SCM-Manager Error{{/title}} + + {{$content}} +

There is an error occurred during SCM-Manager startup.

+ +
+
+        {{ error }}
+      
+
+ {{/content}} +{{/ layout}} diff --git a/scm-webapp/src/main/resources/templates/repository-root.mustache b/scm-webapp/src/main/resources/templates/repository-root.mustache deleted file mode 100644 index 7e5cd5951e..0000000000 --- a/scm-webapp/src/main/resources/templates/repository-root.mustache +++ /dev/null @@ -1,102 +0,0 @@ - - - - - SCM-Manager support information - - - - -

SCM-Manager Repositories

- - - - - diff --git a/scm-webapp/src/main/resources/templates/support.mustache b/scm-webapp/src/main/resources/templates/support.mustache deleted file mode 100644 index 822ac5e9d5..0000000000 --- a/scm-webapp/src/main/resources/templates/support.mustache +++ /dev/null @@ -1,150 +0,0 @@ - - - - - SCM-Manager support information - - - - -

SCM-Manager support information

- -

Information for SCM-Manager support.

- -

Version

- - - -

Configuration

- - - -

Installed Plugins

- - - -

Runtime

- - - -

System

- - - -

Repository Handlers

- - - - - From bc7402053a6c2fb18363f3b15f92eec0032ca72c Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 18:25:38 +0200 Subject: [PATCH 3/7] remove outdated error module --- .../java/sonia/scm/ScmContextListener.java | 35 ++-- .../main/java/sonia/scm/ScmErrorModule.java | 74 ------- .../java/sonia/scm/template/ErrorServlet.java | 191 ------------------ 3 files changed, 12 insertions(+), 288 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/ScmErrorModule.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 874052442d..cc58d34ef3 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -75,7 +75,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList * the logger for ScmContextListener */ private static final Logger LOG = LoggerFactory.getLogger(ScmContextListener.class); - + private final ClassLoader parent; private final Set plugins; private Injector injector; @@ -101,23 +101,16 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList super.contextInitialized(servletContextEvent); afterInjectorCreation(servletContextEvent); } - + private void beforeInjectorCreation() { } private boolean hasStartupErrors() { return SCMContext.getContext().getStartupError() != null; } - + @Override protected List getModules(ServletContext context) { - if (hasStartupErrors()) { - return getErrorModules(); - } - return getDefaultModules(context); - } - - private List getDefaultModules(ServletContext context) { DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, parent, plugins); ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader()); @@ -132,15 +125,15 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList ); appendModules(pluginLoader.getExtensionProcessor(), moduleList); moduleList.addAll(overrides.getModules()); - + if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT){ moduleList.add(new DebugModule()); } moduleList.add(new MapperModule()); - return moduleList; + return moduleList; } - + private void appendModules(ExtensionProcessor ep, List moduleList) { for (Class module : ep.byExtensionPoint(Module.class)) { try { @@ -151,31 +144,27 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList } } } - - private List getErrorModules() { - return Collections.singletonList(new ScmErrorModule()); - } @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) { @@ -198,7 +187,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList } } } - + private void closeCloseables() { // close Scheduler IOUtil.close(injector.getInstance(Scheduler.class)); diff --git a/scm-webapp/src/main/java/sonia/scm/ScmErrorModule.java b/scm-webapp/src/main/java/sonia/scm/ScmErrorModule.java deleted file mode 100644 index 56ae8527f2..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/ScmErrorModule.java +++ /dev/null @@ -1,74 +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; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.multibindings.Multibinder; -import com.google.inject.servlet.ServletModule; - -import sonia.scm.template.ErrorServlet; -import sonia.scm.template.MustacheTemplateEngine; -import sonia.scm.template.TemplateEngine; -import sonia.scm.template.TemplateEngineFactory; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmErrorModule extends ServletModule -{ - - /** - * Method description - * - */ - @Override - protected void configureServlets() - { - SCMContextProvider context = SCMContext.getContext(); - - bind(SCMContextProvider.class).toInstance(context); - - Multibinder engineBinder = - Multibinder.newSetBinder(binder(), TemplateEngine.class); - - engineBinder.addBinding().to(MustacheTemplateEngine.class); - bind(TemplateEngine.class).annotatedWith(Default.class).to( - MustacheTemplateEngine.class); - bind(TemplateEngineFactory.class); - - serve(ScmServletModule.PATTERN_ALL).with(ErrorServlet.class); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java b/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java deleted file mode 100644 index 4d62b823a1..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java +++ /dev/null @@ -1,191 +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.template; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Throwables; -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.SCMContextProvider; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.io.PrintWriter; - -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * - * @author Sebastian Sdorra - */ -@Singleton -public class ErrorServlet extends HttpServlet -{ - - /** Field description */ - private static final String TEMPALTE = "/error.mustache"; - - /** Field description */ - private static final long serialVersionUID = -3289076078469757874L; - - /** - * the logger for ErrorServlet - */ - private static final Logger logger = - LoggerFactory.getLogger(ErrorServlet.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param context - * @param templateEngineFactory - */ - @Inject - public ErrorServlet(SCMContextProvider context, - TemplateEngineFactory templateEngineFactory) - { - this.context = context; - this.templateEngineFactory = templateEngineFactory; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException - { - processRequest(request, response); - } - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException - { - processRequest(request, response); - } - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - private void processRequest(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException - { - PrintWriter writer = null; - - try - { - writer = response.getWriter(); - - Map env = new HashMap(); - String error = Util.EMPTY_STRING; - - if (context.getStartupError() != null) - { - error = Throwables.getStackTraceAsString(context.getStartupError()); - } - - env.put("error", error); - - TemplateEngine engine = templateEngineFactory.getDefaultEngine(); - Template template = engine.getTemplate(TEMPALTE); - - if (template != null) - { - template.execute(writer, env); - } - else if (logger.isWarnEnabled()) - { - logger.warn("could not find template {}", TEMPALTE); - } - } - finally - { - IOUtil.close(writer); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final SCMContextProvider context; - - /** Field description */ - private final TemplateEngineFactory templateEngineFactory; -} From 5c7ae749c213ab713fc89a2baded0b5c452af72e Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 18:26:58 +0200 Subject: [PATCH 4/7] create new error module, which displays errors before migration --- .../scm/boot/BootstrapContextListener.java | 18 ++- .../main/java/sonia/scm/boot/SingleView.java | 109 ++++++++++++++++++ .../sonia/scm/boot/SingleViewServlet.java | 63 ++++++++++ .../sonia/scm/boot/StaticResourceServlet.java | 39 +++++++ .../src/main/java/sonia/scm/boot/View.java | 20 ++++ .../java/sonia/scm/boot/ViewController.java | 11 ++ .../sonia/scm/boot/SingleViewServletTest.java | 90 +++++++++++++++ .../java/sonia/scm/boot/SingleViewTest.java | 99 ++++++++++++++++ .../scm/boot/StaticResourceServletTest.java | 61 ++++++++++ .../resources/sonia/scm/boot/resource.txt | 1 + 10 files changed, 506 insertions(+), 5 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/SingleView.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/SingleViewServlet.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/StaticResourceServlet.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/View.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/ViewController.java create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/StaticResourceServletTest.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/boot/resource.txt 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 76aad46b77..844a04c79f 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -69,7 +69,6 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import java.io.Closeable; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; @@ -126,9 +125,7 @@ public class BootstrapContextListener implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { context = sce.getServletContext(); - File pluginDirectory = getPluginDirectory(); - - createContextListener(pluginDirectory); + createContextListener(); contextListener.contextInitialized(sce); @@ -140,12 +137,23 @@ public class BootstrapContextListener implements ServletContextListener { } } - private void createContextListener(File pluginDirectory) { + private void createContextListener() { + Throwable startupError = SCMContext.getContext().getStartupError(); + if (startupError != null) { + contextListener = SingleView.error(startupError); + } else { + createMigrationOrNormalContextListener(); + } + } + + private void createMigrationOrNormalContextListener() { ClassLoader cl; Set plugins; PluginLoader pluginLoader; try { + File pluginDirectory = getPluginDirectory(); + renameOldPluginsFolder(pluginDirectory); if (!isCorePluginExtractionDisabled()) { diff --git a/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java new file mode 100644 index 0000000000..f1c57ce9c6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java @@ -0,0 +1,109 @@ +package sonia.scm.boot; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Guice; +import com.google.inject.Injector; +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; +import sonia.scm.SCMContextProvider; +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; + +final class SingleView { + + private SingleView() { + } + + static ServletContextListener error(Throwable throwable) { + String error = Throwables.getStackTraceAsString(throwable); + + ViewController controller = new SimpleViewController("/templates/error.mustache", request -> { + Object model = ImmutableMap.of( + "contextPath", request.getContextPath(), + "error", error + ); + return new View(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, model); + }); + return new SingleViewContextListener(controller); + } + + private static class SingleViewContextListener extends GuiceServletContextListener { + + private final ViewController controller; + + private SingleViewContextListener(ViewController controller) { + this.controller = controller; + } + + @Override + protected Injector getInjector() { + return Guice.createInjector(new SingleViewModule(controller)); + } + } + + private static class SingleViewModule extends ServletModule { + + private final ViewController viewController; + + private SingleViewModule(ViewController viewController) { + this.viewController = viewController; + } + + @Override + protected void configureServlets() { + SCMContextProvider context = SCMContext.getContext(); + + bind(SCMContextProvider.class).toInstance(context); + bind(ViewController.class).toInstance(viewController); + + Multibinder engineBinder = + Multibinder.newSetBinder(binder(), TemplateEngine.class); + + engineBinder.addBinding().to(MustacheTemplateEngine.class); + bind(TemplateEngine.class).annotatedWith(Default.class).to( + 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); + } + } + + private static class SimpleViewController implements ViewController { + + private final String template; + private final SimpleViewFactory viewFactory; + + private SimpleViewController(String template, SimpleViewFactory viewFactory) { + this.template = template; + this.viewFactory = viewFactory; + } + + @Override + public String getTemplate() { + return template; + } + + @Override + public View createView(HttpServletRequest request) { + return viewFactory.create(request); + } + } + + @FunctionalInterface + interface SimpleViewFactory { + View create(HttpServletRequest request); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/SingleViewServlet.java b/scm-webapp/src/main/java/sonia/scm/boot/SingleViewServlet.java new file mode 100644 index 0000000000..3621105976 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/SingleViewServlet.java @@ -0,0 +1,63 @@ +package sonia.scm.boot; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +@Singleton +public class SingleViewServlet extends HttpServlet { + + private static final Logger LOG = LoggerFactory.getLogger(SingleViewServlet.class); + + private final Template template; + private final ViewController controller; + + @Inject + public SingleViewServlet(TemplateEngineFactory templateEngineFactory, ViewController controller) { + template = createTemplate(templateEngineFactory, controller.getTemplate()); + this.controller = controller; + } + + private Template createTemplate(TemplateEngineFactory templateEngineFactory, String template) { + TemplateEngine engine = templateEngineFactory.getEngineByExtension(template); + try { + return engine.getTemplate(template); + } catch (IOException e) { + throw new IllegalStateException("failed to parse template: " + template, e); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + process(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + process(req, resp); + } + + private void process(HttpServletRequest request, HttpServletResponse response) { + View view = controller.createView(request); + + response.setStatus(view.getStatusCode()); + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + try (PrintWriter writer = response.getWriter()) { + template.execute(writer, view.getModel()); + } catch (IOException ex) { + LOG.error("failed to write view", ex); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/StaticResourceServlet.java b/scm-webapp/src/main/java/sonia/scm/boot/StaticResourceServlet.java new file mode 100644 index 0000000000..b44fbb64bc --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/StaticResourceServlet.java @@ -0,0 +1,39 @@ +package sonia.scm.boot; + +import com.github.sdorra.webresources.CacheControl; +import com.github.sdorra.webresources.WebResourceSender; +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.util.HttpUtil; + +import javax.inject.Singleton; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +@Singleton +public class StaticResourceServlet extends HttpServlet { + + private final WebResourceSender sender = WebResourceSender.create() + .withGZIP() + .withGZIPMinLength(512) + .withBufferSize(16384) + .withCacheControl(CacheControl.create().noCache()); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + URL resource = createResourceUrlFromRequest(request); + if (resource != null) { + sender.resource(resource).get(request, response); + } else { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } + + private URL createResourceUrlFromRequest(HttpServletRequest request) throws MalformedURLException { + String uri = HttpUtil.getStrippedURI(request); + return request.getServletContext().getResource(uri); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/View.java b/scm-webapp/src/main/java/sonia/scm/boot/View.java new file mode 100644 index 0000000000..6e1f93bd3f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/View.java @@ -0,0 +1,20 @@ +package sonia.scm.boot; + +class View { + + private final int statusCode; + private final Object model; + + View(int statusCode, Object model) { + this.statusCode = statusCode; + this.model = model; + } + + int getStatusCode() { + return statusCode; + } + + Object getModel() { + return model; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/ViewController.java b/scm-webapp/src/main/java/sonia/scm/boot/ViewController.java new file mode 100644 index 0000000000..26f463f9c2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/ViewController.java @@ -0,0 +1,11 @@ +package sonia.scm.boot; + +import javax.servlet.http.HttpServletRequest; + +public interface ViewController { + + String getTemplate(); + + View createView(HttpServletRequest request); + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java new file mode 100644 index 0000000000..9dfebdc571 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java @@ -0,0 +1,90 @@ +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; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SingleViewServletTest { + + @Mock + private TemplateEngineFactory templateEngineFactory; + + @Mock + private TemplateEngine templateEngine; + + @Mock + private Template template; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private PrintWriter writer; + + @Mock + private ViewController controller; + + @Test + void shouldRenderTheTemplateOnGet() throws IOException { + prepareTemplate("/template"); + doReturn(new View(200, "hello")).when(controller).createView(request); + + new SingleViewServlet(templateEngineFactory, controller).doGet(request, response); + + verifyResponse(200, "hello"); + } + + private void verifyResponse(int sc, Object model) throws IOException { + verify(response).setStatus(sc); + verify(response).setContentType("text/html"); + verify(response).setCharacterEncoding("UTF-8"); + + verify(template).execute(writer, model); + } + + @Test + void shouldRenderTheTemplateOnPost() throws IOException { + prepareTemplate("/template"); + + doReturn(new View(201, "hello")).when(controller).createView(request); + + new SingleViewServlet(templateEngineFactory, controller).doPost(request, response); + + verifyResponse(201, "hello"); + } + + @Test + void shouldThrowIllegalStateExceptionOnIOException() throws IOException { + doReturn("/template").when(controller).getTemplate(); + doReturn(templateEngine).when(templateEngineFactory).getEngineByExtension("/template"); + doThrow(IOException.class).when(templateEngine).getTemplate("/template"); + + assertThrows(IllegalStateException.class, () -> new SingleViewServlet(templateEngineFactory, controller)); + } + + private void prepareTemplate(String templatePath) throws IOException { + doReturn(templateEngine).when(templateEngineFactory).getEngineByExtension(templatePath); + doReturn(template).when(templateEngine).getTemplate(templatePath); + doReturn(templatePath).when(controller).getTemplate(); + + doReturn(writer).when(response).getWriter(); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java new file mode 100644 index 0000000000..64c5ab98f8 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java @@ -0,0 +1,99 @@ +package sonia.scm.boot; + +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; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +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(); + FilterConfig config = mock(FilterConfig.class); + doReturn(servletContext).when(config).getServletContext(); + guiceFilter.init(config); + } + + @AfterEach + void tearDownGuiceFilter() { + guiceFilter.destroy(); + } + + @Test + void shouldCreateViewControllerForError() { + ServletContextListener listener = SingleView.error(new IOException("awesome io")); + when(request.getContextPath()).thenReturn("/scm"); + + ViewController instance = findViewController(listener); + assertErrorViewController(instance, "awesome io"); + } + + @Test + void shouldBindServlets() { + ServletContextListener listener = SingleView.error(new IOException("awesome io")); + Injector injector = findInjector(listener); + + assertThat(injector.getInstance(StaticResourceServlet.class)).isNotNull(); + assertThat(injector.getInstance(SingleViewServlet.class)).isNotNull(); + } + + @SuppressWarnings("unchecked") + private void assertErrorViewController(ViewController instance, String contains) { + assertThat(instance.getTemplate()).isEqualTo("/templates/error.mustache"); + + View view = instance.createView(request); + assertThat(view.getStatusCode()).isEqualTo(500); + assertThat(view.getModel()).isInstanceOfSatisfying(Map.class, map -> { + assertThat(map).containsEntry("contextPath", "/scm"); + String error = (String) map.get("error"); + assertThat(error).contains(contains); + } + ); + } + + private ViewController findViewController(ServletContextListener listener) { + Injector injector = findInjector(listener); + return injector.getInstance(ViewController.class); + } + + private Injector findInjector(ServletContextListener listener) { + listener.contextInitialized(new ServletContextEvent(servletContext)); + + verify(servletContext).setAttribute(anyString(), captor.capture()); + + return captor.getValue(); + } + + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/StaticResourceServletTest.java b/scm-webapp/src/test/java/sonia/scm/boot/StaticResourceServletTest.java new file mode 100644 index 0000000000..a350f4d9f3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/StaticResourceServletTest.java @@ -0,0 +1,61 @@ +package sonia.scm.boot; + +import com.google.common.io.Resources; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.servlet.ServletContext; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URL; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class StaticResourceServletTest { + + @Mock + private HttpServletRequest request; + + @Mock + private ServletOutputStream stream; + + @Mock + private HttpServletResponse response; + + @Mock + private ServletContext context; + + @Test + void shouldServeResource() throws IOException { + doReturn("/scm").when(request).getContextPath(); + doReturn("/scm/resource.txt").when(request).getRequestURI(); + doReturn(context).when(request).getServletContext(); + URL resource = Resources.getResource("sonia/scm/boot/resource.txt"); + doReturn(resource).when(context).getResource("/resource.txt"); + doReturn(stream).when(response).getOutputStream(); + + StaticResourceServlet servlet = new StaticResourceServlet(); + servlet.doGet(request, response); + + verify(response).setStatus(HttpServletResponse.SC_OK); + } + + @Test + void shouldReturnNotFound() throws IOException { + doReturn("/scm").when(request).getContextPath(); + doReturn("/scm/resource.txt").when(request).getRequestURI(); + doReturn(context).when(request).getServletContext(); + + StaticResourceServlet servlet = new StaticResourceServlet(); + servlet.doGet(request, response); + + verify(response).setStatus(HttpServletResponse.SC_NOT_FOUND); + } + +} diff --git a/scm-webapp/src/test/resources/sonia/scm/boot/resource.txt b/scm-webapp/src/test/resources/sonia/scm/boot/resource.txt new file mode 100644 index 0000000000..c4a1132fa5 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/boot/resource.txt @@ -0,0 +1 @@ +Resource for testing From 3c5b7ab535daeb425e1c794122cb857c3cc4f1d9 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 13 Jun 2019 09:58:30 +0200 Subject: [PATCH 5/7] display error on startup, if previous version is older than 1.60 --- .../scm/boot/BootstrapContextListener.java | 4 + .../main/java/sonia/scm/boot/SingleView.java | 10 ++ .../main/java/sonia/scm/boot/Versions.java | 77 +++++++++++++ .../main/resources/templates/error.mustache | 2 +- .../main/resources/templates/to-old.mustache | 14 +++ scm-webapp/src/main/webapp/error.mustache | 102 ------------------ .../java/sonia/scm/boot/SingleViewTest.java | 12 +++ .../java/sonia/scm/boot/VersionsTest.java | 86 +++++++++++++++ 8 files changed, 204 insertions(+), 103 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/Versions.java create mode 100644 scm-webapp/src/main/resources/templates/to-old.mustache delete mode 100644 scm-webapp/src/main/webapp/error.mustache create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java 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 844a04c79f..686b638cb3 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -63,6 +63,7 @@ import sonia.scm.util.IOUtil; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import javax.servlet.http.HttpServletResponse; import javax.xml.bind.DataBindingException; import javax.xml.bind.JAXB; import javax.xml.bind.annotation.XmlAccessType; @@ -141,8 +142,11 @@ public class BootstrapContextListener implements ServletContextListener { Throwable startupError = SCMContext.getContext().getStartupError(); if (startupError != null) { contextListener = SingleView.error(startupError); + } else if (Versions.isToOld()) { + contextListener = SingleView.view("/templates/to-old.mustache", HttpServletResponse.SC_CONFLICT); } else { createMigrationOrNormalContextListener(); + Versions.writeNew(); } } 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 f1c57ce9c6..72e2c3522f 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java @@ -37,6 +37,16 @@ final class SingleView { return new SingleViewContextListener(controller); } + static ServletContextListener 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); + } + private static class SingleViewContextListener extends GuiceServletContextListener { private final ViewController controller; diff --git a/scm-webapp/src/main/java/sonia/scm/boot/Versions.java b/scm-webapp/src/main/java/sonia/scm/boot/Versions.java new file mode 100644 index 0000000000..e3c2848ace --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/Versions.java @@ -0,0 +1,77 @@ +package sonia.scm.boot; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContext; +import sonia.scm.SCMContextProvider; +import sonia.scm.util.IOUtil; +import sonia.scm.version.Version; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +class Versions { + + private static final Logger LOG = LoggerFactory.getLogger(Versions.class); + + private static final Version MIN_VERSION = Version.parse("1.60"); + + private final SCMContextProvider contextProvider; + + @VisibleForTesting + Versions(SCMContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @VisibleForTesting + boolean isPreviousVersionToOld() { + return readVersion().map(v -> v.isOlder(MIN_VERSION)).orElse(false); + } + + @VisibleForTesting + void writeNewVersion() { + Path config = contextProvider.resolve(Paths.get("config")); + IOUtil.mkdirs(config.toFile()); + + String version = contextProvider.getVersion(); + LOG.debug("write new version {} to file", version); + Path versionFile = config.resolve("version.txt"); + try { + Files.write(versionFile, version.getBytes()); + } catch (IOException e) { + throw new IllegalStateException("failed to write version file", e); + } + } + + private Optional readVersion() { + Path versionFile = contextProvider.resolve(Paths.get("config", "version.txt")); + if (versionFile.toFile().exists()) { + return Optional.of(readVersionFromFile(versionFile)); + } + return Optional.empty(); + } + + private Version readVersionFromFile(Path versionFile) { + try { + String versionString = new String(Files.readAllBytes(versionFile), StandardCharsets.UTF_8).trim(); + LOG.debug("read previous version {} from file", versionString); + return Version.parse(versionString); + } catch (IOException e) { + throw new IllegalStateException("failed to read version file", e); + } + } + + static boolean isToOld() { + return new Versions(SCMContext.getContext()).isPreviousVersionToOld(); + } + + static void writeNew() { + new Versions(SCMContext.getContext()).writeNewVersion(); + } + +} diff --git a/scm-webapp/src/main/resources/templates/error.mustache b/scm-webapp/src/main/resources/templates/error.mustache index ade00d002b..4b9b0a9086 100644 --- a/scm-webapp/src/main/resources/templates/error.mustache +++ b/scm-webapp/src/main/resources/templates/error.mustache @@ -3,7 +3,7 @@ {{$title}}SCM-Manager Error{{/title}} {{$content}} -

There is an error occurred during SCM-Manager startup.

+

An error occurred during SCM-Manager startup.

diff --git a/scm-webapp/src/main/resources/templates/to-old.mustache b/scm-webapp/src/main/resources/templates/to-old.mustache
new file mode 100644
index 0000000000..2ff578b263
--- /dev/null
+++ b/scm-webapp/src/main/resources/templates/to-old.mustache
@@ -0,0 +1,14 @@
+{{< layout}}
+
+  {{$title}}SCM-Manager Error{{/title}}
+
+  {{$content}}
+    

An error occurred during SCM-Manager startup.

+ +

+ We cannot migrate your SCM-Manager 1 installation, + because the version is too old.
+ Please migrate to version 1.60 or newer, before migration to 2.x. +

+ {{/content}} +{{/ layout}} diff --git a/scm-webapp/src/main/webapp/error.mustache b/scm-webapp/src/main/webapp/error.mustache deleted file mode 100644 index 7a726a2664..0000000000 --- a/scm-webapp/src/main/webapp/error.mustache +++ /dev/null @@ -1,102 +0,0 @@ - - - - - SCM-Manager Error - - - - -

SCM-Manager Error

- -

- There is an error occurred during SCM-Manager startup. -

- -
-      {{error}}
-    
- - - 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 64c5ab98f8..f520aea504 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java @@ -50,6 +50,18 @@ class SingleViewTest { guiceFilter.destroy(); } + @Test + void shouldCreateViewControllerForView() { + ServletContextListener listener = SingleView.view("/my-template", 409); + when(request.getContextPath()).thenReturn("/scm"); + + ViewController instance = findViewController(listener); + assertThat(instance.getTemplate()).isEqualTo("/my-template"); + + View view = instance.createView(request); + assertThat(view.getStatusCode()).isEqualTo(409); + } + @Test void shouldCreateViewControllerForError() { ServletContextListener listener = SingleView.error(new IOException("awesome io")); diff --git a/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java b/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java new file mode 100644 index 0000000000..0506bd5594 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java @@ -0,0 +1,86 @@ +package sonia.scm.boot; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; + +@ExtendWith({MockitoExtension.class, TempDirectory.class}) +class VersionsTest { + + @Mock + private SCMContextProvider contextProvider; + + @InjectMocks + private Versions versions; + + @Test + void shouldReturnTrueForVersionsPreviousTo160(@TempDirectory.TempDir Path directory) throws IOException { + setVersion(directory, "1.59"); + assertThat(versions.isPreviousVersionToOld()).isTrue(); + + setVersion(directory, "1.12"); + assertThat(versions.isPreviousVersionToOld()).isTrue(); + } + + @Test + void shouldReturnFalseForVersion160(@TempDirectory.TempDir Path directory) throws IOException { + setVersion(directory, "1.60"); + assertThat(versions.isPreviousVersionToOld()).isFalse(); + } + + @Test + void shouldNotFailIfVersionContainsLineBreak(@TempDirectory.TempDir Path directory) throws IOException { + setVersion(directory, "1.59\n"); + assertThat(versions.isPreviousVersionToOld()).isTrue(); + } + + @Test + void shouldReturnFalseForVersionsNewerAs160(@TempDirectory.TempDir Path directory) throws IOException { + setVersion(directory, "1.61"); + assertThat(versions.isPreviousVersionToOld()).isFalse(); + + setVersion(directory, "1.82"); + assertThat(versions.isPreviousVersionToOld()).isFalse(); + } + + @Test + void shouldReturnFalseForNonExistingVersionFile(@TempDirectory.TempDir Path directory) { + setVersionFile(directory.resolve("version.txt")); + assertThat(versions.isPreviousVersionToOld()).isFalse(); + } + + @Test + void shouldWriteNewVersion(@TempDirectory.TempDir Path directory) { + Path config = directory.resolve("config"); + doReturn(config).when(contextProvider).resolve(Paths.get("config")); + doReturn("2.0.0").when(contextProvider).getVersion(); + + versions.writeNewVersion(); + + Path versionFile = config.resolve("version.txt"); + assertThat(versionFile).exists().hasContent("2.0.0"); + } + + private void setVersion(Path directory, String version) throws IOException { + Path file = directory.resolve("version.txt"); + Files.write(file, version.getBytes(StandardCharsets.UTF_8)); + setVersionFile(file); + } + + private void setVersionFile(Path file) { + doReturn(file).when(contextProvider).resolve(Paths.get("config", "version.txt")); + } +} From 67a78fd3b2f30883d04d3ac399c3d395589aba36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 13 Jun 2019 10:38:15 +0200 Subject: [PATCH 6/7] Fix typo --- .../sonia/scm/boot/BootstrapContextListener.java | 4 ++-- .../src/main/java/sonia/scm/boot/Versions.java | 6 +++--- .../{to-old.mustache => too-old.mustache} | 0 .../src/test/java/sonia/scm/boot/VersionsTest.java | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) rename scm-webapp/src/main/resources/templates/{to-old.mustache => too-old.mustache} (100%) 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 686b638cb3..363a9e5e19 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -142,8 +142,8 @@ public class BootstrapContextListener implements ServletContextListener { Throwable startupError = SCMContext.getContext().getStartupError(); if (startupError != null) { contextListener = SingleView.error(startupError); - } else if (Versions.isToOld()) { - contextListener = SingleView.view("/templates/to-old.mustache", HttpServletResponse.SC_CONFLICT); + } else if (Versions.isTooOld()) { + contextListener = SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT); } else { createMigrationOrNormalContextListener(); Versions.writeNew(); diff --git a/scm-webapp/src/main/java/sonia/scm/boot/Versions.java b/scm-webapp/src/main/java/sonia/scm/boot/Versions.java index e3c2848ace..6da19fedca 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/Versions.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/Versions.java @@ -29,7 +29,7 @@ class Versions { } @VisibleForTesting - boolean isPreviousVersionToOld() { + boolean isPreviousVersionTooOld() { return readVersion().map(v -> v.isOlder(MIN_VERSION)).orElse(false); } @@ -66,8 +66,8 @@ class Versions { } } - static boolean isToOld() { - return new Versions(SCMContext.getContext()).isPreviousVersionToOld(); + static boolean isTooOld() { + return new Versions(SCMContext.getContext()).isPreviousVersionTooOld(); } static void writeNew() { diff --git a/scm-webapp/src/main/resources/templates/to-old.mustache b/scm-webapp/src/main/resources/templates/too-old.mustache similarity index 100% rename from scm-webapp/src/main/resources/templates/to-old.mustache rename to scm-webapp/src/main/resources/templates/too-old.mustache diff --git a/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java b/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java index 0506bd5594..e5aa8fe3d1 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java @@ -29,37 +29,37 @@ class VersionsTest { @Test void shouldReturnTrueForVersionsPreviousTo160(@TempDirectory.TempDir Path directory) throws IOException { setVersion(directory, "1.59"); - assertThat(versions.isPreviousVersionToOld()).isTrue(); + assertThat(versions.isPreviousVersionTooOld()).isTrue(); setVersion(directory, "1.12"); - assertThat(versions.isPreviousVersionToOld()).isTrue(); + assertThat(versions.isPreviousVersionTooOld()).isTrue(); } @Test void shouldReturnFalseForVersion160(@TempDirectory.TempDir Path directory) throws IOException { setVersion(directory, "1.60"); - assertThat(versions.isPreviousVersionToOld()).isFalse(); + assertThat(versions.isPreviousVersionTooOld()).isFalse(); } @Test void shouldNotFailIfVersionContainsLineBreak(@TempDirectory.TempDir Path directory) throws IOException { setVersion(directory, "1.59\n"); - assertThat(versions.isPreviousVersionToOld()).isTrue(); + assertThat(versions.isPreviousVersionTooOld()).isTrue(); } @Test void shouldReturnFalseForVersionsNewerAs160(@TempDirectory.TempDir Path directory) throws IOException { setVersion(directory, "1.61"); - assertThat(versions.isPreviousVersionToOld()).isFalse(); + assertThat(versions.isPreviousVersionTooOld()).isFalse(); setVersion(directory, "1.82"); - assertThat(versions.isPreviousVersionToOld()).isFalse(); + assertThat(versions.isPreviousVersionTooOld()).isFalse(); } @Test void shouldReturnFalseForNonExistingVersionFile(@TempDirectory.TempDir Path directory) { setVersionFile(directory.resolve("version.txt")); - assertThat(versions.isPreviousVersionToOld()).isFalse(); + assertThat(versions.isPreviousVersionTooOld()).isFalse(); } @Test From ce9f3f29a1f5aba9df9562a9da4e2ed94da101cb Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 13 Jun 2019 09:04:02 +0000 Subject: [PATCH 7/7] Close branch feature/fail_migration_if_to_old