Merge changes from topic 'resource-servlet'
* changes:
Serve GWT UI from ResourceServlet
ResourceServlet: Stream large files, bypassing the cache
ResourceServlet: Respect existing cache headers
Rename StaticServlet to SiteStaticDirectoryServlet
Refactor static content serving
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
index 4b78991..950400a 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -128,6 +128,12 @@
cache(res, "private", age, unit, mustRevalidate);
}
+ public static boolean hasCacheHeader(HttpServletResponse res) {
+ return res.getHeader("Cache-Control") != null
+ || res.getHeader("Expires") != null
+ || "no-cache".equals(res.getHeader("Pragma"));
+ }
+
private static void cache(HttpServletResponse res,
String type, long age, TimeUnit unit, boolean revalidate) {
res.setHeader("Cache-Control", String.format(
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index b29bd2a..da224d5 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -12,7 +12,9 @@
'//gerrit-common:annotations',
'//gerrit-common:server',
'//gerrit-extension-api:api',
+ '//gerrit-gwtexpui:linker_server',
'//gerrit-gwtexpui:server',
+ '//gerrit-launcher:launcher',
'//gerrit-patch-jgit:server',
'//gerrit-prettify:server',
'//gerrit-reviewdb:server',
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 86debdd..7ecf2fa 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -23,7 +23,7 @@
import com.google.gerrit.httpd.raw.LegacyGerritServlet;
import com.google.gerrit.httpd.raw.RobotsServlet;
import com.google.gerrit.httpd.raw.SshInfoServlet;
-import com.google.gerrit.httpd.raw.StaticServlet;
+import com.google.gerrit.httpd.raw.StaticModule;
import com.google.gerrit.httpd.raw.ToolServlet;
import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
@@ -77,7 +77,6 @@
serve("/signout").with(HttpLogoutServlet.class);
}
serve("/ssh_info").with(SshInfoServlet.class);
- serve("/static/*").with(StaticServlet.class);
serve("/Main.class").with(notFound());
serve("/com/google/gerrit/launcher/*").with(notFound());
@@ -107,6 +106,8 @@
filter("/Documentation/").through(QueryDocumentationFilter.class);
serve("/robots.txt").with(RobotsServlet.class);
+
+ install(new StaticModule());
}
private Key<HttpServlet> notFound() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DeveloperGwtUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DeveloperGwtUiServlet.java
new file mode 100644
index 0000000..9d57b17
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DeveloperGwtUiServlet.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2015 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.cache.Cache;
+import com.google.gerrit.common.TimeUtil;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+
+class DeveloperGwtUiServlet extends ResourceServlet {
+ private static final long serialVersionUID = 1L;
+
+ private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
+
+ private final Path ui;
+
+ DeveloperGwtUiServlet(Cache<Path, Resource> cache, Path unpackedWar)
+ throws IOException {
+ super(cache, false);
+ ui = unpackedWar.resolve("gerrit_ui");
+ Files.createDirectory(ui);
+ ui.toFile().deleteOnExit();
+ }
+
+ @Override
+ protected Path getResourcePath(String pathInfo) {
+ return ui.resolve(pathInfo);
+ }
+
+ @Override
+ protected FileTime getLastModifiedTime(Path p) {
+ // Return initialization time of this class, since the GWT outputs from the
+ // build process all have mtimes of 1980/1/1.
+ return NOW;
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index ed49841..ab3728e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -92,7 +92,7 @@
private final Document template;
private final String noCacheName;
private final boolean refreshHeaderFooter;
- private final StaticServlet staticServlet;
+ private final SiteStaticDirectoryServlet staticServlet;
private final boolean isNoteDbEnabled;
private final Integer pluginsLoadTimeout;
private final GetDiffPreferences getDiff;
@@ -108,7 +108,7 @@
DynamicSet<WebUiPlugin> webUiPlugins,
DynamicSet<MessageOfTheDay> motd,
@GerritServerConfig Config cfg,
- StaticServlet ss,
+ SiteStaticDirectoryServlet ss,
NotesMigration migration,
GetDiffPreferences diffPref)
throws IOException, ServletException {
@@ -302,7 +302,7 @@
String src = e.getAttribute("src");
if (src != null && src.startsWith("static/")) {
String name = src.substring("static/".length());
- StaticServlet.Resource r = staticServlet.getResource(name);
+ ResourceServlet.Resource r = staticServlet.getResource(name);
if (r != null) {
e.setAttribute("src", src + "?e=" + r.etag);
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
new file mode 100644
index 0000000..a5bc6c6
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
@@ -0,0 +1,231 @@
+// Copyright (C) 2015 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 java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.escape.Escaper;
+import com.google.common.html.HtmlEscapers;
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gwtexpui.linker.server.UserAgentRule;
+import com.google.gwtexpui.server.CacheHeaders;
+
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class RecompileGwtUiFilter implements Filter {
+ private static final Logger log =
+ LoggerFactory.getLogger(RecompileGwtUiFilter.class);
+
+ private final boolean gwtuiRecompile =
+ System.getProperty("gerrit.disable-gwtui-recompile") == null;
+ private final UserAgentRule rule = new UserAgentRule();
+ private final Set<String> uaInitialized = new HashSet<>();
+ private final Path unpackedWar;
+ private final Path gen;
+ private final Path root;
+
+ private String lastTarget;
+ private long lastTime;
+
+ RecompileGwtUiFilter(Path buckOut, Path unpackedWar) {
+ this.unpackedWar = unpackedWar;
+ gen = buckOut.resolve("gen");
+ root = buckOut.getParent();
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse res,
+ FilterChain chain) throws IOException, ServletException {
+ String pkg = "gerrit-gwtui";
+ String target = "ui_" + rule.select((HttpServletRequest) request);
+ if (gwtuiRecompile || !uaInitialized.contains(target)) {
+ String rule = "//" + pkg + ":" + target;
+ // TODO(davido): instead of assuming specific Buck's internal
+ // target directory for gwt_binary() artifacts, ask Buck for
+ // the location of user agent permutation GWT zip, e. g.:
+ // $ buck targets --show_output //gerrit-gwtui:ui_safari \
+ // | awk '{print $2}'
+ String child = String.format("%s/__gwt_binary_%s__", pkg, target);
+ File zip = gen.resolve(child).resolve(target + ".zip").toFile();
+
+ synchronized (this) {
+ try {
+ build(root, gen, rule);
+ } catch (BuildFailureException e) {
+ displayFailure(rule, e.why, (HttpServletResponse) res);
+ return;
+ }
+
+ if (!target.equals(lastTarget) || lastTime != zip.lastModified()) {
+ lastTarget = target;
+ lastTime = zip.lastModified();
+ unpack(zip, unpackedWar.toFile());
+ }
+ }
+ uaInitialized.add(target);
+ }
+ chain.doFilter(request, res);
+ }
+
+ private void displayFailure(String rule, byte[] why, HttpServletResponse res)
+ throws IOException {
+ res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ res.setContentType("text/html");
+ res.setCharacterEncoding(UTF_8.name());
+ CacheHeaders.setNotCacheable(res);
+
+ Escaper html = HtmlEscapers.htmlEscaper();
+ try (PrintWriter w = res.getWriter()) {
+ w.write("<html><title>BUILD FAILED</title><body>");
+ w.format("<h1>%s FAILED</h1>", html.escape(rule));
+ w.write("<pre>");
+ w.write(html.escape(RawParseUtils.decode(why)));
+ w.write("</pre>");
+ w.write("</body></html>");
+ }
+ }
+
+ @Override
+ public void init(FilterConfig config) {
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ private static void unpack(File srcwar, File dstwar) throws IOException {
+ try (ZipFile zf = new ZipFile(srcwar)) {
+ final Enumeration<? extends ZipEntry> e = zf.entries();
+ while (e.hasMoreElements()) {
+ final ZipEntry ze = e.nextElement();
+ final String name = ze.getName();
+
+ if (ze.isDirectory()
+ || name.startsWith("WEB-INF/")
+ || name.startsWith("META-INF/")
+ || name.startsWith("com/google/gerrit/launcher/")
+ || name.equals("Main.class")) {
+ continue;
+ }
+
+ final File rawtmp = new File(dstwar, name);
+ mkdir(rawtmp.getParentFile());
+ rawtmp.deleteOnExit();
+
+ try (FileOutputStream rawout = new FileOutputStream(rawtmp);
+ InputStream in = zf.getInputStream(ze)) {
+ final byte[] buf = new byte[4096];
+ int n;
+ while ((n = in.read(buf, 0, buf.length)) > 0) {
+ rawout.write(buf, 0, n);
+ }
+ }
+ }
+ }
+ }
+
+ private static void build(Path root, Path gen, String target)
+ throws IOException, BuildFailureException {
+ log.info("buck build " + target);
+ Properties properties = loadBuckProperties(gen);
+ String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck");
+ ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
+ .directory(root.toFile())
+ .redirectErrorStream(true);
+ if (properties.containsKey("PATH")) {
+ proc.environment().put("PATH", properties.getProperty("PATH"));
+ }
+ long start = TimeUtil.nowMs();
+ Process rebuild = proc.start();
+ byte[] out;
+ try (InputStream in = rebuild.getInputStream()) {
+ out = ByteStreams.toByteArray(in);
+ } finally {
+ rebuild.getOutputStream().close();
+ }
+
+ int status;
+ try {
+ status = rebuild.waitFor();
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException("interrupted waiting for " + buck);
+ }
+ if (status != 0) {
+ throw new BuildFailureException(out);
+ }
+
+ long time = TimeUtil.nowMs() - start;
+ log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0));
+ }
+
+ private static Properties loadBuckProperties(Path gen)
+ throws FileNotFoundException, IOException {
+ Properties properties = new Properties();
+ try (InputStream in = new FileInputStream(
+ gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) {
+ properties.load(in);
+ }
+ return properties;
+ }
+
+ @SuppressWarnings("serial")
+ private static class BuildFailureException extends Exception {
+ final byte[] why;
+
+ BuildFailureException(byte[] why) {
+ this.why = why;
+ }
+ }
+
+ private static void mkdir(File dir) throws IOException {
+ if (!dir.isDirectory()) {
+ mkdir(dir.getParentFile());
+ if (!dir.mkdir()) {
+ throw new IOException("Cannot mkdir " + dir.getAbsolutePath());
+ }
+ dir.deleteOnExit();
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
new file mode 100644
index 0000000..a804e2a
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -0,0 +1,314 @@
+// Copyright (C) 2015 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.base.Preconditions.checkNotNull;
+import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
+import static com.google.common.net.HttpHeaders.ETAG;
+import static com.google.common.net.HttpHeaders.IF_MODIFIED_SINCE;
+import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
+import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Base class for serving static resources.
+ * <p>
+ * Supports caching, ETags, basic content type detection, and limited gzip
+ * compression.
+ */
+public abstract class ResourceServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger log =
+ LoggerFactory.getLogger(ResourceServlet.class);
+
+ private static final int CACHE_FILE_SIZE_LIMIT_BYTES = 100 << 10;
+
+ private static final String JS = "application/x-javascript";
+ private static final ImmutableMap<String, String> MIME_TYPES =
+ ImmutableMap.<String, String> builder()
+ .put("css", "text/css")
+ .put("gif", "image/gif")
+ .put("htm", "text/html")
+ .put("html", "text/html")
+ .put("jpeg", "image/jpeg")
+ .put("jpg", "image/jpeg")
+ .put("js", JS)
+ .put("pdf", "application/pdf")
+ .put("png", "image/png")
+ .put("rtf", "text/rtf")
+ .put("svg", "image/svg+xml")
+ .put("text", "text/plain")
+ .put("tif", "image/tiff")
+ .put("tiff", "image/tiff")
+ .put("txt", "text/plain")
+ .build();
+
+ protected static String contentType(String name) {
+ int dot = name.lastIndexOf('.');
+ String ext = 0 < dot ? name.substring(dot + 1) : "";
+ String type = MIME_TYPES.get(ext);
+ return type != null ? type : "application/octet-stream";
+ }
+
+ private final Cache<Path, Resource> cache;
+ private final boolean refresh;
+
+ protected ResourceServlet(Cache<Path, Resource> cache, boolean refresh) {
+ this.cache = checkNotNull(cache, "cache");
+ this.refresh = refresh;
+ }
+
+ /**
+ * Get the resource path on the filesystem that should be served for this
+ * request.
+ *
+ * @param pathInfo result of {@link HttpServletRequest#getPathInfo()}.
+ * @return path where static content can be found.
+ */
+ protected abstract Path getResourcePath(String pathInfo);
+
+ protected FileTime getLastModifiedTime(Path p) throws IOException {
+ return Files.getLastModifiedTime(p);
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
+ throws IOException {
+ String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
+ if (isUnreasonableName(name)) {
+ notFound(rsp);
+ return;
+ }
+ Path p = getResourcePath(name);
+ if (p == null) {
+ notFound(rsp);
+ return;
+ }
+
+ Resource r = cache.getIfPresent(p);
+ if (r == null && maybeStream(p, req, rsp)) {
+ return;
+ }
+
+ if (r == null) {
+ Callable<Resource> loader = newLoader(p);
+ try {
+ r = cache.get(p, loader);
+ if (refresh && r.isStale(p, this)) {
+ cache.invalidate(p);
+ r = cache.get(p, loader);
+ }
+ } catch (ExecutionException | IOException e) {
+ log.warn("Cannot load static resource " + req.getPathInfo(), e);
+ CacheHeaders.setNotCacheable(rsp);
+ rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
+ return;
+ }
+ }
+
+ if (r == Resource.NOT_FOUND) {
+ notFound(rsp);
+ return;
+ }
+
+ String e = req.getParameter("e");
+ if (e != null && !r.etag.equals(e)) {
+ CacheHeaders.setNotCacheable(rsp);
+ rsp.setStatus(SC_NOT_FOUND);
+ return;
+ } else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
+ rsp.setStatus(SC_NOT_MODIFIED);
+ return;
+ }
+
+ byte[] tosend = r.raw;
+ if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
+ byte[] gz = HtmlDomUtil.compress(tosend);
+ if ((gz.length + 24) < tosend.length) {
+ rsp.setHeader(CONTENT_ENCODING, "gzip");
+ tosend = gz;
+ }
+ }
+ if (!CacheHeaders.hasCacheHeader(rsp)) {
+ if (e != null && r.etag.equals(e)) {
+ CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
+ } else {
+ CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
+ }
+ }
+ rsp.setHeader(ETAG, r.etag);
+ rsp.setContentType(r.contentType);
+ rsp.setContentLength(tosend.length);
+ try (OutputStream out = rsp.getOutputStream()) {
+ out.write(tosend);
+ }
+ }
+
+ @Nullable
+ Resource getResource(String name) {
+ try {
+ Path p = getResourcePath(name);
+ return cache.get(p, newLoader(p));
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot load static resource %s", name), e);
+ return null;
+ }
+ }
+
+ private static void notFound(HttpServletResponse rsp) {
+ rsp.setStatus(SC_NOT_FOUND);
+ CacheHeaders.setNotCacheable(rsp);
+ }
+
+ /**
+ * Maybe stream a path to the response, depending on the properties of the
+ * file and cache headers in the request.
+ *
+ * @param p path to stream
+ * @param req HTTP request.
+ * @param rsp HTTP response.
+ * @return true if the response was written (either the file contents or an
+ * error); false if the path is too small to stream and should be cached.
+ */
+ private boolean maybeStream(Path p, HttpServletRequest req,
+ HttpServletResponse rsp) throws IOException {
+ try {
+ if (Files.size(p) < CACHE_FILE_SIZE_LIMIT_BYTES) {
+ return false;
+ }
+ } catch (NoSuchFileException e) {
+ cache.put(p, Resource.NOT_FOUND);
+ notFound(rsp);
+ return true;
+ }
+
+ long lastModified = FileUtil.lastModified(p);
+ if (req.getDateHeader(IF_MODIFIED_SINCE) >= lastModified) {
+ rsp.setStatus(SC_NOT_MODIFIED);
+ return true;
+ }
+
+ if (lastModified > 0) {
+ rsp.setDateHeader(LAST_MODIFIED, lastModified);
+ }
+ if (!CacheHeaders.hasCacheHeader(rsp)) {
+ CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
+ }
+ rsp.setContentType(contentType(p.toString()));
+
+ OutputStream out = rsp.getOutputStream();
+ GZIPOutputStream gz = null;
+ if (RPCServletUtils.acceptsGzipEncoding(req)) {
+ rsp.setHeader(CONTENT_ENCODING, "gzip");
+ gz = new GZIPOutputStream(out);
+ out = gz;
+ }
+ Files.copy(p, out);
+ if (gz != null) {
+ gz.finish();
+ }
+ return true;
+ }
+
+
+ private static boolean isUnreasonableName(String name) {
+ return name.length() < 1
+ || name.contains("\\") // no windows/dos style paths
+ || name.startsWith("../") // no "../etc/passwd"
+ || name.contains("/../") // no "foo/../etc/passwd"
+ || name.contains("/./") // "foo/./foo" is insane to ask
+ || name.contains("//"); // windows UNC path can be "//..."
+ }
+
+ private Callable<Resource> newLoader(final Path p) {
+ return new Callable<Resource>() {
+ @Override
+ public Resource call() throws IOException {
+ try {
+ return new Resource(
+ getLastModifiedTime(p),
+ contentType(p.toString()),
+ Files.readAllBytes(p));
+ } catch (NoSuchFileException e) {
+ return Resource.NOT_FOUND;
+ }
+ }
+ };
+ }
+
+ static class Resource {
+ static final Resource NOT_FOUND =
+ new Resource(FileTime.fromMillis(0), "", new byte[] {});
+
+ final FileTime lastModified;
+ final String contentType;
+ final String etag;
+ final byte[] raw;
+
+ Resource(FileTime lastModified, String contentType, byte[] raw) {
+ this.lastModified = checkNotNull(lastModified, "lastModified");
+ this.contentType = checkNotNull(contentType, "contentType");
+ this.raw = checkNotNull(raw, "raw");
+ this.etag = Hashing.md5().hashBytes(raw).toString();
+ }
+
+ boolean isStale(Path p, ResourceServlet rs) throws IOException {
+ FileTime t = rs.getLastModifiedTime(p);
+ return t.toMillis() == 0
+ || lastModified.toMillis() == 0
+ || !lastModified.equals(t);
+ }
+ }
+
+ static class Weigher
+ implements com.google.common.cache.Weigher<Path, Resource> {
+ @Override
+ public int weigh(Path p, Resource r) {
+ return 2 * p.toString().length() + r.raw.length;
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
new file mode 100644
index 0000000..cf99d3c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
@@ -0,0 +1,65 @@
+// 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.cache.Cache;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+
+/** Sends static content from the site 's {@code static/} subdirectory. */
+@Singleton
+public class SiteStaticDirectoryServlet extends ResourceServlet {
+ private static final long serialVersionUID = 1L;
+
+ private final Path staticBase;
+
+ @Inject
+ SiteStaticDirectoryServlet(
+ SitePaths site,
+ @GerritServerConfig Config cfg,
+ @Named(StaticModule.CACHE) Cache<Path, Resource> cache) {
+ super(cache, cfg.getBoolean("site", "refreshHeaderFooter", true));
+ Path p;
+ try {
+ p = site.static_dir.toRealPath().normalize();
+ } catch (IOException e) {
+ p = site.static_dir.toAbsolutePath().normalize();
+ }
+ staticBase = p;
+ }
+
+ @Override
+ protected Path getResourcePath(String pathInfo) {
+ Path p = staticBase.resolve(pathInfo);
+ try {
+ p = p.toRealPath().normalize();
+ if (!p.startsWith(staticBase)) {
+ return null;
+ }
+ return p;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
new file mode 100644
index 0000000..e18afa0
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2015 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.cache.Cache;
+import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Key;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import com.google.inject.servlet.ServletModule;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+
+import javax.servlet.http.HttpServlet;
+
+public class StaticModule extends ServletModule {
+ private static final String GWT_UI_SERVLET = "GwtUiServlet";
+ static final String CACHE = "static_content";
+
+ private final FileSystem warFs;
+ private final Path buckOut;
+ private final Path unpackedWar;
+
+ public StaticModule() {
+ warFs = getDistributionArchive();
+ if (warFs == null) {
+ buckOut = getDeveloperBuckOut();
+ unpackedWar = makeWarTempDir();
+ } else {
+ buckOut = null;
+ unpackedWar = null;
+ }
+ }
+
+ @Override
+ protected void configureServlets() {
+ serve("/static/*").with(SiteStaticDirectoryServlet.class);
+ serveGwtUi();
+ install(new CacheModule() {
+ @Override
+ protected void configure() {
+ cache(CACHE, Path.class, Resource.class)
+ .maximumWeight(1 << 20)
+ .weigher(ResourceServlet.Weigher.class);
+ }
+ });
+ }
+
+ private void serveGwtUi() {
+ serve("/gerrit_ui/*")
+ .with(Key.get(HttpServlet.class, Names.named(GWT_UI_SERVLET)));
+ if (warFs == null) {
+ filter("/").through(new RecompileGwtUiFilter(buckOut, unpackedWar));
+ }
+ }
+
+ @Provides
+ @Singleton
+ @Named(GWT_UI_SERVLET)
+ HttpServlet getGwtUiServlet(@Named(CACHE) Cache<Path, Resource> cache)
+ throws IOException {
+ if (warFs != null) {
+ return new WarGwtUiServlet(cache, warFs);
+ } else {
+ return new DeveloperGwtUiServlet(cache, unpackedWar);
+ }
+ }
+
+ private static FileSystem getDistributionArchive() {
+ try {
+ return GerritLauncher.getDistributionArchiveFileSystem();
+ } catch (IOException e) {
+ if ((e instanceof FileNotFoundException)
+ && GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) {
+ return null;
+ } else {
+ ProvisionException pe =
+ new ProvisionException("Error reading gerrit.war");
+ pe.initCause(e);
+ throw pe;
+ }
+ }
+ }
+
+ private static Path getDeveloperBuckOut() {
+ try {
+ return GerritLauncher.getDeveloperBuckOut();
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ private static Path makeWarTempDir() {
+ // Obtain our local temporary directory, but it comes back as a file
+ // so we have to switch it to be a directory post creation.
+ //
+ try {
+ File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
+ if (!dstwar.delete() || !dstwar.mkdir()) {
+ throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
+ }
+
+ // Jetty normally refuses to serve out of a symlinked directory, as
+ // a security feature. Try to resolve out any symlinks in the path.
+ //
+ try {
+ return dstwar.getCanonicalFile().toPath();
+ } catch (IOException e) {
+ return dstwar.getAbsoluteFile().toPath();
+ }
+ } catch (IOException e) {
+ ProvisionException pe =
+ new ProvisionException("Cannot create war tempdir");
+ pe.initCause(e);
+ throw pe;
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
deleted file mode 100644
index 570ad57..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
+++ /dev/null
@@ -1,249 +0,0 @@
-// 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 static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
-import static com.google.common.net.HttpHeaders.ETAG;
-import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
-import static com.google.gerrit.common.FileUtil.lastModified;
-import static java.util.concurrent.TimeUnit.DAYS;
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
-import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
-import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
-
-import com.google.common.base.CharMatcher;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.cache.Weigher;
-import com.google.common.collect.Maps;
-import com.google.common.hash.Hashing;
-import com.google.gerrit.common.FileUtil;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtexpui.server.CacheHeaders;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-
-/** Sends static content from the site 's {@code static/} subdirectory. */
-@SuppressWarnings("serial")
-@Singleton
-public class StaticServlet extends HttpServlet {
- private static final Logger log = LoggerFactory.getLogger(StaticServlet.class);
- private static final String JS = "application/x-javascript";
- private static final Map<String, String> MIME_TYPES = Maps.newHashMap();
- static {
- MIME_TYPES.put("html", "text/html");
- MIME_TYPES.put("htm", "text/html");
- MIME_TYPES.put("js", JS);
- MIME_TYPES.put("css", "text/css");
- MIME_TYPES.put("rtf", "text/rtf");
- MIME_TYPES.put("txt", "text/plain");
- MIME_TYPES.put("text", "text/plain");
- MIME_TYPES.put("pdf", "application/pdf");
- MIME_TYPES.put("jpeg", "image/jpeg");
- MIME_TYPES.put("jpg", "image/jpeg");
- MIME_TYPES.put("gif", "image/gif");
- MIME_TYPES.put("png", "image/png");
- MIME_TYPES.put("tiff", "image/tiff");
- MIME_TYPES.put("tif", "image/tiff");
- MIME_TYPES.put("svg", "image/svg+xml");
- }
-
- private static String contentType(final String name) {
- final int dot = name.lastIndexOf('.');
- final String ext = 0 < dot ? name.substring(dot + 1) : "";
- final String type = MIME_TYPES.get(ext);
- return type != null ? type : "application/octet-stream";
- }
-
- private final Path staticBase;
- private final boolean refresh;
- private final LoadingCache<String, Resource> cache;
-
- @Inject
- StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
- Path p;
- try {
- p = site.static_dir.toRealPath().normalize();
- } catch (IOException e) {
- p = site.static_dir.toAbsolutePath().normalize();
- }
- staticBase = p;
- refresh = cfg.getBoolean("site", "refreshHeaderFooter", true);
- cache = CacheBuilder.newBuilder()
- .maximumWeight(1 << 20)
- .weigher(new Weigher<String, Resource>() {
- @Override
- public int weigh(String name, Resource r) {
- return 2 * name.length() + r.raw.length;
- }
- })
- .build(new CacheLoader<String, Resource>() {
- @Override
- public Resource load(String name) throws Exception {
- return loadResource(name);
- }
- });
- }
-
- @Nullable
- Resource getResource(String name) {
- try {
- return cache.get(name);
- } catch (ExecutionException e) {
- log.warn(String.format("Cannot load static resource %s", name), e);
- return null;
- }
- }
-
- private Resource getResource(HttpServletRequest req)
- throws ExecutionException {
- String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
- if (isUnreasonableName(name)) {
- return Resource.NOT_FOUND;
- }
-
- Resource r = cache.get(name);
- if (r == Resource.NOT_FOUND) {
- return Resource.NOT_FOUND;
- }
-
- if (refresh && r.isStale()) {
- cache.invalidate(name);
- r = cache.get(name);
- }
- return r;
- }
-
- private static boolean isUnreasonableName(String name) {
- return name.length() < 1
- || name.contains("\\") // no windows/dos style paths
- || name.startsWith("../") // no "../etc/passwd"
- || name.contains("/../") // no "foo/../etc/passwd"
- || name.contains("/./") // "foo/./foo" is insane to ask
- || name.contains("//"); // windows UNC path can be "//..."
- }
-
- @Override
- protected void doGet(final HttpServletRequest req,
- final HttpServletResponse rsp) throws IOException {
- Resource r;
- try {
- r = getResource(req);
- } catch (ExecutionException e) {
- log.warn(String.format(
- "Cannot load static resource %s",
- req.getPathInfo()), e);
- CacheHeaders.setNotCacheable(rsp);
- rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
- return;
- }
-
- String e = req.getParameter("e");
- if (r == Resource.NOT_FOUND || (e != null && !r.etag.equals(e))) {
- CacheHeaders.setNotCacheable(rsp);
- rsp.setStatus(SC_NOT_FOUND);
- return;
- } else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
- rsp.setStatus(SC_NOT_MODIFIED);
- return;
- }
-
- byte[] tosend = r.raw;
- if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
- byte[] gz = HtmlDomUtil.compress(tosend);
- if ((gz.length + 24) < tosend.length) {
- rsp.setHeader(CONTENT_ENCODING, "gzip");
- tosend = gz;
- }
- }
- if (e != null && r.etag.equals(e)) {
- CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
- } else {
- CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
- }
- rsp.setHeader(ETAG, r.etag);
- rsp.setContentType(r.contentType);
- rsp.setContentLength(tosend.length);
- try (OutputStream out = rsp.getOutputStream()) {
- out.write(tosend);
- }
- }
-
- private Resource loadResource(String name) throws IOException {
- Path p = staticBase.resolve(name);
- try {
- p = p.toRealPath().normalize();
- } catch (IOException e) {
- return Resource.NOT_FOUND;
- }
- if (!p.startsWith(staticBase)) {
- return Resource.NOT_FOUND;
- }
-
- long ts = FileUtil.lastModified(p);
- byte[] raw;
- try {
- raw = Files.readAllBytes(p);
- } catch (NoSuchFileException e) {
- return Resource.NOT_FOUND;
- }
- return new Resource(p, ts, contentType(name), raw);
- }
-
- static class Resource {
- static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
-
- final Path src;
- final long lastModified;
- final String contentType;
- final String etag;
- final byte[] raw;
-
- Resource(Path src, long lastModified, String contentType, byte[] raw) {
- this.src = src;
- this.lastModified = lastModified;
- this.contentType = contentType;
- this.etag = Hashing.md5().hashBytes(raw).toString();
- this.raw = raw;
- }
-
- boolean isStale() {
- return lastModified != lastModified(src);
- }
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java
new file mode 100644
index 0000000..45952cc
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2015 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.cache.Cache;
+import com.google.gerrit.common.TimeUtil;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+
+class WarGwtUiServlet extends ResourceServlet {
+ private static final long serialVersionUID = 1L;
+
+ private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
+
+ private final FileSystem warFs;
+
+ WarGwtUiServlet(Cache<Path, Resource> cache, FileSystem warFs) {
+ super(cache, false);
+ this.warFs = warFs;
+ }
+
+ @Override
+ protected Path getResourcePath(String pathInfo) {
+ return warFs.getPath("/gerrit_ui/" + pathInfo);
+ }
+
+ @Override
+ protected FileTime getLastModifiedTime(Path p) {
+ // Return initialization time of this class, since the GWT outputs from the
+ // build process all have mtimes of 1980/1/1.
+ return NOW;
+ }
+}
diff --git a/gerrit-launcher/BUCK b/gerrit-launcher/BUCK
index 6281a1c..687e02f 100644
--- a/gerrit-launcher/BUCK
+++ b/gerrit-launcher/BUCK
@@ -5,6 +5,7 @@
srcs = ['src/main/java/com/google/gerrit/launcher/GerritLauncher.java'],
visibility = [
'//gerrit-acceptance-tests/...',
+ '//gerrit-httpd:',
'//gerrit-main:main_lib',
'//gerrit-pgm:',
],
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 89c8ec6..fb54bcf 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -27,12 +27,16 @@
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
+import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.SortedMap;
@@ -296,6 +300,7 @@
}
private static volatile File myArchive;
+ private static volatile FileSystem myArchiveFs;
private static volatile File myHome;
/**
@@ -304,11 +309,29 @@
* @return local path of the Gerrit WAR file.
* @throws FileNotFoundException if the code cannot guess the location.
*/
- public static File getDistributionArchive() throws FileNotFoundException {
- if (myArchive == null) {
- myArchive = locateMyArchive();
+ public static File getDistributionArchive()
+ throws FileNotFoundException, IOException {
+ File result = myArchive;
+ if (result == null) {
+ synchronized (GerritLauncher.class) {
+ result = myArchive;
+ if (result != null) {
+ return result;
+ }
+ result = locateMyArchive();
+ myArchiveFs = FileSystems.newFileSystem(
+ URI.create("jar:" + result.toPath().toUri()),
+ Collections.<String, String> emptyMap());
+ myArchive = result;
+ }
}
- return myArchive;
+ return result;
+ }
+
+ public static FileSystem getDistributionArchiveFileSystem()
+ throws FileNotFoundException, IOException {
+ getDistributionArchive();
+ return myArchiveFs;
}
private static File locateMyArchive() throws FileNotFoundException {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 25b351e..0684650 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -14,24 +14,15 @@
package com.google.gerrit.pgm.http.jetty;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
-import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
-import com.google.common.escape.Escaper;
-import com.google.common.html.HtmlEscapers;
-import com.google.common.io.ByteStreams;
-import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtexpui.linker.server.UserAgentRule;
-import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
@@ -60,49 +51,26 @@
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.util.RawParseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InterruptedIOException;
-import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
-import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.EnumSet;
-import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
-import java.util.Properties;
import java.util.Set;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
@Singleton
public class JettyServer {
@@ -158,13 +126,9 @@
private boolean reverseProxy;
- /** Location on disk where our WAR file was unpacked to. */
- private Resource baseResource;
-
@Inject
JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
- final JettyEnv env, final HttpLogFactory httpLogFactory)
- throws MalformedURLException, IOException {
+ final JettyEnv env, final HttpLogFactory httpLogFactory) {
this.site = site;
httpd = new Server(threadPool(cfg));
@@ -372,8 +336,7 @@
return pool;
}
- private Handler makeContext(final JettyEnv env, final Config cfg)
- throws MalformedURLException, IOException {
+ private Handler makeContext(final JettyEnv env, final Config cfg) {
final Set<String> paths = new HashSet<>();
for (URI u : listenURLs(cfg)) {
String p = u.getPath();
@@ -408,7 +371,7 @@
}
private ContextHandler makeContext(final String contextPath,
- final JettyEnv env, final Config cfg) throws MalformedURLException, IOException {
+ final JettyEnv env, final Config cfg) {
final ServletContextHandler app = new ServletContextHandler();
// This enables the use of sessions in Jetty, feature available
@@ -421,12 +384,6 @@
//
app.setContextPath(contextPath);
- // Serve static resources directly from our JAR. This way we don't
- // need to unpack them into yet another temporary directory prior to
- // serving to clients.
- //
- app.setBaseResource(getBaseResource(app));
-
// HTTP front-end filter to be used as surrogate of Apache HTTP
// reverse-proxy filtering.
// It is meant to be used as simpler tiny deployment of custom-made
@@ -478,222 +435,4 @@
app.setWelcomeFiles(new String[0]);
return app;
}
-
- private Resource getBaseResource(ServletContextHandler app)
- throws IOException {
- if (baseResource == null) {
- try {
- baseResource = unpackWar(GerritLauncher.getDistributionArchive());
- } catch (FileNotFoundException err) {
- if (GerritLauncher.NOT_ARCHIVED.equals(err.getMessage())) {
- baseResource = useDeveloperBuild(app);
- } else {
- throw err;
- }
- }
- }
- return baseResource;
- }
-
- private static Resource unpackWar(File srcwar) throws IOException {
- File dstwar = makeWarTempDir();
- unpack(srcwar, dstwar);
- return Resource.newResource(dstwar.toURI());
- }
-
- private static File makeWarTempDir() throws IOException {
- // Obtain our local temporary directory, but it comes back as a file
- // so we have to switch it to be a directory post creation.
- //
- File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
- if (!dstwar.delete() || !dstwar.mkdir()) {
- throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
- }
-
- // Jetty normally refuses to serve out of a symlinked directory, as
- // a security feature. Try to resolve out any symlinks in the path.
- //
- try {
- return dstwar.getCanonicalFile();
- } catch (IOException e) {
- return dstwar.getAbsoluteFile();
- }
- }
-
- private static void unpack(File srcwar, File dstwar) throws IOException {
- try (ZipFile zf = new ZipFile(srcwar)) {
- final Enumeration<? extends ZipEntry> e = zf.entries();
- while (e.hasMoreElements()) {
- final ZipEntry ze = e.nextElement();
- final String name = ze.getName();
-
- if (ze.isDirectory()
- || name.startsWith("WEB-INF/")
- || name.startsWith("META-INF/")
- || name.startsWith("com/google/gerrit/launcher/")
- || name.equals("Main.class")) {
- continue;
- }
-
- final File rawtmp = new File(dstwar, name);
- mkdir(rawtmp.getParentFile());
- rawtmp.deleteOnExit();
-
- try (FileOutputStream rawout = new FileOutputStream(rawtmp);
- InputStream in = zf.getInputStream(ze)) {
- final byte[] buf = new byte[4096];
- int n;
- while ((n = in.read(buf, 0, buf.length)) > 0) {
- rawout.write(buf, 0, n);
- }
- }
- }
- }
- }
-
- private static void mkdir(File dir) throws IOException {
- if (!dir.isDirectory()) {
- mkdir(dir.getParentFile());
- if (!dir.mkdir()) {
- throw new IOException("Cannot mkdir " + dir.getAbsolutePath());
- }
- dir.deleteOnExit();
- }
- }
-
- private Resource useDeveloperBuild(ServletContextHandler app)
- throws IOException {
- final Path dir = GerritLauncher.getDeveloperBuckOut();
- final Path gen = dir.resolve("gen");
- final Path root = dir.getParent();
- final File dstwar = makeWarTempDir();
- File ui = new File(dstwar, "gerrit_ui");
- File p = new File(ui, "permutations");
- mkdir(ui);
- p.createNewFile();
- p.deleteOnExit();
-
- app.addFilter(new FilterHolder(new Filter() {
- private final boolean gwtuiRecompile =
- System.getProperty("gerrit.disable-gwtui-recompile") == null;
- private final UserAgentRule rule = new UserAgentRule();
- private final Set<String> uaInitialized = new HashSet<>();
- private String lastTarget;
- private long lastTime;
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse res,
- FilterChain chain) throws IOException, ServletException {
- String pkg = "gerrit-gwtui";
- String target = "ui_" + rule.select((HttpServletRequest) request);
- if (gwtuiRecompile || !uaInitialized.contains(target)) {
- String rule = "//" + pkg + ":" + target;
- // TODO(davido): instead of assuming specific Buck's internal
- // target directory for gwt_binary() artifacts, ask Buck for
- // the location of user agent permutation GWT zip, e. g.:
- // $ buck targets --show_output //gerrit-gwtui:ui_safari \
- // | awk '{print $2}'
- String child = String.format("%s/__gwt_binary_%s__", pkg, target);
- File zip = gen.resolve(child).resolve(target + ".zip").toFile();
-
- synchronized (this) {
- try {
- build(root, gen, rule);
- } catch (BuildFailureException e) {
- displayFailure(rule, e.why, (HttpServletResponse) res);
- return;
- }
-
- if (!target.equals(lastTarget) || lastTime != zip.lastModified()) {
- lastTarget = target;
- lastTime = zip.lastModified();
- unpack(zip, dstwar);
- }
- }
- uaInitialized.add(target);
- }
- chain.doFilter(request, res);
- }
-
- private void displayFailure(String rule, byte[] why, HttpServletResponse res)
- throws IOException {
- res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- res.setContentType("text/html");
- res.setCharacterEncoding(UTF_8.name());
- CacheHeaders.setNotCacheable(res);
-
- Escaper html = HtmlEscapers.htmlEscaper();
- try (PrintWriter w = res.getWriter()) {
- w.write("<html><title>BUILD FAILED</title><body>");
- w.format("<h1>%s FAILED</h1>", html.escape(rule));
- w.write("<pre>");
- w.write(html.escape(RawParseUtils.decode(why)));
- w.write("</pre>");
- w.write("</body></html>");
- }
- }
-
- @Override
- public void init(FilterConfig config) {
- }
-
- @Override
- public void destroy() {
- }
- }), "/", EnumSet.of(DispatcherType.REQUEST));
- return Resource.newResource(dstwar.toURI());
- }
-
- private static void build(Path root, Path gen, String target)
- throws IOException, BuildFailureException {
- log.info("buck build " + target);
- Properties properties = loadBuckProperties(gen);
- String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck");
- ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
- .directory(root.toFile())
- .redirectErrorStream(true);
- if (properties.containsKey("PATH")) {
- proc.environment().put("PATH", properties.getProperty("PATH"));
- }
- long start = TimeUtil.nowMs();
- Process rebuild = proc.start();
- byte[] out;
- try (InputStream in = rebuild.getInputStream()) {
- out = ByteStreams.toByteArray(in);
- } finally {
- rebuild.getOutputStream().close();
- }
-
- int status;
- try {
- status = rebuild.waitFor();
- } catch (InterruptedException e) {
- throw new InterruptedIOException("interrupted waiting for " + buck);
- }
- if (status != 0) {
- throw new BuildFailureException(out);
- }
-
- long time = TimeUtil.nowMs() - start;
- log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0));
- }
-
- private static Properties loadBuckProperties(Path gen)
- throws FileNotFoundException, IOException {
- Properties properties = new Properties();
- try (InputStream in = new FileInputStream(
- gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) {
- properties.load(in);
- }
- return properties;
- }
-
- @SuppressWarnings("serial")
- private static class BuildFailureException extends Exception {
- final byte[] why;
-
- BuildFailureException(byte[] why) {
- this.why = why;
- }
- }
}