Automatically refresh GWT UI on each page load
This came out of frustration while working on the ChangeScreen2 work.
For some types of UI work it can be easier to leave the Jetty server
running and simply click reload in the browser to recompile the GWT
code and load the new JavaScript.
GWT Jetty
-------------
server startup 6s 1s
initial request 20s 39s
no-op reload 10s 7s
The real win comes from changing class structure in the GWT UI code.
None of the UI classes are loaded into the Jetty server so there are
no class schema compatibility issues. In the hosted mode debugger the
developer must exit the debugger and restart it, bringing the edit-test
cycle to significantly longer than the time it takes to run Buck.
Another benefit is testing runs with a real browser, which can show
different results in JSNI code than in the hosted mode debugger.
Events and navigation is faster too, thanks to everything running
natively in the browser. Overall this can make the UI development
experience much less frustrating.
Change-Id: Ib4a4ff547f60dd10ae32aaacc07f9c84b848cfdc
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 ddc12c8..e167605 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
@@ -77,6 +77,12 @@
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;
@Singleton
public class JettyServer {
@@ -343,7 +349,7 @@
// need to unpack them into yet another temporary directory prior to
// serving to clients.
//
- app.setBaseResource(getBaseResource());
+ app.setBaseResource(getBaseResource(app));
// HTTP front-end filter to be used as surrogate of Apache HTTP
// reverse-proxy filtering.
@@ -397,13 +403,14 @@
return app;
}
- private Resource getBaseResource() throws IOException {
+ private Resource getBaseResource(ServletContextHandler app)
+ throws IOException {
if (baseResource == null) {
try {
baseResource = unpackWar(GerritLauncher.getDistributionArchive());
} catch (FileNotFoundException err) {
if (err.getMessage() == GerritLauncher.NOT_ARCHIVED) {
- baseResource = useDeveloperBuild();
+ baseResource = useDeveloperBuild(app);
} else {
throw err;
}
@@ -412,7 +419,13 @@
return baseResource;
}
- private Resource unpackWar(File srcwar) throws IOException {
+ 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.
//
@@ -425,11 +438,13 @@
// a security feature. Try to resolve out any symlinks in the path.
//
try {
- dstwar = dstwar.getCanonicalFile();
+ return dstwar.getCanonicalFile();
} catch (IOException e) {
- dstwar = dstwar.getAbsoluteFile();
+ return dstwar.getAbsoluteFile();
}
+ }
+ private static void unpack(File srcwar, File dstwar) throws IOException {
final ZipFile zf = new ZipFile(srcwar);
try {
final Enumeration<? extends ZipEntry> e = zf.entries();
@@ -466,11 +481,9 @@
} finally {
zf.close();
}
-
- return Resource.newResource(dstwar.toURI());
}
- private void mkdir(final File dir) throws IOException {
+ private static void mkdir(File dir) throws IOException {
if (!dir.isDirectory()) {
mkdir(dir.getParentFile());
if (!dir.mkdir())
@@ -479,7 +492,8 @@
}
}
- private Resource useDeveloperBuild() throws IOException {
+ private Resource useDeveloperBuild(ServletContextHandler app)
+ throws IOException {
// Find ourselves in the CLASSPATH. We should be a loose class file.
//
URL u = getClass().getResource(getClass().getSimpleName() + ".class");
@@ -510,12 +524,44 @@
dir = dir.getParentFile(); // pop classes
if ("buck-out".equals(dir.getName())) {
+ final File dstwar = makeWarTempDir();
String pkg = "gerrit-gwtui";
String target = targetForBrowser(System.getProperty("gerrit.browser"));
- File gen = new File(dir, "gen");
+ final File gen = new File(dir, "gen");
String out = new File(new File(gen, pkg), target).getAbsolutePath();
- build(dir.getParentFile(), gen, "//" + pkg + ":" + target);
- return unpackWar(new File(out + ".zip"));
+ final File zip = new File(out + ".zip");
+ final File root = dir.getParentFile();
+ final String name = "//" + pkg + ":" + target;
+
+ 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 long last;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse res,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ build(root, gen, name);
+ if (last != zip.lastModified()) {
+ last = zip.lastModified();
+ unpack(zip, dstwar);
+ }
+ chain.doFilter(req, res);
+ }
+
+ @Override
+ public void init(FilterConfig config) {
+ }
+ @Override
+ public void destroy() {
+ }
+ }), "/", EnumSet.of(DispatcherType.REQUEST));
+ return Resource.newResource(dstwar.toURI());
} else if ("target".equals(dir.getName())) {
return useMavenDeveloperBuild(dir);
} else {