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