Merge "Ship initally required data in index.html"
This commit is contained in:
@@ -14,12 +14,21 @@
|
||||
|
||||
package com.google.gerrit.httpd.raw;
|
||||
|
||||
import static com.google.template.soy.data.ordainers.GsonOrdainer.serializeObject;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.extensions.api.GerritApi;
|
||||
import com.google.gerrit.extensions.api.accounts.AccountApi;
|
||||
import com.google.gerrit.extensions.api.config.Server;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.json.OutputFormat;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.template.soy.SoyFileSet;
|
||||
import com.google.template.soy.data.SanitizedContent;
|
||||
import com.google.template.soy.data.SoyMapData;
|
||||
@@ -29,37 +38,58 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
public class IndexServlet extends HttpServlet {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final long serialVersionUID = 1L;
|
||||
protected final byte[] indexSource;
|
||||
|
||||
@Nullable private final String canonicalUrl;
|
||||
@Nullable private final String cdnPath;
|
||||
@Nullable private final String faviconPath;
|
||||
private final GerritApi gerritApi;
|
||||
private final SoyTofu soyTofu;
|
||||
|
||||
IndexServlet(
|
||||
@Nullable String canonicalURL, @Nullable String cdnPath, @Nullable String faviconPath)
|
||||
throws URISyntaxException {
|
||||
String resourcePath = "com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy";
|
||||
SoyFileSet.Builder builder = SoyFileSet.builder();
|
||||
builder.add(Resources.getResource(resourcePath));
|
||||
SoyTofu.Renderer renderer =
|
||||
builder
|
||||
@Nullable String canonicalUrl,
|
||||
@Nullable String cdnPath,
|
||||
@Nullable String faviconPath,
|
||||
GerritApi gerritApi) {
|
||||
this.canonicalUrl = canonicalUrl;
|
||||
this.cdnPath = cdnPath;
|
||||
this.faviconPath = faviconPath;
|
||||
this.gerritApi = gerritApi;
|
||||
this.soyTofu =
|
||||
SoyFileSet.builder()
|
||||
.add(Resources.getResource("com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy"))
|
||||
.build()
|
||||
.compileToTofu()
|
||||
.newRenderer("com.google.gerrit.httpd.raw.Index")
|
||||
.setContentKind(SanitizedContent.ContentKind.HTML)
|
||||
.setData(getTemplateData(canonicalURL, cdnPath, faviconPath));
|
||||
indexSource = renderer.render().getBytes(UTF_8);
|
||||
.compileToTofu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
|
||||
SoyTofu.Renderer renderer;
|
||||
try {
|
||||
SoyMapData templateData = getStaticTemplateData(canonicalUrl, cdnPath, faviconPath);
|
||||
templateData.put("gerritInitialData", getInitialData());
|
||||
renderer =
|
||||
soyTofu
|
||||
.newRenderer("com.google.gerrit.httpd.raw.Index")
|
||||
.setContentKind(SanitizedContent.ContentKind.HTML)
|
||||
.setData(templateData);
|
||||
} catch (URISyntaxException | RestApiException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
rsp.setCharacterEncoding(UTF_8.name());
|
||||
rsp.setContentType("text/html");
|
||||
rsp.setStatus(SC_OK);
|
||||
try (OutputStream w = rsp.getOutputStream()) {
|
||||
w.write(indexSource);
|
||||
w.write(renderer.render().getBytes(UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +104,7 @@ public class IndexServlet extends HttpServlet {
|
||||
return uri.getPath().replaceAll("/$", "");
|
||||
}
|
||||
|
||||
static SoyMapData getTemplateData(String canonicalURL, String cdnPath, String faviconPath)
|
||||
static SoyMapData getStaticTemplateData(String canonicalURL, String cdnPath, String faviconPath)
|
||||
throws URISyntaxException {
|
||||
String canonicalPath = computeCanonicalPath(canonicalURL);
|
||||
|
||||
@@ -96,4 +126,33 @@ public class IndexServlet extends HttpServlet {
|
||||
"staticResourcePath", sanitizedStaticPath,
|
||||
"faviconPath", faviconPath);
|
||||
}
|
||||
|
||||
private Map<String, SanitizedContent> getInitialData() throws RestApiException {
|
||||
Gson gson = OutputFormat.JSON_COMPACT.newGson();
|
||||
Map<String, SanitizedContent> initialData = new HashMap<>();
|
||||
Server serverApi = gerritApi.config().server();
|
||||
initialData.put("\"/config/server/info\"", serializeObject(gson, serverApi.getInfo()));
|
||||
initialData.put("\"/config/server/version\"", serializeObject(gson, serverApi.getVersion()));
|
||||
initialData.put("\"/config/server/top-menus\"", serializeObject(gson, serverApi.topMenus()));
|
||||
|
||||
try {
|
||||
AccountApi accountApi = gerritApi.accounts().self();
|
||||
initialData.put("\"/accounts/self/detail\"", serializeObject(gson, accountApi.get()));
|
||||
initialData.put(
|
||||
"\"/accounts/self/preferences\"", serializeObject(gson, accountApi.getPreferences()));
|
||||
initialData.put(
|
||||
"\"/accounts/self/preferences.diff\"",
|
||||
serializeObject(gson, accountApi.getDiffPreferences()));
|
||||
initialData.put(
|
||||
"\"/accounts/self/preferences.edit\"",
|
||||
serializeObject(gson, accountApi.getEditPreferences()));
|
||||
} catch (AuthException e) {
|
||||
logger.atFine().withCause(e).log(
|
||||
"Can't inline account-related data because user is unauthenticated");
|
||||
// Don't render data
|
||||
// TODO(hiesel): Tell the client that the user is not authenticated so that it doesn't have to
|
||||
// fetch anyway. This requires more client side modifications.
|
||||
}
|
||||
return initialData;
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ import com.google.common.cache.Cache;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.extensions.api.GerritApi;
|
||||
import com.google.gerrit.httpd.XsrfCookieFilter;
|
||||
import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
|
||||
import com.google.gerrit.launcher.GerritLauncher;
|
||||
@@ -41,7 +42,6 @@ import com.google.inject.servlet.ServletModule;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
import javax.servlet.Filter;
|
||||
@@ -218,11 +218,12 @@ public class StaticModule extends ServletModule {
|
||||
@Singleton
|
||||
@Named(POLYGERRIT_INDEX_SERVLET)
|
||||
HttpServlet getPolyGerritUiIndexServlet(
|
||||
@CanonicalWebUrl @Nullable String canonicalUrl, @GerritServerConfig Config cfg)
|
||||
throws URISyntaxException {
|
||||
@CanonicalWebUrl @Nullable String canonicalUrl,
|
||||
@GerritServerConfig Config cfg,
|
||||
GerritApi gerritApi) {
|
||||
String cdnPath = cfg.getString("gerrit", null, "cdnPath");
|
||||
String faviconPath = cfg.getString("gerrit", null, "faviconPath");
|
||||
return new IndexServlet(canonicalUrl, cdnPath, faviconPath);
|
||||
return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
@@ -15,53 +15,52 @@
|
||||
package com.google.gerrit.httpd.raw;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.easymock.EasyMock.createMock;
|
||||
import static org.easymock.EasyMock.expect;
|
||||
import static org.easymock.EasyMock.replay;
|
||||
import static org.easymock.EasyMock.verify;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.gerrit.extensions.api.GerritApi;
|
||||
import com.google.gerrit.extensions.api.accounts.Accounts;
|
||||
import com.google.gerrit.extensions.api.config.Config;
|
||||
import com.google.gerrit.extensions.api.config.Server;
|
||||
import com.google.gerrit.extensions.common.ServerInfo;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
|
||||
import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
|
||||
import com.google.template.soy.data.SoyMapData;
|
||||
import java.net.URISyntaxException;
|
||||
import org.junit.Test;
|
||||
|
||||
public class IndexServletTest {
|
||||
static class TestIndexServlet extends IndexServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
TestIndexServlet(String canonicalURL, String cdnPath, String faviconPath)
|
||||
throws URISyntaxException {
|
||||
super(canonicalURL, cdnPath, faviconPath);
|
||||
}
|
||||
|
||||
String getIndexSource() {
|
||||
return new String(indexSource, UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noPathAndNoCDN() throws URISyntaxException {
|
||||
SoyMapData data = IndexServlet.getTemplateData("http://example.com/", null, null);
|
||||
public void noPathAndNoCDN() throws Exception {
|
||||
SoyMapData data = IndexServlet.getStaticTemplateData("http://example.com/", null, null);
|
||||
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
|
||||
assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathAndNoCDN() throws URISyntaxException {
|
||||
SoyMapData data = IndexServlet.getTemplateData("http://example.com/gerrit/", null, null);
|
||||
public void pathAndNoCDN() throws Exception {
|
||||
SoyMapData data = IndexServlet.getStaticTemplateData("http://example.com/gerrit/", null, null);
|
||||
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
|
||||
assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("/gerrit");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noPathAndCDN() throws URISyntaxException {
|
||||
public void noPathAndCDN() throws Exception {
|
||||
SoyMapData data =
|
||||
IndexServlet.getTemplateData("http://example.com/", "http://my-cdn.com/foo/bar/", null);
|
||||
IndexServlet.getStaticTemplateData(
|
||||
"http://example.com/", "http://my-cdn.com/foo/bar/", null);
|
||||
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
|
||||
assertThat(data.getSingle("staticResourcePath").stringValue())
|
||||
.isEqualTo("http://my-cdn.com/foo/bar/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathAndCDN() throws URISyntaxException {
|
||||
public void pathAndCDN() throws Exception {
|
||||
SoyMapData data =
|
||||
IndexServlet.getTemplateData(
|
||||
IndexServlet.getStaticTemplateData(
|
||||
"http://example.com/gerrit", "http://my-cdn.com/foo/bar/", null);
|
||||
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
|
||||
assertThat(data.getSingle("staticResourcePath").stringValue())
|
||||
@@ -69,12 +68,45 @@ public class IndexServletTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderTemplate() throws URISyntaxException {
|
||||
public void renderTemplate() throws Exception {
|
||||
Accounts accountsApi = createMock(Accounts.class);
|
||||
expect(accountsApi.self()).andThrow(new AuthException("user needs to be authenticated"));
|
||||
|
||||
Server serverApi = createMock(Server.class);
|
||||
expect(serverApi.getVersion()).andReturn("123");
|
||||
expect(serverApi.topMenus()).andReturn(ImmutableList.of());
|
||||
ServerInfo serverInfo = new ServerInfo();
|
||||
serverInfo.defaultTheme = "my-default-theme";
|
||||
expect(serverApi.getInfo()).andReturn(serverInfo);
|
||||
|
||||
Config configApi = createMock(Config.class);
|
||||
expect(configApi.server()).andReturn(serverApi);
|
||||
|
||||
GerritApi gerritApi = createMock(GerritApi.class);
|
||||
expect(gerritApi.accounts()).andReturn(accountsApi);
|
||||
expect(gerritApi.config()).andReturn(configApi);
|
||||
|
||||
String testCanonicalUrl = "foo-url";
|
||||
String testCdnPath = "bar-cdn";
|
||||
String testFaviconURL = "zaz-url";
|
||||
TestIndexServlet servlet = new TestIndexServlet(testCanonicalUrl, testCdnPath, testFaviconURL);
|
||||
String output = servlet.getIndexSource();
|
||||
IndexServlet servlet =
|
||||
new IndexServlet(testCanonicalUrl, testCdnPath, testFaviconURL, gerritApi);
|
||||
|
||||
FakeHttpServletResponse response = new FakeHttpServletResponse();
|
||||
|
||||
replay(gerritApi);
|
||||
replay(configApi);
|
||||
replay(serverApi);
|
||||
replay(accountsApi);
|
||||
|
||||
servlet.doGet(new FakeHttpServletRequest(), response);
|
||||
|
||||
verify(gerritApi);
|
||||
verify(configApi);
|
||||
verify(serverApi);
|
||||
verify(accountsApi);
|
||||
|
||||
String output = response.getActualBodyString();
|
||||
assertThat(output).contains("<!DOCTYPE html>");
|
||||
assertThat(output).contains("window.CANONICAL_PATH = '" + testCanonicalUrl);
|
||||
assertThat(output).contains("<link rel=\"preload\" href=\"" + testCdnPath);
|
||||
@@ -84,5 +116,12 @@ public class IndexServletTest {
|
||||
+ testCanonicalUrl
|
||||
+ "/"
|
||||
+ testFaviconURL);
|
||||
assertThat(output)
|
||||
.contains(
|
||||
"window.INITIAL_DATA = JSON.parse("
|
||||
+ "'\\x7b\\x22\\/config\\/server\\/version\\x22: \\x22123\\x22, "
|
||||
+ "\\x22\\/config\\/server\\/info\\x22: \\x7b\\x22default_theme\\x22:"
|
||||
+ "\\x22my-default-theme\\x22\\x7d, \\x22\\/config\\/server\\/top-menus\\x22: "
|
||||
+ "\\x5b\\x5d\\x7d');</script>");
|
||||
}
|
||||
}
|
||||
|
@@ -144,6 +144,14 @@
|
||||
constructor() {
|
||||
// Container of per-canonical-path caches.
|
||||
this._data = new Map();
|
||||
if (window.INITIAL_DATA != undefined) {
|
||||
// Put all data shipped with index.html into the cache. This makes it
|
||||
// so that we spare more round trips to the server when the app loads
|
||||
// initially.
|
||||
Object
|
||||
.entries(window.INITIAL_DATA)
|
||||
.forEach(e => this._cache().set(e[0], e[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the cache for the current canonical path.
|
||||
|
@@ -19,6 +19,7 @@
|
||||
{template .Index}
|
||||
{@param canonicalPath: ?}
|
||||
{@param staticResourcePath: ?}
|
||||
{@param gerritInitialData: /** {string} map of REST endpoint to response for startup. */ ?}
|
||||
{@param? assetsPath: ?} /** {string} URL to static assets root, if served from CDN. */
|
||||
{@param? assetsBundle: ?} /** {string} Assets bundle .html file, served from $assetsPath. */
|
||||
{@param? faviconPath: ?}
|
||||
@@ -42,6 +43,17 @@
|
||||
{if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
|
||||
{if $assetsPath}window.ASSETS_PATH = '{$assetsPath}';{/if}
|
||||
{if $polymer2}window.POLYMER2 = true;{/if}
|
||||
{if $gerritInitialData}
|
||||
// INITIAL_DATA is a string that represents a JSON map. It's inlined here so that we can
|
||||
// spare calls to the API when starting up the app.
|
||||
// The map maps from endpoint to returned value. This matches Gerrit's REST API 1:1, so the
|
||||
// values here can be used as a drop-in replacement for calls to the API.
|
||||
//
|
||||
// Example:
|
||||
// '/config/server/version' => '3.0.0-468-g0757b52a7d'
|
||||
// '/accounts/self/detail' => { 'username' : 'gerrit-user' }
|
||||
window.INITIAL_DATA = JSON.parse({$gerritInitialData});
|
||||
{/if}
|
||||
</script>{\n}
|
||||
|
||||
{if $faviconPath}
|
||||
|
Reference in New Issue
Block a user