Factor out template generation code into reusable class
This refactoring improves readability and lets us use the dynamic content part inside Google. For now, we have to pass in a converter to sanitize a string to be used in a script tag because our internal version of Soy does not support UnsafeSanitizedContentOrdainer. Remove SoyDataMap because it is marked as deprecated. Change-Id: I6d34708c98c6a4edae75defb6f45a0eab4c976bf
This commit is contained in:
135
java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
Normal file
135
java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright (C) 2019 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.httpd.raw;
|
||||
|
||||
import static com.google.template.soy.data.ordainers.GsonOrdainer.serializeObject;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.common.UsedAt;
|
||||
import com.google.gerrit.common.UsedAt.Project;
|
||||
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.data.SanitizedContent;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/** Helper for generating parts of {@code index.html}. */
|
||||
public class IndexHtmlUtil {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/**
|
||||
* Returns both static and dynamic parameters of {@code index.html}. The result is to be used when
|
||||
* rendering the soy template.
|
||||
*/
|
||||
public static ImmutableMap<String, Object> templateData(
|
||||
GerritApi gerritApi,
|
||||
String canonicalURL,
|
||||
String cdnPath,
|
||||
String faviconPath,
|
||||
Function<String, SanitizedContent> urlInScriptTagOrdainer)
|
||||
throws URISyntaxException, RestApiException {
|
||||
return ImmutableMap.<String, Object>builder()
|
||||
.putAll(staticTemplateData(canonicalURL, cdnPath, faviconPath, urlInScriptTagOrdainer))
|
||||
.putAll(dynamicTemplateData(gerritApi))
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Returns dynamic parameters of {@code index.html}. */
|
||||
@UsedAt(Project.GOOGLE)
|
||||
public static Map<String, Map<String, SanitizedContent>> dynamicTemplateData(GerritApi gerritApi)
|
||||
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 ImmutableMap.of("gerritInitialData", initialData);
|
||||
}
|
||||
|
||||
/** Returns all static parameters of {@code index.html}. */
|
||||
static Map<String, Object> staticTemplateData(
|
||||
String canonicalURL,
|
||||
String cdnPath,
|
||||
String faviconPath,
|
||||
Function<String, SanitizedContent> urlInScriptTagOrdainer)
|
||||
throws URISyntaxException {
|
||||
String canonicalPath = computeCanonicalPath(canonicalURL);
|
||||
|
||||
String staticPath = "";
|
||||
if (cdnPath != null) {
|
||||
staticPath = cdnPath;
|
||||
} else if (canonicalPath != null) {
|
||||
staticPath = canonicalPath;
|
||||
}
|
||||
|
||||
SanitizedContent sanitizedStaticPath = urlInScriptTagOrdainer.apply(staticPath);
|
||||
ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
|
||||
if (canonicalPath != null) {
|
||||
data.put("canonicalPath", canonicalPath);
|
||||
}
|
||||
if (sanitizedStaticPath != null) {
|
||||
data.put("staticResourcePath", sanitizedStaticPath);
|
||||
}
|
||||
if (faviconPath != null) {
|
||||
data.put("faviconPath", faviconPath);
|
||||
}
|
||||
return data.build();
|
||||
}
|
||||
|
||||
private static String computeCanonicalPath(@Nullable String canonicalURL)
|
||||
throws URISyntaxException {
|
||||
if (Strings.isNullOrEmpty(canonicalURL)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// If we serving from a sub-directory rather than root, determine the path
|
||||
// from the cannonical web URL.
|
||||
URI uri = new URI(canonicalURL);
|
||||
return uri.getPath().replaceAll("/$", "");
|
||||
}
|
||||
|
||||
private IndexHtmlUtil() {}
|
||||
}
|
@@ -14,38 +14,26 @@
|
||||
|
||||
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;
|
||||
import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
|
||||
import com.google.template.soy.tofu.SoyTofu;
|
||||
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 java.util.function.Function;
|
||||
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;
|
||||
|
||||
@Nullable private final String canonicalUrl;
|
||||
@@ -53,6 +41,7 @@ public class IndexServlet extends HttpServlet {
|
||||
@Nullable private final String faviconPath;
|
||||
private final GerritApi gerritApi;
|
||||
private final SoyTofu soyTofu;
|
||||
private final Function<String, SanitizedContent> urlOrdainer;
|
||||
|
||||
IndexServlet(
|
||||
@Nullable String canonicalUrl,
|
||||
@@ -68,19 +57,24 @@ public class IndexServlet extends HttpServlet {
|
||||
.add(Resources.getResource("com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy"))
|
||||
.build()
|
||||
.compileToTofu();
|
||||
this.urlOrdainer =
|
||||
(s) ->
|
||||
UnsafeSanitizedContentOrdainer.ordainAsSafe(
|
||||
s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
|
||||
SoyTofu.Renderer renderer;
|
||||
try {
|
||||
SoyMapData templateData = getStaticTemplateData(canonicalUrl, cdnPath, faviconPath);
|
||||
templateData.put("gerritInitialData", getInitialData());
|
||||
// TODO(hiesel): Remove URL ordainer as parameter once Soy is consistent
|
||||
renderer =
|
||||
soyTofu
|
||||
.newRenderer("com.google.gerrit.httpd.raw.Index")
|
||||
.setContentKind(SanitizedContent.ContentKind.HTML)
|
||||
.setData(templateData);
|
||||
.setData(
|
||||
IndexHtmlUtil.templateData(
|
||||
gerritApi, canonicalUrl, cdnPath, faviconPath, urlOrdainer));
|
||||
} catch (URISyntaxException | RestApiException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
@@ -92,67 +86,4 @@ public class IndexServlet extends HttpServlet {
|
||||
w.write(renderer.render().getBytes(UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
static String computeCanonicalPath(@Nullable String canonicalURL) throws URISyntaxException {
|
||||
if (Strings.isNullOrEmpty(canonicalURL)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// If we serving from a sub-directory rather than root, determine the path
|
||||
// from the cannonical web URL.
|
||||
URI uri = new URI(canonicalURL);
|
||||
return uri.getPath().replaceAll("/$", "");
|
||||
}
|
||||
|
||||
static SoyMapData getStaticTemplateData(String canonicalURL, String cdnPath, String faviconPath)
|
||||
throws URISyntaxException {
|
||||
String canonicalPath = computeCanonicalPath(canonicalURL);
|
||||
|
||||
String staticPath = "";
|
||||
if (cdnPath != null) {
|
||||
staticPath = cdnPath;
|
||||
} else if (canonicalPath != null) {
|
||||
staticPath = canonicalPath;
|
||||
}
|
||||
|
||||
// The resource path must be typed as safe for use in a script src.
|
||||
// TODO(wyatta): Upgrade this to use an appropriate safe URL type.
|
||||
SanitizedContent sanitizedStaticPath =
|
||||
UnsafeSanitizedContentOrdainer.ordainAsSafe(
|
||||
staticPath, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
|
||||
|
||||
return new SoyMapData(
|
||||
"canonicalPath", canonicalPath,
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
66
javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
Normal file
66
javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (C) 2019 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.httpd.raw;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.gerrit.httpd.raw.IndexHtmlUtil.staticTemplateData;
|
||||
|
||||
import com.google.template.soy.data.SanitizedContent;
|
||||
import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
|
||||
import org.junit.Test;
|
||||
|
||||
public class IndexHtmlUtilTest {
|
||||
@Test
|
||||
public void noPathAndNoCDN() throws Exception {
|
||||
assertThat(staticTemplateData("http://example.com/", null, null, IndexHtmlUtilTest::ordain))
|
||||
.containsExactly("canonicalPath", "", "staticResourcePath", ordain(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathAndNoCDN() throws Exception {
|
||||
assertThat(
|
||||
staticTemplateData("http://example.com/gerrit/", null, null, IndexHtmlUtilTest::ordain))
|
||||
.containsExactly("canonicalPath", "/gerrit", "staticResourcePath", ordain("/gerrit"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noPathAndCDN() throws Exception {
|
||||
assertThat(
|
||||
staticTemplateData(
|
||||
"http://example.com/",
|
||||
"http://my-cdn.com/foo/bar/",
|
||||
null,
|
||||
IndexHtmlUtilTest::ordain))
|
||||
.containsExactly(
|
||||
"canonicalPath", "", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathAndCDN() throws Exception {
|
||||
assertThat(
|
||||
staticTemplateData(
|
||||
"http://example.com/gerrit",
|
||||
"http://my-cdn.com/foo/bar/",
|
||||
null,
|
||||
IndexHtmlUtilTest::ordain))
|
||||
.containsExactly(
|
||||
"canonicalPath", "/gerrit", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
|
||||
}
|
||||
|
||||
private static SanitizedContent ordain(String s) {
|
||||
return UnsafeSanitizedContentOrdainer.ordainAsSafe(
|
||||
s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
|
||||
}
|
||||
}
|
@@ -29,43 +29,9 @@ 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 org.junit.Test;
|
||||
|
||||
public class IndexServletTest {
|
||||
@Test
|
||||
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 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 Exception {
|
||||
SoyMapData data =
|
||||
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 Exception {
|
||||
SoyMapData data =
|
||||
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())
|
||||
.isEqualTo("http://my-cdn.com/foo/bar/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderTemplate() throws Exception {
|
||||
|
Reference in New Issue
Block a user