diff --git a/scm-ui/public/index.html b/scm-ui/public/index.mustache similarity index 56% rename from scm-ui/public/index.html rename to scm-ui/public/index.mustache index 23dc1b9782..802be2ca97 100644 --- a/scm-ui/public/index.html +++ b/scm-ui/public/index.mustache @@ -8,20 +8,11 @@ manifest.json provides metadata used when your web app is added to the homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ --> - - - - - - + + SCM-Manager @@ -41,9 +32,9 @@ To create a production bundle, use `npm run build` or `yarn build`. --> - - + + diff --git a/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java b/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java deleted file mode 100644 index ae9ef97499..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java +++ /dev/null @@ -1,27 +0,0 @@ -package sonia.scm; - -import sonia.scm.util.HttpUtil; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * This dispatcher forwards every request to the index.html of the application. - * - * @since 2.0.0 - */ -public class ForwardingPushStateDispatcher implements PushStateDispatcher { - @Override - public void dispatch(HttpServletRequest request, HttpServletResponse response, String uri) throws IOException { - String path = HttpUtil.append(request.getContextPath(), "index.html"); - RequestDispatcher dispatcher = request.getRequestDispatcher(path); - try { - dispatcher.forward(request, response); - } catch (ServletException e) { - throw new IOException("failed to forward request", e); - } - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java b/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java index 653f7b4bdc..f0d2807497 100644 --- a/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java @@ -3,12 +3,13 @@ package sonia.scm; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import javax.inject.Inject; import javax.inject.Provider; /** * Injection Provider for the {@link PushStateDispatcher}. The provider will return a {@link ProxyPushStateDispatcher} * if the system property {@code PushStateDispatcherProvider#PROPERTY_TARGET} is set to a proxy target url, otherwise - * a {@link ForwardingPushStateDispatcher} is used. + * a {@link TemplatingPushStateDispatcher} is used. * * @since 2.0.0 */ @@ -17,11 +18,18 @@ public class PushStateDispatcherProvider implements Provider templatingPushStateDispatcherProvider; + + @Inject + public PushStateDispatcherProvider(Provider templatingPushStateDispatcherProvider) { + this.templatingPushStateDispatcherProvider = templatingPushStateDispatcherProvider; + } + @Override public PushStateDispatcher get() { String target = System.getProperty(PROPERTY_TARGET); if (Strings.isNullOrEmpty(target)) { - return new ForwardingPushStateDispatcher(); + return templatingPushStateDispatcherProvider.get(); } return new ProxyPushStateDispatcher(target); } diff --git a/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java b/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java new file mode 100644 index 0000000000..6652975c4a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java @@ -0,0 +1,61 @@ +package sonia.scm; + +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Writer; + +/** + * This dispatcher renders the /index.mustache template, which is merged in from the scm-ui package. + * + * @since 2.0.0 + */ +public class TemplatingPushStateDispatcher implements PushStateDispatcher { + + @VisibleForTesting + static final String TEMPLATE = "/index.mustache"; + + private final TemplateEngine templateEngine; + + @Inject + public TemplatingPushStateDispatcher(TemplateEngineFactory templateEngineFactory) { + this(templateEngineFactory.getDefaultEngine()); + } + + @VisibleForTesting + TemplatingPushStateDispatcher(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + @Override + public void dispatch(HttpServletRequest request, HttpServletResponse response, String uri) throws IOException { + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + Template template = templateEngine.getTemplate(TEMPLATE); + try (Writer writer = response.getWriter()) { + template.execute(writer, new IndexHtmlModel(request)); + } + } + + @VisibleForTesting + static class IndexHtmlModel { + + private final HttpServletRequest request; + + private IndexHtmlModel(HttpServletRequest request) { + this.request = request; + } + + public String getContextPath() { + return request.getContextPath(); + } + + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java b/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java deleted file mode 100644 index c5a42d2346..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package sonia.scm; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import java.io.IOException; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class ForwardingPushStateDispatcherTest { - - @Mock - private HttpServletRequest request; - - @Mock - private RequestDispatcher requestDispatcher; - - @Mock - private HttpServletResponse response; - - private ForwardingPushStateDispatcher dispatcher = new ForwardingPushStateDispatcher(); - - @Test - public void testDispatch() throws ServletException, IOException { - when(request.getContextPath()).thenReturn(""); - when(request.getRequestDispatcher("/index.html")).thenReturn(requestDispatcher); - - dispatcher.dispatch(request, response, "/something"); - - verify(requestDispatcher).forward(request, response); - } - - @Test - public void testDispatchWithContextPath() throws ServletException, IOException { - when(request.getContextPath()).thenReturn("/scm"); - when(request.getRequestDispatcher("/scm/index.html")).thenReturn(requestDispatcher); - - dispatcher.dispatch(request, response, "/something"); - - verify(requestDispatcher).forward(request, response); - } - - @Test(expected = IOException.class) - public void testWrapServletException() throws ServletException, IOException { - when(request.getContextPath()).thenReturn(""); - when(request.getRequestDispatcher("/index.html")).thenReturn(requestDispatcher); - doThrow(ServletException.class).when(requestDispatcher).forward(request, response); - - dispatcher.dispatch(request, response, "/something"); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java b/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java index 31e5f7c6dc..4316d9bc06 100644 --- a/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java @@ -1,14 +1,23 @@ package sonia.scm; +import com.google.inject.util.Providers; import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.template.TemplateEngine; -import static org.junit.Assert.*; - +@RunWith(MockitoJUnitRunner.class) public class PushStateDispatcherProviderTest { - private PushStateDispatcherProvider provider = new PushStateDispatcherProvider(); + @Mock + private TemplateEngine templateEngine; + + private PushStateDispatcherProvider provider = new PushStateDispatcherProvider( + Providers.of(new TemplatingPushStateDispatcher(templateEngine)) + ); @Test public void testGetProxyPushStateWithPropertySet() { @@ -20,7 +29,7 @@ public class PushStateDispatcherProviderTest { @Test public void testGetProxyPushStateWithoutProperty() { PushStateDispatcher dispatcher = provider.get(); - Assertions.assertThat(dispatcher).isInstanceOf(ForwardingPushStateDispatcher.class); + Assertions.assertThat(dispatcher).isInstanceOf(TemplatingPushStateDispatcher.class); } @After diff --git a/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java b/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java new file mode 100644 index 0000000000..126ba9ac0f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java @@ -0,0 +1,66 @@ +package sonia.scm; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TemplatingPushStateDispatcherTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private TemplateEngine templateEngine; + + @Mock + private Template template; + + private TemplatingPushStateDispatcher dispatcher; + + @Before + public void setUpMocks() { + dispatcher = new TemplatingPushStateDispatcher(templateEngine); + } + + @Test + public void testDispatch() throws IOException { + when(request.getContextPath()).thenReturn("/scm"); + when(templateEngine.getTemplate(TemplatingPushStateDispatcher.TEMPLATE)).thenReturn(template); + + when(response.getWriter()).thenReturn(new PrintWriter(new StringWriter())); + + dispatcher.dispatch(request, response, "/someurl"); + + verify(response).setContentType("text/html"); + verify(response).setCharacterEncoding("UTF-8"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + + verify(template).execute(any(Writer.class), captor.capture()); + + TemplatingPushStateDispatcher.IndexHtmlModel model = (TemplatingPushStateDispatcher.IndexHtmlModel) captor.getValue(); + assertEquals("/scm", model.getContextPath()); + } + +}