blob: 6cdd9bd3f854013387f49d0ab3bfb7830d8b9374 [file] [log] [blame]
// Copyright (C) 2008 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 com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.common.data.HostPageData;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gwtexpui.linker.server.Permutation;
import com.google.gwtexpui.linker.server.PermutationSelector;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtjsonrpc.server.JsonServlet;
import com.google.gwtjsonrpc.server.RPCServletUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** Sends the Gerrit host page to clients. */
@SuppressWarnings("serial")
@Singleton
public class HostPageServlet extends HttpServlet {
private static final Logger log =
LoggerFactory.getLogger(HostPageServlet.class);
private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
private static final String HPD_ID = "gerrit_hostpagedata";
private final Provider<CurrentUser> currentUser;
private final Provider<WebSession> session;
private final GerritConfig config;
private final DynamicSet<WebUiPlugin> plugins;
private final HostPageData.Theme signedOutTheme;
private final HostPageData.Theme signedInTheme;
private final SitePaths site;
private final Document template;
private final String noCacheName;
private final PermutationSelector selector;
private final boolean refreshHeaderFooter;
private volatile Page page;
@Inject
HostPageServlet(final Provider<CurrentUser> cu, final Provider<WebSession> w,
final SitePaths sp, final ThemeFactory themeFactory,
final GerritConfig gc, final ServletContext servletContext,
final DynamicSet<WebUiPlugin> webUiPlugins,
@GerritServerConfig final Config cfg)
throws IOException, ServletException {
currentUser = cu;
session = w;
config = gc;
plugins = webUiPlugins;
signedOutTheme = themeFactory.getSignedOutTheme();
signedInTheme = themeFactory.getSignedInTheme();
site = sp;
refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
boolean checkUserAgent = cfg.getBoolean("site", "checkUserAgent", true);
final String pageName = "HostPage.html";
template = HtmlDomUtil.parseFile(getClass(), pageName);
if (template == null) {
throw new FileNotFoundException("No " + pageName + " in webapp");
}
if (HtmlDomUtil.find(template, "gerrit_module") == null) {
throw new ServletException("No gerrit_module in " + pageName);
}
if (HtmlDomUtil.find(template, HPD_ID) == null) {
throw new ServletException("No " + HPD_ID + " in " + pageName);
}
String src = "gerrit_ui/gerrit_ui.nocache.js";
if (!IS_DEV) {
Element devmode = HtmlDomUtil.find(template, "gwtdevmode");
if (devmode != null) {
devmode.getParentNode().removeChild(devmode);
}
InputStream in = servletContext.getResourceAsStream("/" + src);
if (in != null) {
Hasher md = Hashing.md5().newHasher();
try {
try {
final byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) > 0) {
md.putBytes(buf, 0, n);
}
} finally {
in.close();
}
} catch (IOException e) {
throw new IOException("Failed reading " + src, e);
}
src += "?content=" + md.hash().toString();
} else {
log.debug("No " + src + " in webapp root; keeping noncache.js URL");
}
}
noCacheName = src;
selector = new PermutationSelector("gerrit_ui");
if (checkUserAgent && !IS_DEV) {
selector.init(servletContext);
}
page = new Page();
}
private void json(final Object data, final StringWriter w) {
JsonServlet.defaultGsonBuilder().create().toJson(data, w);
}
private Page get() {
Page p = page;
if (refreshHeaderFooter && p.isStale()) {
final Page newPage;
try {
newPage = new Page();
} catch (IOException e) {
log.error("Cannot refresh site header/footer", e);
return p;
}
p = newPage;
page = p;
}
return p;
}
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException {
final Page.Content page = select(req);
final StringWriter w = new StringWriter();
final CurrentUser user = currentUser.get();
if (user instanceof IdentifiedUser) {
w.write(HPD_ID + ".account=");
json(((IdentifiedUser) user).getAccount(), w);
w.write(";");
w.write(HPD_ID + ".xGerritAuth=");
json(session.get().getXGerritAuth(), w);
w.write(";");
w.write(HPD_ID + ".accountDiffPref=");
json(((IdentifiedUser) user).getAccountDiffPreference(), w);
w.write(";");
w.write(HPD_ID + ".theme=");
json(signedInTheme, w);
w.write(";");
} else {
w.write(HPD_ID + ".theme=");
json(signedOutTheme, w);
w.write(";");
}
plugins(w);
final byte[] hpd = w.toString().getBytes("UTF-8");
final byte[] raw = Bytes.concat(page.part1, hpd, page.part2);
final byte[] tosend;
if (RPCServletUtils.acceptsGzipEncoding(req)) {
rsp.setHeader("Content-Encoding", "gzip");
tosend = HtmlDomUtil.compress(raw);
} else {
tosend = raw;
}
CacheHeaders.setNotCacheable(rsp);
rsp.setContentType("text/html");
rsp.setCharacterEncoding(HtmlDomUtil.ENC);
rsp.setContentLength(tosend.length);
final OutputStream out = rsp.getOutputStream();
try {
out.write(tosend);
} finally {
out.close();
}
}
private void plugins(StringWriter w) {
List<String> urls = Lists.newArrayList();
for (WebUiPlugin u : plugins) {
urls.add(String.format("plugins/%s/%s",
u.getPluginName(),
u.getJavaScriptResourcePath()));
}
if (!urls.isEmpty()) {
w.write(HPD_ID + ".plugins=");
json(urls, w);
w.write(";");
}
}
private Page.Content select(HttpServletRequest req) {
Page pg = get();
if ("1".equals(req.getParameter("dbg"))) {
return pg.debug;
} else if ("0".equals(req.getParameter("s"))) {
// If s=0 is used in the URL, the user has explicitly asked us
// to not perform selection on the server side, perhaps due to
// it incorrectly guessing their user agent.
return pg.get(null);
}
return pg.get(selector.select(req));
}
private static class FileInfo {
private final File path;
private final long time;
FileInfo(final File p) {
path = p;
time = path.lastModified();
}
boolean isStale() {
return time != path.lastModified();
}
}
private class Page {
private final FileInfo css;
private final FileInfo header;
private final FileInfo footer;
private final Map<Permutation, Content> permutations;
private final Content debug;
Page() throws IOException {
Document hostDoc = HtmlDomUtil.clone(template);
css = injectCssFile(hostDoc, "gerrit_sitecss", site.site_css);
header = injectXmlFile(hostDoc, "gerrit_header", site.site_header);
footer = injectXmlFile(hostDoc, "gerrit_footer", site.site_footer);
final HostPageData pageData = new HostPageData();
pageData.config = config;
final StringWriter w = new StringWriter();
w.write("var " + HPD_ID + "=");
json(pageData, w);
w.write(";");
final Element data = HtmlDomUtil.find(hostDoc, HPD_ID);
asScript(data);
data.appendChild(hostDoc.createTextNode(w.toString()));
data.appendChild(hostDoc.createComment(HPD_ID));
permutations = new HashMap<Permutation, Content>();
for (Permutation p : selector.getPermutations()) {
final Document d = HtmlDomUtil.clone(hostDoc);
Element nocache = HtmlDomUtil.find(d, "gerrit_module");
nocache.getParentNode().removeChild(nocache);
p.inject(d);
permutations.put(p, new Content(d));
}
Element nocache = HtmlDomUtil.find(hostDoc, "gerrit_module");
asScript(nocache);
nocache.removeAttribute("id");
nocache.setAttribute("src", noCacheName);
permutations.put(null, new Content(hostDoc));
nocache.setAttribute("src", "gerrit_ui/gerrit_dbg.nocache.js");
debug = new Content(hostDoc);
}
Content get(Permutation p) {
Content c = permutations.get(p);
if (c == null) {
c = permutations.get(null);
}
return c;
}
boolean isStale() {
return css.isStale() || header.isStale() || footer.isStale();
}
private void asScript(final Element scriptNode) {
scriptNode.setAttribute("type", "text/javascript");
scriptNode.setAttribute("language", "javascript");
}
class Content {
final byte[] part1;
final byte[] part2;
Content(Document hostDoc) throws IOException {
final String raw = HtmlDomUtil.toString(hostDoc);
final int p = raw.indexOf("<!--" + HPD_ID);
if (p < 0) {
throw new IOException("No tag in transformed host page HTML");
}
part1 = raw.substring(0, p).getBytes("UTF-8");
part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes("UTF-8");
}
}
private FileInfo injectCssFile(final Document hostDoc, final String id,
final File src) throws IOException {
final FileInfo info = new FileInfo(src);
final Element banner = HtmlDomUtil.find(hostDoc, id);
if (banner == null) {
return info;
}
while (banner.getFirstChild() != null) {
banner.removeChild(banner.getFirstChild());
}
String css = HtmlDomUtil.readFile(src.getParentFile(), src.getName());
if (css == null) {
banner.getParentNode().removeChild(banner);
return info;
}
banner.removeAttribute("id");
banner.appendChild(hostDoc.createCDATASection("\n" + css + "\n"));
return info;
}
private FileInfo injectXmlFile(final Document hostDoc, final String id,
final File src) throws IOException {
final FileInfo info = new FileInfo(src);
final Element banner = HtmlDomUtil.find(hostDoc, id);
if (banner == null) {
return info;
}
while (banner.getFirstChild() != null) {
banner.removeChild(banner.getFirstChild());
}
Document html = HtmlDomUtil.parseFile(src);
if (html == null) {
banner.getParentNode().removeChild(banner);
return info;
}
final Element content = html.getDocumentElement();
banner.appendChild(hostDoc.importNode(content, true));
return info;
}
}
}