allow changing context path, by using a template for index.html

This commit is contained in:
Sebastian Sdorra
2018-09-10 14:52:32 +02:00
parent 13b8b92273
commit 0cb44e3317
7 changed files with 157 additions and 112 deletions

View File

@@ -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/
-->
<link rel="manifest" href="/scm/manifest.json">
<link rel="shortcut icon" href="/scm/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<link rel="manifest" href="{{ contextPath }}/manifest.json">
<link rel="shortcut icon" href="{{ contextPath }}/favicon.ico">
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<base href="/scm">
<link rel="stylesheet" type="text/css" href="/scm/styles/scm.css">
<base href="{{ contextPath }}">
<link rel="stylesheet" type="text/css" href="{{ contextPath }}/styles/scm.css">
<title>SCM-Manager</title>
</head>
@@ -41,9 +32,9 @@
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script>
window.ctxPath = "/scm";
window.ctxPath = "{{ contextPath }}";
</script>
<script src="/scm/vendor.bundle.js"></script>
<script src="/scm/scm-ui.bundle.js"></script>
<script src="{{ contextPath }}/vendor.bundle.js"></script>
<script src="{{ contextPath }}/scm-ui.bundle.js"></script>
</body>
</html>

View File

@@ -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);
}
}
}

View File

@@ -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<PushStateDispatcher
@VisibleForTesting
static final String PROPERTY_TARGET = "sonia.scm.ui.proxy";
private Provider<TemplatingPushStateDispatcher> templatingPushStateDispatcherProvider;
@Inject
public PushStateDispatcherProvider(Provider<TemplatingPushStateDispatcher> 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);
}

View File

@@ -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();
}
}
}

View File

@@ -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");
}
}

View File

@@ -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

View File

@@ -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<Object> captor = ArgumentCaptor.forClass(Object.class);
verify(template).execute(any(Writer.class), captor.capture());
TemplatingPushStateDispatcher.IndexHtmlModel model = (TemplatingPushStateDispatcher.IndexHtmlModel) captor.getValue();
assertEquals("/scm", model.getContextPath());
}
}