diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java index a414e84d8d..622c9c2f2e 100644 --- a/java/com/google/gerrit/httpd/raw/IndexServlet.java +++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java @@ -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 getInitialData() throws RestApiException { + Gson gson = OutputFormat.JSON_COMPACT.newGson(); + Map 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; + } } diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java index cf21fcd0e9..2b11015d35 100644 --- a/java/com/google/gerrit/httpd/raw/StaticModule.java +++ b/java/com/google/gerrit/httpd/raw/StaticModule.java @@ -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 diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java index 307a23e5fd..22751bb34c 100644 --- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java +++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java @@ -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(""); assertThat(output).contains("window.CANONICAL_PATH = '" + testCanonicalUrl); assertThat(output).contains(""); } } diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js index e69c8fc032..48484ac463 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js @@ -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. diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy index 38270bfe7d..693604fc3d 100644 --- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy +++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy @@ -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} {\n} {if $faviconPath}