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

View File

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

View File

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

View File

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

View File

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