Merge "Ship initally required data in index.html"

This commit is contained in:
Patrick Hiesel
2019-05-29 13:32:30 +00:00
committed by Gerrit Code Review
5 changed files with 164 additions and 45 deletions

View File

@@ -14,12 +14,21 @@
package com.google.gerrit.httpd.raw; 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 java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import com.google.gerrit.common.Nullable; 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.SoyFileSet;
import com.google.template.soy.data.SanitizedContent; import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.SoyMapData; import com.google.template.soy.data.SoyMapData;
@@ -29,37 +38,58 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
public class IndexServlet extends HttpServlet { public class IndexServlet extends HttpServlet {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final long serialVersionUID = 1L; 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( IndexServlet(
@Nullable String canonicalURL, @Nullable String cdnPath, @Nullable String faviconPath) @Nullable String canonicalUrl,
throws URISyntaxException { @Nullable String cdnPath,
String resourcePath = "com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy"; @Nullable String faviconPath,
SoyFileSet.Builder builder = SoyFileSet.builder(); GerritApi gerritApi) {
builder.add(Resources.getResource(resourcePath)); this.canonicalUrl = canonicalUrl;
SoyTofu.Renderer renderer = this.cdnPath = cdnPath;
builder this.faviconPath = faviconPath;
this.gerritApi = gerritApi;
this.soyTofu =
SoyFileSet.builder()
.add(Resources.getResource("com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy"))
.build() .build()
.compileToTofu() .compileToTofu();
.newRenderer("com.google.gerrit.httpd.raw.Index")
.setContentKind(SanitizedContent.ContentKind.HTML)
.setData(getTemplateData(canonicalURL, cdnPath, faviconPath));
indexSource = renderer.render().getBytes(UTF_8);
} }
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { 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.setCharacterEncoding(UTF_8.name());
rsp.setContentType("text/html"); rsp.setContentType("text/html");
rsp.setStatus(SC_OK); rsp.setStatus(SC_OK);
try (OutputStream w = rsp.getOutputStream()) { 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("/$", ""); return uri.getPath().replaceAll("/$", "");
} }
static SoyMapData getTemplateData(String canonicalURL, String cdnPath, String faviconPath) static SoyMapData getStaticTemplateData(String canonicalURL, String cdnPath, String faviconPath)
throws URISyntaxException { throws URISyntaxException {
String canonicalPath = computeCanonicalPath(canonicalURL); String canonicalPath = computeCanonicalPath(canonicalURL);
@@ -96,4 +126,33 @@ public class IndexServlet extends HttpServlet {
"staticResourcePath", sanitizedStaticPath, "staticResourcePath", sanitizedStaticPath,
"faviconPath", faviconPath); "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;
}
} }

View File

@@ -22,6 +22,7 @@ import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.httpd.XsrfCookieFilter; import com.google.gerrit.httpd.XsrfCookieFilter;
import com.google.gerrit.httpd.raw.ResourceServlet.Resource; import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
import com.google.gerrit.launcher.GerritLauncher; import com.google.gerrit.launcher.GerritLauncher;
@@ -41,7 +42,6 @@ import com.google.inject.servlet.ServletModule;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Path; import java.nio.file.Path;
import javax.servlet.Filter; import javax.servlet.Filter;
@@ -218,11 +218,12 @@ public class StaticModule extends ServletModule {
@Singleton @Singleton
@Named(POLYGERRIT_INDEX_SERVLET) @Named(POLYGERRIT_INDEX_SERVLET)
HttpServlet getPolyGerritUiIndexServlet( HttpServlet getPolyGerritUiIndexServlet(
@CanonicalWebUrl @Nullable String canonicalUrl, @GerritServerConfig Config cfg) @CanonicalWebUrl @Nullable String canonicalUrl,
throws URISyntaxException { @GerritServerConfig Config cfg,
GerritApi gerritApi) {
String cdnPath = cfg.getString("gerrit", null, "cdnPath"); String cdnPath = cfg.getString("gerrit", null, "cdnPath");
String faviconPath = cfg.getString("gerrit", null, "faviconPath"); String faviconPath = cfg.getString("gerrit", null, "faviconPath");
return new IndexServlet(canonicalUrl, cdnPath, faviconPath); return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi);
} }
@Provides @Provides

View File

@@ -15,53 +15,52 @@
package com.google.gerrit.httpd.raw; package com.google.gerrit.httpd.raw;
import static com.google.common.truth.Truth.assertThat; 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 com.google.template.soy.data.SoyMapData;
import java.net.URISyntaxException;
import org.junit.Test; import org.junit.Test;
public class IndexServletTest { 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 @Test
public void noPathAndNoCDN() throws URISyntaxException { public void noPathAndNoCDN() throws Exception {
SoyMapData data = IndexServlet.getTemplateData("http://example.com/", null, null); SoyMapData data = IndexServlet.getStaticTemplateData("http://example.com/", null, null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo(""); assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo(""); assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("");
} }
@Test @Test
public void pathAndNoCDN() throws URISyntaxException { public void pathAndNoCDN() throws Exception {
SoyMapData data = IndexServlet.getTemplateData("http://example.com/gerrit/", null, null); SoyMapData data = IndexServlet.getStaticTemplateData("http://example.com/gerrit/", null, null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit"); assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("/gerrit"); assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("/gerrit");
} }
@Test @Test
public void noPathAndCDN() throws URISyntaxException { public void noPathAndCDN() throws Exception {
SoyMapData data = 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("canonicalPath").stringValue()).isEqualTo("");
assertThat(data.getSingle("staticResourcePath").stringValue()) assertThat(data.getSingle("staticResourcePath").stringValue())
.isEqualTo("http://my-cdn.com/foo/bar/"); .isEqualTo("http://my-cdn.com/foo/bar/");
} }
@Test @Test
public void pathAndCDN() throws URISyntaxException { public void pathAndCDN() throws Exception {
SoyMapData data = SoyMapData data =
IndexServlet.getTemplateData( IndexServlet.getStaticTemplateData(
"http://example.com/gerrit", "http://my-cdn.com/foo/bar/", null); "http://example.com/gerrit", "http://my-cdn.com/foo/bar/", null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit"); assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
assertThat(data.getSingle("staticResourcePath").stringValue()) assertThat(data.getSingle("staticResourcePath").stringValue())
@@ -69,12 +68,45 @@ public class IndexServletTest {
} }
@Test @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 testCanonicalUrl = "foo-url";
String testCdnPath = "bar-cdn"; String testCdnPath = "bar-cdn";
String testFaviconURL = "zaz-url"; String testFaviconURL = "zaz-url";
TestIndexServlet servlet = new TestIndexServlet(testCanonicalUrl, testCdnPath, testFaviconURL); IndexServlet servlet =
String output = servlet.getIndexSource(); 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("<!DOCTYPE html>");
assertThat(output).contains("window.CANONICAL_PATH = '" + testCanonicalUrl); assertThat(output).contains("window.CANONICAL_PATH = '" + testCanonicalUrl);
assertThat(output).contains("<link rel=\"preload\" href=\"" + testCdnPath); assertThat(output).contains("<link rel=\"preload\" href=\"" + testCdnPath);
@@ -84,5 +116,12 @@ public class IndexServletTest {
+ testCanonicalUrl + testCanonicalUrl
+ "/" + "/"
+ testFaviconURL); + 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>");
} }
} }

View File

@@ -144,6 +144,14 @@
constructor() { constructor() {
// Container of per-canonical-path caches. // Container of per-canonical-path caches.
this._data = new Map(); 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. // Returns the cache for the current canonical path.

View File

@@ -19,6 +19,7 @@
{template .Index} {template .Index}
{@param canonicalPath: ?} {@param canonicalPath: ?}
{@param staticResourcePath: ?} {@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? assetsPath: ?} /** {string} URL to static assets root, if served from CDN. */
{@param? assetsBundle: ?} /** {string} Assets bundle .html file, served from $assetsPath. */ {@param? assetsBundle: ?} /** {string} Assets bundle .html file, served from $assetsPath. */
{@param? faviconPath: ?} {@param? faviconPath: ?}
@@ -42,6 +43,17 @@
{if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if} {if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
{if $assetsPath}window.ASSETS_PATH = '{$assetsPath}';{/if} {if $assetsPath}window.ASSETS_PATH = '{$assetsPath}';{/if}
{if $polymer2}window.POLYMER2 = true;{/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} </script>{\n}
{if $faviconPath} {if $faviconPath}